From 2de72b52bd764ab030c8cd81d1d511a0e3e9b2f2 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 8 Oct 2017 19:50:15 -0400 Subject: [PATCH 001/145] Refactored setting of env vars passed via the env flag. --- bonobo/commands/run.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index a37282c..23298f5 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -1,3 +1,4 @@ +import codecs import os import bonobo @@ -41,7 +42,7 @@ def _install_requirements(requirements): def read(filename, module, install=False, quiet=False, verbose=False, env=None): - import re + import runpy from bonobo import Graph, settings @@ -52,10 +53,21 @@ def read(filename, module, install=False, quiet=False, verbose=False, env=None): settings.DEBUG.set(True) if env: - quote_killer = re.compile('["\']') + __escape_decoder = codecs.getdecoder('unicode_escape') + + def decode_escaped(escaped): + return __escape_decoder(escaped)[0] + for e in env: - var_name, var_value = e.split('=') - os.environ[var_name] = quote_killer.sub('', var_value) + ename, evalue = e.split('=', 1) + + if len(evalue) > 0: + quoted = evalue[0] == evalue[len(evalue) - 1] in ['"', "'"] + + if quoted: + evalue = decode_escaped(evalue[1:-1]) + + os.environ[ename] = evalue if filename: if os.path.isdir(filename): From e3125e7e02d461ee7f47e4b5d70dcd3493e68783 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 8 Oct 2017 19:52:25 -0400 Subject: [PATCH 002/145] Further Refactored the setting of env vars passed via the env flag. --- bonobo/commands/run.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 23298f5..4757a54 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -62,9 +62,7 @@ def read(filename, module, install=False, quiet=False, verbose=False, env=None): ename, evalue = e.split('=', 1) if len(evalue) > 0: - quoted = evalue[0] == evalue[len(evalue) - 1] in ['"', "'"] - - if quoted: + if evalue[0] == evalue[len(evalue) - 1] in ['"', "'"]: evalue = decode_escaped(evalue[1:-1]) os.environ[ename] = evalue From 88956ba6fefb01e359cb9b1fbe912a77f1a1c5ad Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 8 Oct 2017 21:02:31 -0400 Subject: [PATCH 003/145] default-env-file, default-env, and env-file now in place alongside env. default-env-file and default-env both use os.environ.setdefault so as not to overwrite existing variables (system environment) while env-file and env will overwrite existing variables. All four allow for multiple values (***How might this affect multiple default-env and default-env-file values, I expect that unlike env-file and env the first passed variables would win). --- bonobo/commands/run.py | 69 ++++++++++++++----- .../examples/env_vars/get_passed_env_file.py | 22 ++++++ tests/test_commands.py | 27 ++++++++ 3 files changed, 100 insertions(+), 18 deletions(-) create mode 100644 bonobo/examples/env_vars/get_passed_env_file.py diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 4757a54..59190be 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -1,8 +1,10 @@ import codecs import os +from pathlib import Path import bonobo from bonobo.constants import DEFAULT_SERVICES_ATTR, DEFAULT_SERVICES_FILENAME +from dotenv import load_dotenv DEFAULT_GRAPH_FILENAMES = ('__main__.py', 'main.py', ) DEFAULT_GRAPH_ATTR = 'get_graph' @@ -41,7 +43,7 @@ def _install_requirements(requirements): importlib.reload(site) -def read(filename, module, install=False, quiet=False, verbose=False, env=None): +def read(filename, module, install=False, quiet=False, verbose=False, default_env_file=None, default_env=None, env_file=None, env=None): import runpy from bonobo import Graph, settings @@ -52,21 +54,6 @@ def read(filename, module, install=False, quiet=False, verbose=False, env=None): if verbose: settings.DEBUG.set(True) - if env: - __escape_decoder = codecs.getdecoder('unicode_escape') - - def decode_escaped(escaped): - return __escape_decoder(escaped)[0] - - for e in env: - ename, evalue = e.split('=', 1) - - if len(evalue) > 0: - if evalue[0] == evalue[len(evalue) - 1] in ['"', "'"]: - evalue = decode_escaped(evalue[1:-1]) - - os.environ[ename] = evalue - if filename: if os.path.isdir(filename): if install: @@ -84,12 +71,38 @@ def read(filename, module, install=False, quiet=False, verbose=False, env=None): requirements = os.path.join(os.path.dirname(filename), 'requirements.txt') _install_requirements(requirements) context = runpy.run_path(filename, run_name='__bonobo__') + env_dir = Path(filename).parent elif module: context = runpy.run_module(module, run_name='__bonobo__') filename = context['__file__'] + env_dir = Path(module) else: raise RuntimeError('UNEXPECTED: argparse should not allow this.') + if default_env_file: + for f in default_env_file: + env_file_path = env_dir.joinpath(f) + load_dotenv(env_file_path) + else: + try: + env_file_path = env_dir.joinpath('.env') + load_dotenv(env_file_path) + except FileNotFoundError: + pass + + if default_env: + for e in default_env: + set_env_var(e) + + if env_file: + for f in env_file: + env_file_path = env_dir.joinpath(f) + load_dotenv(env_file_path, override=True) + + if env: + for e in env: + set_env_var(e, override=True) + graphs = dict((k, v) for k, v in context.items() if isinstance(v, Graph)) assert len(graphs) == 1, ( @@ -106,8 +119,25 @@ def read(filename, module, install=False, quiet=False, verbose=False, env=None): return graph, plugins, services -def execute(filename, module, install=False, quiet=False, verbose=False, env=None): - graph, plugins, services = read(filename, module, install, quiet, verbose, env) +def set_env_var(e, override=False): + __escape_decoder = codecs.getdecoder('unicode_escape') + ename, evalue = e.split('=', 1) + + def decode_escaped(escaped): + return __escape_decoder(escaped)[0] + + if len(evalue) > 0: + if evalue[0] == evalue[len(evalue) - 1] in ['"', "'"]: + evalue = decode_escaped(evalue[1:-1]) + + if override: + os.environ[ename] = evalue + else: + os.environ.setdefault(ename, evalue) + + +def execute(filename, module, install=False, quiet=False, verbose=False, default_env_file=None, default_env=None, env_file=None, env=None): + graph, plugins, services = read(filename, module, install, quiet, verbose, default_env_file, default_env, env_file, env) return bonobo.run(graph, plugins=plugins, services=services) @@ -116,6 +146,9 @@ def register_generic_run_arguments(parser, required=True): source_group = parser.add_mutually_exclusive_group(required=required) source_group.add_argument('filename', nargs='?', type=str) source_group.add_argument('--module', '-m', type=str) + parser.add_argument('--default-env-file', action='append') + parser.add_argument('--default-env', action='append') + parser.add_argument('--env-file', action='append') parser.add_argument('--env', '-e', action='append') return parser diff --git a/bonobo/examples/env_vars/get_passed_env_file.py b/bonobo/examples/env_vars/get_passed_env_file.py new file mode 100644 index 0000000..e7a0952 --- /dev/null +++ b/bonobo/examples/env_vars/get_passed_env_file.py @@ -0,0 +1,22 @@ +import os + +import bonobo + + +def extract(): + my_secret = os.getenv('MY_SECRET') + test_user_password = os.getenv('TEST_USER_PASSWORD') + user = os.getenv('USERNAME') + path = os.getenv('PATH') + + return my_secret, test_user_password, user, path + + +def load(s: str): + print(s) + + +graph = bonobo.Graph(extract, load) + +if __name__ == '__main__': + bonobo.run(graph) diff --git a/tests/test_commands.py b/tests/test_commands.py index a29465c..ce7b582 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -98,6 +98,33 @@ def test_version(runner, capsys): assert __version__ in out +@all_runners +def test_run_module_with_default_env_file(runner, capsys): + runner( + 'run', '--quiet', get_examples_path('env_vars/get_passed_env_file.py') + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'sweetpassword' + assert out[2] != 'not_cwandrews_123' + assert out[3] != 'marzo' + + +# @all_runners +# def test_run_with_env_file(runner, capsys): +# runner( +# 'run', '--quiet', +# get_examples_path('env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123', '--env', +# 'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'" +# ) +# out, err = capsys.readouterr() +# out = out.split('\n') +# assert out[0] == 'cwandrews' +# assert out[1] == '123' +# assert out[2] == 'my_test_string' + + @all_runners def test_run_with_env(runner, capsys): runner( From e469ba30ba66f743e52b9b52f70d827087b77250 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 8 Oct 2017 21:13:51 -0400 Subject: [PATCH 004/145] Updated requirements.txt and requirements-dev.txt to include python-dotenv and dependencies. --- requirements-dev.txt | 1 + requirements.txt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4e005a7..2fa4ef8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -23,6 +23,7 @@ pytest-sugar==0.8.0 pytest-timeout==1.2.0 pytest==3.2.3 python-dateutil==2.6.1 +python-dotenv==0.7.1 pytz==2017.2 requests==2.18.4 six==1.11.0 diff --git a/requirements.txt b/requirements.txt index d6439df..9e604ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ appdirs==1.4.3 certifi==2017.7.27.1 chardet==3.0.4 +click==6.7 colorama==0.3.9 fs==2.0.11 idna==2.6 @@ -9,6 +10,7 @@ packaging==16.8 pbr==3.1.1 psutil==5.3.1 pyparsing==2.2.0 +python-dotenv==0.7.1 pytz==2017.2 requests==2.18.4 six==1.11.0 From af15647ab4832a4442aa6ba4f5470ddc97631a09 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Tue, 10 Oct 2017 22:54:31 -0400 Subject: [PATCH 005/145] Added tests for running file with combinations of multiple default env files, env files, and env vars. Also reorganized environment directory in examples. --- bonobo/commands/run.py | 6 - bonobo/examples/environment/env_files/.env2 | 2 + .../env_files}/__init__.py | 0 .../env_files}/get_passed_env_file.py | 3 +- .../examples/environment/env_vars/__init__.py | 0 .../env_vars/get_passed_env.py | 0 tests/test_commands.py | 104 ++++++++++++++---- 7 files changed, 83 insertions(+), 32 deletions(-) create mode 100644 bonobo/examples/environment/env_files/.env2 rename bonobo/examples/{env_vars => environment/env_files}/__init__.py (100%) rename bonobo/examples/{env_vars => environment/env_files}/get_passed_env_file.py (77%) create mode 100644 bonobo/examples/environment/env_vars/__init__.py rename bonobo/examples/{ => environment}/env_vars/get_passed_env.py (100%) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 59190be..033e17c 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -83,12 +83,6 @@ def read(filename, module, install=False, quiet=False, verbose=False, default_en for f in default_env_file: env_file_path = env_dir.joinpath(f) load_dotenv(env_file_path) - else: - try: - env_file_path = env_dir.joinpath('.env') - load_dotenv(env_file_path) - except FileNotFoundError: - pass if default_env: for e in default_env: diff --git a/bonobo/examples/environment/env_files/.env2 b/bonobo/examples/environment/env_files/.env2 new file mode 100644 index 0000000..1b91848 --- /dev/null +++ b/bonobo/examples/environment/env_files/.env2 @@ -0,0 +1,2 @@ +TEST_USER_PASSWORD='not_sweet_password' +PATH='abril' \ No newline at end of file diff --git a/bonobo/examples/env_vars/__init__.py b/bonobo/examples/environment/env_files/__init__.py similarity index 100% rename from bonobo/examples/env_vars/__init__.py rename to bonobo/examples/environment/env_files/__init__.py diff --git a/bonobo/examples/env_vars/get_passed_env_file.py b/bonobo/examples/environment/env_files/get_passed_env_file.py similarity index 77% rename from bonobo/examples/env_vars/get_passed_env_file.py rename to bonobo/examples/environment/env_files/get_passed_env_file.py index e7a0952..bb83e67 100644 --- a/bonobo/examples/env_vars/get_passed_env_file.py +++ b/bonobo/examples/environment/env_files/get_passed_env_file.py @@ -6,10 +6,9 @@ import bonobo def extract(): my_secret = os.getenv('MY_SECRET') test_user_password = os.getenv('TEST_USER_PASSWORD') - user = os.getenv('USERNAME') path = os.getenv('PATH') - return my_secret, test_user_password, user, path + return my_secret, test_user_password, path def load(s: str): diff --git a/bonobo/examples/environment/env_vars/__init__.py b/bonobo/examples/environment/env_vars/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bonobo/examples/env_vars/get_passed_env.py b/bonobo/examples/environment/env_vars/get_passed_env.py similarity index 100% rename from bonobo/examples/env_vars/get_passed_env.py rename to bonobo/examples/environment/env_vars/get_passed_env.py diff --git a/tests/test_commands.py b/tests/test_commands.py index ce7b582..fbede11 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -99,38 +99,92 @@ def test_version(runner, capsys): @all_runners -def test_run_module_with_default_env_file(runner, capsys): +def test_run_file_with_default_env_file(runner, capsys): runner( - 'run', '--quiet', get_examples_path('env_vars/get_passed_env_file.py') + 'run', '--quiet', '--default-env-file', '.env', + get_examples_path('environment/env_files/get_passed_env_file.py') ) out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '321' assert out[1] == 'sweetpassword' - assert out[2] != 'not_cwandrews_123' - assert out[3] != 'marzo' - - -# @all_runners -# def test_run_with_env_file(runner, capsys): -# runner( -# 'run', '--quiet', -# get_examples_path('env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123', '--env', -# 'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'" -# ) -# out, err = capsys.readouterr() -# out = out.split('\n') -# assert out[0] == 'cwandrews' -# assert out[1] == '123' -# assert out[2] == 'my_test_string' + assert out[2] != 'marzo' @all_runners -def test_run_with_env(runner, capsys): +def test_run_file_with_multiple_default_env_files(runner, capsys): + runner( + 'run', '--quiet', '--default-env-file', '.env', + '--default-env-file', '.env2', + get_examples_path('environment/env_files/get_passed_env_file.py') + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'sweetpassword' + assert out[2] != 'marzo' + + +@all_runners +def test_run_file_with_env_file(runner, capsys): + runner( + 'run', '--quiet', '--env-file', '.env', + get_examples_path('environment/env_files/get_passed_env_file.py') + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'sweetpassword' + assert out[2] == 'marzo' + + +@all_runners +def test_run_file_with_multiple_env_files(runner, capsys): + runner( + 'run', '--quiet', '--env-file', '.env', '--env-file', '.env2', + get_examples_path('environment/env_files/get_passed_env_file.py') + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'not_sweet_password' + assert out[2] == 'abril' + + +@all_runners +def test_run_file_with_default_env_file_and_env_file(runner, capsys): + runner( + 'run', '--quiet', '--default-env-file', '.env', '--env-file', '.env2', + get_examples_path('environment/env_files/get_passed_env_file.py') + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'not_sweet_password' + assert out[2] == 'abril' + + +@all_runners +def test_run_file_with_default_env_file_and_env_file_and_env_vars(runner, capsys): + runner( + 'run', '--quiet', '--default-env-file', '.env', '--env-file', '.env2', + '--env', 'TEST_USER_PASSWORD=SWEETpass', '--env', 'MY_SECRET=444', + get_examples_path('environment/env_files/get_passed_env_file.py') + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '444' + assert out[1] == 'SWEETpass' + assert out[2] == 'abril' + + +@all_runners +def test_run_file_with_env_vars(runner, capsys): runner( 'run', '--quiet', - get_examples_path('env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123', '--env', - 'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'" + get_examples_path('environment/env_vars/get_passed_env.py'), + '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', + '--env', "ENV_TEST_STRING='my_test_string'" ) out, err = capsys.readouterr() out = out.split('\n') @@ -140,10 +194,12 @@ def test_run_with_env(runner, capsys): @all_runners -def test_run_module_with_env(runner, capsys): +def test_run_module_with_env_vars(runner, capsys): runner( - 'run', '--quiet', '-m', 'bonobo.examples.env_vars.get_passed_env', '--env', 'ENV_TEST_NUMBER=123', '--env', - 'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'" + 'run', '--quiet', '-m', + 'bonobo.examples.environment.env_vars.get_passed_env', + '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', + '--env', "ENV_TEST_STRING='my_test_string'" ) out, err = capsys.readouterr() out = out.split('\n') From 1aada995965f1b2738837246a8be4bb391d5740a Mon Sep 17 00:00:00 2001 From: cwandrews Date: Tue, 10 Oct 2017 22:56:58 -0400 Subject: [PATCH 006/145] Small adjustment to test parameters. --- tests/test_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index fbede11..92c279a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -168,13 +168,13 @@ def test_run_file_with_default_env_file_and_env_file(runner, capsys): def test_run_file_with_default_env_file_and_env_file_and_env_vars(runner, capsys): runner( 'run', '--quiet', '--default-env-file', '.env', '--env-file', '.env2', - '--env', 'TEST_USER_PASSWORD=SWEETpass', '--env', 'MY_SECRET=444', + '--env', 'TEST_USER_PASSWORD=SWEETpassWORD', '--env', 'MY_SECRET=444', get_examples_path('environment/env_files/get_passed_env_file.py') ) out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '444' - assert out[1] == 'SWEETpass' + assert out[1] == 'SWEETpassWORD' assert out[2] == 'abril' From 489d22cbcb0e33609fff246484d3d27a51459d70 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Wed, 11 Oct 2017 20:49:57 -0400 Subject: [PATCH 007/145] Moved default-env-file tests to class. --- tests/test_commands.py | 73 +++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 92c279a..adb0317 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -98,31 +98,58 @@ def test_version(runner, capsys): assert __version__ in out -@all_runners -def test_run_file_with_default_env_file(runner, capsys): - runner( - 'run', '--quiet', '--default-env-file', '.env', - get_examples_path('environment/env_files/get_passed_env_file.py') - ) - out, err = capsys.readouterr() - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'sweetpassword' - assert out[2] != 'marzo' +class TestDefaultEnvFile(object): + @all_runners + def test_run_file_with_default_env_file(self, runner, capsys): + runner( + 'run', '--quiet', '--default-env-file', '.env', + get_examples_path('environment/env_files/get_passed_env_file.py') + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'sweetpassword' + assert out[2] != 'marzo' -@all_runners -def test_run_file_with_multiple_default_env_files(runner, capsys): - runner( - 'run', '--quiet', '--default-env-file', '.env', - '--default-env-file', '.env2', - get_examples_path('environment/env_files/get_passed_env_file.py') - ) - out, err = capsys.readouterr() - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'sweetpassword' - assert out[2] != 'marzo' + @all_runners + def test_run_file_with_multiple_default_env_files(self, runner, capsys): + runner( + 'run', '--quiet', '--default-env-file', '.env', + '--default-env-file', '.env2', + get_examples_path('environment/env_files/get_passed_env_file.py') + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'sweetpassword' + assert out[2] != 'marzo' + + @all_runners + def test_run_module_with_default_env_file(self, runner, capsys): + runner( + 'run', '--quiet', '-m', + 'bonobo.examples.environment.env_files.get_passed_env_file', + '--default-env-file', '.env' + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'sweetpassword' + assert out[2] != 'marzo' + + @all_runners + def test_run_module_with_multiple_default_env_files(self, runner, capsys): + runner( + 'run', '--quiet', '-m', + 'bonobo.examples.environment.env_files.get_passed_env_file', + '--default-env-file', '.env', '--default-env-file', '.env2', + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'sweetpassword' + assert out[2] != 'marzo' @all_runners From 53f6cc055fb9bbc1cb15f393ef0cec434101eb82 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Wed, 11 Oct 2017 21:49:39 -0400 Subject: [PATCH 008/145] Fixed bug involved in finding env when running module. --- bonobo/commands/run.py | 4 +- bonobo/examples/environment/env_files/.env2 | 2 - .../examples/environment/env_files/.env_two | 2 + tests/test_commands.py | 87 ++++++++++++------- 4 files changed, 59 insertions(+), 36 deletions(-) delete mode 100644 bonobo/examples/environment/env_files/.env2 create mode 100644 bonobo/examples/environment/env_files/.env_two diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 033e17c..7b23b9d 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -71,14 +71,14 @@ def read(filename, module, install=False, quiet=False, verbose=False, default_en requirements = os.path.join(os.path.dirname(filename), 'requirements.txt') _install_requirements(requirements) context = runpy.run_path(filename, run_name='__bonobo__') - env_dir = Path(filename).parent elif module: context = runpy.run_module(module, run_name='__bonobo__') filename = context['__file__'] - env_dir = Path(module) else: raise RuntimeError('UNEXPECTED: argparse should not allow this.') + env_dir = Path(filename).parent or Path(module).parent + if default_env_file: for f in default_env_file: env_file_path = env_dir.joinpath(f) diff --git a/bonobo/examples/environment/env_files/.env2 b/bonobo/examples/environment/env_files/.env2 deleted file mode 100644 index 1b91848..0000000 --- a/bonobo/examples/environment/env_files/.env2 +++ /dev/null @@ -1,2 +0,0 @@ -TEST_USER_PASSWORD='not_sweet_password' -PATH='abril' \ No newline at end of file diff --git a/bonobo/examples/environment/env_files/.env_two b/bonobo/examples/environment/env_files/.env_two new file mode 100644 index 0000000..672d6d2 --- /dev/null +++ b/bonobo/examples/environment/env_files/.env_two @@ -0,0 +1,2 @@ +TEST_USER_PASSWORD=not_sweet_password +PATH='abril' \ No newline at end of file diff --git a/tests/test_commands.py b/tests/test_commands.py index adb0317..59f3db8 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -98,9 +98,8 @@ def test_version(runner, capsys): assert __version__ in out +@all_runners class TestDefaultEnvFile(object): - - @all_runners def test_run_file_with_default_env_file(self, runner, capsys): runner( 'run', '--quiet', '--default-env-file', '.env', @@ -112,11 +111,10 @@ class TestDefaultEnvFile(object): assert out[1] == 'sweetpassword' assert out[2] != 'marzo' - @all_runners def test_run_file_with_multiple_default_env_files(self, runner, capsys): runner( 'run', '--quiet', '--default-env-file', '.env', - '--default-env-file', '.env2', + '--default-env-file', '.env_two', get_examples_path('environment/env_files/get_passed_env_file.py') ) out, err = capsys.readouterr() @@ -125,7 +123,6 @@ class TestDefaultEnvFile(object): assert out[1] == 'sweetpassword' assert out[2] != 'marzo' - @all_runners def test_run_module_with_default_env_file(self, runner, capsys): runner( 'run', '--quiet', '-m', @@ -138,12 +135,11 @@ class TestDefaultEnvFile(object): assert out[1] == 'sweetpassword' assert out[2] != 'marzo' - @all_runners def test_run_module_with_multiple_default_env_files(self, runner, capsys): runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', - '--default-env-file', '.env', '--default-env-file', '.env2', + '--default-env-file', '.env', '--default-env-file', '.env_two', ) out, err = capsys.readouterr() out = out.split('\n') @@ -153,36 +149,62 @@ class TestDefaultEnvFile(object): @all_runners -def test_run_file_with_env_file(runner, capsys): - runner( - 'run', '--quiet', '--env-file', '.env', - get_examples_path('environment/env_files/get_passed_env_file.py') - ) - out, err = capsys.readouterr() - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'sweetpassword' - assert out[2] == 'marzo' +class TestEnvFile(object): + def test_run_file_with_file(self, runner, capsys): + runner( + 'run', '--quiet', + get_examples_path('environment/env_files/get_passed_env_file.py'), + '--env-file', '.env', + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'sweetpassword' + assert out[2] == 'marzo' + def test_run_file_with_multiple_files(self, runner, capsys): + runner( + 'run', '--quiet', + get_examples_path('environment/env_files/get_passed_env_file.py'), + '--env-file', '.env', '--env-file', '.env_two', + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'not_sweet_password' + assert out[2] == 'abril' -@all_runners -def test_run_file_with_multiple_env_files(runner, capsys): - runner( - 'run', '--quiet', '--env-file', '.env', '--env-file', '.env2', - get_examples_path('environment/env_files/get_passed_env_file.py') - ) - out, err = capsys.readouterr() - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'not_sweet_password' - assert out[2] == 'abril' + def test_run_module_with_file(self, runner, capsys): + runner( + 'run', '--quiet', '-m', + 'bonobo.examples.environment.env_files.get_passed_env_file', + '--env-file', '.env', + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'sweetpassword' + assert out[2] == 'marzo' + + def test_run_module_with_multiple_files(self, runner, capsys): + runner( + 'run', '--quiet', '-m', + 'bonobo.examples.environment.env_files.get_passed_env_file', + '--env-file', '.env', '--env-file', '.env_two', + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'not_sweet_password' + assert out[2] == 'abril' @all_runners def test_run_file_with_default_env_file_and_env_file(runner, capsys): runner( - 'run', '--quiet', '--default-env-file', '.env', '--env-file', '.env2', - get_examples_path('environment/env_files/get_passed_env_file.py') + 'run', '--quiet', + get_examples_path('environment/env_files/get_passed_env_file.py'), + '--default-env-file', '.env', '--env-file', '.env_two', ) out, err = capsys.readouterr() out = out.split('\n') @@ -194,9 +216,10 @@ def test_run_file_with_default_env_file_and_env_file(runner, capsys): @all_runners def test_run_file_with_default_env_file_and_env_file_and_env_vars(runner, capsys): runner( - 'run', '--quiet', '--default-env-file', '.env', '--env-file', '.env2', + 'run', '--quiet', + get_examples_path('environment/env_files/get_passed_env_file.py'), + '--default-env-file', '.env', '--env-file', '.env_two', '--env', 'TEST_USER_PASSWORD=SWEETpassWORD', '--env', 'MY_SECRET=444', - get_examples_path('environment/env_files/get_passed_env_file.py') ) out, err = capsys.readouterr() out = out.split('\n') From fe4964b9c7da08d52e81630ad94a2709e5abe159 Mon Sep 17 00:00:00 2001 From: mouadhkaabachi Date: Thu, 12 Oct 2017 17:46:28 +0200 Subject: [PATCH 009/145] comparison to None|True|False should be 'if cond is None:' --- tests/config/test_configurables.py | 10 +++++----- tests/test_settings.py | 6 +++--- tests/util/test_iterators.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/config/test_configurables.py b/tests/config/test_configurables.py index e1555d7..bee501e 100644 --- a/tests/config/test_configurables.py +++ b/tests/config/test_configurables.py @@ -67,7 +67,7 @@ def test_defaults(): assert o.required_str == 'hello' assert o.default_str == 'foo' - assert o.integer == None + assert o.integer is None def test_str_type_factory(): @@ -78,7 +78,7 @@ def test_str_type_factory(): assert o.required_str == '42' assert o.default_str == 'foo' - assert o.integer == None + assert o.integer is None def test_int_type_factory(): @@ -100,8 +100,8 @@ def test_bool_type_factory(): assert o.required_str == 'yes' assert o.default_str == 'foo' - assert o.integer == None - assert o.also_required == True + assert o.integer is None + assert o.also_required is True def test_option_resolution_order(): @@ -112,7 +112,7 @@ def test_option_resolution_order(): assert o.required_str == 'kaboom' assert o.default_str == 'foo' - assert o.integer == None + assert o.integer is None def test_option_positional(): diff --git a/tests/test_settings.py b/tests/test_settings.py index c8313c5..5771dc4 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -42,9 +42,9 @@ def test_setting(): def test_default_settings(): settings.clear_all() - assert settings.DEBUG.get() == False - assert settings.PROFILE.get() == False - assert settings.QUIET.get() == False + assert settings.DEBUG.get() is False + assert settings.PROFILE.get() is False + assert settings.QUIET.get() is False assert settings.LOGGING_LEVEL.get() == logging._checkLevel('INFO') with patch.dict(environ, {'DEBUG': 't'}): diff --git a/tests/util/test_iterators.py b/tests/util/test_iterators.py index 22b34d7..3d0249e 100644 --- a/tests/util/test_iterators.py +++ b/tests/util/test_iterators.py @@ -18,5 +18,5 @@ def test_force_iterator_with_generator(): yield 'ccc' iterator = force_iterator(generator()) - assert type(iterator) == types.GeneratorType + assert isinstance(iterator, types.GeneratorType) assert list(iterator) == ['aaa', 'bbb', 'ccc'] From 2898902ebde81500c5dccc6d2cda259b97312413 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 12 Oct 2017 19:01:35 +0200 Subject: [PATCH 010/145] [cli] adds ability to override reader/writer options from cli convert. --- bonobo/commands/convert.py | 64 ++++++++++++++++++++++++------- bonobo/commands/util/__init__.py | 0 bonobo/commands/util/arguments.py | 26 +++++++++++++ bonobo/nodes/basics.py | 15 +++++++- bonobo/nodes/io/base.py | 1 + 5 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 bonobo/commands/util/__init__.py create mode 100644 bonobo/commands/util/arguments.py diff --git a/bonobo/commands/convert.py b/bonobo/commands/convert.py index 17b98c2..f02e8ab 100644 --- a/bonobo/commands/convert.py +++ b/bonobo/commands/convert.py @@ -1,7 +1,10 @@ import mimetypes import os +import re + import bonobo +from bonobo.commands.util.arguments import parse_variable_argument SHORTCUTS = { 'csv': 'text/csv', @@ -23,7 +26,7 @@ READER = 'reader' WRITER = 'writer' -def resolve_factory(name, filename, factory_type): +def resolve_factory(name, filename, factory_type, options=None): """ Try to resolve which transformation factory to use for this filename. User eventually provided a name, which has priority, otherwise we try to detect it using the mimetype detection on filename. @@ -42,6 +45,12 @@ def resolve_factory(name, filename, factory_type): if _ext in SHORTCUTS: name = SHORTCUTS[_ext] + if options: + options = dict(map(parse_variable_argument, options)) + + else: + options = dict() + if not name in REGISTRY: raise RuntimeError( 'Could not resolve {factory_type} factory for {filename} ({name}). Try providing it explicitely using -{opt} .'. @@ -49,19 +58,22 @@ def resolve_factory(name, filename, factory_type): ) if factory_type == READER: - return REGISTRY[name][0] + return REGISTRY[name][0], options elif factory_type == WRITER: - return REGISTRY[name][1] + return REGISTRY[name][1], options else: raise ValueError('Invalid factory type.') def execute(input, output, reader=None, reader_options=None, writer=None, writer_options=None, options=None): - reader = resolve_factory(reader, input, READER)(input) - writer = resolve_factory(writer, output, WRITER)(output) + reader_factory, reader_options = resolve_factory(reader, input, READER, (options or []) + (reader_options or [])) + writer_factory, writer_options = resolve_factory(writer, output, WRITER, (options or []) + (writer_options or [])) graph = bonobo.Graph() - graph.add_chain(reader, writer) + graph.add_chain( + reader_factory(input, **reader_options), + writer_factory(output, **writer_options), + ) return bonobo.run( graph, services={ @@ -71,11 +83,37 @@ def execute(input, output, reader=None, reader_options=None, writer=None, writer def register(parser): - parser.add_argument('input') - parser.add_argument('output') - parser.add_argument('--' + READER, '-r') - parser.add_argument('--' + WRITER, '-w') - # parser.add_argument('--reader-option', '-ro', dest='reader_options') - # parser.add_argument('--writer-option', '-wo', dest='writer_options') - # parser.add_argument('--option', '-o', dest='options') + parser.add_argument('input', help='Input filename.') + parser.add_argument('output', help='Output filename.') + parser.add_argument( + '--' + READER, + '-r', + help='Choose the reader factory if it cannot be detected from extension, or if detection is wrong.' + ) + parser.add_argument( + '--' + WRITER, + '-w', + help='Choose the writer factory if it cannot be detected from extension, or if detection is wrong.' + ) + parser.add_argument( + '--option', + '-O', + dest='options', + action='append', + help='Add a named option to both reader and writer factories (i.e. foo="bar").' + ) + parser.add_argument( + '--' + READER + '-option', + '-' + READER[0].upper(), + dest=READER + '_options', + action='append', + help='Add a named option to the reader factory.' + ) + parser.add_argument( + '--' + WRITER + '-option', + '-' + WRITER[0].upper(), + dest=WRITER + '_options', + action='append', + help='Add a named option to the writer factory.' + ) return execute diff --git a/bonobo/commands/util/__init__.py b/bonobo/commands/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bonobo/commands/util/arguments.py b/bonobo/commands/util/arguments.py new file mode 100644 index 0000000..435c6f5 --- /dev/null +++ b/bonobo/commands/util/arguments.py @@ -0,0 +1,26 @@ +import json + + +def parse_variable_argument(arg): + try: + key, val = arg.split('=', 1) + except ValueError: + return arg, True + + try: + val = json.loads(val) + except json.JSONDecodeError: + pass + + return key, val + + +def test_parse_variable_argument(): + assert parse_variable_argument('foo=bar') == ('foo', 'bar') + assert parse_variable_argument('foo="bar"') == ('foo', 'bar') + assert parse_variable_argument('sep=";"') == ('sep', ';') + assert parse_variable_argument('foo') == ('foo', True) + + +if __name__ == '__main__': + test_parse_var() diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index e23dd05..b346404 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -70,7 +70,17 @@ def _count_counter(self, context): context.send(Bag(counter._value)) +def _shorten(s, w): + if w and len(s) > w: + s = s[0:w - 3] + '...' + return s + + class PrettyPrinter(Configurable): + max_width = Option(int, 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 @@ -82,7 +92,10 @@ class PrettyPrinter(Configurable): def _format_console(self, i, item, value): return ' '.join( - ((' ' if i else '•'), str(item), '=', str(value).strip().replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL) + ( + (' ' if i else '•'), str(item), '=', _shorten(str(value).strip(), + self.max_width).replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL + ) ) diff --git a/bonobo/nodes/io/base.py b/bonobo/nodes/io/base.py index 496a0e8..3cecb70 100644 --- a/bonobo/nodes/io/base.py +++ b/bonobo/nodes/io/base.py @@ -50,6 +50,7 @@ class FileHandler(Configurable): eol = Option(str, default='\n') # type: str mode = Option(str) # type: str encoding = Option(str, default='utf-8') # type: str + fs = Service('fs') # type: str @ContextProcessor From 99351a638bf664c4719569bbdab567496d6d431d Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 12 Oct 2017 19:01:47 +0200 Subject: [PATCH 011/145] [doc] formating --- docs/_templates/alabaster/__init__.py | 4 +- docs/_templates/alabaster/support.py | 130 ++++++++++++-------------- 2 files changed, 63 insertions(+), 71 deletions(-) diff --git a/docs/_templates/alabaster/__init__.py b/docs/_templates/alabaster/__init__.py index 39f1407..a4bebf2 100644 --- a/docs/_templates/alabaster/__init__.py +++ b/docs/_templates/alabaster/__init__.py @@ -14,11 +14,11 @@ def get_path(): def update_context(app, pagename, templatename, context, doctree): context['alabaster_version'] = version.__version__ + def setup(app): # add_html_theme is new in Sphinx 1.6+ if hasattr(app, 'add_html_theme'): theme_path = os.path.abspath(os.path.dirname(__file__)) app.add_html_theme('alabaster', theme_path) app.connect('html-page-context', update_context) - return {'version': version.__version__, - 'parallel_read_safe': True} + return {'version': version.__version__, 'parallel_read_safe': True} diff --git a/docs/_templates/alabaster/support.py b/docs/_templates/alabaster/support.py index 0f3aa8c..9147b52 100644 --- a/docs/_templates/alabaster/support.py +++ b/docs/_templates/alabaster/support.py @@ -7,82 +7,74 @@ from pygments.token import Keyword, Name, Comment, String, Error, \ # Originally based on FlaskyStyle which was based on 'tango'. class Alabaster(Style): - background_color = "#f8f8f8" # doesn't seem to override CSS 'pre' styling? + background_color = "#f8f8f8" # doesn't seem to override CSS 'pre' styling? default_style = "" styles = { # No corresponding class for the following: #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' + Whitespace: "underline #f8f8f8", # class: 'w' + Error: "#a40000 border:#ef2929", # class: 'err' + Other: "#000000", # class 'x' + Comment: "italic #8f5902", # class: 'c' + Comment.Preproc: "noitalic", # class: 'cp' + Keyword: "bold #004461", # class: 'k' + Keyword.Constant: "bold #004461", # class: 'kc' + Keyword.Declaration: "bold #004461", # class: 'kd' + Keyword.Namespace: "bold #004461", # class: 'kn' + Keyword.Pseudo: "bold #004461", # class: 'kp' + Keyword.Reserved: "bold #004461", # class: 'kr' + Keyword.Type: "bold #004461", # class: 'kt' + Operator: "#582800", # class: 'o' + Operator.Word: "bold #004461", # class: 'ow' - like keywords + Punctuation: "bold #000000", # class: 'p' # because special names such as Name.Class, Name.Function, etc. # are not recognized as such later in the parsing, we choose them # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' + Name: "#000000", # class: 'n' + Name.Attribute: "#c4a000", # class: 'na' - to be revised + Name.Builtin: "#004461", # class: 'nb' + Name.Builtin.Pseudo: "#3465a4", # class: 'bp' + Name.Class: "#000000", # class: 'nc' - to be revised + Name.Constant: "#000000", # class: 'no' - to be revised + Name.Decorator: "#888", # class: 'nd' - to be revised + Name.Entity: "#ce5c00", # class: 'ni' + Name.Exception: "bold #cc0000", # class: 'ne' + Name.Function: "#000000", # class: 'nf' + Name.Property: "#000000", # class: 'py' + Name.Label: "#f57900", # class: 'nl' + Name.Namespace: "#000000", # class: 'nn' - to be revised + Name.Other: "#000000", # class: 'nx' + Name.Tag: "bold #004461", # class: 'nt' - like a keyword + Name.Variable: "#000000", # class: 'nv' - to be revised + Name.Variable.Class: "#000000", # class: 'vc' - to be revised + Name.Variable.Global: "#000000", # class: 'vg' - to be revised + Name.Variable.Instance: "#000000", # class: 'vi' - to be revised + Number: "#990000", # class: 'm' + Literal: "#000000", # class: 'l' + Literal.Date: "#000000", # class: 'ld' + String: "#4e9a06", # class: 's' + String.Backtick: "#4e9a06", # class: 'sb' + String.Char: "#4e9a06", # class: 'sc' + String.Doc: "italic #8f5902", # class: 'sd' - like a comment + String.Double: "#4e9a06", # class: 's2' + String.Escape: "#4e9a06", # class: 'se' + String.Heredoc: "#4e9a06", # class: 'sh' + String.Interpol: "#4e9a06", # class: 'si' + String.Other: "#4e9a06", # class: 'sx' + String.Regex: "#4e9a06", # class: 'sr' + String.Single: "#4e9a06", # class: 's1' + String.Symbol: "#4e9a06", # class: 'ss' + Generic: "#000000", # class: 'g' + Generic.Deleted: "#a40000", # class: 'gd' + Generic.Emph: "italic #000000", # class: 'ge' + Generic.Error: "#ef2929", # class: 'gr' + Generic.Heading: "bold #000080", # class: 'gh' + Generic.Inserted: "#00A000", # class: 'gi' + Generic.Output: "#888", # class: 'go' + Generic.Prompt: "#745334", # class: 'gp' + Generic.Strong: "bold #000000", # class: 'gs' + Generic.Subheading: "bold #800080", # class: 'gu' + Generic.Traceback: "bold #a40000", # class: 'gt' } From 721ed499bbe028c8984925fb075845feef445447 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 12 Oct 2017 19:02:11 +0200 Subject: [PATCH 012/145] [config] adds a __doc__ constructor kwarg to set option documentation inline. --- bonobo/config/options.py | 4 +++- bonobo/nodes/basics.py | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bonobo/config/options.py b/bonobo/config/options.py index 065cc9d..cad4ca8 100644 --- a/bonobo/config/options.py +++ b/bonobo/config/options.py @@ -53,13 +53,15 @@ class Option: _creation_counter = 0 - def __init__(self, type=None, *, required=True, positional=False, default=None): + def __init__(self, type=None, *, required=True, positional=False, default=None, __doc__=None): self.name = None self.type = type self.required = required if default is None else False self.positional = positional self.default = default + self.__doc__ = __doc__ or self.__doc__ + # This hack is necessary for python3.5 self._creation_counter = Option._creation_counter Option._creation_counter += 1 diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index b346404..fa74e40 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -77,9 +77,13 @@ def _shorten(s, w): class PrettyPrinter(Configurable): - max_width = Option(int, required=False, __doc__=''' + max_width = Option( + int, + 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 From b1d7498054da969b81215ab851c7fb25a5575f87 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 12 Oct 2017 19:12:10 +0200 Subject: [PATCH 013/145] [cli] convert, remove useless import. --- bonobo/commands/convert.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bonobo/commands/convert.py b/bonobo/commands/convert.py index f02e8ab..05d3504 100644 --- a/bonobo/commands/convert.py +++ b/bonobo/commands/convert.py @@ -1,8 +1,6 @@ import mimetypes import os -import re - import bonobo from bonobo.commands.util.arguments import parse_variable_argument @@ -47,14 +45,13 @@ def resolve_factory(name, filename, factory_type, options=None): if options: options = dict(map(parse_variable_argument, options)) - else: options = dict() if not name in REGISTRY: raise RuntimeError( 'Could not resolve {factory_type} factory for {filename} ({name}). Try providing it explicitely using -{opt} .'. - format(name=name, filename=filename, factory_type=factory_type, opt=factory_type[0]) + format(name=name, filename=filename, factory_type=factory_type, opt=factory_type[0]) ) if factory_type == READER: From 64875a05bd8cf5bcf64c443269d5dd35d8ee07a1 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Fri, 13 Oct 2017 17:21:25 +0200 Subject: [PATCH 014/145] [cli] Adds a --filter option to "convert" command, allowing to use arbitrary filters to a command line conversion. Also adds --print and "-" output to pretty print to terminal instead of file output. --- bonobo/commands/convert.py | 69 +++++++++++++++++++++++++++++++------- bonobo/util/python.py | 13 +++++-- 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/bonobo/commands/convert.py b/bonobo/commands/convert.py index 05d3504..01441e4 100644 --- a/bonobo/commands/convert.py +++ b/bonobo/commands/convert.py @@ -3,6 +3,9 @@ import os import bonobo from bonobo.commands.util.arguments import parse_variable_argument +from bonobo.util import require +from bonobo.util.iterators import tuplize +from bonobo.util.python import WorkingDirectoryModulesRegistry SHORTCUTS = { 'csv': 'text/csv', @@ -51,7 +54,7 @@ def resolve_factory(name, filename, factory_type, options=None): if not name in REGISTRY: raise RuntimeError( 'Could not resolve {factory_type} factory for {filename} ({name}). Try providing it explicitely using -{opt} .'. - format(name=name, filename=filename, factory_type=factory_type, opt=factory_type[0]) + format(name=name, filename=filename, factory_type=factory_type, opt=factory_type[0]) ) if factory_type == READER: @@ -62,14 +65,42 @@ def resolve_factory(name, filename, factory_type, options=None): raise ValueError('Invalid factory type.') -def execute(input, output, reader=None, reader_options=None, writer=None, writer_options=None, options=None): - reader_factory, reader_options = resolve_factory(reader, input, READER, (options or []) + (reader_options or [])) - writer_factory, writer_options = resolve_factory(writer, output, WRITER, (options or []) + (writer_options or [])) +@tuplize +def resolve_filters(filters): + registry = WorkingDirectoryModulesRegistry() + for f in filters: + try: + mod, attr = f.split(':', 1) + yield getattr(registry.require(mod), attr) + except ValueError: + yield getattr(bonobo, f) + + +def execute( + input, + output, + reader=None, + reader_option=None, + writer=None, + writer_option=None, + option=None, + filter=None, + do_print=False +): + reader_factory, reader_option = resolve_factory(reader, input, READER, (option or []) + (reader_option or [])) + + if output == '-': + writer_factory, writer_option = bonobo.PrettyPrinter, {} + else: + writer_factory, writer_option = resolve_factory(writer, output, WRITER, (option or []) + (writer_option or [])) + + filters = resolve_filters(filter) graph = bonobo.Graph() graph.add_chain( - reader_factory(input, **reader_options), - writer_factory(output, **writer_options), + reader_factory(input, **reader_option), + *filters, + writer_factory(output, **writer_option), ) return bonobo.run( @@ -92,25 +123,39 @@ def register(parser): '-w', help='Choose the writer factory if it cannot be detected from extension, or if detection is wrong.' ) + parser.add_argument( + '--filter', + '-f', + dest='filter', + action='append', + help='Add a filter between input and output', + ) + parser.add_argument( + '--print', + '-p', + dest='do_print', + action='store_true', + help='Replace the output by a pretty printer.', + ) parser.add_argument( '--option', '-O', - dest='options', + dest='option', action='append', - help='Add a named option to both reader and writer factories (i.e. foo="bar").' + help='Add a named option to both reader and writer factories (i.e. foo="bar").', ) parser.add_argument( '--' + READER + '-option', '-' + READER[0].upper(), - dest=READER + '_options', + dest=READER + '_option', action='append', - help='Add a named option to the reader factory.' + help='Add a named option to the reader factory.', ) parser.add_argument( '--' + WRITER + '-option', '-' + WRITER[0].upper(), - dest=WRITER + '_options', + dest=WRITER + '_option', action='append', - help='Add a named option to the writer factory.' + help='Add a named option to the writer factory.', ) return execute diff --git a/bonobo/util/python.py b/bonobo/util/python.py index a496e19..e55c24c 100644 --- a/bonobo/util/python.py +++ b/bonobo/util/python.py @@ -9,14 +9,23 @@ class _RequiredModule: class _RequiredModulesRegistry(dict): + @property + def pathname(self): + return os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.stack()[1][0]))) + def require(self, name): if name not in self: bits = name.split('.') - pathname = os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.stack()[1][0]))) - filename = os.path.join(pathname, *bits[:-1], bits[-1] + '.py') + filename = os.path.join(self.pathname, *bits[:-1], bits[-1] + '.py') self[name] = _RequiredModule(runpy.run_path(filename, run_name=name)) return self[name] +class WorkingDirectoryModulesRegistry(_RequiredModulesRegistry): + @property + def pathname(self): + return os.getcwd() + + registry = _RequiredModulesRegistry() require = registry.require From dc59c88c3d52d51ee8f710a3d1c14ecbaff4e51e Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Fri, 13 Oct 2017 17:25:42 +0200 Subject: [PATCH 015/145] [cli/util] fix requires to use the right stack frame, remove --print as "-" does the job --- bonobo/commands/convert.py | 10 +--------- bonobo/util/python.py | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/bonobo/commands/convert.py b/bonobo/commands/convert.py index 01441e4..2d13ab4 100644 --- a/bonobo/commands/convert.py +++ b/bonobo/commands/convert.py @@ -85,7 +85,6 @@ def execute( writer_option=None, option=None, filter=None, - do_print=False ): reader_factory, reader_option = resolve_factory(reader, input, READER, (option or []) + (reader_option or [])) @@ -121,7 +120,7 @@ def register(parser): parser.add_argument( '--' + WRITER, '-w', - help='Choose the writer factory if it cannot be detected from extension, or if detection is wrong.' + help='Choose the writer factory if it cannot be detected from extension, or if detection is wrong (use - for console pretty print).' ) parser.add_argument( '--filter', @@ -130,13 +129,6 @@ def register(parser): action='append', help='Add a filter between input and output', ) - parser.add_argument( - '--print', - '-p', - dest='do_print', - action='store_true', - help='Replace the output by a pretty printer.', - ) parser.add_argument( '--option', '-O', diff --git a/bonobo/util/python.py b/bonobo/util/python.py index e55c24c..8648f16 100644 --- a/bonobo/util/python.py +++ b/bonobo/util/python.py @@ -11,7 +11,7 @@ class _RequiredModule: class _RequiredModulesRegistry(dict): @property def pathname(self): - return os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.stack()[1][0]))) + return os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.stack()[2][0]))) def require(self, name): if name not in self: From f6d78ceeb5e7e020284e99822468ae03b447f2e5 Mon Sep 17 00:00:00 2001 From: arimbr Date: Sun, 15 Oct 2017 16:02:47 +0200 Subject: [PATCH 016/145] Set cookiecutter overwrite_if_exists parameter to True if current directory is empty --- bonobo/commands/init.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index 948f747..9a157ca 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -1,4 +1,6 @@ -def execute(name, branch): +import os + +def execute(name, branch, overwrite_if_exists=False): try: from cookiecutter.main import cookiecutter except ImportError as exc: @@ -6,11 +8,15 @@ def execute(name, branch): 'You must install "cookiecutter" to use this command.\n\n $ pip install cookiecutter\n' ) from exc + if os.listdir(os.getcwd()) == []: + overwrite_if_exists = True + return cookiecutter( 'https://github.com/python-bonobo/cookiecutter-bonobo.git', extra_context={'name': name}, no_input=True, - checkout=branch + checkout=branch, + overwrite_if_exists=overwrite_if_exists ) From 92cc400fe70fc3fcba86ca72ea8f977c9231110e Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 15 Oct 2017 21:37:22 +0200 Subject: [PATCH 017/145] [core] Refactoring IOFormats so there is one and only obvious way to send it. This is the commit where I admit that having more than one input/output format for readers and writers was complicating the code too much for a very small gain, and that it would be easier to only have one way to do it. So such way is now: - Returning (or yielding) a dict if you have key-value type collections. - Returning (or yielding) a tuple if you have a list-type collection. - Returning (or yielding) something else otherwise, which will continue to work like the old "arg0" format. IOFORMAT options has been removed in favour of a RemovedOption, which will complain if you're still trying to set it to anything else than the one value allowed. --- bonobo/commands/convert.py | 3 +- bonobo/config/options.py | 22 ++++++- bonobo/config/processors.py | 12 ++-- bonobo/execution/base.py | 10 ++++ bonobo/execution/graph.py | 44 +++++++++----- bonobo/execution/node.py | 33 +++++++---- bonobo/nodes/io/base.py | 35 ------------ bonobo/nodes/io/csv.py | 13 +++-- bonobo/nodes/io/json.py | 13 +++-- bonobo/nodes/io/pickle.py | 9 +-- bonobo/settings.py | 3 + bonobo/strategies/base.py | 9 ++- bonobo/strategies/executor.py | 69 ++++++++++++---------- bonobo/strategies/naive.py | 18 ++++-- bonobo/structs/bags.py | 24 +++++++- bonobo/util/__init__.py | 7 +++ bonobo/util/inspect.py | 20 +++++++ bonobo/util/iterators.py | 2 +- bonobo/util/testing.py | 45 ++++++++++++--- tests/execution/test_node.py | 104 ++++++++++++++++++++++++++++++++++ tests/io/test_csv.py | 67 ++++------------------ tests/io/test_file.py | 20 +++---- tests/io/test_json.py | 34 ++--------- tests/io/test_pickle.py | 31 ++++------ tests/test_basicusage.py | 1 + tests/test_commands.py | 10 +++- tests/test_execution.py | 38 ++++++------- 27 files changed, 427 insertions(+), 269 deletions(-) create mode 100644 tests/execution/test_node.py diff --git a/bonobo/commands/convert.py b/bonobo/commands/convert.py index 2d13ab4..48caaa3 100644 --- a/bonobo/commands/convert.py +++ b/bonobo/commands/convert.py @@ -120,7 +120,8 @@ def register(parser): parser.add_argument( '--' + WRITER, '-w', - help='Choose the writer factory if it cannot be detected from extension, or if detection is wrong (use - for console pretty print).' + help= + 'Choose the writer factory if it cannot be detected from extension, or if detection is wrong (use - for console pretty print).' ) parser.add_argument( '--filter', diff --git a/bonobo/config/options.py b/bonobo/config/options.py index cad4ca8..3462a31 100644 --- a/bonobo/config/options.py +++ b/bonobo/config/options.py @@ -66,10 +66,10 @@ class Option: self._creation_counter = Option._creation_counter Option._creation_counter += 1 - def __get__(self, inst, typ): + def __get__(self, inst, type_): # XXX If we call this on the type, then either return overriden value or ... ??? if inst is None: - return vars(type).get(self.name, self) + return vars(type_).get(self.name, self) if not self.name in inst._options_values: inst._options_values[self.name] = self.get_default() @@ -96,6 +96,24 @@ class Option: return self.default() if callable(self.default) else self.default +class RemovedOption(Option): + def __init__(self, *args, value, **kwargs): + kwargs['required'] = False + super(RemovedOption, self).__init__(*args, **kwargs) + self.value = value + + def clean(self, value): + if value != self.value: + raise ValueError( + 'Removed options cannot change value, {!r} must now be {!r} (and you should remove setting the value explicitely, as it is deprecated and will be removed quite soon.'. + format(self.name, self.value) + ) + return self.value + + def get_default(self): + return self.value + + class Method(Option): """ A Method is a special callable-valued option, that can be used in three different ways (but for same purpose). diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index 27f8703..73c9949 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -1,9 +1,8 @@ from collections import Iterable from contextlib import contextmanager -from bonobo.config.options import Option -from bonobo.util.compat import deprecated_alias -from bonobo.util.iterators import ensure_tuple +from bonobo.config import Option +from bonobo.util import deprecated_alias, ensure_tuple _CONTEXT_PROCESSORS_ATTR = '__processors__' @@ -24,7 +23,7 @@ class ContextProcessor(Option): Example: >>> from bonobo.config import Configurable - >>> from bonobo.util.objects import ValueHolder + >>> from bonobo.util import ValueHolder >>> class Counter(Configurable): ... @ContextProcessor @@ -91,7 +90,10 @@ class ContextCurrifier: self._stack, self._stack_values = list(), list() for processor in resolve_processors(self.wrapped): _processed = processor(self.wrapped, *context, *self.context) - _append_to_context = next(_processed) + try: + _append_to_context = next(_processed) + except TypeError as exc: + raise TypeError('Context processor should be generators (using yield).') from exc self._stack_values.append(_append_to_context) if _append_to_context is not None: self.context += ensure_tuple(_append_to_context) diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py index abb3516..81ac74e 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/base.py @@ -5,6 +5,7 @@ from time import sleep from bonobo.config import create_container from bonobo.config.processors import ContextCurrifier from bonobo.plugins import get_enhancers +from bonobo.util import inspect_node, isconfigurabletype from bonobo.util.errors import print_error from bonobo.util.objects import Wrapper, get_name @@ -72,6 +73,15 @@ class LoopingExecutionContext(Wrapper): self._started = True self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context()) + 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...' + ) + # XXX enhance that, maybe giving hints on what's missing. + # print(inspect_node(self.wrapped)) + self._stack.setup(self) for enhancer in self._enhancers: diff --git a/bonobo/execution/graph.py b/bonobo/execution/graph.py index 91e4aef..77e01fa 100644 --- a/bonobo/execution/graph.py +++ b/bonobo/execution/graph.py @@ -1,3 +1,4 @@ +import time from functools import partial from bonobo.config import create_container @@ -7,6 +8,9 @@ from bonobo.execution.plugin import PluginExecutionContext class GraphExecutionContext: + NodeExecutionContextType = NodeExecutionContext + PluginExecutionContextType = PluginExecutionContext + @property def started(self): return any(node.started for node in self.nodes) @@ -21,15 +25,17 @@ class GraphExecutionContext: def __init__(self, graph, plugins=None, services=None): self.graph = graph - self.nodes = [NodeExecutionContext(node, parent=self) for node in self.graph] - self.plugins = [PluginExecutionContext(plugin, parent=self) for plugin in plugins or ()] + self.nodes = [self.create_node_execution_context_for(node) for node in self.graph] + self.plugins = [self.create_plugin_execution_context_for(plugin) for plugin in plugins or ()] self.services = create_container(services) # Probably not a good idea to use it unless you really know what you're doing. But you can access the context. self.services['__graph_context'] = self for i, node_context in enumerate(self): - node_context.outputs = [self[j].input for j in self.graph.outputs_of(i)] + 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_finalize = partial(node_context.stop) @@ -43,6 +49,12 @@ class GraphExecutionContext: def __iter__(self): yield from self.nodes + def create_node_execution_context_for(self, node): + return self.NodeExecutionContextType(node, parent=self) + + def create_plugin_execution_context_for(self, plugin): + return self.PluginExecutionContextType(plugin, parent=self) + def write(self, *messages): """Push a list of messages in the inputs of this graph's inputs, matching the output of special node "BEGIN" in our graph.""" @@ -51,17 +63,23 @@ class GraphExecutionContext: for message in messages: self[i].write(message) - def start(self): - # todo use strategy + def start(self, starter=None): for node in self.nodes: - node.start() + if starter is None: + node.start() + else: + starter(node) - def stop(self): - # todo use strategy - for node in self.nodes: - node.stop() + def start_plugins(self, starter=None): + for plugin in self.plugins: + if starter is None: + plugin.start() + else: + starter(plugin) - def loop(self): - # todo use strategy + def stop(self, stopper=None): for node in self.nodes: - node.loop() + if stopper is None: + node.stop() + else: + stopper(node) diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index e8869ac..2aa626c 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -2,15 +2,15 @@ import traceback from queue import Empty from time import sleep -from bonobo.constants import INHERIT_INPUT, NOT_MODIFIED +from bonobo import settings +from bonobo.constants import INHERIT_INPUT, NOT_MODIFIED, BEGIN, END from bonobo.errors import InactiveReadableError, UnrecoverableError from bonobo.execution.base import LoopingExecutionContext from bonobo.structs.bags import Bag from bonobo.structs.inputs import Input +from bonobo.util import get_name, iserrorbag, isloopbackbag, isdict, istuple from bonobo.util.compat import deprecated_alias -from bonobo.util.inspect import iserrorbag, isloopbackbag from bonobo.util.iterators import iter_if_not_sequence -from bonobo.util.objects import get_name from bonobo.util.statistics import WithStatistics @@ -28,12 +28,12 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): def alive_str(self): return '+' if self.alive else '-' - def __init__(self, wrapped, parent=None, services=None): + def __init__(self, wrapped, parent=None, services=None, _input=None, _outputs=None): LoopingExecutionContext.__init__(self, wrapped, parent=parent, services=services) WithStatistics.__init__(self, 'in', 'out', 'err') - self.input = Input() - self.outputs = [] + self.input = _input or Input() + self.outputs = _outputs or [] def __str__(self): return self.alive_str + ' ' + self.__name__ + self.get_statistics_as_string(prefix=' ') @@ -51,6 +51,11 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): for message in messages: self.input.put(message) + def write_sync(self, *messages): + self.write(BEGIN, *messages, END) + for _ in messages: + self.step() + # XXX deprecated alias recv = deprecated_alias('recv', write) @@ -143,12 +148,18 @@ def _resolve(input_bag, output): return output # If it does not look like a bag, let's create one for easier manipulation - if hasattr(output, 'apply'): + if hasattr(output, 'apply'): # XXX TODO use isbag() ? # Already a bag? Check if we need to set parent. if INHERIT_INPUT in output.flags: output.set_parent(input_bag) - else: - # Not a bag? Let's encapsulate it. - output = Bag(output) + return output - return output + # If we're using kwargs ioformat, then a dict means kwargs. + if settings.IOFORMAT == settings.IOFORMAT_KWARGS and isdict(output): + return Bag(**output) + + if istuple(output): + return Bag(*output) + + # Either we use arg0 format, either it's "just" a value. + return Bag(output) diff --git a/bonobo/nodes/io/base.py b/bonobo/nodes/io/base.py index 3cecb70..af9e609 100644 --- a/bonobo/nodes/io/base.py +++ b/bonobo/nodes/io/base.py @@ -1,39 +1,4 @@ -from bonobo import settings from bonobo.config import Configurable, ContextProcessor, Option, Service -from bonobo.errors import UnrecoverableValueError, UnrecoverableNotImplementedError -from bonobo.structs.bags import Bag - - -class IOFormatEnabled(Configurable): - ioformat = Option(default=settings.IOFORMAT.get) - - def get_input(self, *args, **kwargs): - if self.ioformat == settings.IOFORMAT_ARG0: - if len(args) != 1 or len(kwargs): - raise UnrecoverableValueError( - 'Wrong input formating: IOFORMAT=ARG0 implies one arg and no kwargs, got args={!r} and kwargs={!r}.'. - format(args, kwargs) - ) - return args[0] - - if self.ioformat == settings.IOFORMAT_KWARGS: - if len(args) or not len(kwargs): - raise UnrecoverableValueError( - 'Wrong input formating: IOFORMAT=KWARGS ioformat implies no arg, got args={!r} and kwargs={!r}.'. - format(args, kwargs) - ) - return kwargs - - raise UnrecoverableNotImplementedError('Unsupported format.') - - def get_output(self, row): - if self.ioformat == settings.IOFORMAT_ARG0: - return row - - if self.ioformat == settings.IOFORMAT_KWARGS: - return Bag(**row) - - raise UnrecoverableNotImplementedError('Unsupported format.') class FileHandler(Configurable): diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index 75fffe8..c504c16 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -1,10 +1,11 @@ import csv from bonobo.config import Option +from bonobo.config.options import RemovedOption from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED +from bonobo.nodes.io.base import FileHandler from bonobo.nodes.io.file import FileReader, FileWriter -from bonobo.nodes.io.base import FileHandler, IOFormatEnabled from bonobo.util.objects import ValueHolder @@ -27,9 +28,10 @@ class CsvHandler(FileHandler): delimiter = Option(str, default=';') quotechar = Option(str, default='"') headers = Option(tuple, required=False) + ioformat = RemovedOption(positional=False, value='kwargs') -class CsvReader(IOFormatEnabled, FileReader, CsvHandler): +class CsvReader(FileReader, CsvHandler): """ Reads a CSV and yield the values as dicts. @@ -62,18 +64,17 @@ class CsvReader(IOFormatEnabled, FileReader, CsvHandler): if len(row) != field_count: raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count, )) - yield self.get_output(dict(zip(_headers, row))) + yield dict(zip(_headers, row)) -class CsvWriter(IOFormatEnabled, FileWriter, CsvHandler): +class CsvWriter(FileWriter, CsvHandler): @ContextProcessor def writer(self, context, fs, file, lineno): writer = csv.writer(file, delimiter=self.delimiter, quotechar=self.quotechar, lineterminator=self.eol) headers = ValueHolder(list(self.headers) if self.headers else None) yield writer, headers - def write(self, fs, file, lineno, writer, headers, *args, **kwargs): - row = self.get_input(*args, **kwargs) + def write(self, fs, file, lineno, writer, headers, **row): if not lineno: headers.set(headers.value or row.keys()) writer.writerow(headers.get()) diff --git a/bonobo/nodes/io/json.py b/bonobo/nodes/io/json.py index f1c6df0..533d628 100644 --- a/bonobo/nodes/io/json.py +++ b/bonobo/nodes/io/json.py @@ -1,8 +1,9 @@ import json +from bonobo.config.options import RemovedOption from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED -from bonobo.nodes.io.base import FileHandler, IOFormatEnabled +from bonobo.nodes.io.base import FileHandler from bonobo.nodes.io.file import FileReader, FileWriter from bonobo.structs.bags import Bag @@ -10,14 +11,15 @@ from bonobo.structs.bags import Bag class JsonHandler(FileHandler): eol = ',\n' prefix, suffix = '[', ']' + ioformat = RemovedOption(positional=False, value='kwargs') -class JsonReader(IOFormatEnabled, FileReader, JsonHandler): +class JsonReader(FileReader, JsonHandler): loader = staticmethod(json.load) def read(self, fs, file): for line in self.loader(file): - yield self.get_output(line) + yield line class JsonDictItemsReader(JsonReader): @@ -26,21 +28,20 @@ class JsonDictItemsReader(JsonReader): yield Bag(*line) -class JsonWriter(IOFormatEnabled, FileWriter, JsonHandler): +class JsonWriter(FileWriter, JsonHandler): @ContextProcessor def envelope(self, context, fs, file, lineno): file.write(self.prefix) yield file.write(self.suffix) - def write(self, fs, file, lineno, *args, **kwargs): + def write(self, fs, file, lineno, **row): """ Write a json row on the next line of file pointed by ctx.file. :param ctx: :param row: """ - row = self.get_input(*args, **kwargs) self._write_line(file, (self.eol if lineno.value else '') + json.dumps(row)) lineno += 1 return NOT_MODIFIED diff --git a/bonobo/nodes/io/pickle.py b/bonobo/nodes/io/pickle.py index d9da55f..216c21b 100644 --- a/bonobo/nodes/io/pickle.py +++ b/bonobo/nodes/io/pickle.py @@ -1,9 +1,10 @@ import pickle from bonobo.config import Option +from bonobo.config.options import RemovedOption from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED -from bonobo.nodes.io.base import FileHandler, IOFormatEnabled +from bonobo.nodes.io.base import FileHandler from bonobo.nodes.io.file import FileReader, FileWriter from bonobo.util.objects import ValueHolder @@ -20,7 +21,7 @@ class PickleHandler(FileHandler): item_names = Option(tuple, required=False) -class PickleReader(IOFormatEnabled, FileReader, PickleHandler): +class PickleReader(FileReader, PickleHandler): """ Reads a Python pickle object and yields the items in dicts. """ @@ -54,10 +55,10 @@ class PickleReader(IOFormatEnabled, FileReader, PickleHandler): if len(i) != item_count: raise ValueError('Received an object with %d items, expecting %d.' % (len(i), item_count, )) - yield self.get_output(dict(zip(i)) if is_dict else dict(zip(pickle_headers.value, i))) + yield dict(zip(i)) if is_dict else dict(zip(pickle_headers.value, i)) -class PickleWriter(IOFormatEnabled, FileWriter, PickleHandler): +class PickleWriter(FileWriter, PickleHandler): mode = Option(str, default='wb') def write(self, fs, file, lineno, item): diff --git a/bonobo/settings.py b/bonobo/settings.py index e5edd83..ef4be2d 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -42,6 +42,9 @@ class Setting: def __repr__(self): return ''.format(self.name, self.get()) + def __eq__(self, other): + return self.get() == other + def set(self, value): value = self.formatter(value) if self.formatter else value if self.validator and not self.validator(value): diff --git a/bonobo/strategies/base.py b/bonobo/strategies/base.py index 4b345d4..47f7db4 100644 --- a/bonobo/strategies/base.py +++ b/bonobo/strategies/base.py @@ -6,10 +6,13 @@ class Strategy: Base class for execution strategies. """ - graph_execution_context_factory = GraphExecutionContext + GraphExecutionContextType = GraphExecutionContext - def create_graph_execution_context(self, graph, *args, **kwargs): - return self.graph_execution_context_factory(graph, *args, **kwargs) + def __init__(self, GraphExecutionContextType=None): + self.GraphExecutionContextType = GraphExecutionContextType or self.GraphExecutionContextType + + def create_graph_execution_context(self, graph, *args, GraphExecutionContextType=None, **kwargs): + return (GraphExecutionContextType or self.GraphExecutionContextType)(graph, *args, **kwargs) def execute(self, graph, *args, **kwargs): raise NotImplementedError diff --git a/bonobo/strategies/executor.py b/bonobo/strategies/executor.py index a0bd4f4..3bfabc6 100644 --- a/bonobo/strategies/executor.py +++ b/bonobo/strategies/executor.py @@ -19,42 +19,16 @@ class ExecutorStrategy(Strategy): def create_executor(self): return self.executor_factory() - def execute(self, graph, *args, plugins=None, services=None, **kwargs): - context = self.create_graph_execution_context(graph, plugins=plugins, services=services) + def execute(self, graph, **kwargs): + context = self.create_graph_execution_context(graph, **kwargs) context.write(BEGIN, Bag(), END) executor = self.create_executor() futures = [] - for plugin_context in context.plugins: - - def _runner(plugin_context=plugin_context): - with plugin_context: - try: - plugin_context.loop() - except Exception as exc: - print_error(exc, traceback.format_exc(), context=plugin_context) - - futures.append(executor.submit(_runner)) - - for node_context in context.nodes: - - def _runner(node_context=node_context): - try: - node_context.start() - except Exception as exc: - print_error(exc, traceback.format_exc(), context=node_context, method='start') - node_context.input.on_end() - else: - node_context.loop() - - try: - node_context.stop() - except Exception as exc: - print_error(exc, traceback.format_exc(), context=node_context, method='stop') - - futures.append(executor.submit(_runner)) + context.start_plugins(self.get_plugin_starter(executor, futures)) + context.start(self.get_starter(executor, futures)) while context.alive: time.sleep(0.1) @@ -62,10 +36,45 @@ class ExecutorStrategy(Strategy): for plugin_context in context.plugins: plugin_context.shutdown() + context.stop() + executor.shutdown() return context + def get_starter(self, executor, futures): + def starter(node): + def _runner(): + try: + node.start() + except Exception as exc: + print_error(exc, traceback.format_exc(), context=node, method='start') + node.input.on_end() + else: + node.loop() + + try: + node.stop() + except Exception as exc: + print_error(exc, traceback.format_exc(), context=node, method='stop') + + futures.append(executor.submit(_runner)) + + return starter + + def get_plugin_starter(self, executor, futures): + def plugin_starter(plugin): + def _runner(): + with plugin: + try: + plugin.loop() + except Exception as exc: + print_error(exc, traceback.format_exc(), context=plugin) + + futures.append(executor.submit(_runner)) + + return plugin_starter + class ThreadPoolExecutorStrategy(ExecutorStrategy): executor_factory = ThreadPoolExecutor diff --git a/bonobo/strategies/naive.py b/bonobo/strategies/naive.py index cab9c57..20477c1 100644 --- a/bonobo/strategies/naive.py +++ b/bonobo/strategies/naive.py @@ -4,13 +4,23 @@ from bonobo.structs.bags import Bag class NaiveStrategy(Strategy): - def execute(self, graph, *args, plugins=None, **kwargs): - context = self.create_graph_execution_context(graph, plugins=plugins) + # TODO: how to run plugins in "naive" mode ? + + def execute(self, graph, **kwargs): + context = self.create_graph_execution_context(graph, **kwargs) context.write(BEGIN, Bag(), END) - # TODO: how to run plugins in "naive" mode ? + # start context.start() - context.loop() + + # loop + nodes = list(context.nodes) + while len(nodes): + for node in nodes: + node.loop() + nodes = list(node for node in nodes if node.alive) + + # stop context.stop() return context diff --git a/bonobo/structs/bags.py b/bonobo/structs/bags.py index 3eae9ff..31bc870 100644 --- a/bonobo/structs/bags.py +++ b/bonobo/structs/bags.py @@ -96,7 +96,29 @@ class Bag: return cls(*args, _flags=(INHERIT_INPUT, ), **kwargs) def __eq__(self, other): - return isinstance(other, Bag) and other.args == self.args and other.kwargs == self.kwargs + # 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 of (tuple, dict) + if isinstance(other, tuple) and len(other) == 2 and other[0] == self.args and other[1] == self.kwargs: + return True + + # tuple (aka args) + if isinstance(other, tuple) and other == self.args: + return True + + # dict (aka kwargs) + if isinstance(other, dict) and not self.args and other == self.kwargs: + return True + + # arg0 + if len(self.args) == 1 and not len(self.kwargs) and self.args[0] == other: + return True + + return False def __repr__(self): return '<{} ({})>'.format( diff --git a/bonobo/util/__init__.py b/bonobo/util/__init__.py index df14e9a..e2eebe1 100644 --- a/bonobo/util/__init__.py +++ b/bonobo/util/__init__.py @@ -1,14 +1,18 @@ from bonobo.util.collections import sortedlist +from bonobo.util.iterators import ensure_tuple +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, istype, ) from bonobo.util.objects import (get_name, get_attribute_or_create, ValueHolder) @@ -17,6 +21,8 @@ from bonobo.util.python import require # Bonobo's util API __all__ = [ 'ValueHolder', + 'deprecated', + 'deprecated_alias', 'get_attribute_or_create', 'get_name', 'inspect_node', @@ -24,6 +30,7 @@ __all__ = [ 'isconfigurable', 'isconfigurabletype', 'iscontextprocessor', + 'isdict', 'iserrorbag', 'isloopbackbag', 'ismethod', diff --git a/bonobo/util/inspect.py b/bonobo/util/inspect.py index f9ae4d8..a3c71d7 100644 --- a/bonobo/util/inspect.py +++ b/bonobo/util/inspect.py @@ -68,6 +68,26 @@ def istype(mixed): return isinstance(mixed, type) +def isdict(mixed): + """ + Check if the given argument is a dict. + + :param mixed: + :return: bool + """ + return isinstance(mixed, dict) + + +def istuple(mixed): + """ + Check if the given argument is a tuple. + + :param mixed: + :return: bool + """ + return isinstance(mixed, tuple) + + def isbag(mixed): """ Check if the given argument is an instance of a :class:`bonobo.Bag`. diff --git a/bonobo/util/iterators.py b/bonobo/util/iterators.py index 82f8518..04c81a5 100644 --- a/bonobo/util/iterators.py +++ b/bonobo/util/iterators.py @@ -38,6 +38,6 @@ def tuplize(generator): def iter_if_not_sequence(mixed): - if isinstance(mixed, (dict, list, str)): + if isinstance(mixed, (dict, list, str, bytes, )): raise TypeError(type(mixed).__name__) return iter(mixed) diff --git a/bonobo/util/testing.py b/bonobo/util/testing.py index 7c07256..6fc7d60 100644 --- a/bonobo/util/testing.py +++ b/bonobo/util/testing.py @@ -1,16 +1,10 @@ from contextlib import contextmanager -from unittest.mock import MagicMock -from bonobo import open_fs +from bonobo import open_fs, Token +from bonobo.execution import GraphExecutionContext from bonobo.execution.node import NodeExecutionContext -class CapturingNodeExecutionContext(NodeExecutionContext): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.send = MagicMock() - - @contextmanager def optional_contextmanager(cm, *, ignore=False): if cm is None or ignore: @@ -35,3 +29,38 @@ class FilesystemTester: def get_services_for_writer(self, tmpdir): fs, filename = open_fs(tmpdir), 'output.' + self.extension return fs, filename, {'fs': fs} + + +class QueueList(list): + def append(self, item): + if not isinstance(item, Token): + super(QueueList, self).append(item) + + put = append + + +class BufferingContext: + def __init__(self, buffer=None): + if buffer is None: + buffer = QueueList() + self.buffer = buffer + + def get_buffer(self): + return self.buffer + + +class BufferingNodeExecutionContext(BufferingContext, NodeExecutionContext): + def __init__(self, *args, buffer=None, **kwargs): + BufferingContext.__init__(self, buffer) + NodeExecutionContext.__init__(self, *args, **kwargs, _outputs=[self.buffer]) + + +class BufferingGraphExecutionContext(BufferingContext, GraphExecutionContext): + NodeExecutionContextType = BufferingNodeExecutionContext + + def __init__(self, *args, buffer=None, **kwargs): + BufferingContext.__init__(self, buffer) + GraphExecutionContext.__init__(self, *args, **kwargs) + + def create_node_execution_context_for(self, node): + return self.NodeExecutionContextType(node, parent=self, buffer=self.buffer) diff --git a/tests/execution/test_node.py b/tests/execution/test_node.py new file mode 100644 index 0000000..23748d4 --- /dev/null +++ b/tests/execution/test_node.py @@ -0,0 +1,104 @@ +from bonobo import Bag, Graph +from bonobo.strategies import NaiveStrategy +from bonobo.util.testing import BufferingNodeExecutionContext, BufferingGraphExecutionContext + + +def test_node_string(): + def f(): + return 'foo' + + with BufferingNodeExecutionContext(f) as context: + context.write_sync(Bag()) + output = context.get_buffer() + + assert len(output) == 1 + assert output[0] == (('foo', ), {}) + + def g(): + yield 'foo' + yield 'bar' + + with BufferingNodeExecutionContext(g) as context: + context.write_sync(Bag()) + output = context.get_buffer() + + assert len(output) == 2 + assert output[0] == (('foo', ), {}) + assert output[1] == (('bar', ), {}) + + +def test_node_bytes(): + def f(): + return b'foo' + + with BufferingNodeExecutionContext(f) as context: + context.write_sync(Bag()) + + output = context.get_buffer() + assert len(output) == 1 + assert output[0] == ((b'foo', ), {}) + + def g(): + yield b'foo' + yield b'bar' + + with BufferingNodeExecutionContext(g) as context: + context.write_sync(Bag()) + output = context.get_buffer() + + assert len(output) == 2 + assert output[0] == ((b'foo', ), {}) + assert output[1] == ((b'bar', ), {}) + + +def test_node_dict(): + def f(): + return {'id': 1, 'name': 'foo'} + + with BufferingNodeExecutionContext(f) as context: + context.write_sync(Bag()) + output = context.get_buffer() + + assert len(output) == 1 + 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()) + output = context.get_buffer() + + assert len(output) == 2 + 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'} + + graph = Graph(f, uppercase_name) + context = strategy.execute(graph) + output = context.get_buffer() + + assert len(output) == 1 + assert output[0] == {'id': 1, 'name': 'FOO'} + + def g(): + yield {'id': 1, 'name': 'foo'} + yield {'id': 2, 'name': 'bar'} + + graph = Graph(g, uppercase_name) + context = strategy.execute(graph) + output = context.get_buffer() + + assert len(output) == 2 + assert output[0] == {'id': 1, 'name': 'FOO'} + assert output[1] == {'id': 2, 'name': 'BAR'} diff --git a/tests/io/test_csv.py b/tests/io/test_csv.py index 9a9480c..fc189ac 100644 --- a/tests/io/test_csv.py +++ b/tests/io/test_csv.py @@ -3,25 +3,19 @@ import pytest from bonobo import Bag, CsvReader, CsvWriter, settings from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext -from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester +from bonobo.util.testing import FilesystemTester, BufferingNodeExecutionContext csv_tester = FilesystemTester('csv') csv_tester.input_data = 'a,b,c\na foo,b foo,c foo\na bar,b bar,c bar' -def test_write_csv_to_file_arg0(tmpdir): +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 NodeExecutionContext(CsvWriter(path=filename, ioformat=settings.IOFORMAT_ARG0), services=services) as context: - context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END) - context.step() - context.step() - - with fs.open(filename) as fp: - assert fp.read() == 'foo\nbar\nbaz\n' - - with pytest.raises(AttributeError): - getattr(context, 'file') + with pytest.raises(ValueError): + CsvReader(path=filename, delimiter=',', ioformat=settings.IOFORMAT_ARG0), @pytest.mark.parametrize('add_kwargs', ({}, { @@ -30,7 +24,7 @@ def test_write_csv_to_file_arg0(tmpdir): def test_write_csv_to_file_kwargs(tmpdir, add_kwargs): fs, filename, services = csv_tester.get_services_for_writer(tmpdir) - with NodeExecutionContext(CsvWriter(path=filename, **add_kwargs), services=services) as context: + with NodeExecutionContext(CsvWriter(filename, **add_kwargs), services=services) as context: context.write(BEGIN, Bag(**{'foo': 'bar'}), Bag(**{'foo': 'baz', 'ignore': 'this'}), END) context.step() context.step() @@ -42,61 +36,24 @@ def test_write_csv_to_file_kwargs(tmpdir, add_kwargs): getattr(context, 'file') -def test_read_csv_from_file_arg0(tmpdir): - fs, filename, services = csv_tester.get_services_for_reader(tmpdir) - - with CapturingNodeExecutionContext( - CsvReader(path=filename, delimiter=',', ioformat=settings.IOFORMAT_ARG0), - services=services, - ) as context: - context.write(BEGIN, Bag(), END) - context.step() - - assert len(context.send.mock_calls) == 2 - - args0, kwargs0 = context.send.call_args_list[0] - assert len(args0) == 1 and not len(kwargs0) - args1, kwargs1 = context.send.call_args_list[1] - assert len(args1) == 1 and not len(kwargs1) - - assert args0[0].args[0] == { - 'a': 'a foo', - 'b': 'b foo', - 'c': 'c foo', - } - assert args1[0].args[0] == { - 'a': 'a bar', - 'b': 'b bar', - 'c': 'c bar', - } - - def test_read_csv_from_file_kwargs(tmpdir): fs, filename, services = csv_tester.get_services_for_reader(tmpdir) - with CapturingNodeExecutionContext( + with BufferingNodeExecutionContext( CsvReader(path=filename, delimiter=','), services=services, ) as context: context.write(BEGIN, Bag(), END) context.step() + output = context.get_buffer() - assert len(context.send.mock_calls) == 2 - - args0, kwargs0 = context.send.call_args_list[0] - assert len(args0) == 1 and not len(kwargs0) - args1, kwargs1 = context.send.call_args_list[1] - assert len(args1) == 1 and not len(kwargs1) - - _args, _kwargs = args0[0].get() - assert not len(_args) and _kwargs == { + assert len(output) == 2 + assert output[0] == { 'a': 'a foo', 'b': 'b foo', 'c': 'c foo', } - - _args, _kwargs = args1[0].get() - assert not len(_args) and _kwargs == { + assert output[1] == { 'a': 'a bar', 'b': 'b bar', 'c': 'c bar', diff --git a/tests/io/test_file.py b/tests/io/test_file.py index 07a15eb..d7645e7 100644 --- a/tests/io/test_file.py +++ b/tests/io/test_file.py @@ -3,7 +3,7 @@ import pytest from bonobo import Bag, FileReader, FileWriter from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext -from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester +from bonobo.util.testing import BufferingNodeExecutionContext, FilesystemTester txt_tester = FilesystemTester('txt') txt_tester.input_data = 'Hello\nWorld\n' @@ -41,16 +41,10 @@ def test_file_writer_in_context(tmpdir, lines, output): def test_file_reader(tmpdir): fs, filename, services = txt_tester.get_services_for_reader(tmpdir) - with CapturingNodeExecutionContext(FileReader(path=filename), services=services) as context: - context.write(BEGIN, Bag(), END) - context.step() + with BufferingNodeExecutionContext(FileReader(path=filename), services=services) as context: + context.write_sync(Bag()) + output = context.get_buffer() - assert len(context.send.mock_calls) == 2 - - args0, kwargs0 = context.send.call_args_list[0] - assert len(args0) == 1 and not len(kwargs0) - args1, kwargs1 = context.send.call_args_list[1] - assert len(args1) == 1 and not len(kwargs1) - - assert args0[0].args[0] == 'Hello' - assert args1[0].args[0] == 'World' + assert len(output) == 2 + assert output[0] == 'Hello' + assert output[1] == 'World' diff --git a/tests/io/test_json.py b/tests/io/test_json.py index 75350ce..66c7f94 100644 --- a/tests/io/test_json.py +++ b/tests/io/test_json.py @@ -3,21 +3,20 @@ import pytest from bonobo import Bag, JsonReader, JsonWriter, settings from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext -from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester +from bonobo.util.testing import FilesystemTester json_tester = FilesystemTester('json') json_tester.input_data = '''[{"x": "foo"},{"x": "bar"}]''' -def test_write_json_arg0(tmpdir): +def test_write_json_ioformat_arg0(tmpdir): fs, filename, services = json_tester.get_services_for_writer(tmpdir) - with NodeExecutionContext(JsonWriter(filename, ioformat=settings.IOFORMAT_ARG0), services=services) as context: - context.write(BEGIN, Bag({'foo': 'bar'}), END) - context.step() + with pytest.raises(ValueError): + JsonWriter(filename, ioformat=settings.IOFORMAT_ARG0) - with fs.open(filename) as fp: - assert fp.read() == '[{"foo": "bar"}]' + with pytest.raises(ValueError): + JsonReader(filename, ioformat=settings.IOFORMAT_ARG0), @pytest.mark.parametrize('add_kwargs', ({}, { @@ -32,24 +31,3 @@ def test_write_json_kwargs(tmpdir, add_kwargs): with fs.open(filename) as fp: assert fp.read() == '[{"foo": "bar"}]' - - -def test_read_json_arg0(tmpdir): - fs, filename, services = json_tester.get_services_for_reader(tmpdir) - - with CapturingNodeExecutionContext( - JsonReader(filename, ioformat=settings.IOFORMAT_ARG0), - services=services, - ) as context: - context.write(BEGIN, Bag(), END) - context.step() - - assert len(context.send.mock_calls) == 2 - - args0, kwargs0 = context.send.call_args_list[0] - assert len(args0) == 1 and not len(kwargs0) - args1, kwargs1 = context.send.call_args_list[1] - assert len(args1) == 1 and not len(kwargs1) - - assert args0[0].args[0] == {'x': 'foo'} - assert args1[0].args[0] == {'x': 'bar'} diff --git a/tests/io/test_pickle.py b/tests/io/test_pickle.py index aff7796..eca3493 100644 --- a/tests/io/test_pickle.py +++ b/tests/io/test_pickle.py @@ -2,10 +2,9 @@ import pickle import pytest -from bonobo import Bag, PickleReader, PickleWriter, settings -from bonobo.constants import BEGIN, END +from bonobo import Bag, PickleReader, PickleWriter from bonobo.execution.node import NodeExecutionContext -from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester +from bonobo.util.testing import BufferingNodeExecutionContext, FilesystemTester pickle_tester = FilesystemTester('pkl', mode='wb') pickle_tester.input_data = pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar']]) @@ -14,10 +13,8 @@ pickle_tester.input_data = pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c def test_write_pickled_dict_to_file(tmpdir): fs, filename, services = pickle_tester.get_services_for_writer(tmpdir) - with NodeExecutionContext(PickleWriter(filename, ioformat=settings.IOFORMAT_ARG0), services=services) as context: - context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END) - context.step() - context.step() + with NodeExecutionContext(PickleWriter(filename), services=services) as context: + context.write_sync(Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'})) with fs.open(filename, 'rb') as fp: assert pickle.loads(fp.read()) == {'foo': 'bar'} @@ -29,25 +26,17 @@ def test_write_pickled_dict_to_file(tmpdir): def test_read_pickled_list_from_file(tmpdir): fs, filename, services = pickle_tester.get_services_for_reader(tmpdir) - with CapturingNodeExecutionContext( - PickleReader(filename, ioformat=settings.IOFORMAT_ARG0), services=services - ) as context: - context.write(BEGIN, Bag(), END) - context.step() + with BufferingNodeExecutionContext(PickleReader(filename), services=services) as context: + context.write_sync(Bag()) + output = context.get_buffer() - assert len(context.send.mock_calls) == 2 - - args0, kwargs0 = context.send.call_args_list[0] - assert len(args0) == 1 and not len(kwargs0) - args1, kwargs1 = context.send.call_args_list[1] - assert len(args1) == 1 and not len(kwargs1) - - assert args0[0].args[0] == { + assert len(output) == 2 + assert output[0] == { 'a': 'a foo', 'b': 'b foo', 'c': 'c foo', } - assert args1[0].args[0] == { + assert output[1] == { 'a': 'a bar', 'b': 'b bar', 'c': 'c bar', diff --git a/tests/test_basicusage.py b/tests/test_basicusage.py index a43831d..58a1212 100644 --- a/tests/test_basicusage.py +++ b/tests/test_basicusage.py @@ -12,4 +12,5 @@ def test_run_graph_noop(): with patch('bonobo._api._is_interactive_console', side_effect=lambda: False): result = bonobo.run(graph) + assert isinstance(result, GraphExecutionContext) diff --git a/tests/test_commands.py b/tests/test_commands.py index a29465c..59cf5f4 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -30,9 +30,13 @@ def test_entrypoint(): for command in pkg_resources.iter_entry_points('bonobo.commands'): commands[command.name] = command - assert 'init' in commands - assert 'run' in commands - assert 'version' in commands + assert not { + 'convert', + 'init', + 'inspect', + 'run', + 'version', + }.difference(set(commands)) @all_runners diff --git a/tests/test_execution.py b/tests/test_execution.py index 70e12ac..6fb33e4 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -51,31 +51,31 @@ def test_simple_execution_context(): graph = Graph() graph.add_chain(*chain) - ctx = GraphExecutionContext(graph) - assert len(ctx.nodes) == len(chain) - assert not len(ctx.plugins) + context = GraphExecutionContext(graph) + assert len(context.nodes) == len(chain) + assert not len(context.plugins) for i, node in enumerate(chain): - assert ctx[i].wrapped is node + assert context[i].wrapped is node - assert not ctx.alive - assert not ctx.started - assert not ctx.stopped + assert not context.alive + assert not context.started + assert not context.stopped - ctx.write(BEGIN, Bag(), END) + context.write(BEGIN, Bag(), END) - assert not ctx.alive - assert not ctx.started - assert not ctx.stopped + assert not context.alive + assert not context.started + assert not context.stopped - ctx.start() + context.start() - assert ctx.alive - assert ctx.started - assert not ctx.stopped + assert context.alive + assert context.started + assert not context.stopped - ctx.stop() + context.stop() - assert not ctx.alive - assert ctx.started - assert ctx.stopped + assert not context.alive + assert context.started + assert context.stopped From 84e197b209dc1ae0381a02af66a31dade90545fd Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 15 Oct 2017 15:39:23 -0400 Subject: [PATCH 018/145] Updated .env >>> .env_one to include in repo (.env ignored). --- .../examples/environment/env_files/.env_one | 3 +++ tests/test_commands.py | 20 +++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 bonobo/examples/environment/env_files/.env_one diff --git a/bonobo/examples/environment/env_files/.env_one b/bonobo/examples/environment/env_files/.env_one new file mode 100644 index 0000000..65f2b17 --- /dev/null +++ b/bonobo/examples/environment/env_files/.env_one @@ -0,0 +1,3 @@ +MY_SECRET=321 +TEST_USER_PASSWORD=sweetpassword +PATH=marzo \ No newline at end of file diff --git a/tests/test_commands.py b/tests/test_commands.py index 59f3db8..3f996cb 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -102,7 +102,7 @@ def test_version(runner, capsys): class TestDefaultEnvFile(object): def test_run_file_with_default_env_file(self, runner, capsys): runner( - 'run', '--quiet', '--default-env-file', '.env', + 'run', '--quiet', '--default-env-file', '.env_one', get_examples_path('environment/env_files/get_passed_env_file.py') ) out, err = capsys.readouterr() @@ -113,7 +113,7 @@ class TestDefaultEnvFile(object): def test_run_file_with_multiple_default_env_files(self, runner, capsys): runner( - 'run', '--quiet', '--default-env-file', '.env', + 'run', '--quiet', '--default-env-file', '.env_one', '--default-env-file', '.env_two', get_examples_path('environment/env_files/get_passed_env_file.py') ) @@ -127,7 +127,7 @@ class TestDefaultEnvFile(object): runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', - '--default-env-file', '.env' + '--default-env-file', '.env_one' ) out, err = capsys.readouterr() out = out.split('\n') @@ -139,7 +139,7 @@ class TestDefaultEnvFile(object): runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', - '--default-env-file', '.env', '--default-env-file', '.env_two', + '--default-env-file', '.env_one', '--default-env-file', '.env_two', ) out, err = capsys.readouterr() out = out.split('\n') @@ -154,7 +154,7 @@ class TestEnvFile(object): runner( 'run', '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), - '--env-file', '.env', + '--env-file', '.env_one', ) out, err = capsys.readouterr() out = out.split('\n') @@ -166,7 +166,7 @@ class TestEnvFile(object): runner( 'run', '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), - '--env-file', '.env', '--env-file', '.env_two', + '--env-file', '.env_one', '--env-file', '.env_two', ) out, err = capsys.readouterr() out = out.split('\n') @@ -178,7 +178,7 @@ class TestEnvFile(object): runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', - '--env-file', '.env', + '--env-file', '.env_one', ) out, err = capsys.readouterr() out = out.split('\n') @@ -190,7 +190,7 @@ class TestEnvFile(object): runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', - '--env-file', '.env', '--env-file', '.env_two', + '--env-file', '.env_one', '--env-file', '.env_two', ) out, err = capsys.readouterr() out = out.split('\n') @@ -204,7 +204,7 @@ def test_run_file_with_default_env_file_and_env_file(runner, capsys): runner( 'run', '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), - '--default-env-file', '.env', '--env-file', '.env_two', + '--default-env-file', '.env_one', '--env-file', '.env_two', ) out, err = capsys.readouterr() out = out.split('\n') @@ -218,7 +218,7 @@ def test_run_file_with_default_env_file_and_env_file_and_env_vars(runner, capsys runner( 'run', '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), - '--default-env-file', '.env', '--env-file', '.env_two', + '--default-env-file', '.env_one', '--env-file', '.env_two', '--env', 'TEST_USER_PASSWORD=SWEETpassWORD', '--env', 'MY_SECRET=444', ) out, err = capsys.readouterr() From dc34ab4a8b560ec163cba5e9dd98f7d8025df335 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 15 Oct 2017 15:49:14 -0400 Subject: [PATCH 019/145] Moved env vars tests to class. --- .../environment/env_vars/get_passed_env.py | 6 +- tests/test_commands.py | 73 ++++++++++++------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/bonobo/examples/environment/env_vars/get_passed_env.py b/bonobo/examples/environment/env_vars/get_passed_env.py index 54a3280..6ef64d1 100644 --- a/bonobo/examples/environment/env_vars/get_passed_env.py +++ b/bonobo/examples/environment/env_vars/get_passed_env.py @@ -4,9 +4,9 @@ import bonobo def extract(): - env_test_user = os.getenv('ENV_TEST_USER') - env_test_number = os.getenv('ENV_TEST_NUMBER') - env_test_string = os.getenv('ENV_TEST_STRING') + env_test_user = os.getenv('ENV_TEST_USER', 'user') + env_test_number = os.getenv('ENV_TEST_NUMBER', 'number') + env_test_string = os.getenv('ENV_TEST_STRING', 'string') return env_test_user, env_test_number, env_test_string diff --git a/tests/test_commands.py b/tests/test_commands.py index 3f996cb..4508869 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -229,30 +229,53 @@ def test_run_file_with_default_env_file_and_env_file_and_env_vars(runner, capsys @all_runners -def test_run_file_with_env_vars(runner, capsys): - runner( - 'run', '--quiet', - get_examples_path('environment/env_vars/get_passed_env.py'), - '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', - '--env', "ENV_TEST_STRING='my_test_string'" - ) - out, err = capsys.readouterr() - out = out.split('\n') - assert out[0] == 'cwandrews' - assert out[1] == '123' - assert out[2] == 'my_test_string' +class TestEnvVars(object): + def test_run_file_with_env_var(self, runner, capsys): + runner( + 'run', '--quiet', + get_examples_path('environment/env_vars/get_passed_env.py'), + '--env', 'ENV_TEST_NUMBER=123' + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] != 'test_user' + assert out[1] == '123' + assert out[2] == 'string' + def test_run_file_with_env_vars(self, runner, capsys): + runner( + 'run', '--quiet', + get_examples_path('environment/env_vars/get_passed_env.py'), + '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', + '--env', "ENV_TEST_STRING='my_test_string'" + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == 'cwandrews' + assert out[1] == '123' + assert out[2] == 'my_test_string' -@all_runners -def test_run_module_with_env_vars(runner, capsys): - runner( - 'run', '--quiet', '-m', - 'bonobo.examples.environment.env_vars.get_passed_env', - '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', - '--env', "ENV_TEST_STRING='my_test_string'" - ) - out, err = capsys.readouterr() - out = out.split('\n') - assert out[0] == 'cwandrews' - assert out[1] == '123' - assert out[2] == 'my_test_string' + def test_run_module_with_env_var(self, runner, capsys): + runner( + 'run', '--quiet', '-m', + 'bonobo.examples.environment.env_vars.get_passed_env', + '--env', 'ENV_TEST_NUMBER=123' + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == 'cwandrews' + assert out[1] == '123' + assert out[2] == 'my_test_string' + + def test_run_module_with_env_vars(self, runner, capsys): + runner( + 'run', '--quiet', '-m', + 'bonobo.examples.environment.env_vars.get_passed_env', + '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', + '--env', "ENV_TEST_STRING='my_test_string'" + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == 'cwandrews' + assert out[1] == '123' + assert out[2] == 'my_test_string' From cb7a18f20f9ea4ea7e13413a8d8d03981d1f4ad7 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 15 Oct 2017 16:14:14 -0400 Subject: [PATCH 020/145] Added more tests and moved all env and env file testing to classes (it might make more sense to just move them to separate files?). --- bonobo/commands/run.py | 4 - .../environment/env_vars/get_passed_env.py | 3 +- tests/test_commands.py | 103 ++++++++++++++---- 3 files changed, 81 insertions(+), 29 deletions(-) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 7b23b9d..8c0186c 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -78,21 +78,17 @@ def read(filename, module, install=False, quiet=False, verbose=False, default_en raise RuntimeError('UNEXPECTED: argparse should not allow this.') env_dir = Path(filename).parent or Path(module).parent - if default_env_file: for f in default_env_file: env_file_path = env_dir.joinpath(f) load_dotenv(env_file_path) - if default_env: for e in default_env: set_env_var(e) - if env_file: for f in env_file: env_file_path = env_dir.joinpath(f) load_dotenv(env_file_path, override=True) - if env: for e in env: set_env_var(e, override=True) diff --git a/bonobo/examples/environment/env_vars/get_passed_env.py b/bonobo/examples/environment/env_vars/get_passed_env.py index 6ef64d1..f236ba7 100644 --- a/bonobo/examples/environment/env_vars/get_passed_env.py +++ b/bonobo/examples/environment/env_vars/get_passed_env.py @@ -7,7 +7,8 @@ def extract(): env_test_user = os.getenv('ENV_TEST_USER', 'user') env_test_number = os.getenv('ENV_TEST_NUMBER', 'number') env_test_string = os.getenv('ENV_TEST_STRING', 'string') - return env_test_user, env_test_number, env_test_string + env_user = os.getenv('USER') + return env_test_user, env_test_number, env_test_string, env_user def load(s: str): diff --git a/tests/test_commands.py b/tests/test_commands.py index 4508869..0e6205d 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -200,32 +200,87 @@ class TestEnvFile(object): @all_runners -def test_run_file_with_default_env_file_and_env_file(runner, capsys): - runner( - 'run', '--quiet', - get_examples_path('environment/env_files/get_passed_env_file.py'), - '--default-env-file', '.env_one', '--env-file', '.env_two', - ) - out, err = capsys.readouterr() - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'not_sweet_password' - assert out[2] == 'abril' +class TestEnvFileCombinations(object): + def test_run_file_with_default_env_file_and_env_file(self, runner, capsys): + runner( + 'run', '--quiet', + get_examples_path('environment/env_files/get_passed_env_file.py'), + '--default-env-file', '.env_one', '--env-file', '.env_two', + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '321' + assert out[1] == 'not_sweet_password' + assert out[2] == 'abril' + + def test_run_file_with_default_env_file_and_env_file_and_env_vars(self, runner, capsys): + runner( + 'run', '--quiet', + get_examples_path('environment/env_files/get_passed_env_file.py'), + '--default-env-file', '.env_one', '--env-file', '.env_two', + '--env', 'TEST_USER_PASSWORD=SWEETpassWORD', '--env', + 'MY_SECRET=444', + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == '444' + assert out[1] == 'SWEETpassWORD' + assert out[2] == 'abril' @all_runners -def test_run_file_with_default_env_file_and_env_file_and_env_vars(runner, capsys): - runner( - 'run', '--quiet', - get_examples_path('environment/env_files/get_passed_env_file.py'), - '--default-env-file', '.env_one', '--env-file', '.env_two', - '--env', 'TEST_USER_PASSWORD=SWEETpassWORD', '--env', 'MY_SECRET=444', - ) - out, err = capsys.readouterr() - out = out.split('\n') - assert out[0] == '444' - assert out[1] == 'SWEETpassWORD' - assert out[2] == 'abril' +class TestDefaultEnvVars(object): + def test_run_file_with_default_env_var(self, runner, capsys): + runner( + 'run', '--quiet', + get_examples_path('environment/env_vars/get_passed_env.py'), + '--default-env', 'USER=clowncity' + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == 'user' + assert out[1] == 'number' + assert out[2] == 'string' + assert out[3] != 'clowncity' + + def test_run_file_with_default_env_vars(self, runner, capsys): + runner( + 'run', '--quiet', + get_examples_path('environment/env_vars/get_passed_env.py'), + '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', + '--default-env', "ENV_TEST_STRING='my_test_string'" + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == 'cwandrews' + assert out[1] == '123' + assert out[2] == 'my_test_string' + + def test_run_module_with_default_env_var(self, runner, capsys): + runner( + 'run', '--quiet', '-m', + 'bonobo.examples.environment.env_vars.get_passed_env', + '--env', 'ENV_TEST_NUMBER=123', + '--default-env', 'ENV_TEST_STRING=string' + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == 'cwandrews' + assert out[1] == '123' + assert out[2] != 'string' + + def test_run_module_with_default_env_vars(self, runner, capsys): + runner( + 'run', '--quiet', '-m', + 'bonobo.examples.environment.env_vars.get_passed_env', + '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', + '--default-env', "ENV_TEST_STRING='string'" + ) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0] == 'cwandrews' + assert out[1] == '123' + assert out[2] != 'string' @all_runners @@ -240,7 +295,7 @@ class TestEnvVars(object): out = out.split('\n') assert out[0] != 'test_user' assert out[1] == '123' - assert out[2] == 'string' + assert out[2] == 'my_test_string' def test_run_file_with_env_vars(self, runner, capsys): runner( From d6d063ad43e499a69dcd94630c26f2cc54f02368 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 15 Oct 2017 16:43:28 -0400 Subject: [PATCH 021/145] Updated environment documentation in guides to account for env files. --- docs/guide/environment.rst | 67 +++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/docs/guide/environment.rst b/docs/guide/environment.rst index 203368d..ba21e62 100644 --- a/docs/guide/environment.rst +++ b/docs/guide/environment.rst @@ -23,25 +23,76 @@ simply to use the optional ``--env`` argument when running bonobo from the shell syntax ``VAR_NAME=VAR_VALUE``. Multiple environment variables can be passed by using multiple ``--env`` / ``-e`` flags (i.e. ``bonobo run --env FIZZ=buzz ...`` and ``bonobo run --env FIZZ=buzz --env Foo=bar ...``). Additionally, in bash you can also set environment variables by listing those you wish to set before the `bonobo run` command with space -separating the key-value pairs (i.e. ``FIZZ=buzz bonobo run ...`` or ``FIZZ=buzz FOO=bar bonobo run ...``). +separating the key-value pairs (i.e. ``FIZZ=buzz bonobo run ...`` or ``FIZZ=buzz FOO=bar bonobo run ...``). Additionally, +bonobo is able to pull environment variables from local '.env' files rather than having to pass each key-value pair +individually at runtime. Importantly, a strict 'order of priority' is followed when setting environment variables so +it is advisable to read and understand the order listed below to prevent + + +The order of priority is from lower to higher with the higher "winning" if set: + + 1. default values + ``os.getenv("VARNAME", default_value)`` + The user/writer/creator of the graph is responsible for setting these. + + 2. ``--default-env-file`` values + Specify file to read default env values from. Each env var in the file is used if the var isn't already a corresponding value set at the system environment (system environment vars not overwritten). + + 3. ``--default-env`` values + Works like #2 but the default ``NAME=var`` are passed individually, with one ``key=value`` pair for each ``--default-env`` flag rather than gathered from a specified file. + + 4. system environment values + Env vars already set at the system level. It is worth noting that passed env vars via ``NAME=value bonobo run ...`` falls here in the order of priority. + + 5. ``--env-file`` values + Env vars specified here are set like those in #2 albeit that these values have priority over those set at the system level. + + 6. ``--env`` values + Env vars set using the ``--env`` / ``-e`` flag work like #3 but take priority over all other env vars. + + + +Examples +:::::::: The Examples below demonstrate setting one or multiple variables using both of these methods: .. code-block:: bash - # Using one environment variable via --env flag: + # Using one environment variable via --env and --defualt-env flags: bonobo run csvsanitizer --env SECRET_TOKEN=secret123 + bonobo run csvsanitizer --defaul-env SECRET_TOKEN=secret123 - # Using multiple environment variables via -e (env) flag: + # Using multiple environment variables via -e (env) and --default-env flags: bonobo run csvsanitizer -e SRC_FILE=inventory.txt -e DST_FILE=inventory_processed.csv - - # Using one environment variable inline (bash only): + bonobo run csvsanitizer --default-env SRC_FILE=inventory.txt --default-env DST_FILE=inventory_processed.csv + + # Using one environment variable inline (bash-like shells only): SECRET_TOKEN=secret123 bonobo run csvsanitizer - # Using multiple environment variables inline (bash only): + # Using multiple environment variables inline (bash-like shells only): SRC_FILE=inventory.txt DST_FILE=inventory_processed.csv bonobo run csvsanitizer - -*Though not-yet implemented, the bonobo roadmap includes implementing environment / .env files as well.* + + # Using an env file for default env values: + bonobo run csvsanitizer --default-env-file .default_env + + # Using an env file for env values: + bonobo run csvsanitizer --env-file '.env' + + +ENV File Structure +:::::::::::::::::: + +The file structure for env files is incredibly simple. The only text in the file +should be `NAME=value` pairs with one pair per line like the below. + +.. code-block:: text + + # .env + + DB_USER='bonobo' + DB_PASS='cicero' + Accessing Environment Variables from within the Graph Context ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: From bad598a4d7ce3986f61b4db4d0f6e0572bf61ac6 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 15 Oct 2017 16:52:14 -0400 Subject: [PATCH 022/145] Cast env_dir to string before passing to load_dotenv as passing a PosixPath to load_dotenv raises an exception in 3.5. --- bonobo/commands/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 8c0186c..a2a4edc 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -80,14 +80,14 @@ def read(filename, module, install=False, quiet=False, verbose=False, default_en env_dir = Path(filename).parent or Path(module).parent if default_env_file: for f in default_env_file: - env_file_path = env_dir.joinpath(f) + env_file_path = str(env_dir.joinpath(f)) load_dotenv(env_file_path) if default_env: for e in default_env: set_env_var(e) if env_file: for f in env_file: - env_file_path = env_dir.joinpath(f) + env_file_path = str(env_dir.joinpath(f)) load_dotenv(env_file_path, override=True) if env: for e in env: From 945d8501391d69ddc2c18948b63aa28b2daf917c Mon Sep 17 00:00:00 2001 From: CW Andrews Date: Sun, 15 Oct 2017 17:11:10 -0400 Subject: [PATCH 023/145] Update environment.rst Fixed typo (I think). --- docs/guide/environment.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/environment.rst b/docs/guide/environment.rst index ba21e62..ce199a1 100644 --- a/docs/guide/environment.rst +++ b/docs/guide/environment.rst @@ -59,7 +59,7 @@ The Examples below demonstrate setting one or multiple variables using both of t .. code-block:: bash - # Using one environment variable via --env and --defualt-env flags: + # Using one environment variable via a --env or --defualt-env flag: bonobo run csvsanitizer --env SECRET_TOKEN=secret123 bonobo run csvsanitizer --defaul-env SECRET_TOKEN=secret123 From 5d41f6df2d074908b56608f7d932652f61b1f042 Mon Sep 17 00:00:00 2001 From: CW Andrews Date: Sun, 15 Oct 2017 17:15:30 -0400 Subject: [PATCH 024/145] Update environment.rst Updated examples to use preferred naming conventions for default and private/local .env files per request of @hartym. --- docs/guide/environment.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/environment.rst b/docs/guide/environment.rst index ce199a1..972f45d 100644 --- a/docs/guide/environment.rst +++ b/docs/guide/environment.rst @@ -74,10 +74,10 @@ The Examples below demonstrate setting one or multiple variables using both of t SRC_FILE=inventory.txt DST_FILE=inventory_processed.csv bonobo run csvsanitizer # Using an env file for default env values: - bonobo run csvsanitizer --default-env-file .default_env + bonobo run csvsanitizer --default-env-file .env # Using an env file for env values: - bonobo run csvsanitizer --env-file '.env' + bonobo run csvsanitizer --env-file '.env.private' ENV File Structure From f1e9969a8843c493fd5155066cf35bf33b937384 Mon Sep 17 00:00:00 2001 From: arimbr Date: Sun, 15 Oct 2017 23:54:26 +0200 Subject: [PATCH 025/145] Add tests for bonobo init new directory and init within empty directory --- tests/test_commands.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_commands.py b/tests/test_commands.py index a29465c..e9ba1bd 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,6 +1,7 @@ import os import runpy import sys +import shutil from unittest.mock import patch import pkg_resources @@ -43,6 +44,27 @@ def test_no_command(runner, capsys): assert 'error: the following arguments are required: command' in err +@all_runners +def test_init(runner, capsys): + runner('init', 'project-name') + out, err = capsys.readouterr() + out = out.strip() + shutil.rmtree('project-name') + assert out == '' + + +@all_runners +def test_init_within_empty_directory(runner, capsys): + os.mkdir('empty-directory') + os.chdir('empty-directory') + runner('init', '.') + out, err = capsys.readouterr() + out = out.strip() + os.chdir('..') + shutil.rmtree('empty-directory') + assert out == '' + + @all_runners def test_run(runner, capsys): runner('run', '--quiet', get_examples_path('types/strings.py')) From d8c04138f6c3c7f9f0a1d38788e0ba4f90b128f1 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 15 Oct 2017 19:45:54 -0400 Subject: [PATCH 026/145] Updated Projectfile to include python-dotenv dependency. --- Projectfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Projectfile b/Projectfile index 0973c1f..73efcaa 100644 --- a/Projectfile +++ b/Projectfile @@ -45,6 +45,7 @@ python.add_requirements( 'psutil >=5.2,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0', + 'python-dotenv >=0.7.1,<1.0', dev=[ 'cookiecutter >=1.5,<1.6', 'pytest-sugar >=0.8,<0.9', From 3f3bda632cec45713f174c900db74b27b51a5a85 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 15 Oct 2017 19:50:27 -0400 Subject: [PATCH 027/145] Updated requirements files using edgy-project. --- Makefile | 6 +++--- requirements-dev.txt | 1 - requirements-docker.txt | 6 +++--- requirements-jupyter.txt | 3 +-- requirements.txt | 4 ++-- setup.py | 2 +- 6 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 221b012..c2f6f48 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-10-05 18:56:33.985014 +# Updated at 2017-10-15 19:49:24.146904 PACKAGE ?= bonobo PYTHON ?= $(shell which python) @@ -27,13 +27,13 @@ VERSION ?= $(shell git describe 2>/dev/null || echo dev) # Installs the local project dependencies. install: if [ -z "$(QUICK)" ]; then \ - $(PIP) install -U pip wheel $(PIP_INSTALL_OPTIONS) -r $(PYTHON_REQUIREMENTS_FILE) ; \ + $(PIP) install -U pip wheel $(PYTHON_PIP_INSTALL_OPTIONS) -r $(PYTHON_REQUIREMENTS_FILE) ; \ fi # Installs the local project dependencies, including development-only libraries. install-dev: if [ -z "$(QUICK)" ]; then \ - $(PIP) install -U pip wheel $(PIP_INSTALL_OPTIONS) -r $(PYTHON_REQUIREMENTS_DEV_FILE) ; \ + $(PIP) install -U pip wheel $(PYTHON_PIP_INSTALL_OPTIONS) -r $(PYTHON_REQUIREMENTS_DEV_FILE) ; \ fi # Cleans up the local mess. diff --git a/requirements-dev.txt b/requirements-dev.txt index 2fa4ef8..4e005a7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -23,7 +23,6 @@ pytest-sugar==0.8.0 pytest-timeout==1.2.0 pytest==3.2.3 python-dateutil==2.6.1 -python-dotenv==0.7.1 pytz==2017.2 requests==2.18.4 six==1.11.0 diff --git a/requirements-docker.txt b/requirements-docker.txt index 976b56d..9e68208 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,16 +1,16 @@ -e .[docker] appdirs==1.4.3 -bonobo-docker==0.2.11 +bonobo-docker==0.2.12 certifi==2017.7.27.1 chardet==3.0.4 colorama==0.3.9 docker-pycreds==0.2.1 docker==2.3.0 -fs==2.0.11 +fs==2.0.12 idna==2.6 packaging==16.8 pbr==3.1.1 -psutil==5.3.1 +psutil==5.4.0 pyparsing==2.2.0 pytz==2017.2 requests==2.18.4 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index e1b0ba7..e860433 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -1,5 +1,4 @@ -e .[jupyter] -appnope==0.1.0 bleach==2.1.1 decorator==4.1.2 entrypoints==0.2.3 @@ -19,7 +18,7 @@ markupsafe==1.0 mistune==0.7.4 nbconvert==5.3.1 nbformat==4.4.0 -notebook==5.1.0 +notebook==5.2.0 pandocfilters==1.4.2 parso==0.1.0 pexpect==4.2.1 diff --git a/requirements.txt b/requirements.txt index 9e604ed..7384e3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,11 @@ certifi==2017.7.27.1 chardet==3.0.4 click==6.7 colorama==0.3.9 -fs==2.0.11 +fs==2.0.12 idna==2.6 packaging==16.8 pbr==3.1.1 -psutil==5.3.1 +psutil==5.4.0 pyparsing==2.2.0 python-dotenv==0.7.1 pytz==2017.2 diff --git a/setup.py b/setup.py index be97d0c..7ff3be3 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ setup( include_package_data=True, install_requires=[ 'colorama (>= 0.3, < 1.0)', 'fs (>= 2.0, < 3.0)', 'packaging (>= 16, < 17)', 'psutil (>= 5.2, < 6.0)', - 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.21, < 2.0)' + 'python-dotenv (>= 0.7.1, < 1.0)', 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.21, < 2.0)' ], extras_require={ 'dev': [ From b87f674eb251eb1446d270b42d412691826b5953 Mon Sep 17 00:00:00 2001 From: cwandrews Date: Sun, 15 Oct 2017 19:55:37 -0400 Subject: [PATCH 028/145] Test tweak to work for Windows CI. --- tests/test_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 0e6205d..281a996 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -234,7 +234,7 @@ class TestDefaultEnvVars(object): runner( 'run', '--quiet', get_examples_path('environment/env_vars/get_passed_env.py'), - '--default-env', 'USER=clowncity' + '--default-env', 'USER=clowncity', '--env', 'USER=ted' ) out, err = capsys.readouterr() out = out.split('\n') From 650b49a41a41012b583fc0188dc911645195a25d Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 22 Oct 2017 09:24:34 +0200 Subject: [PATCH 029/145] [django, google] Implements basic extensions for django and google oauth systems. Using those extensions means you have the correct dependencies installed, and that you know about the external system. Django: just provide an ETLCommand class that contains all the shortcuts to write django management commands based on Bonobo. Google: shortcuts to create the necessary objects for oauth flow, with local caching of credentials. Both those extensions are not stable and will evolve. --- bonobo/events.py | 3 +++ bonobo/execution/base.py | 12 +-------- bonobo/execution/graph.py | 9 +++++++ bonobo/execution/plugin.py | 6 ++--- bonobo/ext/console.py | 45 ++++++++++++++++++++++----------- bonobo/ext/django.py | 47 +++++++++++++++++++++++++++++++++++ bonobo/ext/google.py | 43 ++++++++++++++++++++++++++++++++ bonobo/plugins.py | 28 +-------------------- bonobo/strategies/executor.py | 1 + docs/conf.py | 5 ++-- 10 files changed, 141 insertions(+), 58 deletions(-) create mode 100644 bonobo/events.py create mode 100644 bonobo/ext/django.py create mode 100644 bonobo/ext/google.py diff --git a/bonobo/events.py b/bonobo/events.py new file mode 100644 index 0000000..9a0cbba --- /dev/null +++ b/bonobo/events.py @@ -0,0 +1,3 @@ +ON_START = 'bonobo.on_start' +ON_TICK = 'bonobo.on_tick' +ON_STOP = 'bonobo.on_stop' diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py index 81ac74e..b9bce36 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/base.py @@ -4,8 +4,7 @@ from time import sleep from bonobo.config import create_container from bonobo.config.processors import ContextCurrifier -from bonobo.plugins import get_enhancers -from bonobo.util import inspect_node, isconfigurabletype +from bonobo.util import isconfigurabletype from bonobo.util.errors import print_error from bonobo.util.objects import Wrapper, get_name @@ -56,9 +55,6 @@ class LoopingExecutionContext(Wrapper): self._started, self._stopped = False, False self._stack = None - # XXX enhancers - self._enhancers = get_enhancers(self.wrapped) - def __enter__(self): self.start() return self @@ -79,15 +75,9 @@ class LoopingExecutionContext(Wrapper): raise TypeError( 'The Configurable should be fully instanciated by now, unfortunately I got a PartiallyConfigured object...' ) - # XXX enhance that, maybe giving hints on what's missing. - # print(inspect_node(self.wrapped)) self._stack.setup(self) - for enhancer in self._enhancers: - with unrecoverable(self.handle_error): - enhancer.start(self) - def loop(self): """Generic loop. A bit boring. """ while self.alive: diff --git a/bonobo/execution/graph.py b/bonobo/execution/graph.py index 77e01fa..33e77bc 100644 --- a/bonobo/execution/graph.py +++ b/bonobo/execution/graph.py @@ -5,6 +5,7 @@ from bonobo.config import create_container from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext from bonobo.execution.plugin import PluginExecutionContext +from whistle import EventDispatcher class GraphExecutionContext: @@ -23,6 +24,14 @@ class GraphExecutionContext: def alive(self): return any(node.alive for node in self.nodes) + @property + def dispatcher(self): + try: + return self._dispatcher + except AttributeError: + self._dispatcher = EventDispatcher() + return self._dispatcher + def __init__(self, graph, plugins=None, services=None): self.graph = graph self.nodes = [self.create_node_execution_context_for(node) for node in self.graph] diff --git a/bonobo/execution/plugin.py b/bonobo/execution/plugin.py index a207f23..3379fc0 100644 --- a/bonobo/execution/plugin.py +++ b/bonobo/execution/plugin.py @@ -13,14 +13,14 @@ class PluginExecutionContext(LoopingExecutionContext): super().start() with recoverable(self.handle_error): - self.wrapped.initialize() + self.wrapped.on_start() def shutdown(self): if self.started: with recoverable(self.handle_error): - self.wrapped.finalize() + self.wrapped.on_stop() self.alive = False def step(self): with recoverable(self.handle_error): - self.wrapped.run() + self.wrapped.on_tick() diff --git a/bonobo/ext/console.py b/bonobo/ext/console.py index 1053e04..02bcafa 100644 --- a/bonobo/ext/console.py +++ b/bonobo/ext/console.py @@ -1,6 +1,6 @@ import io import sys -from contextlib import redirect_stdout +from contextlib import redirect_stdout, redirect_stderr from colorama import Style, Fore, init @@ -50,35 +50,50 @@ class ConsoleOutputPlugin(Plugin): """ - def initialize(self): + # Standard outputs descriptors backup here, also used to override if needed. + _stdout = sys.stdout + _stderr = sys.stderr + + # When the plugin is started, we'll set the real value of this. + isatty = False + + # Whether we're on windows, or a real operating system. + iswindows = (sys.platform == 'win32') + + def on_start(self): self.prefix = '' self.counter = 0 self._append_cache = '' - self.isatty = sys.stdout.isatty() - self.iswindows = (sys.platform == 'win32') - self._stdout = sys.stdout + self.isatty = self._stdout.isatty() + self.stdout = IOBuffer() self.redirect_stdout = redirect_stdout(self._stdout if self.iswindows else self.stdout) self.redirect_stdout.__enter__() - def run(self): + self.stderr = IOBuffer() + self.redirect_stderr = redirect_stderr(self._stderr if self.iswindows else self.stderr) + self.redirect_stderr.__enter__() + + def on_tick(self): if self.isatty and not self.iswindows: self._write(self.context.parent, rewind=True) else: pass # not a tty, or windows, so we'll ignore stats output - def finalize(self): + def on_stop(self): self._write(self.context.parent, rewind=False) + self.redirect_stderr.__exit__(None, None, None) self.redirect_stdout.__exit__(None, None, None) def write(self, context, prefix='', rewind=True, append=None): t_cnt = len(context) if not self.iswindows: - buffered = self.stdout.switch() - for line in buffered.split('\n')[:-1]: - print(line + CLEAR_EOL, file=sys.stderr) + for line in self.stdout.switch().split('\n')[:-1]: + print(line + CLEAR_EOL, file=self._stdout) + for line in self.stderr.switch().split('\n')[:-1]: + print(line + CLEAR_EOL, file=self._stderr) alive_color = Style.BRIGHT dead_color = Style.BRIGHT + Fore.BLACK @@ -117,7 +132,7 @@ class ConsoleOutputPlugin(Plugin): ' ', ) ) - print(prefix + _line + '\033[0K', file=sys.stderr) + print(prefix + _line + CLEAR_EOL, file=self._stderr) if append: # todo handle multiline @@ -128,13 +143,13 @@ class ConsoleOutputPlugin(Plugin): CLEAR_EOL ) ), - file=sys.stderr + file=self._stderr ) t_cnt += 1 if rewind: - print(CLEAR_EOL, file=sys.stderr) - print(MOVE_CURSOR_UP(t_cnt + 2), file=sys.stderr) + print(CLEAR_EOL, file=self._stderr) + print(MOVE_CURSOR_UP(t_cnt + 2), file=self._stderr) def _write(self, graph_context, rewind): if settings.PROFILE.get(): @@ -154,4 +169,4 @@ class ConsoleOutputPlugin(Plugin): def memory_usage(): import os, psutil process = psutil.Process(os.getpid()) - return process.memory_info()[0] / float(2**20) + return process.memory_info()[0] / float(2 ** 20) diff --git a/bonobo/ext/django.py b/bonobo/ext/django.py new file mode 100644 index 0000000..1bd3fff --- /dev/null +++ b/bonobo/ext/django.py @@ -0,0 +1,47 @@ +from colorama import Fore, Back, Style +from django.core.management.base import BaseCommand, OutputWrapper +from logging import getLogger + +import bonobo +import bonobo.util +from bonobo.commands.run import get_default_services +from bonobo.ext.console import ConsoleOutputPlugin +from bonobo.util.term import CLEAR_EOL + +class ETLCommand(BaseCommand): + GraphType = bonobo.Graph + + def get_graph(self, *args, **options): + def not_implemented(): + raise NotImplementedError('You must implement {}.get_graph() method.'.format(self)) + + return self.GraphType(not_implemented) + + def get_services(self): + return get_default_services(type(self).__file__) + + @property + def logger(self): + try: + return self._logger + except AttributeError: + self._logger = getLogger(type(self).__module__) + return self._logger + + def info(self, *args, **kwargs): + self.logger.info(*args, **kwargs) + + def handle(self, *args, **options): + _stdout_backup, _stderr_backup = self.stdout, self.stderr + + self.stdout = OutputWrapper(ConsoleOutputPlugin._stdout, ending=CLEAR_EOL + '\n') + self.stderr = OutputWrapper(ConsoleOutputPlugin._stderr, ending=CLEAR_EOL + '\n') + self.stderr.style_func = lambda x: Fore.LIGHTRED_EX + Back.RED + '!' + Style.RESET_ALL + ' ' + x + + result = bonobo.run( + self.get_graph(*args, **options), + services=self.get_services(), + ) + self.stdout = _stdout_backup + + return '\nReturn Value: ' + str(result) diff --git a/bonobo/ext/google.py b/bonobo/ext/google.py new file mode 100644 index 0000000..920af9d --- /dev/null +++ b/bonobo/ext/google.py @@ -0,0 +1,43 @@ +import os + +import httplib2 +from apiclient import discovery +from oauth2client import client, tools +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(): + """Gets valid user credentials from storage. + + If nothing has been stored, or if the stored credentials are invalid, + the OAuth2 flow is completed to obtain the new credentials. + + Returns: + Credentials, the obtained credential. + """ + credential_dir = os.path.join(HOME_DIR, '.cache', __package__, 'credentials') + if not os.path.exists(credential_dir): + os.makedirs(credential_dir) + credential_path = os.path.join(credential_dir, 'googleapis.json') + + store = Storage(credential_path) + credentials = store.get() + if not credentials or credentials.invalid: + flow = client.flow_from_clientsecrets(GOOGLE_SECRETS, GOOGLE_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) + print('Storing credentials to ' + credential_path) + return credentials + + +def get_google_spreadsheets_api_client(): + credentials = get_credentials() + 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) diff --git a/bonobo/plugins.py b/bonobo/plugins.py index 4fa1e18..7a0f5d1 100644 --- a/bonobo/plugins.py +++ b/bonobo/plugins.py @@ -1,7 +1,3 @@ -from bonobo.config import Configurable -from bonobo.util.objects import get_attribute_or_create - - class Plugin: """ A plugin is an extension to the core behavior of bonobo. If you're writing transformations, you should not need @@ -11,30 +7,8 @@ class Plugin: respectively permits an interactive output on an ANSI console and a rich output in a jupyter notebook. Warning: THE PLUGIN API IS PRE-ALPHA AND WILL EVOLVE BEFORE 1.0, DO NOT RELY ON IT BEING STABLE! - + """ def __init__(self, context): self.context = context - - def initialize(self): - pass - - def run(self): - pass - - def finalize(self): - pass - - -def get_enhancers(obj): - try: - return get_attribute_or_create(obj, '__enhancers__', list()) - except AttributeError: - return list() - - -class NodeEnhancer(Configurable): - def __matmul__(self, other): - get_enhancers(other).append(self) - return other diff --git a/bonobo/strategies/executor.py b/bonobo/strategies/executor.py index 3bfabc6..57e2524 100644 --- a/bonobo/strategies/executor.py +++ b/bonobo/strategies/executor.py @@ -6,6 +6,7 @@ from bonobo.constants import BEGIN, END from bonobo.strategies.base import Strategy from bonobo.structs.bags import Bag from bonobo.util.errors import print_error +from whistle import EventDispatcher class ExecutorStrategy(Strategy): diff --git a/docs/conf.py b/docs/conf.py index afbbe83..93895a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import sys +import datetime import os +import sys sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('_themes')) @@ -36,8 +37,8 @@ master_doc = 'index' # General information about the project. project = 'Bonobo' -copyright = '2012-2017, Romain Dorgueil' author = 'Romain Dorgueil' +copyright = '2012-{}, {}'.format(datetime.datetime.now().year, author) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 42c1fee6f112b83809e87cb34b228cffc403e64c Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 22 Oct 2017 16:21:44 +0200 Subject: [PATCH 030/145] [core] (..., dict) means Bag(..., **dict) --- bonobo/execution/node.py | 2 ++ bonobo/ext/django.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index 2aa626c..28a20b3 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -159,6 +159,8 @@ def _resolve(input_bag, output): return Bag(**output) if istuple(output): + if len(output) > 1 and isdict(output[-1]): + return Bag(*output[0:-1], **output[-1]) return Bag(*output) # Either we use arg0 format, either it's "just" a value. diff --git a/bonobo/ext/django.py b/bonobo/ext/django.py index 1bd3fff..232fd5a 100644 --- a/bonobo/ext/django.py +++ b/bonobo/ext/django.py @@ -1,6 +1,7 @@ +from logging import getLogger + from colorama import Fore, Back, Style from django.core.management.base import BaseCommand, OutputWrapper -from logging import getLogger import bonobo import bonobo.util From 0c58d21b12211146f26b0aa4f508d05de4010068 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 22 Oct 2017 16:22:28 +0200 Subject: [PATCH 031/145] [django, misc] adds create_or_update to djangos ETLCommand class, adds getitem/setitem/contains dunders to ValueHolder. --- bonobo/ext/django.py | 25 +++++++++++++++++++++++++ bonobo/util/objects.py | 17 +++++++++++------ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/bonobo/ext/django.py b/bonobo/ext/django.py index 232fd5a..8d73428 100644 --- a/bonobo/ext/django.py +++ b/bonobo/ext/django.py @@ -9,9 +9,34 @@ from bonobo.commands.run import get_default_services from bonobo.ext.console import ConsoleOutputPlugin from bonobo.util.term import CLEAR_EOL + class ETLCommand(BaseCommand): GraphType = bonobo.Graph + def create_or_update(self, model, *, defaults=None, save=True, **kwargs): + """ + Create or update a django model instance. + + :param model: + :param defaults: + :param kwargs: + :return: object, created, updated + + """ + obj, created = model._default_manager.get_or_create(defaults=defaults, **kwargs) + + updated = False + if not created: + for k, v in defaults.items(): + if getattr(obj, k) != v: + setattr(obj, k, v) + updated = True + + if updated and save: + obj.save() + + return obj, created, updated + def get_graph(self, *args, **options): def not_implemented(): raise NotImplementedError('You must implement {}.get_graph() method.'.format(self)) diff --git a/bonobo/util/objects.py b/bonobo/util/objects.py index 34fc6e7..acae2ad 100644 --- a/bonobo/util/objects.py +++ b/bonobo/util/objects.py @@ -1,7 +1,3 @@ -import functools -from functools import partial - - def get_name(mixed): try: return mixed.__name__ @@ -146,10 +142,10 @@ class ValueHolder: return divmod(other, self._value) def __pow__(self, other): - return self._value**other + return self._value ** other def __rpow__(self, other): - return other**self._value + return other ** self._value def __ipow__(self, other): self._value **= other @@ -220,6 +216,15 @@ class ValueHolder: def __len__(self): return len(self._value) + def __contains__(self, item): + return item in self._value + + def __getitem__(self, item): + return self._value[item] + + def __setitem__(self, key, value): + self._value[key] = value + def get_attribute_or_create(obj, attr, default): try: From 321bb83aa4e2174f9db28049c824176ed473750a Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 22 Oct 2017 16:57:46 +0200 Subject: [PATCH 032/145] Documentation for new behaviour. --- docs/guide/transformations.rst | 94 ++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/docs/guide/transformations.rst b/docs/guide/transformations.rst index e108a44..5b6b954 100644 --- a/docs/guide/transformations.rst +++ b/docs/guide/transformations.rst @@ -32,6 +32,100 @@ Iterable Something we can iterate on, in python, so basically anything you'd be able to use in a `for` loop. +Concepts +:::::::: + +Whatever kind of transformation you want to use, there are a few common concepts you should know about. + +Input +----- + +All input is retrieved via the call arguments. Each line of input means one call to the callable provided. Arguments +will be, in order: + +* Injected dependencies (database, http, filesystem, ...) +* Position based arguments +* Keyword based arguments + +You'll see below how to pass each of those. + +Output +------ + +Each callable can return/yield different things (all examples will use yield, but if there is only one output per input +line, you can also return your output row and expect the exact same behaviour). + +Let's see the rules (first to match wins). + +1. A flag, eventually followed by something else, marks a special behaviour. If it supports it, the remaining part of + the output line will be interpreted using the same rules, and some flags can be combined. + + **NOT_MODIFIED** + + **NOT_MODIFIED** tells bonobo to use the input row unmodified as the output. + + *CANNOT be combined* + + Example: + + .. code-block:: python + + from bonobo import NOT_MODIFIED + + def output_will_be_same_as_input(*args, **kwargs): + yield NOT_MODIFIED + + **APPEND** + + **APPEND** tells bonobo to append this output to the input (positional arguments will equal `input_args + output_args`, + keyword arguments will equal `{**input_kwargs, **output_kwargs}`). + + *CAN be combined, but not with itself* + + .. code-block:: python + + from bonobo import APPEND + + def output_will_be_appended_to_input(*args, **kwargs): + yield APPEND, 'foo', 'bar', {'eat_at': 'joe'} + + **LOOPBACK** + + **LOOPBACK** tells bonobo that this output must be looped back into our own input queue, allowing to create the stream + processing version of recursive algorithms. + + *CAN be combined, but not with itself* + + .. code-block:: python + + from bonobo import LOOPBACK + + def output_will_be_sent_to_self(*args, **kwargs): + yield LOOPBACK, 'Hello, I am the future "you".' + + **CHANNEL(...)** + + **CHANNEL(...)** tells bonobo that this output does not use the default channel and is routed through another path. + This is something you should probably not use unless your data flow design is complex, and if you're not certain + about it, it probably means that it is not the feature you're looking for. + + *CAN be combined, but not with itself* + + .. code-block:: python + + from bonobo import CHANNEL + + def output_will_be_sent_to_self(*args, **kwargs): + yield CHANNEL("errors"), 'That is not cool.' + +2. Once all flags are "consumed", the remaining part is interpreted. + + * If it is a :class:`bonobo.Bag` instance, then it's used directly. + * If it is a :class:`dict` then a kwargs-only :class:`bonobo.Bag` will be created. + * If it is a :class:`tuple` then an args-only :class:`bonobo.Bag` will be created, unless its last argument is a + :class:`dict` in which case a args+kwargs :class:`bonobo.Bag` will be created. + * If it's something else, it will be used to create a one-arg-only :class:`bonobo.Bag`. + Function based transformations :::::::::::::::::::::::::::::: From 80006ba24dde40df616a47cefa85e185550dc914 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 22 Oct 2017 17:23:19 +0200 Subject: [PATCH 033/145] [tests] fix uncaptured output in test_commands --- tests/test_commands.py | 147 ++++++++++++++++++++--------------------- 1 file changed, 71 insertions(+), 76 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 1b09a45..605892b 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,6 +1,9 @@ +import functools +import io import os import runpy import sys +from contextlib import redirect_stdout, redirect_stderr from unittest.mock import patch import pkg_resources @@ -10,12 +13,27 @@ from bonobo import __main__, __version__, get_examples_path from bonobo.commands import entrypoint -def runner_entrypoint(*args): +def runner(f): + @functools.wraps(f) + def wrapped_runner(*args): + with redirect_stdout(io.StringIO()) as stdout, redirect_stderr(io.StringIO()) as stderr: + try: + f(list(args)) + except BaseException as exc: + return stdout.getvalue(), stderr.getvalue(), exc + return stdout.getvalue(), stderr.getvalue() + + return wrapped_runner + + +@runner +def runner_entrypoint(args): """ Run bonobo using the python command entrypoint directly (bonobo.commands.entrypoint). """ - return entrypoint(list(args)) + return entrypoint(args) -def runner_module(*args): +@runner +def runner_module(args): """ Run bonobo using the bonobo.__main__ file, which is equivalent as doing "python -m bonobo ...".""" with patch.object(sys, 'argv', ['bonobo', *args]): return runpy.run_path(__main__.__file__, run_name='__main__') @@ -40,17 +58,15 @@ def test_entrypoint(): @all_runners -def test_no_command(runner, capsys): - with pytest.raises(SystemExit): - runner() - _, err = capsys.readouterr() +def test_no_command(runner): + _, err, exc = runner() + assert type(exc) == SystemExit assert 'error: the following arguments are required: command' in err @all_runners -def test_run(runner, capsys): - runner('run', '--quiet', get_examples_path('types/strings.py')) - out, err = capsys.readouterr() +def test_run(runner): + out, err = runner('run', '--quiet', get_examples_path('types/strings.py')) out = out.split('\n') assert out[0].startswith('Foo ') assert out[1].startswith('Bar ') @@ -58,9 +74,8 @@ def test_run(runner, capsys): @all_runners -def test_run_module(runner, capsys): - runner('run', '--quiet', '-m', 'bonobo.examples.types.strings') - out, err = capsys.readouterr() +def test_run_module(runner): + out, err = runner('run', '--quiet', '-m', 'bonobo.examples.types.strings') out = out.split('\n') assert out[0].startswith('Foo ') assert out[1].startswith('Bar ') @@ -68,9 +83,8 @@ def test_run_module(runner, capsys): @all_runners -def test_run_path(runner, capsys): - runner('run', '--quiet', get_examples_path('types')) - out, err = capsys.readouterr() +def test_run_path(runner): + out, err = runner('run', '--quiet', get_examples_path('types')) out = out.split('\n') assert out[0].startswith('Foo ') assert out[1].startswith('Bar ') @@ -94,9 +108,8 @@ def test_install_requirements_for_file(runner): @all_runners -def test_version(runner, capsys): - runner('version') - out, err = capsys.readouterr() +def test_version(runner): + out, err = runner('version') out = out.strip() assert out.startswith('bonobo ') assert __version__ in out @@ -104,48 +117,44 @@ def test_version(runner, capsys): @all_runners class TestDefaultEnvFile(object): - def test_run_file_with_default_env_file(self, runner, capsys): - runner( + def test_run_file_with_default_env_file(self, runner): + out, err = runner( 'run', '--quiet', '--default-env-file', '.env_one', get_examples_path('environment/env_files/get_passed_env_file.py') ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '321' assert out[1] == 'sweetpassword' assert out[2] != 'marzo' - def test_run_file_with_multiple_default_env_files(self, runner, capsys): - runner( + def test_run_file_with_multiple_default_env_files(self, runner): + out, err = runner( 'run', '--quiet', '--default-env-file', '.env_one', '--default-env-file', '.env_two', get_examples_path('environment/env_files/get_passed_env_file.py') ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '321' assert out[1] == 'sweetpassword' assert out[2] != 'marzo' - def test_run_module_with_default_env_file(self, runner, capsys): - runner( + def test_run_module_with_default_env_file(self, runner): + out, err = runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', '--default-env-file', '.env_one' ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '321' assert out[1] == 'sweetpassword' assert out[2] != 'marzo' - def test_run_module_with_multiple_default_env_files(self, runner, capsys): - runner( + def test_run_module_with_multiple_default_env_files(self, runner): + out, err = runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', '--default-env-file', '.env_one', '--default-env-file', '.env_two', ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '321' assert out[1] == 'sweetpassword' @@ -154,49 +163,45 @@ class TestDefaultEnvFile(object): @all_runners class TestEnvFile(object): - def test_run_file_with_file(self, runner, capsys): - runner( + def test_run_file_with_file(self, runner): + out, err = runner( 'run', '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), '--env-file', '.env_one', ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '321' assert out[1] == 'sweetpassword' assert out[2] == 'marzo' - def test_run_file_with_multiple_files(self, runner, capsys): - runner( + def test_run_file_with_multiple_files(self, runner): + out, err = runner( 'run', '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), '--env-file', '.env_one', '--env-file', '.env_two', ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '321' assert out[1] == 'not_sweet_password' assert out[2] == 'abril' - def test_run_module_with_file(self, runner, capsys): - runner( + def test_run_module_with_file(self, runner): + out, err = runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', '--env-file', '.env_one', ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '321' assert out[1] == 'sweetpassword' assert out[2] == 'marzo' - def test_run_module_with_multiple_files(self, runner, capsys): - runner( + def test_run_module_with_multiple_files(self, runner): + out, err = runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', '--env-file', '.env_one', '--env-file', '.env_two', ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '321' assert out[1] == 'not_sweet_password' @@ -204,28 +209,26 @@ class TestEnvFile(object): @all_runners -class TestEnvFileCombinations(object): - def test_run_file_with_default_env_file_and_env_file(self, runner, capsys): - runner( +class TestEnvFileCombinations: + def test_run_file_with_default_env_file_and_env_file(self, runner): + out, err = runner( 'run', '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), '--default-env-file', '.env_one', '--env-file', '.env_two', ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '321' assert out[1] == 'not_sweet_password' assert out[2] == 'abril' - def test_run_file_with_default_env_file_and_env_file_and_env_vars(self, runner, capsys): - runner( + def test_run_file_with_default_env_file_and_env_file_and_env_vars(self, runner): + out, err = runner( 'run', '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), '--default-env-file', '.env_one', '--env-file', '.env_two', '--env', 'TEST_USER_PASSWORD=SWEETpassWORD', '--env', 'MY_SECRET=444', ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == '444' assert out[1] == 'SWEETpassWORD' @@ -233,54 +236,50 @@ class TestEnvFileCombinations(object): @all_runners -class TestDefaultEnvVars(object): - def test_run_file_with_default_env_var(self, runner, capsys): - runner( +class TestDefaultEnvVars: + def test_run_file_with_default_env_var(self, runner): + out, err = runner( 'run', '--quiet', get_examples_path('environment/env_vars/get_passed_env.py'), '--default-env', 'USER=clowncity', '--env', 'USER=ted' ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == 'user' assert out[1] == 'number' assert out[2] == 'string' assert out[3] != 'clowncity' - def test_run_file_with_default_env_vars(self, runner, capsys): - runner( + def test_run_file_with_default_env_vars(self, runner): + out, err = runner( 'run', '--quiet', get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', '--default-env', "ENV_TEST_STRING='my_test_string'" ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == 'cwandrews' assert out[1] == '123' assert out[2] == 'my_test_string' - def test_run_module_with_default_env_var(self, runner, capsys): - runner( + def test_run_module_with_default_env_var(self, runner): + out, err = runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', 'ENV_TEST_NUMBER=123', '--default-env', 'ENV_TEST_STRING=string' ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == 'cwandrews' assert out[1] == '123' assert out[2] != 'string' - def test_run_module_with_default_env_vars(self, runner, capsys): - runner( + def test_run_module_with_default_env_vars(self, runner): + out, err = runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', '--default-env', "ENV_TEST_STRING='string'" ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == 'cwandrews' assert out[1] == '123' @@ -288,52 +287,48 @@ class TestDefaultEnvVars(object): @all_runners -class TestEnvVars(object): - def test_run_file_with_env_var(self, runner, capsys): - runner( +class TestEnvVars: + def test_run_file_with_env_var(self, runner): + out, err = runner( 'run', '--quiet', get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123' ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] != 'test_user' assert out[1] == '123' assert out[2] == 'my_test_string' - def test_run_file_with_env_vars(self, runner, capsys): - runner( + def test_run_file_with_env_vars(self, runner): + out, err = runner( 'run', '--quiet', get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'" ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == 'cwandrews' assert out[1] == '123' assert out[2] == 'my_test_string' - def test_run_module_with_env_var(self, runner, capsys): - runner( + def test_run_module_with_env_var(self, runner): + out, err = runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', 'ENV_TEST_NUMBER=123' ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == 'cwandrews' assert out[1] == '123' assert out[2] == 'my_test_string' - def test_run_module_with_env_vars(self, runner, capsys): - runner( + def test_run_module_with_env_vars(self, runner): + out, err = runner( 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'" ) - out, err = capsys.readouterr() out = out.split('\n') assert out[0] == 'cwandrews' assert out[1] == '123' From bc01b5d404ed7a79ee6eb6b20cae2c5bf84353eb Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 22 Oct 2017 17:37:49 +0200 Subject: [PATCH 034/145] [core] simplification of result interpretation. --- .../env_files/get_passed_env_file.py | 4 +++- .../environment/env_vars/get_passed_env.py | 6 ++++- bonobo/execution/node.py | 22 ++++++++---------- bonobo/util/iterators.py | 9 -------- tests/execution/test_node.py | 23 +++++++++++++++++++ 5 files changed, 41 insertions(+), 23 deletions(-) diff --git a/bonobo/examples/environment/env_files/get_passed_env_file.py b/bonobo/examples/environment/env_files/get_passed_env_file.py index bb83e67..bb45540 100644 --- a/bonobo/examples/environment/env_files/get_passed_env_file.py +++ b/bonobo/examples/environment/env_files/get_passed_env_file.py @@ -8,7 +8,9 @@ def extract(): test_user_password = os.getenv('TEST_USER_PASSWORD') path = os.getenv('PATH') - return my_secret, test_user_password, path + yield my_secret + yield test_user_password + yield path def load(s: str): diff --git a/bonobo/examples/environment/env_vars/get_passed_env.py b/bonobo/examples/environment/env_vars/get_passed_env.py index f236ba7..e0c6c45 100644 --- a/bonobo/examples/environment/env_vars/get_passed_env.py +++ b/bonobo/examples/environment/env_vars/get_passed_env.py @@ -8,7 +8,11 @@ def extract(): env_test_number = os.getenv('ENV_TEST_NUMBER', 'number') env_test_string = os.getenv('ENV_TEST_STRING', 'string') env_user = os.getenv('USER') - return env_test_user, env_test_number, env_test_string, env_user + + yield env_test_user + yield env_test_number + yield env_test_string + yield env_user def load(s: str): diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index 28a20b3..c943ba0 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -1,6 +1,7 @@ import traceback from queue import Empty from time import sleep +from types import GeneratorType from bonobo import settings from bonobo.constants import INHERIT_INPUT, NOT_MODIFIED, BEGIN, END @@ -10,7 +11,6 @@ from bonobo.structs.bags import Bag from bonobo.structs.inputs import Input from bonobo.util import get_name, iserrorbag, isloopbackbag, isdict, istuple from bonobo.util.compat import deprecated_alias -from bonobo.util.iterators import iter_if_not_sequence from bonobo.util.statistics import WithStatistics @@ -120,23 +120,21 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): def handle_results(self, input_bag, results): # self._exec_time += timer.duration # Put data onto output channels - try: - results = iter_if_not_sequence(results) - except TypeError: # not an iterator - if results: - self.send(_resolve(input_bag, results)) - else: - # case with no result, an execution went through anyway, use for stats. - # self._exec_count += 1 - pass - else: - while True: # iterator + + if isinstance(results, GeneratorType): + while True: try: result = next(results) except StopIteration: break else: self.send(_resolve(input_bag, result)) + elif results: + self.send(_resolve(input_bag, results)) + else: + # case with no result, an execution went through anyway, use for stats. + # self._exec_count += 1 + pass def _resolve(input_bag, output): diff --git a/bonobo/util/iterators.py b/bonobo/util/iterators.py index ee45614..1ed09ac 100644 --- a/bonobo/util/iterators.py +++ b/bonobo/util/iterators.py @@ -37,12 +37,3 @@ def tuplize(generator): return tuplized -def iter_if_not_sequence(mixed): - if isinstance(mixed, ( - dict, - list, - str, - bytes, - )): - raise TypeError(type(mixed).__name__) - return iter(mixed) diff --git a/tests/execution/test_node.py b/tests/execution/test_node.py index 23748d4..7870323 100644 --- a/tests/execution/test_node.py +++ b/tests/execution/test_node.py @@ -102,3 +102,26 @@ def test_node_dict_chained(): assert len(output) == 2 assert output[0] == {'id': 1, 'name': 'FOO'} assert output[1] == {'id': 2, 'name': 'BAR'} + +def test_node_tuple(): + def f(): + return 'foo', 'bar' + + with BufferingNodeExecutionContext(f) as context: + context.write_sync(Bag()) + output = context.get_buffer() + + assert len(output) == 1 + assert output[0] == ('foo', 'bar') + + def g(): + yield 'foo', 'bar' + yield 'foo', 'baz' + + with BufferingNodeExecutionContext(g) as context: + context.write_sync(Bag()) + output = context.get_buffer() + + assert len(output) == 2 + assert output[0] == ('foo', 'bar') + assert output[1] == ('foo', 'baz') From 28fe41c0bdd0d7d8f4784da540650d5c192575b0 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 22 Oct 2017 18:00:16 +0200 Subject: [PATCH 035/145] [core] Testing and fixing new args/kwargs behaviour. --- bonobo/structs/bags.py | 27 ++++++++-------- tests/execution/test_node.py | 63 ++++++++++++++++++++++++++++++++---- tests/structs/test_bags.py | 17 +++++++++- 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/bonobo/structs/bags.py b/bonobo/structs/bags.py index 20db9fa..22b507d 100644 --- a/bonobo/structs/bags.py +++ b/bonobo/structs/bags.py @@ -96,7 +96,7 @@ class Bag: @classmethod def inherit(cls, *args, **kwargs): - return cls(*args, _flags=(INHERIT_INPUT, ), **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. @@ -105,23 +105,24 @@ class Bag: if isinstance(other, Bag) and other.args == self.args and other.kwargs == self.kwargs: return True - # tuple of (tuple, dict) - if isinstance(other, tuple) and len(other) == 2 and other[0] == self.args and other[1] == self.kwargs: - return True + # tuple + if isinstance(other, tuple): + # self == () + if not len(other): + return not self.args and not self.kwargs - # tuple (aka args) - if isinstance(other, tuple) and other == self.args: - return True + 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 - # arg0 - if len(self.args) == 1 and not len(self.kwargs) and self.args[0] == other: - return True - - return False + return len(self.args) == 1 and not self.kwargs and self.args[0] == other def __repr__(self): return '<{} ({})>'.format( @@ -135,7 +136,7 @@ class Bag: class LoopbackBag(Bag): - default_flags = (LOOPBACK, ) + default_flags = (LOOPBACK,) class ErrorBag(Bag): diff --git a/tests/execution/test_node.py b/tests/execution/test_node.py index 7870323..cb0f9c3 100644 --- a/tests/execution/test_node.py +++ b/tests/execution/test_node.py @@ -12,7 +12,7 @@ def test_node_string(): output = context.get_buffer() assert len(output) == 1 - assert output[0] == (('foo', ), {}) + assert output[0] == 'foo' def g(): yield 'foo' @@ -23,8 +23,8 @@ def test_node_string(): 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(): @@ -36,7 +36,7 @@ def test_node_bytes(): output = context.get_buffer() assert len(output) == 1 - assert output[0] == ((b'foo', ), {}) + assert output[0] == b'foo' def g(): yield b'foo' @@ -47,8 +47,8 @@ def test_node_bytes(): 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(): @@ -125,3 +125,54 @@ def test_node_tuple(): assert len(output) == 2 assert output[0] == ('foo', 'bar') assert output[1] == ('foo', 'baz') + +def test_node_tuple_chained(): + strategy = NaiveStrategy(GraphExecutionContextType=BufferingGraphExecutionContext) + + def uppercase(*args): + return tuple(map(str.upper, args)) + + def f(): + return 'foo', 'bar' + + graph = Graph(f, uppercase) + context = strategy.execute(graph) + output = context.get_buffer() + + assert len(output) == 1 + assert output[0] == ('FOO', 'BAR') + + def g(): + yield 'foo', 'bar' + yield 'foo', 'baz' + + graph = Graph(g, uppercase) + context = strategy.execute(graph) + output = context.get_buffer() + + assert len(output) == 2 + assert output[0] == ('FOO', 'BAR') + assert output[1] == ('FOO', 'BAZ') + +def test_node_tuple_dict(): + def f(): + return 'foo', 'bar', {'id': 1} + + with BufferingNodeExecutionContext(f) as context: + context.write_sync(Bag()) + output = context.get_buffer() + + assert len(output) == 1 + assert output[0] == ('foo', 'bar', {'id': 1}) + + def g(): + yield 'foo', 'bar', {'id': 1} + yield 'foo', 'baz', {'id': 2} + + with BufferingNodeExecutionContext(g) as context: + context.write_sync(Bag()) + output = context.get_buffer() + + assert len(output) == 2 + assert output[0] == ('foo', 'bar', {'id': 1}) + assert output[1] == ('foo', 'baz', {'id': 2}) diff --git a/tests/structs/test_bags.py b/tests/structs/test_bags.py index d52a6c6..53d3ae7 100644 --- a/tests/structs/test_bags.py +++ b/tests/structs/test_bags.py @@ -92,13 +92,28 @@ def test_pickle(): assert unpickled == bag -def test_eq_operator(): +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) From 3c453f0be74dc52d4d8d8a4c64fbc0c30e72f3a6 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 22 Oct 2017 18:05:39 +0200 Subject: [PATCH 036/145] Code formating. --- Makefile | 4 +- bonobo/commands/run.py | 28 ++++++++- bonobo/ext/console.py | 2 +- bonobo/ext/google.py | 2 +- bonobo/structs/bags.py | 4 +- bonobo/util/iterators.py | 2 - bonobo/util/objects.py | 4 +- requirements-docker.txt | 2 +- tests/execution/test_node.py | 3 + tests/structs/test_bags.py | 18 +++++- tests/test_commands.py | 107 ++++++++++++++++++++--------------- 11 files changed, 115 insertions(+), 61 deletions(-) diff --git a/Makefile b/Makefile index fdab6c6..1ea5ea5 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -# This file has been auto-generated by Medikit. All changes will be lost. -# Updated on 2017-10-21. +# Generated by Medikit 0.4a5 on 2017-10-22. +# All changes will be overriden. PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index ad19230..2359f99 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -46,7 +46,17 @@ def _install_requirements(requirements): importlib.reload(site) -def read(filename, module, install=False, quiet=False, verbose=False, default_env_file=None, default_env=None, env_file=None, env=None): +def read( + filename, + module, + install=False, + quiet=False, + verbose=False, + default_env_file=None, + default_env=None, + env_file=None, + env=None +): import runpy from bonobo import Graph, settings @@ -129,8 +139,20 @@ def set_env_var(e, override=False): os.environ.setdefault(ename, evalue) -def execute(filename, module, install=False, quiet=False, verbose=False, default_env_file=None, default_env=None, env_file=None, env=None): - graph, plugins, services = read(filename, module, install, quiet, verbose, default_env_file, default_env, env_file, env) +def execute( + filename, + module, + install=False, + quiet=False, + verbose=False, + default_env_file=None, + default_env=None, + env_file=None, + env=None +): + graph, plugins, services = read( + filename, module, install, quiet, verbose, default_env_file, default_env, env_file, env + ) return bonobo.run(graph, plugins=plugins, services=services) diff --git a/bonobo/ext/console.py b/bonobo/ext/console.py index 02bcafa..0e6abb3 100644 --- a/bonobo/ext/console.py +++ b/bonobo/ext/console.py @@ -169,4 +169,4 @@ class ConsoleOutputPlugin(Plugin): def memory_usage(): import os, psutil process = psutil.Process(os.getpid()) - return process.memory_info()[0] / float(2 ** 20) + return process.memory_info()[0] / float(2**20) diff --git a/bonobo/ext/google.py b/bonobo/ext/google.py index 920af9d..e8e66b5 100644 --- a/bonobo/ext/google.py +++ b/bonobo/ext/google.py @@ -7,7 +7,7 @@ from oauth2client.file import Storage from oauth2client.tools import argparser HOME_DIR = os.path.expanduser('~') -GOOGLE_SCOPES = ('https://www.googleapis.com/auth/spreadsheets',) +GOOGLE_SCOPES = ('https://www.googleapis.com/auth/spreadsheets', ) GOOGLE_SECRETS = os.path.join(HOME_DIR, '.cache/secrets/client_secrets.json') diff --git a/bonobo/structs/bags.py b/bonobo/structs/bags.py index 22b507d..47194d2 100644 --- a/bonobo/structs/bags.py +++ b/bonobo/structs/bags.py @@ -96,7 +96,7 @@ class Bag: @classmethod def inherit(cls, *args, **kwargs): - return cls(*args, _flags=(INHERIT_INPUT,), **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. @@ -136,7 +136,7 @@ class Bag: class LoopbackBag(Bag): - default_flags = (LOOPBACK,) + default_flags = (LOOPBACK, ) class ErrorBag(Bag): diff --git a/bonobo/util/iterators.py b/bonobo/util/iterators.py index 1ed09ac..5bc8fb2 100644 --- a/bonobo/util/iterators.py +++ b/bonobo/util/iterators.py @@ -35,5 +35,3 @@ def tuplize(generator): return tuple(generator(*args, **kwargs)) return tuplized - - diff --git a/bonobo/util/objects.py b/bonobo/util/objects.py index acae2ad..e4dd29e 100644 --- a/bonobo/util/objects.py +++ b/bonobo/util/objects.py @@ -142,10 +142,10 @@ class ValueHolder: return divmod(other, self._value) def __pow__(self, other): - return self._value ** other + return self._value**other def __rpow__(self, other): - return other ** self._value + return other**self._value def __ipow__(self, other): self._value **= other diff --git a/requirements-docker.txt b/requirements-docker.txt index 9e68208..54bac73 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,6 +1,6 @@ -e .[docker] appdirs==1.4.3 -bonobo-docker==0.2.12 +bonobo-docker==0.5.0 certifi==2017.7.27.1 chardet==3.0.4 colorama==0.3.9 diff --git a/tests/execution/test_node.py b/tests/execution/test_node.py index cb0f9c3..fef385c 100644 --- a/tests/execution/test_node.py +++ b/tests/execution/test_node.py @@ -103,6 +103,7 @@ def test_node_dict_chained(): assert output[0] == {'id': 1, 'name': 'FOO'} assert output[1] == {'id': 2, 'name': 'BAR'} + def test_node_tuple(): def f(): return 'foo', 'bar' @@ -126,6 +127,7 @@ def test_node_tuple(): assert output[0] == ('foo', 'bar') assert output[1] == ('foo', 'baz') + def test_node_tuple_chained(): strategy = NaiveStrategy(GraphExecutionContextType=BufferingGraphExecutionContext) @@ -154,6 +156,7 @@ def test_node_tuple_chained(): assert output[0] == ('FOO', 'BAR') assert output[1] == ('FOO', 'BAZ') + def test_node_tuple_dict(): def f(): return 'foo', 'bar', {'id': 1} diff --git a/tests/structs/test_bags.py b/tests/structs/test_bags.py index 53d3ae7..1de04ce 100644 --- a/tests/structs/test_bags.py +++ b/tests/structs/test_bags.py @@ -99,22 +99,36 @@ def test_eq_operator_bag(): 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( + 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) == "" diff --git a/tests/test_commands.py b/tests/test_commands.py index 605892b..e467bb3 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -129,8 +129,7 @@ class TestDefaultEnvFile(object): def test_run_file_with_multiple_default_env_files(self, runner): out, err = runner( - 'run', '--quiet', '--default-env-file', '.env_one', - '--default-env-file', '.env_two', + 'run', '--quiet', '--default-env-file', '.env_one', '--default-env-file', '.env_two', get_examples_path('environment/env_files/get_passed_env_file.py') ) out = out.split('\n') @@ -140,9 +139,8 @@ class TestDefaultEnvFile(object): def test_run_module_with_default_env_file(self, runner): out, err = runner( - 'run', '--quiet', '-m', - 'bonobo.examples.environment.env_files.get_passed_env_file', - '--default-env-file', '.env_one' + 'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', '--default-env-file', + '.env_one' ) out = out.split('\n') assert out[0] == '321' @@ -151,9 +149,14 @@ class TestDefaultEnvFile(object): def test_run_module_with_multiple_default_env_files(self, runner): out, err = runner( - 'run', '--quiet', '-m', + 'run', + '--quiet', + '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', - '--default-env-file', '.env_one', '--default-env-file', '.env_two', + '--default-env-file', + '.env_one', + '--default-env-file', + '.env_two', ) out = out.split('\n') assert out[0] == '321' @@ -165,9 +168,11 @@ class TestDefaultEnvFile(object): class TestEnvFile(object): def test_run_file_with_file(self, runner): out, err = runner( - 'run', '--quiet', + 'run', + '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), - '--env-file', '.env_one', + '--env-file', + '.env_one', ) out = out.split('\n') assert out[0] == '321' @@ -176,9 +181,13 @@ class TestEnvFile(object): def test_run_file_with_multiple_files(self, runner): out, err = runner( - 'run', '--quiet', + 'run', + '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), - '--env-file', '.env_one', '--env-file', '.env_two', + '--env-file', + '.env_one', + '--env-file', + '.env_two', ) out = out.split('\n') assert out[0] == '321' @@ -187,9 +196,12 @@ class TestEnvFile(object): def test_run_module_with_file(self, runner): out, err = runner( - 'run', '--quiet', '-m', + 'run', + '--quiet', + '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', - '--env-file', '.env_one', + '--env-file', + '.env_one', ) out = out.split('\n') assert out[0] == '321' @@ -198,9 +210,14 @@ class TestEnvFile(object): def test_run_module_with_multiple_files(self, runner): out, err = runner( - 'run', '--quiet', '-m', + 'run', + '--quiet', + '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', - '--env-file', '.env_one', '--env-file', '.env_two', + '--env-file', + '.env_one', + '--env-file', + '.env_two', ) out = out.split('\n') assert out[0] == '321' @@ -212,9 +229,13 @@ class TestEnvFile(object): class TestEnvFileCombinations: def test_run_file_with_default_env_file_and_env_file(self, runner): out, err = runner( - 'run', '--quiet', + 'run', + '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), - '--default-env-file', '.env_one', '--env-file', '.env_two', + '--default-env-file', + '.env_one', + '--env-file', + '.env_two', ) out = out.split('\n') assert out[0] == '321' @@ -223,10 +244,16 @@ class TestEnvFileCombinations: def test_run_file_with_default_env_file_and_env_file_and_env_vars(self, runner): out, err = runner( - 'run', '--quiet', + 'run', + '--quiet', get_examples_path('environment/env_files/get_passed_env_file.py'), - '--default-env-file', '.env_one', '--env-file', '.env_two', - '--env', 'TEST_USER_PASSWORD=SWEETpassWORD', '--env', + '--default-env-file', + '.env_one', + '--env-file', + '.env_two', + '--env', + 'TEST_USER_PASSWORD=SWEETpassWORD', + '--env', 'MY_SECRET=444', ) out = out.split('\n') @@ -240,8 +267,8 @@ class TestDefaultEnvVars: def test_run_file_with_default_env_var(self, runner): out, err = runner( 'run', '--quiet', - get_examples_path('environment/env_vars/get_passed_env.py'), - '--default-env', 'USER=clowncity', '--env', 'USER=ted' + get_examples_path('environment/env_vars/get_passed_env.py'), '--default-env', 'USER=clowncity', '--env', + 'USER=ted' ) out = out.split('\n') assert out[0] == 'user' @@ -252,9 +279,8 @@ class TestDefaultEnvVars: def test_run_file_with_default_env_vars(self, runner): out, err = runner( 'run', '--quiet', - get_examples_path('environment/env_vars/get_passed_env.py'), - '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', - '--default-env', "ENV_TEST_STRING='my_test_string'" + get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123', '--env', + 'ENV_TEST_USER=cwandrews', '--default-env', "ENV_TEST_STRING='my_test_string'" ) out = out.split('\n') assert out[0] == 'cwandrews' @@ -263,10 +289,8 @@ class TestDefaultEnvVars: def test_run_module_with_default_env_var(self, runner): out, err = runner( - 'run', '--quiet', '-m', - 'bonobo.examples.environment.env_vars.get_passed_env', - '--env', 'ENV_TEST_NUMBER=123', - '--default-env', 'ENV_TEST_STRING=string' + 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', + 'ENV_TEST_NUMBER=123', '--default-env', 'ENV_TEST_STRING=string' ) out = out.split('\n') assert out[0] == 'cwandrews' @@ -275,10 +299,8 @@ class TestDefaultEnvVars: def test_run_module_with_default_env_vars(self, runner): out, err = runner( - 'run', '--quiet', '-m', - 'bonobo.examples.environment.env_vars.get_passed_env', - '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', - '--default-env', "ENV_TEST_STRING='string'" + 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', + 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', '--default-env', "ENV_TEST_STRING='string'" ) out = out.split('\n') assert out[0] == 'cwandrews' @@ -291,8 +313,7 @@ class TestEnvVars: def test_run_file_with_env_var(self, runner): out, err = runner( 'run', '--quiet', - get_examples_path('environment/env_vars/get_passed_env.py'), - '--env', 'ENV_TEST_NUMBER=123' + get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123' ) out = out.split('\n') assert out[0] != 'test_user' @@ -302,9 +323,8 @@ class TestEnvVars: def test_run_file_with_env_vars(self, runner): out, err = runner( 'run', '--quiet', - get_examples_path('environment/env_vars/get_passed_env.py'), - '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', - '--env', "ENV_TEST_STRING='my_test_string'" + get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123', '--env', + 'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'" ) out = out.split('\n') assert out[0] == 'cwandrews' @@ -313,9 +333,8 @@ class TestEnvVars: def test_run_module_with_env_var(self, runner): out, err = runner( - 'run', '--quiet', '-m', - 'bonobo.examples.environment.env_vars.get_passed_env', - '--env', 'ENV_TEST_NUMBER=123' + 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', + 'ENV_TEST_NUMBER=123' ) out = out.split('\n') assert out[0] == 'cwandrews' @@ -324,10 +343,8 @@ class TestEnvVars: def test_run_module_with_env_vars(self, runner): out, err = runner( - 'run', '--quiet', '-m', - 'bonobo.examples.environment.env_vars.get_passed_env', - '--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', - '--env', "ENV_TEST_STRING='my_test_string'" + 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', + 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'" ) out = out.split('\n') assert out[0] == 'cwandrews' From 01a652cd05545596b2553dca37de9c6ef5a056d0 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 22 Oct 2017 18:08:03 +0200 Subject: [PATCH 037/145] Remove dispatcher as it is not a dependency, for now, and as such breaks the continuous integration. --- bonobo/execution/graph.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/bonobo/execution/graph.py b/bonobo/execution/graph.py index 33e77bc..77e01fa 100644 --- a/bonobo/execution/graph.py +++ b/bonobo/execution/graph.py @@ -5,7 +5,6 @@ from bonobo.config import create_container from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext from bonobo.execution.plugin import PluginExecutionContext -from whistle import EventDispatcher class GraphExecutionContext: @@ -24,14 +23,6 @@ class GraphExecutionContext: def alive(self): return any(node.alive for node in self.nodes) - @property - def dispatcher(self): - try: - return self._dispatcher - except AttributeError: - self._dispatcher = EventDispatcher() - return self._dispatcher - def __init__(self, graph, plugins=None, services=None): self.graph = graph self.nodes = [self.create_node_execution_context_for(node) for node in self.graph] From f18889830ba3c6d92fc1975da7274326b6da81ee Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 22 Oct 2017 18:11:11 +0200 Subject: [PATCH 038/145] Remove dispatcher as it is not a dependency, for now, and as such breaks the continuous integration (yes, again.). --- bonobo/strategies/executor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bonobo/strategies/executor.py b/bonobo/strategies/executor.py index 57e2524..3bfabc6 100644 --- a/bonobo/strategies/executor.py +++ b/bonobo/strategies/executor.py @@ -6,7 +6,6 @@ from bonobo.constants import BEGIN, END from bonobo.strategies.base import Strategy from bonobo.structs.bags import Bag from bonobo.util.errors import print_error -from whistle import EventDispatcher class ExecutorStrategy(Strategy): From df45251622e6b935b27022e36fcbd79e9228f989 Mon Sep 17 00:00:00 2001 From: arimbr Date: Sun, 22 Oct 2017 23:05:58 +0200 Subject: [PATCH 039/145] Check if target directory is empty instead of current directory and remove overwrite_if_exists argument --- bonobo/commands/init.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index 9a157ca..e69156c 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -1,6 +1,6 @@ import os -def execute(name, branch, overwrite_if_exists=False): +def execute(name, branch): try: from cookiecutter.main import cookiecutter except ImportError as exc: @@ -8,7 +8,9 @@ def execute(name, branch, overwrite_if_exists=False): 'You must install "cookiecutter" to use this command.\n\n $ pip install cookiecutter\n' ) from exc - if os.listdir(os.getcwd()) == []: + overwrite_if_exists = False + project_path = os.path.join(os.getcwd(), name) + if os.path.isdir(project_path) and not os.listdir(project_path): overwrite_if_exists = True return cookiecutter( From 9820fca2b4ecb2c4abffba173c1cc0b52bf9ee14 Mon Sep 17 00:00:00 2001 From: arimbr Date: Sun, 22 Oct 2017 23:08:25 +0200 Subject: [PATCH 040/145] Use pytest tmpdir fixture and add more init tests --- tests/test_commands.py | 43 ++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index e9ba1bd..fadf6e2 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -6,9 +6,11 @@ from unittest.mock import patch import pkg_resources import pytest +from cookiecutter.exceptions import OutputDirExistsException from bonobo import __main__, __version__, get_examples_path from bonobo.commands import entrypoint +from bonobo.commands.run import DEFAULT_GRAPH_FILENAMES def runner_entrypoint(*args): @@ -45,24 +47,37 @@ def test_no_command(runner, capsys): @all_runners -def test_init(runner, capsys): - runner('init', 'project-name') - out, err = capsys.readouterr() - out = out.strip() - shutil.rmtree('project-name') - assert out == '' +def test_init(runner, tmpdir): + name = 'project' + os.chdir(tmpdir) + runner('init', name) + assert os.path.isdir(name) + assert set(os.listdir(name)) & set(DEFAULT_GRAPH_FILENAMES) @all_runners -def test_init_within_empty_directory(runner, capsys): - os.mkdir('empty-directory') - os.chdir('empty-directory') +def test_init_in_empty_directory(runner, tmpdir): + name = 'project' + os.chdir(tmpdir) + os.mkdir(name) + runner('init', name) + assert set(os.listdir(name)) & set(DEFAULT_GRAPH_FILENAMES) + + +@all_runners +def test_init_in_non_empty_directory(runner, tmpdir): + name = 'project' + os.chdir(tmpdir) + runner('init', name) + with pytest.raises(OutputDirExistsException): + runner('init', name) + + +@all_runners +def test_init_within_empty_directory(runner, tmpdir): + os.chdir(tmpdir) runner('init', '.') - out, err = capsys.readouterr() - out = out.strip() - os.chdir('..') - shutil.rmtree('empty-directory') - assert out == '' + assert set(os.listdir()) & set(DEFAULT_GRAPH_FILENAMES) @all_runners From 7c0071bfdfc3a543d64a0cfe6f28f82b710330b3 Mon Sep 17 00:00:00 2001 From: arimbr Date: Sun, 22 Oct 2017 23:30:23 +0200 Subject: [PATCH 041/145] Remove unused shutil import --- tests/test_commands.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index fadf6e2..1ff3a3b 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,7 +1,6 @@ import os import runpy import sys -import shutil from unittest.mock import patch import pkg_resources From 4641425e4cef83d619ed39ea62ea2e6963ad6416 Mon Sep 17 00:00:00 2001 From: arimbr Date: Mon, 23 Oct 2017 00:30:10 +0200 Subject: [PATCH 042/145] Fix python 3.5 os.chdir not accepting LocalPath --- tests/test_commands.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 1ff3a3b..069cead 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -48,7 +48,7 @@ def test_no_command(runner, capsys): @all_runners def test_init(runner, tmpdir): name = 'project' - os.chdir(tmpdir) + tmpdir.chdir() runner('init', name) assert os.path.isdir(name) assert set(os.listdir(name)) & set(DEFAULT_GRAPH_FILENAMES) @@ -57,7 +57,7 @@ def test_init(runner, tmpdir): @all_runners def test_init_in_empty_directory(runner, tmpdir): name = 'project' - os.chdir(tmpdir) + tmpdir.chdir() os.mkdir(name) runner('init', name) assert set(os.listdir(name)) & set(DEFAULT_GRAPH_FILENAMES) @@ -66,7 +66,7 @@ def test_init_in_empty_directory(runner, tmpdir): @all_runners def test_init_in_non_empty_directory(runner, tmpdir): name = 'project' - os.chdir(tmpdir) + tmpdir.chdir() runner('init', name) with pytest.raises(OutputDirExistsException): runner('init', name) @@ -74,7 +74,7 @@ def test_init_in_non_empty_directory(runner, tmpdir): @all_runners def test_init_within_empty_directory(runner, tmpdir): - os.chdir(tmpdir) + tmpdir.chdir() runner('init', '.') assert set(os.listdir()) & set(DEFAULT_GRAPH_FILENAMES) From 9a54f7b4aa6358bf8e8a127738735044a5152d88 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 23 Oct 2017 21:18:02 +0200 Subject: [PATCH 043/145] [core] Still refactoring the core behaviour of bags, starting to be much simpler. --- bonobo/commands/convert.py | 113 ++++++------------------------ bonobo/commands/util/__init__.py | 0 bonobo/commands/util/arguments.py | 26 ------- bonobo/execution/node.py | 25 ++----- bonobo/ext/django.py | 2 +- bonobo/ext/opendatasoft.py | 20 +++--- bonobo/nodes/factory.py | 49 +++---------- bonobo/registry.py | 90 ++++++++++++++++++++++++ bonobo/structs/bags.py | 49 ++++++++++++- bonobo/util/__init__.py | 3 +- bonobo/util/collections.py | 44 +++++++++++- bonobo/util/iterators.py | 37 ---------- bonobo/util/resolvers.py | 61 ++++++++++++++++ tests/__init__.py | 0 tests/io/test_csv.py | 4 +- tests/io/test_json.py | 6 +- tests/io/test_pickle.py | 4 +- tests/nodes/factory.py | 66 +++++++++++++++++ tests/structs/test_bags.py | 30 +++++++- tests/util/test_collections.py | 30 ++++++++ tests/util/test_iterators.py | 22 ------ tests/util/test_resolvers.py | 18 +++++ 22 files changed, 437 insertions(+), 262 deletions(-) delete mode 100644 bonobo/commands/util/__init__.py delete mode 100644 bonobo/commands/util/arguments.py create mode 100644 bonobo/registry.py delete mode 100644 bonobo/util/iterators.py create mode 100644 bonobo/util/resolvers.py delete mode 100644 tests/__init__.py create mode 100644 tests/nodes/factory.py create mode 100644 tests/util/test_collections.py delete mode 100644 tests/util/test_iterators.py create mode 100644 tests/util/test_resolvers.py diff --git a/bonobo/commands/convert.py b/bonobo/commands/convert.py index 48caaa3..e9039fd 100644 --- a/bonobo/commands/convert.py +++ b/bonobo/commands/convert.py @@ -1,105 +1,34 @@ -import mimetypes -import os - import bonobo -from bonobo.commands.util.arguments import parse_variable_argument -from bonobo.util import require -from bonobo.util.iterators import tuplize -from bonobo.util.python import WorkingDirectoryModulesRegistry - -SHORTCUTS = { - 'csv': 'text/csv', - 'json': 'application/json', - 'pickle': 'pickle', - 'plain': 'text/plain', - 'text': 'text/plain', - 'txt': 'text/plain', -} - -REGISTRY = { - 'application/json': (bonobo.JsonReader, bonobo.JsonWriter), - 'pickle': (bonobo.PickleReader, bonobo.PickleWriter), - 'text/csv': (bonobo.CsvReader, bonobo.CsvWriter), - 'text/plain': (bonobo.FileReader, bonobo.FileWriter), -} - -READER = 'reader' -WRITER = 'writer' - - -def resolve_factory(name, filename, factory_type, options=None): - """ - Try to resolve which transformation factory to use for this filename. User eventually provided a name, which has - priority, otherwise we try to detect it using the mimetype detection on filename. - - """ - if name is None: - name = mimetypes.guess_type(filename)[0] - - if name in SHORTCUTS: - name = SHORTCUTS[name] - - if name is None: - _, _ext = os.path.splitext(filename) - if _ext: - _ext = _ext[1:] - if _ext in SHORTCUTS: - name = SHORTCUTS[_ext] - - if options: - options = dict(map(parse_variable_argument, options)) - else: - options = dict() - - if not name in REGISTRY: - raise RuntimeError( - 'Could not resolve {factory_type} factory for {filename} ({name}). Try providing it explicitely using -{opt} .'. - format(name=name, filename=filename, factory_type=factory_type, opt=factory_type[0]) - ) - - if factory_type == READER: - return REGISTRY[name][0], options - elif factory_type == WRITER: - return REGISTRY[name][1], options - else: - raise ValueError('Invalid factory type.') - - -@tuplize -def resolve_filters(filters): - registry = WorkingDirectoryModulesRegistry() - for f in filters: - try: - mod, attr = f.split(':', 1) - yield getattr(registry.require(mod), attr) - except ValueError: - yield getattr(bonobo, f) +from bonobo.registry import READER, WRITER, default_registry +from bonobo.util.resolvers import _resolve_transformations, _resolve_options def execute( - input, - output, + input_filename, + output_filename, reader=None, reader_option=None, writer=None, writer_option=None, option=None, - filter=None, + transformation=None, ): - reader_factory, reader_option = resolve_factory(reader, input, READER, (option or []) + (reader_option or [])) + reader_factory = default_registry.get_reader_factory_for(input_filename, format=reader) + reader_options = _resolve_options((option or []) + (reader_option or [])) - if output == '-': - writer_factory, writer_option = bonobo.PrettyPrinter, {} + if output_filename == '-': + writer_factory = bonobo.PrettyPrinter else: - writer_factory, writer_option = resolve_factory(writer, output, WRITER, (option or []) + (writer_option or [])) + writer_factory = default_registry.get_writer_factory_for(output_filename, format=writer) + writer_options = _resolve_options((option or []) + (writer_option or [])) - filters = resolve_filters(filter) + transformations = _resolve_transformations(transformation) graph = bonobo.Graph() graph.add_chain( - reader_factory(input, **reader_option), - *filters, - writer_factory(output, **writer_option), + reader_factory(input_filename, **reader_options), + *transformations, + writer_factory(output_filename, **writer_options), ) return bonobo.run( @@ -110,8 +39,8 @@ def execute( def register(parser): - parser.add_argument('input', help='Input filename.') - parser.add_argument('output', help='Output filename.') + parser.add_argument('input-filename', help='Input filename.') + parser.add_argument('output-filename', help='Output filename.') parser.add_argument( '--' + READER, '-r', @@ -124,11 +53,11 @@ def register(parser): 'Choose the writer factory if it cannot be detected from extension, or if detection is wrong (use - for console pretty print).' ) parser.add_argument( - '--filter', - '-f', - dest='filter', + '--transformation', + '-t', + dest='transformation', action='append', - help='Add a filter between input and output', + help='Add a transformation between input and output (can be used multiple times, order is preserved).', ) parser.add_argument( '--option', diff --git a/bonobo/commands/util/__init__.py b/bonobo/commands/util/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bonobo/commands/util/arguments.py b/bonobo/commands/util/arguments.py deleted file mode 100644 index 435c6f5..0000000 --- a/bonobo/commands/util/arguments.py +++ /dev/null @@ -1,26 +0,0 @@ -import json - - -def parse_variable_argument(arg): - try: - key, val = arg.split('=', 1) - except ValueError: - return arg, True - - try: - val = json.loads(val) - except json.JSONDecodeError: - pass - - return key, val - - -def test_parse_variable_argument(): - assert parse_variable_argument('foo=bar') == ('foo', 'bar') - assert parse_variable_argument('foo="bar"') == ('foo', 'bar') - assert parse_variable_argument('sep=";"') == ('sep', ';') - assert parse_variable_argument('foo') == ('foo', True) - - -if __name__ == '__main__': - test_parse_var() diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index c943ba0..445c2f6 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -3,13 +3,13 @@ from queue import Empty from time import sleep from types import GeneratorType -from bonobo import settings -from bonobo.constants import INHERIT_INPUT, NOT_MODIFIED, BEGIN, END +from bonobo.constants import NOT_MODIFIED, BEGIN, END from bonobo.errors import InactiveReadableError, UnrecoverableError from bonobo.execution.base import LoopingExecutionContext from bonobo.structs.bags import Bag from bonobo.structs.inputs import Input -from bonobo.util import get_name, iserrorbag, isloopbackbag, isdict, istuple +from bonobo.structs.tokens import Token +from bonobo.util import get_name, iserrorbag, isloopbackbag from bonobo.util.compat import deprecated_alias from bonobo.util.statistics import WithStatistics @@ -49,7 +49,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): :param mixed value: message """ for message in messages: - self.input.put(message) + self.input.put(message if isinstance(message, (Bag, Token)) else Bag(message)) def write_sync(self, *messages): self.write(BEGIN, *messages, END) @@ -145,21 +145,4 @@ def _resolve(input_bag, output): if iserrorbag(output): return output - # If it does not look like a bag, let's create one for easier manipulation - if hasattr(output, 'apply'): # XXX TODO use isbag() ? - # Already a bag? Check if we need to set parent. - if INHERIT_INPUT in output.flags: - output.set_parent(input_bag) - return output - - # If we're using kwargs ioformat, then a dict means kwargs. - if settings.IOFORMAT == settings.IOFORMAT_KWARGS and isdict(output): - return Bag(**output) - - if istuple(output): - if len(output) > 1 and isdict(output[-1]): - return Bag(*output[0:-1], **output[-1]) - return Bag(*output) - - # Either we use arg0 format, either it's "just" a value. return Bag(output) diff --git a/bonobo/ext/django.py b/bonobo/ext/django.py index 8d73428..06f31a7 100644 --- a/bonobo/ext/django.py +++ b/bonobo/ext/django.py @@ -44,7 +44,7 @@ class ETLCommand(BaseCommand): return self.GraphType(not_implemented) def get_services(self): - return get_default_services(type(self).__file__) + return {} @property def logger(self): diff --git a/bonobo/ext/opendatasoft.py b/bonobo/ext/opendatasoft.py index 2dc54c0..5144e59 100644 --- a/bonobo/ext/opendatasoft.py +++ b/bonobo/ext/opendatasoft.py @@ -14,14 +14,14 @@ def path_str(path): class OpenDataSoftAPI(Configurable): dataset = Option(str, positional=True) - endpoint = Option(str, default='{scheme}://{netloc}{path}') - scheme = Option(str, default='https') - netloc = Option(str, default='data.opendatasoft.com') - path = Option(path_str, default='/api/records/1.0/search/') - rows = Option(int, default=500) + endpoint = Option(str, required=False, default='{scheme}://{netloc}{path}') + scheme = Option(str, required=False, default='https') + netloc = Option(str, required=False, default='data.opendatasoft.com') + path = Option(path_str, required=False, default='/api/records/1.0/search/') + rows = Option(int, required=False, default=500) limit = Option(int, required=False) - timezone = Option(str, default='Europe/Paris') - kwargs = Option(dict, default=dict) + timezone = Option(str, required=False, default='Europe/Paris') + kwargs = Option(dict, required=False, default=dict) @ContextProcessor def compute_path(self, context): @@ -44,7 +44,11 @@ class OpenDataSoftAPI(Configurable): break for row in records: - yield {**row.get('fields', {}), 'geometry': row.get('geometry', {})} + yield { + **row.get('fields', {}), + 'geometry': row.get('geometry', {}), + 'recordid': row.get('recordid'), + } start += self.rows diff --git a/bonobo/nodes/factory.py b/bonobo/nodes/factory.py index 2a1c30b..bd77e03 100644 --- a/bonobo/nodes/factory.py +++ b/bonobo/nodes/factory.py @@ -75,24 +75,24 @@ class Cursor(): self.item = item @operation('dict') - def dict(self, x): + def as_dict(self, x): return x if isinstance(x, dict) else dict(x) @operation('int') - def int(self): - pass + def as_int(self, x): + return x if isinstance(x, int) else int(x) @operation('str') - def str(self, x): + def as_str(self, x): return x if isinstance(x, str) else str(x) @operation('list') - def list(self): - pass + def as_list(self, x): + return x if isinstance(x, list) else list(x) @operation('tuple') - def tuple(self): - pass + def as_tuple(self, x): + return x if isinstance(x, tuple) else tuple(x) def __getattr__(self, item): """ @@ -147,7 +147,7 @@ class Factory(Configurable): def __init__(self, *args, **kwargs): warnings.warn( - __file__ + + type(self).__name__ + ' is experimental, API may change in the future, use it as a preview only and knowing the risks.', FutureWarning ) @@ -180,40 +180,9 @@ class Factory(Configurable): raise RuntimeError('Houston, we have a problem...') def __call__(self, *args, **kwargs): - print('factory call on', args, kwargs) for operation in self.operations: args, kwargs = operation.apply(*args, **kwargs) - print(' ... after', operation, 'got', args, kwargs) return Bag(*args, **kwargs) def __getitem__(self, item): return CURSOR_TYPES[self.default_cursor_type](self, item) - - -if __name__ == '__main__': - f = Factory() - - f[0].dict().map_keys({'foo': 'F00'}) - f['foo'].str().upper() - - print('operations:', f.operations) - print(f({'foo': 'bisou'}, foo='blah')) -''' -specs: - -- rename keys of an input dict (in args, or kwargs) using a translation map. - - -f = Factory() - -f[0] -f['xxx'] = - -f[0].dict().get('foo.bar').move_to('foo.baz').apply(str.upper) -f[0].get('foo.*').items().map(str.lower) - -f['foo'].keys_map({ - 'a': 'b' -}) - -''' diff --git a/bonobo/registry.py b/bonobo/registry.py new file mode 100644 index 0000000..be8d47b --- /dev/null +++ b/bonobo/registry.py @@ -0,0 +1,90 @@ +import mimetypes + +import os + +from bonobo import JsonReader, CsvReader, PickleReader, FileReader, FileWriter, PickleWriter, CsvWriter, JsonWriter + +FILETYPE_CSV = 'text/csv' +FILETYPE_JSON = 'application/json' +FILETYPE_PICKLE = 'pickle' +FILETYPE_PLAIN = 'text/plain' + +READER = 'reader' +WRITER = 'writer' + + +class Registry: + ALIASES = { + 'csv': FILETYPE_CSV, + 'json': FILETYPE_JSON, + 'pickle': FILETYPE_PICKLE, + 'plain': FILETYPE_PLAIN, + 'text': FILETYPE_PLAIN, + 'txt': FILETYPE_PLAIN, + } + + FACTORIES = { + READER: { + FILETYPE_JSON: JsonReader, + FILETYPE_CSV: CsvReader, + FILETYPE_PICKLE: PickleReader, + FILETYPE_PLAIN: FileReader, + }, + WRITER: { + FILETYPE_JSON: JsonWriter, + FILETYPE_CSV: CsvWriter, + FILETYPE_PICKLE: PickleWriter, + FILETYPE_PLAIN: FileWriter, + }, + } + + def get_factory_for(self, kind, name, *, format=None): + if not kind in self.FACTORIES: + raise KeyError('Unknown factory kind {!r}.'.format(kind)) + + if format is None and name is None: + raise RuntimeError('Cannot guess factory without at least a filename or a format.') + + # Guess mimetype if possible + if format is None: + format = mimetypes.guess_type(name)[0] + + # Guess from extension if possible + if format is None: + _, _ext = os.path.splitext(name) + if _ext: + format = _ext[1:] + + # Apply aliases + if format in self.ALIASES: + format = self.ALIASES[format] + + if format is None or not format in self.FACTORIES[kind]: + raise RuntimeError( + 'Could not resolve {kind} factory for {name} ({format}).'.format(kind=kind, name=name, format=format) + ) + + return self.FACTORIES[kind][format] + + def get_reader_factory_for(self, name, *, format=None): + """ + Returns a callable to build a reader for the provided filename, eventually forcing a format. + + :param name: filename + :param format: format + :return: type + """ + return self.get_factory_for(READER, name, format=format) + + def get_writer_factory_for(self, name, *, format=None): + """ + Returns a callable to build a writer for the provided filename, eventually forcing a format. + + :param name: filename + :param format: format + :return: type + """ + return self.get_factory_for(WRITER, name, format=format) + + +default_registry = Registry() diff --git a/bonobo/structs/bags.py b/bonobo/structs/bags.py index 47194d2..8683175 100644 --- a/bonobo/structs/bags.py +++ b/bonobo/structs/bags.py @@ -1,5 +1,6 @@ import itertools +from bonobo.structs.tokens import Token from bonobo.constants import INHERIT_INPUT, LOOPBACK __all__ = [ @@ -35,11 +36,55 @@ class Bag: default_flags = () + 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, **kwargs): self._flags = type(self).default_flags + (_flags or ()) self._parent = _parent - self._args = args - self._kwargs = kwargs + + 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 @property def args(self): diff --git a/bonobo/util/__init__.py b/bonobo/util/__init__.py index e2eebe1..4ef136e 100644 --- a/bonobo/util/__init__.py +++ b/bonobo/util/__init__.py @@ -1,5 +1,4 @@ -from bonobo.util.collections import sortedlist -from bonobo.util.iterators import ensure_tuple +from bonobo.util.collections import sortedlist, ensure_tuple from bonobo.util.compat import deprecated, deprecated_alias from bonobo.util.inspect import ( inspect_node, diff --git a/bonobo/util/collections.py b/bonobo/util/collections.py index b97630a..d53a7da 100644 --- a/bonobo/util/collections.py +++ b/bonobo/util/collections.py @@ -1,6 +1,48 @@ import bisect +import functools class sortedlist(list): def insort(self, x): - bisect.insort(self, x) \ No newline at end of file + bisect.insort(self, x) + + +def ensure_tuple(tuple_or_mixed): + """ + If it's not a tuple, let's make a tuple of one item. + Otherwise, not changed. + + :param tuple_or_mixed: + :return: tuple + + """ + if isinstance(tuple_or_mixed, tuple): + return tuple_or_mixed + return (tuple_or_mixed, ) + + +def tuplize(generator): + """ Takes a generator and make it a tuple-returning function. As a side + effect, it can also decorate any iterator-returning function to force + return value to be a tuple. + + >>> tuplized_lambda = tuplize(lambda: [1, 2, 3]) + >>> tuplized_lambda() + (1, 2, 3) + + >>> @tuplize + ... def my_generator(): + ... yield 1 + ... yield 2 + ... yield 3 + ... + >>> my_generator() + (1, 2, 3) + + """ + + @functools.wraps(generator) + def tuplized(*args, **kwargs): + return tuple(generator(*args, **kwargs)) + + return tuplized diff --git a/bonobo/util/iterators.py b/bonobo/util/iterators.py deleted file mode 100644 index 5bc8fb2..0000000 --- a/bonobo/util/iterators.py +++ /dev/null @@ -1,37 +0,0 @@ -""" Iterator utilities. """ -import functools - - -def force_iterator(mixed): - """Sudo make me an iterator. - - Deprecated? - - :param mixed: - :return: Iterator, baby. - """ - if isinstance(mixed, str): - return [mixed] - try: - return iter(mixed) - except TypeError: - return [mixed] if mixed else [] - - -def ensure_tuple(tuple_or_mixed): - if isinstance(tuple_or_mixed, tuple): - return tuple_or_mixed - return (tuple_or_mixed, ) - - -def tuplize(generator): - """ Takes a generator and make it a tuple-returning function. As a side - effect, it can also decorate any iterator-returning function to force - return value to be a tuple. - """ - - @functools.wraps(generator) - def tuplized(*args, **kwargs): - return tuple(generator(*args, **kwargs)) - - return tuplized diff --git a/bonobo/util/resolvers.py b/bonobo/util/resolvers.py new file mode 100644 index 0000000..0590fc7 --- /dev/null +++ b/bonobo/util/resolvers.py @@ -0,0 +1,61 @@ +""" +This package is considered private, and should only be used within bonobo. + +""" + +import json + +import bonobo +from bonobo.util.collections import tuplize +from bonobo.util.python import WorkingDirectoryModulesRegistry + + +def _parse_option(option): + """ + Parse a 'key=val' option string into a python (key, val) pair + + :param option: str + :return: tuple + """ + try: + key, val = option.split('=', 1) + except ValueError: + return option, True + + try: + val = json.loads(val) + except json.JSONDecodeError: + pass + + return key, val + + +def _resolve_options(options=None): + """ + Resolve a collection of option strings (eventually coming from command line) into a python dictionary. + + :param options: tuple[str] + :return: dict + """ + if options: + return dict(map(_parse_option, options)) + return dict() + + +@tuplize +def _resolve_transformations(transformations): + """ + Resolve a collection of strings into the matching python objects, defaulting to bonobo namespace if no package is provided. + + Syntax for each string is path.to.package:attribute + + :param transformations: tuple(str) + :return: tuple(object) + """ + registry = WorkingDirectoryModulesRegistry() + for t in transformations: + try: + mod, attr = t.split(':', 1) + yield getattr(registry.require(mod), attr) + except ValueError: + yield getattr(bonobo, t) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/io/test_csv.py b/tests/io/test_csv.py index 1de6f89..9048bef 100644 --- a/tests/io/test_csv.py +++ b/tests/io/test_csv.py @@ -28,9 +28,7 @@ def test_write_csv_to_file_kwargs(tmpdir, add_kwargs): fs, filename, services = csv_tester.get_services_for_writer(tmpdir) with NodeExecutionContext(CsvWriter(filename, **add_kwargs), services=services) as context: - context.write(BEGIN, Bag(**{'foo': 'bar'}), Bag(**{'foo': 'baz', 'ignore': 'this'}), END) - context.step() - context.step() + context.write_sync({'foo': 'bar'}, {'foo': 'baz', 'ignore': 'this'}) with fs.open(filename) as fp: assert fp.read() == 'foo\nbar\nbaz\n' diff --git a/tests/io/test_json.py b/tests/io/test_json.py index bbeb17f..c0124cd 100644 --- a/tests/io/test_json.py +++ b/tests/io/test_json.py @@ -1,7 +1,6 @@ import pytest -from bonobo import Bag, JsonReader, JsonWriter, settings -from bonobo.constants import BEGIN, END +from bonobo import JsonReader, JsonWriter, settings from bonobo.execution.node import NodeExecutionContext from bonobo.util.testing import FilesystemTester @@ -29,8 +28,7 @@ def test_write_json_kwargs(tmpdir, add_kwargs): fs, filename, services = json_tester.get_services_for_writer(tmpdir) with NodeExecutionContext(JsonWriter(filename, **add_kwargs), services=services) as context: - context.write(BEGIN, Bag(**{'foo': 'bar'}), END) - context.step() + context.write_sync({'foo': 'bar'}) with fs.open(filename) as fp: assert fp.read() == '[{"foo": "bar"}]' diff --git a/tests/io/test_pickle.py b/tests/io/test_pickle.py index eca3493..1f95309 100644 --- a/tests/io/test_pickle.py +++ b/tests/io/test_pickle.py @@ -14,7 +14,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(Bag(({'foo': 'bar'}, {})), Bag(({'foo': 'baz', 'ignore': 'this'}, {}))) with fs.open(filename, 'rb') as fp: assert pickle.loads(fp.read()) == {'foo': 'bar'} @@ -27,7 +27,7 @@ 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(Bag()) + context.write_sync(()) output = context.get_buffer() assert len(output) == 2 diff --git a/tests/nodes/factory.py b/tests/nodes/factory.py new file mode 100644 index 0000000..8443cb6 --- /dev/null +++ b/tests/nodes/factory.py @@ -0,0 +1,66 @@ +from unittest import TestCase + +import pytest + +from bonobo import Bag +from bonobo.nodes.factory import Factory +from bonobo.util.testing import BufferingNodeExecutionContext + + +@pytest.mark.filterwarnings('ignore:Factory') +class FactoryTypeTest(TestCase): + def execute_node(self, node, *rows): + with BufferingNodeExecutionContext(node) as context: + context.write_sync(*map(Bag, rows)) + return context.get_buffer() + + def test_args_as_str(self): + f = Factory() + f[0].as_str().upper() + + output = self.execute_node(f, 'foo', 'bar', 'baz') + + assert len(output) == 3 + assert output[0] == 'FOO' + assert output[1] == 'BAR' + assert output[2] == 'BAZ' + + def test_kwargs_as_str(self): + f = Factory() + f['foo'].as_str().upper() + + output = self.execute_node(f, {'foo': 'bar'}, {'foo': 'baz'}) + assert len(output) == 2 + assert output[0] == {'foo': 'BAR'} + assert output[1] == {'foo': 'BAZ'} + + +""" +draft below. + +if __name__ == '__main__': + f = Factory() + + f[0].dict().map_keys({'foo': 'F00'}) + + print('operations:', f.operations) + print(f({'foo': 'bisou'}, foo='blah')) + +specs: + +- rename keys of an input dict (in args, or kwargs) using a translation map. + + +f = Factory() + +f[0] +f['xxx'] = + +f[0].dict().get('foo.bar').move_to('foo.baz').apply(str.upper) +f[0].get('foo.*').items().map(str.lower) + +f['foo'].keys_map({ + 'a': 'b' +}) + +""" diff --git a/tests/structs/test_bags.py b/tests/structs/test_bags.py index 1de04ce..b5517e3 100644 --- a/tests/structs/test_bags.py +++ b/tests/structs/test_bags.py @@ -1,8 +1,10 @@ import pickle from unittest.mock import Mock +import pytest + from bonobo import Bag -from bonobo.constants import INHERIT_INPUT +from bonobo.constants import INHERIT_INPUT, BEGIN from bonobo.structs import Token args = ( @@ -31,6 +33,32 @@ def test_basic(): 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) diff --git a/tests/util/test_collections.py b/tests/util/test_collections.py new file mode 100644 index 0000000..3a1e517 --- /dev/null +++ b/tests/util/test_collections.py @@ -0,0 +1,30 @@ +from bonobo.util import sortedlist, ensure_tuple +from bonobo.util.collections import tuplize + + +def test_sortedlist(): + l = sortedlist() + l.insort(2) + l.insort(1) + l.insort(3) + l.insort(2) + assert l == [1, 2, 2, 3] + + +def test_ensure_tuple(): + assert ensure_tuple('a') == ('a', ) + assert ensure_tuple(('a', )) == ('a', ) + assert ensure_tuple(()) is () + + +def test_tuplize(): + tuplized_lambda = tuplize(lambda: [1, 2, 3]) + assert tuplized_lambda() == (1, 2, 3) + + @tuplize + def some_generator(): + yield 'c' + yield 'b' + yield 'a' + + assert some_generator() == ('c', 'b', 'a') diff --git a/tests/util/test_iterators.py b/tests/util/test_iterators.py deleted file mode 100644 index 3d0249e..0000000 --- a/tests/util/test_iterators.py +++ /dev/null @@ -1,22 +0,0 @@ -import types - -from bonobo.util.iterators import force_iterator - - -def test_force_iterator_with_string(): - assert force_iterator('foo') == ['foo'] - - -def test_force_iterator_with_none(): - assert force_iterator(None) == [] - - -def test_force_iterator_with_generator(): - def generator(): - yield 'aaa' - yield 'bbb' - yield 'ccc' - - iterator = force_iterator(generator()) - assert isinstance(iterator, types.GeneratorType) - assert list(iterator) == ['aaa', 'bbb', 'ccc'] diff --git a/tests/util/test_resolvers.py b/tests/util/test_resolvers.py new file mode 100644 index 0000000..0de3003 --- /dev/null +++ b/tests/util/test_resolvers.py @@ -0,0 +1,18 @@ +import bonobo +from bonobo.util.resolvers import _parse_option, _resolve_options, _resolve_transformations + + +def test_parse_option(): + assert _parse_option('foo=bar') == ('foo', 'bar') + assert _parse_option('foo="bar"') == ('foo', 'bar') + assert _parse_option('sep=";"') == ('sep', ';') + assert _parse_option('foo') == ('foo', True) + + +def test_resolve_options(): + assert _resolve_options(('foo=bar', 'bar="baz"')) == {'foo': 'bar', 'bar': 'baz'} + assert _resolve_options() == {} + + +def test_resolve_transformations(): + assert _resolve_transformations(('PrettyPrinter', )) == (bonobo.PrettyPrinter, ) From ece764b95cfeefb9e2284192f07f3c7092de8026 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 23 Oct 2017 21:28:49 +0200 Subject: [PATCH 044/145] [tests] rename factory test and move bag detecting so any bag is returned as is as an output. --- bonobo/execution/node.py | 4 ++-- tests/nodes/{factory.py => test_factory.py} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename tests/nodes/{factory.py => test_factory.py} (96%) diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index 445c2f6..6c52e7d 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -9,7 +9,7 @@ from bonobo.execution.base import LoopingExecutionContext 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 +from bonobo.util import get_name, iserrorbag, isloopbackbag, isbag from bonobo.util.compat import deprecated_alias from bonobo.util.statistics import WithStatistics @@ -142,7 +142,7 @@ def _resolve(input_bag, output): if output is NOT_MODIFIED: return input_bag - if iserrorbag(output): + if isbag(output): return output return Bag(output) diff --git a/tests/nodes/factory.py b/tests/nodes/test_factory.py similarity index 96% rename from tests/nodes/factory.py rename to tests/nodes/test_factory.py index 8443cb6..781ba57 100644 --- a/tests/nodes/factory.py +++ b/tests/nodes/test_factory.py @@ -11,7 +11,7 @@ from bonobo.util.testing import BufferingNodeExecutionContext class FactoryTypeTest(TestCase): def execute_node(self, node, *rows): with BufferingNodeExecutionContext(node) as context: - context.write_sync(*map(Bag, rows)) + context.write_sync(*rows) return context.get_buffer() def test_args_as_str(self): From dd28e08f0f8de5d446052497f3b750e90cbe703d Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 23 Oct 2017 21:56:13 +0200 Subject: [PATCH 045/145] [nodes] Removing draft quality factory from bonobo main package, will live in separate personnal package until it is good enough to live here. --- bonobo/examples/nodes/factory.py | 18 --- bonobo/nodes/factory.py | 188 ------------------------------- tests/nodes/test_factory.py | 66 ----------- 3 files changed, 272 deletions(-) delete mode 100644 bonobo/examples/nodes/factory.py delete mode 100644 bonobo/nodes/factory.py delete mode 100644 tests/nodes/test_factory.py diff --git a/bonobo/examples/nodes/factory.py b/bonobo/examples/nodes/factory.py deleted file mode 100644 index c1f3818..0000000 --- a/bonobo/examples/nodes/factory.py +++ /dev/null @@ -1,18 +0,0 @@ -import bonobo -from bonobo.commands.run import get_default_services -from bonobo.nodes.factory import Factory -from bonobo.nodes.io.json import JsonDictItemsReader - -normalize = Factory() -normalize[0].str().title() -normalize.move(0, 'title') -normalize.move(0, 'address') - -graph = bonobo.Graph( - JsonDictItemsReader('datasets/coffeeshops.json'), - normalize, - bonobo.PrettyPrinter(), -) - -if __name__ == '__main__': - bonobo.run(graph, services=get_default_services(__file__)) diff --git a/bonobo/nodes/factory.py b/bonobo/nodes/factory.py deleted file mode 100644 index bd77e03..0000000 --- a/bonobo/nodes/factory.py +++ /dev/null @@ -1,188 +0,0 @@ -import functools -import warnings -from functools import partial - -from bonobo import Bag -from bonobo.config import Configurable, Method - -_isarg = lambda item: type(item) is int -_iskwarg = lambda item: type(item) is str - - -class Operation(): - def __init__(self, item, callable): - self.item = item - self.callable = callable - - def __repr__(self): - return ''.format(self.callable.__name__, self.item) - - def apply(self, *args, **kwargs): - if _isarg(self.item): - return (*args[0:self.item], self.callable(args[self.item]), *args[self.item + 1:]), kwargs - if _iskwarg(self.item): - return args, {**kwargs, self.item: self.callable(kwargs.get(self.item))} - raise RuntimeError('Houston, we have a problem...') - - -class FactoryOperation(): - def __init__(self, factory, callable): - self.factory = factory - self.callable = callable - - def __repr__(self): - return ''.format(self.callable.__name__) - - def apply(self, *args, **kwargs): - return self.callable(*args, **kwargs) - - -CURSOR_TYPES = {} - - -def operation(mixed): - def decorator(m, ctype=mixed): - def lazy_operation(self, *args, **kwargs): - @functools.wraps(m) - def actual_operation(x): - return m(self, x, *args, **kwargs) - - self.factory.operations.append(Operation(self.item, actual_operation)) - return CURSOR_TYPES[ctype](self.factory, self.item) if ctype else self - - return lazy_operation - - return decorator if isinstance(mixed, str) else decorator(mixed, ctype=None) - - -def factory_operation(m): - def lazy_operation(self, *config): - @functools.wraps(m) - def actual_operation(*args, **kwargs): - return m(self, *config, *args, **kwargs) - - self.operations.append(FactoryOperation(self, actual_operation)) - return self - - return lazy_operation - - -class Cursor(): - _type = None - - def __init__(self, factory, item): - self.factory = factory - self.item = item - - @operation('dict') - def as_dict(self, x): - return x if isinstance(x, dict) else dict(x) - - @operation('int') - def as_int(self, x): - return x if isinstance(x, int) else int(x) - - @operation('str') - def as_str(self, x): - return x if isinstance(x, str) else str(x) - - @operation('list') - def as_list(self, x): - return x if isinstance(x, list) else list(x) - - @operation('tuple') - def as_tuple(self, x): - return x if isinstance(x, tuple) else tuple(x) - - def __getattr__(self, item): - """ - Fallback to type methods if they exist, for example StrCursor.upper will use str.upper if not overriden, etc. - - :param item: - """ - if self._type and item in self._type.__dict__: - method = self._type.__dict__[item] - - @operation - @functools.wraps(method) - def _operation(self, x, *args, **kwargs): - return method(x, *args, **kwargs) - - setattr(self, item, partial(_operation, self)) - return getattr(self, item) - - raise AttributeError('Unknown operation {}.{}().'.format( - type(self).__name__, - item, - )) - - -CURSOR_TYPES['default'] = Cursor - - -class DictCursor(Cursor): - _type = dict - - @operation('default') - def get(self, x, path): - return x.get(path) - - @operation - def map_keys(self, x, mapping): - return {mapping.get(k): v for k, v in x.items()} - - -CURSOR_TYPES['dict'] = DictCursor - - -class StringCursor(Cursor): - _type = str - - -CURSOR_TYPES['str'] = StringCursor - - -class Factory(Configurable): - initialize = Method(required=False) - - def __init__(self, *args, **kwargs): - warnings.warn( - type(self).__name__ + - ' is experimental, API may change in the future, use it as a preview only and knowing the risks.', - FutureWarning - ) - super(Factory, self).__init__(*args, **kwargs) - self.default_cursor_type = 'default' - self.operations = [] - - if self.initialize is not None: - self.initialize(self) - - @factory_operation - def move(self, _from, _to, *args, **kwargs): - if _from == _to: - return args, kwargs - - if _isarg(_from): - value = args[_from] - args = args[:_from] + args[_from + 1:] - elif _iskwarg(_from): - value = kwargs[_from] - kwargs = {k: v for k, v in kwargs if k != _from} - else: - raise RuntimeError('Houston, we have a problem...') - - if _isarg(_to): - return (*args[:_to], value, *args[_to + 1:]), kwargs - elif _iskwarg(_to): - return args, {**kwargs, _to: value} - else: - raise RuntimeError('Houston, we have a problem...') - - def __call__(self, *args, **kwargs): - for operation in self.operations: - args, kwargs = operation.apply(*args, **kwargs) - return Bag(*args, **kwargs) - - def __getitem__(self, item): - return CURSOR_TYPES[self.default_cursor_type](self, item) diff --git a/tests/nodes/test_factory.py b/tests/nodes/test_factory.py deleted file mode 100644 index 781ba57..0000000 --- a/tests/nodes/test_factory.py +++ /dev/null @@ -1,66 +0,0 @@ -from unittest import TestCase - -import pytest - -from bonobo import Bag -from bonobo.nodes.factory import Factory -from bonobo.util.testing import BufferingNodeExecutionContext - - -@pytest.mark.filterwarnings('ignore:Factory') -class FactoryTypeTest(TestCase): - def execute_node(self, node, *rows): - with BufferingNodeExecutionContext(node) as context: - context.write_sync(*rows) - return context.get_buffer() - - def test_args_as_str(self): - f = Factory() - f[0].as_str().upper() - - output = self.execute_node(f, 'foo', 'bar', 'baz') - - assert len(output) == 3 - assert output[0] == 'FOO' - assert output[1] == 'BAR' - assert output[2] == 'BAZ' - - def test_kwargs_as_str(self): - f = Factory() - f['foo'].as_str().upper() - - output = self.execute_node(f, {'foo': 'bar'}, {'foo': 'baz'}) - assert len(output) == 2 - assert output[0] == {'foo': 'BAR'} - assert output[1] == {'foo': 'BAZ'} - - -""" -draft below. - -if __name__ == '__main__': - f = Factory() - - f[0].dict().map_keys({'foo': 'F00'}) - - print('operations:', f.operations) - print(f({'foo': 'bisou'}, foo='blah')) - -specs: - -- rename keys of an input dict (in args, or kwargs) using a translation map. - - -f = Factory() - -f[0] -f['xxx'] = - -f[0].dict().get('foo.bar').move_to('foo.baz').apply(str.upper) -f[0].get('foo.*').items().map(str.lower) - -f['foo'].keys_map({ - 'a': 'b' -}) - -""" From a79c17c3e923214a315bc321fda0596e98b49bbd Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 23 Oct 2017 22:15:19 +0200 Subject: [PATCH 046/145] [tests] bonobo.util.objects --- tests/util/test_objects.py | 69 +++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/tests/util/test_objects.py b/tests/util/test_objects.py index c6e30b2..9a3696e 100644 --- a/tests/util/test_objects.py +++ b/tests/util/test_objects.py @@ -2,7 +2,7 @@ import operator import pytest -from bonobo.util.objects import Wrapper, get_name, ValueHolder +from bonobo.util.objects import Wrapper, get_name, ValueHolder, get_attribute_or_create from bonobo.util.testing import optional_contextmanager @@ -59,6 +59,73 @@ def test_valueholder(): assert repr(x) == repr(y) == repr(43) +def test_valueholder_notequal(): + x = ValueHolder(42) + assert x != 41 + assert not (x != 42) + + +@pytest.mark.parametrize('rlo,rhi', [ + (1, 2), + ('a', 'b'), +]) +def test_valueholder_ordering(rlo, rhi): + vlo, vhi = ValueHolder(rlo), ValueHolder(rhi) + + for lo in (rlo, vlo): + for hi in (rhi, vhi): + assert lo < hi + assert hi > lo + assert lo <= lo + assert not (lo < lo) + assert lo >= lo + + +def test_valueholder_negpos(): + neg, zero, pos = ValueHolder(-1), ValueHolder(0), ValueHolder(1) + + assert -neg == pos + assert -pos == neg + assert -zero == zero + assert +pos == pos + assert +neg == neg + + +def test_valueholders_containers(): + x = ValueHolder({1, 2, 3, 5, 8, 13}) + + assert 5 in x + assert 42 not in x + + y = ValueHolder({'foo': 'bar', 'corp': 'acme'}) + + assert 'foo' in y + assert y['foo'] == 'bar' + with pytest.raises(KeyError): + y['no'] + y['no'] = 'oh, wait' + assert 'no' in y + assert 'oh, wait' == y['no'] + + +def test_get_attribute_or_create(): + class X: + pass + + x = X() + + with pytest.raises(AttributeError): + x.foo + + foo = get_attribute_or_create(x, 'foo', 'bar') + assert foo == 'bar' + assert x.foo == 'bar' + + foo = get_attribute_or_create(x, 'foo', 'baz') + assert foo == 'bar' + assert x.foo == 'bar' + + unsupported_operations = { int: {operator.matmul}, str: { From cb97b18dca63440ac3432ed52945042b2271aa07 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 23 Oct 2017 22:58:35 +0200 Subject: [PATCH 047/145] [tests] trying to speed up the init test. --- tests/test_commands.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 2b9cfb6..a96634c 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -17,11 +17,15 @@ from bonobo.commands.run import DEFAULT_GRAPH_FILENAMES def runner(f): @functools.wraps(f) - def wrapped_runner(*args): + def wrapped_runner(*args, catch_errors=False): with redirect_stdout(io.StringIO()) as stdout, redirect_stderr(io.StringIO()) as stderr: try: f(list(args)) except BaseException as exc: + if not catch_errors: + raise + elif isinstance(catch_errors, BaseException) and not isinstance(exc, catch_errors): + raise return stdout.getvalue(), stderr.getvalue(), exc return stdout.getvalue(), stderr.getvalue() @@ -42,6 +46,7 @@ def runner_module(args): all_runners = pytest.mark.parametrize('runner', [runner_entrypoint, runner_module]) +single_runner = pytest.mark.parametrize('runner', [runner_module]) def test_entrypoint(): @@ -61,7 +66,7 @@ def test_entrypoint(): @all_runners def test_no_command(runner): - _, err, exc = runner() + _, err, exc = runner(catch_errors=True) assert type(exc) == SystemExit assert 'error: the following arguments are required: command' in err @@ -74,26 +79,22 @@ def test_init(runner, tmpdir): assert os.path.isdir(name) assert set(os.listdir(name)) & set(DEFAULT_GRAPH_FILENAMES) - -@all_runners -def test_init_in_empty_directory(runner, tmpdir): +@single_runner +def test_init_in_empty_then_nonempty_directory(runner, tmpdir): name = 'project' tmpdir.chdir() os.mkdir(name) + + # run in empty dir runner('init', name) assert set(os.listdir(name)) & set(DEFAULT_GRAPH_FILENAMES) - -@all_runners -def test_init_in_non_empty_directory(runner, tmpdir): - name = 'project' - tmpdir.chdir() - runner('init', name) + # run in non empty dir with pytest.raises(OutputDirExistsException): runner('init', name) -@all_runners +@single_runner def test_init_within_empty_directory(runner, tmpdir): tmpdir.chdir() runner('init', '.') From bf5f870304da6d6707950717b1a8b087be215b69 Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Sat, 28 Oct 2017 12:10:39 +0200 Subject: [PATCH 048/145] Add a note about the graph variable It needs to be global and publicly available. Some people reading the tutorial may want to put it in its own function or the __main__ conditional, which won't work. --- docs/tutorial/tut01.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/tutorial/tut01.rst b/docs/tutorial/tut01.rst index d6aa604..3d6f9eb 100644 --- a/docs/tutorial/tut01.rst +++ b/docs/tutorial/tut01.rst @@ -105,6 +105,9 @@ To do this, it needs to know what data-flow you want to achieve, and you'll use The `if __name__ == '__main__':` section is not required, unless you want to run it directly using the python interpreter. + The name of the `graph` variable is arbitrary, but this variable must be global and available unconditionally. + Do not put it in its own function or in the `if __name__ == '__main__':` section. + Execute the job ::::::::::::::: From 9c5e98b18bdbec8443b325908cfaa4675d1d26b8 Mon Sep 17 00:00:00 2001 From: Peter Uebele Date: Sat, 28 Oct 2017 11:33:16 +0200 Subject: [PATCH 049/145] adapt tutorial "Working with files" to the latest develop version --- bonobo/examples/tutorials/tut02e02_write.py | 4 ++-- bonobo/examples/tutorials/tut02e03_writeasmap.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bonobo/examples/tutorials/tut02e02_write.py b/bonobo/examples/tutorials/tut02e02_write.py index e5a8445..c4b065d 100644 --- a/bonobo/examples/tutorials/tut02e02_write.py +++ b/bonobo/examples/tutorials/tut02e02_write.py @@ -2,14 +2,14 @@ import bonobo def split_one(line): - return line.split(', ', 1) + return dict(zip(("name", "address"), line.split(', ', 1))) graph = bonobo.Graph( bonobo.FileReader('coffeeshops.txt'), split_one, bonobo.JsonWriter( - 'coffeeshops.json', fs='fs.output', ioformat='arg0' + 'coffeeshops.json', fs='fs.output' ), ) diff --git a/bonobo/examples/tutorials/tut02e03_writeasmap.py b/bonobo/examples/tutorials/tut02e03_writeasmap.py index e234f22..c7c7711 100644 --- a/bonobo/examples/tutorials/tut02e03_writeasmap.py +++ b/bonobo/examples/tutorials/tut02e03_writeasmap.py @@ -11,7 +11,7 @@ def split_one_to_map(line): class MyJsonWriter(bonobo.JsonWriter): prefix, suffix = '{', '}' - def write(self, fs, file, lineno, row): + def write(self, fs, file, lineno, **row): return bonobo.FileWriter.write( self, fs, file, lineno, json.dumps(row)[1:-1] ) @@ -20,7 +20,7 @@ class MyJsonWriter(bonobo.JsonWriter): graph = bonobo.Graph( bonobo.FileReader('coffeeshops.txt'), split_one_to_map, - MyJsonWriter('coffeeshops.json', fs='fs.output', ioformat='arg0'), + MyJsonWriter('coffeeshops.json', fs='fs.output'), ) From a94efeaafc04a108e8dd5ec42fad993d72383b11 Mon Sep 17 00:00:00 2001 From: Stefan Zimmermann Date: Sat, 28 Oct 2017 13:08:48 +0200 Subject: [PATCH 050/145] commands.run: Enable relative imports in main.py Do project's main.py loading with importlib instead of runpy and define a __bonobo__ parent package for the user directory --- bonobo/commands/run.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 0a11577..5ed0441 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -1,4 +1,6 @@ +from importlib.util import spec_from_file_location, module_from_spec import os +import sys import bonobo from bonobo.constants import DEFAULT_SERVICES_ATTR, DEFAULT_SERVICES_FILENAME @@ -76,7 +78,12 @@ def read(filename, module, install=False, quiet=False, verbose=False, env=None): elif install: requirements = os.path.join(os.path.dirname(filename), 'requirements.txt') _install_requirements(requirements) - context = runpy.run_path(filename, run_name='__bonobo__') + spec = spec_from_file_location('__bonobo__', filename) + main = sys.modules['__bonobo__'] = module_from_spec(spec) + main.__path__ = [os.path.dirname(filename)] + main.__package__ = '__bonobo__' + spec.loader.exec_module(main) + context = main.__dict__ elif module: context = runpy.run_module(module, run_name='__bonobo__') filename = context['__file__'] From 9e86abca60c1f90cc9d79de86840772b4b2c867c Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Sat, 28 Oct 2017 14:08:53 +0200 Subject: [PATCH 051/145] Issue #134: add a `bonobo download url` command This enables users on different platforms to download the examples in the tutorial using the same command. --- bonobo/commands/download.py | 43 +++++++++++++++++++++++++++++++++++++ docs/changelog.rst | 11 ++++++++++ setup.py | 3 ++- tests/test_commands.py | 24 +++++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 bonobo/commands/download.py diff --git a/bonobo/commands/download.py b/bonobo/commands/download.py new file mode 100644 index 0000000..897d7fc --- /dev/null +++ b/bonobo/commands/download.py @@ -0,0 +1,43 @@ +import io +import re +import urllib.request + +import bonobo + +EXAMPLES_BASE_URL = 'https://raw.githubusercontent.com/python-bonobo/bonobo/master/bonobo/examples/' +"""The URL to our git repository, in raw mode.""" + + +def _save_stream(fin, fout): + """Read the input stream and write it to the output stream block-by-block.""" + while True: + data = fin.read(io.DEFAULT_BUFFER_SIZE) + if data: + fout.write(data) + else: + break + + +def _open_url(url): + """Open a HTTP connection to the URL and return a file-like object.""" + response = urllib.request.urlopen(url) + if response.getcode() != 200: + raise IOError('unable to download {}, HTTP {}'.format(url, response.getcode())) + return response + + +def execute(path, *args, **kwargs): + path = path.lstrip('/') + if not path.startswith('examples'): + raise ValueError('download command currently supports examples only') + examples_path = re.sub('^examples/', '', path) + output_path = bonobo.get_examples_path(examples_path) + fin = _open_url(EXAMPLES_BASE_URL + examples_path) + with open(output_path, 'wb') as fout: + _save_stream(fin, fout) + print('saved to {}'.format(output_path)) + + +def register(parser): + parser.add_argument('path', help='The relative path of the thing to download.') + return execute diff --git a/docs/changelog.rst b/docs/changelog.rst index a049822..60a8d2b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ Changelog ========= +Unreleased +:::::::::: + +New features +------------ + +Command line +............ + +* `bonobo download /examples/datasets/coffeeshops.txt` now downloads the coffeeshops example + v.0.5.0 - 5 october 2017 :::::::::::::::::::::::: diff --git a/setup.py b/setup.py index 8278209..ba95cb8 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,8 @@ setup( 'bonobo.commands': [ 'convert = bonobo.commands.convert:register', 'init = bonobo.commands.init:register', 'inspect = bonobo.commands.inspect:register', 'run = bonobo.commands.run:register', - 'version = bonobo.commands.version:register' + 'version = bonobo.commands.version:register', + 'download = bonobo.commands.download:register', ], 'console_scripts': ['bonobo = bonobo.commands:entrypoint'] }, diff --git a/tests/test_commands.py b/tests/test_commands.py index a96634c..9b52445 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -13,6 +13,7 @@ from cookiecutter.exceptions import OutputDirExistsException from bonobo import __main__, __version__, get_examples_path from bonobo.commands import entrypoint from bonobo.commands.run import DEFAULT_GRAPH_FILENAMES +from bonobo.commands.download import EXAMPLES_BASE_URL def runner(f): @@ -152,6 +153,29 @@ def test_version(runner): assert __version__ in out +@all_runners +def test_download_works_for_examples(runner): + fout = io.BytesIO() + fout.close = lambda: None + + expected_bytes = b'hello world' + with patch('bonobo.commands.download._open_url') as mock_open_url, \ + patch('bonobo.commands.download.open') as mock_open: + mock_open_url.return_value = io.BytesIO(expected_bytes) + mock_open.return_value = fout + runner('download', 'examples/datasets/coffeeshops.txt') + expected_url = EXAMPLES_BASE_URL + 'datasets/coffeeshops.txt' + mock_open_url.assert_called_once_with(expected_url) + + assert fout.getvalue() == expected_bytes + + +@all_runners +def test_download_fails_non_example(runner): + with pytest.raises(ValueError): + runner('download', '/something/entirely/different.txt') + + @all_runners class TestDefaultEnvFile(object): def test_run_file_with_default_env_file(self, runner): From edc2321c54daf269d141f6ffd5a81960a329bff1 Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Sat, 28 Oct 2017 14:20:53 +0200 Subject: [PATCH 052/145] Issue #134: update documentation --- docs/tutorial/tut02.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/tutorial/tut02.rst b/docs/tutorial/tut02.rst index 9ad5ae3..7f51558 100644 --- a/docs/tutorial/tut02.rst +++ b/docs/tutorial/tut02.rst @@ -59,13 +59,7 @@ available in **Bonobo**'s repository: .. code-block:: shell-session - $ curl https://raw.githubusercontent.com/python-bonobo/bonobo/master/bonobo/examples/datasets/coffeeshops.txt > `python3 -c 'import bonobo; print(bonobo.get_examples_path("datasets/coffeeshops.txt"))'` - -.. note:: - - The "example dataset download" step will be easier in the future. - - https://github.com/python-bonobo/bonobo/issues/134 + $ bonobo download examples/datasets/coffeeshops.txt .. literalinclude:: ../../bonobo/examples/tutorials/tut02e01_read.py :language: python From 66bda718c558c79cfd296eb30b29dbd9ee2ea752 Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Sat, 28 Oct 2017 15:58:07 +0200 Subject: [PATCH 053/145] update Projectfile with download entry point --- Projectfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Projectfile b/Projectfile index c1abd79..6873522 100644 --- a/Projectfile +++ b/Projectfile @@ -34,6 +34,7 @@ python.setup( 'inspect = bonobo.commands.inspect:register', 'run = bonobo.commands.run:register', 'version = bonobo.commands.version:register', + 'download = bonobo.commands.download:register', ], } ) From eabc79c8ecc16d66e48a58e5e29e6501a5db312f Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Sat, 28 Oct 2017 16:11:58 +0200 Subject: [PATCH 054/145] Issue #134: use requests instead of urllib --- bonobo/commands/download.py | 25 +++++++++++-------------- tests/test_commands.py | 14 +++++++++++--- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/bonobo/commands/download.py b/bonobo/commands/download.py index 897d7fc..0106645 100644 --- a/bonobo/commands/download.py +++ b/bonobo/commands/download.py @@ -1,6 +1,7 @@ import io import re -import urllib.request + +import requests import bonobo @@ -8,21 +9,17 @@ EXAMPLES_BASE_URL = 'https://raw.githubusercontent.com/python-bonobo/bonobo/mast """The URL to our git repository, in raw mode.""" -def _save_stream(fin, fout): - """Read the input stream and write it to the output stream block-by-block.""" - while True: - data = fin.read(io.DEFAULT_BUFFER_SIZE) - if data: - fout.write(data) - else: - break +def _write_response(response, fout): + """Read the response and write it to the output stream in chunks.""" + for chunk in response.iter_content(io.DEFAULT_BUFFER_SIZE): + fout.write(chunk) def _open_url(url): """Open a HTTP connection to the URL and return a file-like object.""" - response = urllib.request.urlopen(url) - if response.getcode() != 200: - raise IOError('unable to download {}, HTTP {}'.format(url, response.getcode())) + response = requests.get(url, stream=True) + if response.status_code != 200: + raise IOError('unable to download {}, HTTP {}'.format(url, response.status_code)) return response @@ -32,9 +29,9 @@ def execute(path, *args, **kwargs): raise ValueError('download command currently supports examples only') examples_path = re.sub('^examples/', '', path) output_path = bonobo.get_examples_path(examples_path) - fin = _open_url(EXAMPLES_BASE_URL + examples_path) + response = _open_url(EXAMPLES_BASE_URL + examples_path) with open(output_path, 'wb') as fout: - _save_stream(fin, fout) + _write_response(response, fout) print('saved to {}'.format(output_path)) diff --git a/tests/test_commands.py b/tests/test_commands.py index 9b52445..f68657c 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -4,7 +4,7 @@ import os import runpy import sys from contextlib import redirect_stdout, redirect_stderr -from unittest.mock import patch +from unittest.mock import patch, Mock import pkg_resources import pytest @@ -155,13 +155,21 @@ def test_version(runner): @all_runners def test_download_works_for_examples(runner): + expected_bytes = b'hello world' + + class MockResponse(object): + def __init__(self): + self.status_code = 200 + + def iter_content(self, *args, **kwargs): + return [expected_bytes] + fout = io.BytesIO() fout.close = lambda: None - expected_bytes = b'hello world' with patch('bonobo.commands.download._open_url') as mock_open_url, \ patch('bonobo.commands.download.open') as mock_open: - mock_open_url.return_value = io.BytesIO(expected_bytes) + mock_open_url.return_value = MockResponse() mock_open.return_value = fout runner('download', 'examples/datasets/coffeeshops.txt') expected_url = EXAMPLES_BASE_URL + 'datasets/coffeeshops.txt' From 3e7898a9877a64fec0d96b733e77b11784b60f95 Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Sat, 28 Oct 2017 16:19:05 +0200 Subject: [PATCH 055/145] Issue #134: use requests.get as a context manager --- bonobo/commands/download.py | 3 +-- tests/test_commands.py | 6 ++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bonobo/commands/download.py b/bonobo/commands/download.py index 0106645..fd51951 100644 --- a/bonobo/commands/download.py +++ b/bonobo/commands/download.py @@ -29,8 +29,7 @@ def execute(path, *args, **kwargs): raise ValueError('download command currently supports examples only') examples_path = re.sub('^examples/', '', path) output_path = bonobo.get_examples_path(examples_path) - response = _open_url(EXAMPLES_BASE_URL + examples_path) - with open(output_path, 'wb') as fout: + with _open_url(EXAMPLES_BASE_URL + examples_path) as response, open(output_path, 'wb') as fout: _write_response(response, fout) print('saved to {}'.format(output_path)) diff --git a/tests/test_commands.py b/tests/test_commands.py index f68657c..c78fa5f 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -164,6 +164,12 @@ def test_download_works_for_examples(runner): def iter_content(self, *args, **kwargs): return [expected_bytes] + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + pass + fout = io.BytesIO() fout.close = lambda: None From efb31126b4d2bd44765afe002f5561168f3262c9 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 28 Oct 2017 17:24:26 +0200 Subject: [PATCH 056/145] [tests] simplify assertion --- tests/io/test_csv.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/io/test_csv.py b/tests/io/test_csv.py index 9048bef..473d243 100644 --- a/tests/io/test_csv.py +++ b/tests/io/test_csv.py @@ -44,18 +44,18 @@ def test_read_csv_from_file_kwargs(tmpdir): CsvReader(path=filename, delimiter=','), services=services, ) as context: - context.write(BEGIN, Bag(), END) - context.step() - output = context.get_buffer() + context.write_sync(()) + + assert context.get_buffer() == [ + { + 'a': 'a foo', + 'b': 'b foo', + 'c': 'c foo', + }, + { + 'a': 'a bar', + 'b': 'b bar', + 'c': 'c bar', + } + ] - 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', - } From 08f0950f02fffcc2303576127b7fca8cb7cba638 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 28 Oct 2017 17:29:05 +0200 Subject: [PATCH 057/145] Update Makefile/setup. --- Makefile | 2 +- setup.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1ea5ea5..50039c7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4a5 on 2017-10-22. +# Generated by Medikit 0.4a5 on 2017-10-28. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/setup.py b/setup.py index ba95cb8..29c6255 100644 --- a/setup.py +++ b/setup.py @@ -69,8 +69,7 @@ setup( 'bonobo.commands': [ 'convert = bonobo.commands.convert:register', 'init = bonobo.commands.init:register', 'inspect = bonobo.commands.inspect:register', 'run = bonobo.commands.run:register', - 'version = bonobo.commands.version:register', - 'download = bonobo.commands.download:register', + 'version = bonobo.commands.version:register', 'download = bonobo.commands.download:register' ], 'console_scripts': ['bonobo = bonobo.commands:entrypoint'] }, From 10a82cce62eb61ba06a2f341f15745b544946b24 Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Sat, 28 Oct 2017 15:57:37 +0200 Subject: [PATCH 058/145] Support line-delimited JSON New nodes for handling line-delimited JSON. https://en.wikipedia.org/wiki/JSON_Streaming --- bonobo/_api.py | 3 +++ bonobo/nodes/io/__init__.py | 4 +++- bonobo/nodes/io/json.py | 18 ++++++++++++++++++ docs/changelog.rst | 5 +++++ tests/io/test_json.py | 32 +++++++++++++++++++++++++++++++- 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index 84b5e19..a2c3856 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -2,6 +2,7 @@ import logging from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ PickleReader, PickleWriter, PrettyPrinter, RateLimited, Tee, arg0_to_kwargs, count, identity, kwargs_to_arg0, noop +from bonobo.nodes import LdjsonReader, LdjsonWriter from bonobo.strategies import create_strategy from bonobo.structs import Bag, ErrorBag, Graph, Token from bonobo.util import get_name @@ -110,6 +111,8 @@ register_api_group( Filter, JsonReader, JsonWriter, + LdjsonReader, + LdjsonWriter, Limit, PickleReader, PickleWriter, diff --git a/bonobo/nodes/io/__init__.py b/bonobo/nodes/io/__init__.py index f364dd9..4e7fbe6 100644 --- a/bonobo/nodes/io/__init__.py +++ b/bonobo/nodes/io/__init__.py @@ -1,7 +1,7 @@ """ Readers and writers for common file formats. """ from .file import FileReader, FileWriter -from .json import JsonReader, JsonWriter +from .json import JsonReader, JsonWriter, LdjsonReader, LdjsonWriter from .csv import CsvReader, CsvWriter from .pickle import PickleReader, PickleWriter @@ -12,6 +12,8 @@ __all__ = [ 'FileWriter', 'JsonReader', 'JsonWriter', + 'LdjsonReader', + 'LdjsonWriter', 'PickleReader', 'PickleWriter', ] diff --git a/bonobo/nodes/io/json.py b/bonobo/nodes/io/json.py index 533d628..54e7b71 100644 --- a/bonobo/nodes/io/json.py +++ b/bonobo/nodes/io/json.py @@ -45,3 +45,21 @@ class JsonWriter(FileWriter, JsonHandler): self._write_line(file, (self.eol if lineno.value else '') + json.dumps(row)) 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: + print(line) + yield self.loader(line) + + +class LdjsonWriter(FileWriter): + """Write a stream of JSON objects, one object per line.""" + def write(self, fs, file, lineno, **row): + lineno += 1 # class-level variable + file.write(json.dumps(row) + '\n') + return NOT_MODIFIED diff --git a/docs/changelog.rst b/docs/changelog.rst index 60a8d2b..a222414 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,11 @@ Command line * `bonobo download /examples/datasets/coffeeshops.txt` now downloads the coffeeshops example +Graphs and Nodes +................ + +* New `LdjsonReader` and `LdjsonWriter` nodes for handling `line-delimited JSON `_. + v.0.5.0 - 5 october 2017 :::::::::::::::::::::::: diff --git a/tests/io/test_json.py b/tests/io/test_json.py index c0124cd..726d35f 100644 --- a/tests/io/test_json.py +++ b/tests/io/test_json.py @@ -1,8 +1,9 @@ import pytest from bonobo import JsonReader, JsonWriter, settings +from bonobo import LdjsonReader, LdjsonWriter from bonobo.execution.node import NodeExecutionContext -from bonobo.util.testing import FilesystemTester +from bonobo.util.testing import FilesystemTester, BufferingNodeExecutionContext json_tester = FilesystemTester('json') json_tester.input_data = '''[{"x": "foo"},{"x": "bar"}]''' @@ -32,3 +33,32 @@ def test_write_json_kwargs(tmpdir, add_kwargs): with fs.open(filename) as fp: assert fp.read() == '[{"foo": "bar"}]' + + +stream_json_tester = FilesystemTester('json') +stream_json_tester.input_data = '''{"foo": "bar"}\n{"baz": "boz"}''' + + +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() + + expected = [{"foo": "bar"}, {"baz": "boz"}] + assert expected == actual + + +def test_write_stream_json(tmpdir): + fs, filename, services = stream_json_tester.get_services_for_reader(tmpdir) + + with BufferingNodeExecutionContext(LdjsonWriter(filename), + services=services) as context: + context.write_sync({'foo': 'bar'}) + context.write_sync({'baz': 'boz'}) + + expected = '''{"foo": "bar"}\n{"baz": "boz"}\n''' + with fs.open(filename) as fin: + actual = fin.read() + assert expected == actual From f9c809f626497dfaa80d10f37de3973ad9feac07 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 29 Oct 2017 12:05:29 +0100 Subject: [PATCH 059/145] [core] Change the token parsing part in prevision of different flags. --- bonobo/execution/node.py | 47 +++++++++++++++++++++++++---- bonobo/nodes/io/json.py | 1 - tests/features/test_not_modified.py | 24 +++++++++++++++ 3 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 tests/features/test_not_modified.py diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index 6c52e7d..22582e6 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -9,7 +9,7 @@ from bonobo.execution.base import LoopingExecutionContext 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 +from bonobo.util import get_name, iserrorbag, isloopbackbag, isbag, istuple from bonobo.util.compat import deprecated_alias from bonobo.util.statistics import WithStatistics @@ -137,12 +137,47 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): pass -def _resolve(input_bag, output): - # NotModified means to send the input unmodified to output. - if output is NOT_MODIFIED: - return input_bag +def isflag(param): + return isinstance(param, Token) and param in (NOT_MODIFIED,) + +def split_tokens(output): + """ + Split an output into token tuple, real output tuple. + + :param output: + :return: tuple, tuple + """ + if isinstance(output, Token): + # just a flag + return (output,), () + + if not istuple(output): + # no flag + return (), (output,) + + i = 0 + while isflag(output[i]): + 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 - return Bag(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) diff --git a/bonobo/nodes/io/json.py b/bonobo/nodes/io/json.py index 54e7b71..404cdcb 100644 --- a/bonobo/nodes/io/json.py +++ b/bonobo/nodes/io/json.py @@ -53,7 +53,6 @@ class LdjsonReader(FileReader): def read(self, fs, file): for line in file: - print(line) yield self.loader(line) diff --git a/tests/features/test_not_modified.py b/tests/features/test_not_modified.py new file mode 100644 index 0000000..ddc537b --- /dev/null +++ b/tests/features/test_not_modified.py @@ -0,0 +1,24 @@ +from bonobo.constants import NOT_MODIFIED +from bonobo.util.testing import BufferingNodeExecutionContext + + +def useless(*args, **kwargs): + return NOT_MODIFIED + + +def test_not_modified(): + input_messages = [ + ('foo', 'bar'), + {'foo': 'bar'}, + ('foo', {'bar': 'baz'}), + (), + ] + + with BufferingNodeExecutionContext(useless) as context: + context.write_sync(*input_messages) + + assert context.get_buffer() == input_messages + + + + From cac6920040177648ab5da8f64ac5f32ab4b2c064 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 29 Oct 2017 12:13:52 +0100 Subject: [PATCH 060/145] Minor test change. --- tests/io/test_json.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/io/test_json.py b/tests/io/test_json.py index 726d35f..a3b25f5 100644 --- a/tests/io/test_json.py +++ b/tests/io/test_json.py @@ -20,10 +20,10 @@ def test_write_json_ioformat_arg0(tmpdir): @pytest.mark.parametrize('add_kwargs', ( - {}, - { - 'ioformat': settings.IOFORMAT_KWARGS, - }, + {}, + { + 'ioformat': settings.IOFORMAT_KWARGS, + }, )) def test_write_json_kwargs(tmpdir, add_kwargs): fs, filename, services = json_tester.get_services_for_writer(tmpdir) @@ -55,8 +55,10 @@ def test_write_stream_json(tmpdir): with BufferingNodeExecutionContext(LdjsonWriter(filename), services=services) as context: - context.write_sync({'foo': 'bar'}) - context.write_sync({'baz': 'boz'}) + context.write_sync( + {'foo': 'bar'}, + {'baz': 'boz'}, + ) expected = '''{"foo": "bar"}\n{"baz": "boz"}\n''' with fs.open(filename) as fin: From 8351897e3a21921df7d57e002b03cc8471686caa Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 29 Oct 2017 19:23:50 +0100 Subject: [PATCH 061/145] [core] Refactoring of commands to move towards a more pythonic way of running the jobs. Commands are now classes, and bonobo "graph" related commands now hooks into bonobo.run() calls so it will use what you actually put in your __main__ block. --- MANIFEST.in | 1 + Makefile | 2 +- Projectfile | 23 +-- bonobo/_api.py | 7 +- bonobo/commands/__init__.py | 166 ++++++++++++++++- bonobo/commands/convert.py | 138 +++++++-------- bonobo/commands/download.py | 37 ++-- bonobo/commands/init.py | 51 +++--- bonobo/commands/inspect.py | 47 ++--- bonobo/commands/run.py | 215 ++++++----------------- bonobo/commands/templates/default.py-tpl | 50 ++++++ bonobo/commands/version.py | 52 +++--- bonobo/examples/datasets/coffeeshops.py | 2 +- bonobo/examples/datasets/fablabs.py | 2 +- bonobo/examples/files/csv_handlers.py | 2 +- bonobo/examples/files/json_handlers.py | 2 +- bonobo/examples/files/pickle_handlers.py | 2 +- bonobo/examples/files/text_handlers.py | 2 +- bonobo/ext/django.py | 2 +- bonobo/settings.py | 6 + bonobo/structs/graphs.py | 26 +++ requirements-dev.txt | 2 +- requirements-docker.txt | 2 + requirements-jupyter.txt | 2 +- requirements.txt | 2 + setup.py | 11 +- 26 files changed, 483 insertions(+), 371 deletions(-) create mode 100644 bonobo/commands/templates/default.py-tpl diff --git a/MANIFEST.in b/MANIFEST.in index ab30e9a..4c2c662 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include *.txt +include *.py-tpl diff --git a/Makefile b/Makefile index 50039c7..bb68335 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4a5 on 2017-10-28. +# Generated by Medikit 0.4a5 on 2017-10-29. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index 6873522..d730328 100644 --- a/Projectfile +++ b/Projectfile @@ -29,24 +29,25 @@ python.setup( 'bonobo = bonobo.commands:entrypoint', ], 'bonobo.commands': [ - 'convert = bonobo.commands.convert:register', - 'init = bonobo.commands.init:register', - 'inspect = bonobo.commands.inspect:register', - 'run = bonobo.commands.run:register', - 'version = bonobo.commands.version:register', - 'download = bonobo.commands.download:register', + 'convert = bonobo.commands.convert:ConvertCommand', + 'init = bonobo.commands.init:InitCommand', + 'inspect = bonobo.commands.inspect:InspectCommand', + 'run = bonobo.commands.run:RunCommand', + 'version = bonobo.commands.version:VersionCommand', + 'download = bonobo.commands.download:DownloadCommand', ], } ) python.add_requirements( - 'colorama >=0.3,<1.0', - 'fs >=2.0,<3.0', + 'colorama >=0.3,<0.4', + 'fs >=2.0,<2.1', + 'jinja2 >=2.9,<2.10', 'packaging >=16,<17', - 'psutil >=5.2,<6.0', + 'psutil >=5.4,<6.0', + 'python-dotenv >=0.7,<0.8', 'requests >=2.0,<3.0', - 'stevedore >=1.21,<2.0', - 'python-dotenv >=0.7.1,<1.0', + 'stevedore >=1.27,<1.28', dev=[ 'cookiecutter >=1.5,<1.6', 'pytest-sugar >=0.8,<0.9', diff --git a/bonobo/_api.py b/bonobo/_api.py index a2c3856..fb1ef78 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,5 +1,3 @@ -import logging - from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ PickleReader, PickleWriter, PrettyPrinter, RateLimited, Tee, arg0_to_kwargs, count, identity, kwargs_to_arg0, noop from bonobo.nodes import LdjsonReader, LdjsonWriter @@ -21,7 +19,7 @@ def register_api_group(*args): @register_api -def run(graph, strategy=None, plugins=None, services=None): +def run(graph, *, plugins=None, services=None, **options): """ Main entry point of bonobo. It takes a graph and creates all the necessary plumbery around to execute it. @@ -41,7 +39,7 @@ def run(graph, strategy=None, plugins=None, services=None): :param dict services: The implementations of services this graph will use. :return bonobo.execution.graph.GraphExecutionContext: """ - strategy = create_strategy(strategy) + strategy = create_strategy(options.pop('strategy', None)) plugins = plugins or [] @@ -58,6 +56,7 @@ def run(graph, strategy=None, plugins=None, services=None): try: from bonobo.ext.jupyter import JupyterOutputPlugin except ImportError: + import logging logging.warning( 'Failed to load jupyter widget. Easiest way is to install the optional "jupyter" ' 'dependencies with «pip install bonobo[jupyter]», but you can also install a specific ' diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index 4e183a3..be877d7 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -1,10 +1,97 @@ import argparse +import codecs +import os +import os.path +import runpy +from contextlib import contextmanager +from functools import partial -from bonobo import logging, settings +from bonobo import settings, logging +from bonobo.constants import DEFAULT_SERVICES_FILENAME, DEFAULT_SERVICES_ATTR +from bonobo.util import get_name logger = logging.get_logger() +class BaseCommand: + @property + def logger(self): + try: + return self._logger + except AttributeError: + self._logger = logging.get_logger(get_name(self)) + return self._logger + + def add_arguments(self, parser): + """ + Entry point for subclassed commands to add custom arguments. + """ + pass + + def handle(self, *args, **options): + """ + The actual logic of the command. Subclasses must implement this method. + """ + raise NotImplementedError('Subclasses of BaseCommand must provide a handle() method') + + +class BaseGraphCommand(BaseCommand): + required = True + + def add_arguments(self, parser): + # target arguments (cannot provide both). + source_group = parser.add_mutually_exclusive_group(required=self.required) + source_group.add_argument('file', nargs='?', type=str) + source_group.add_argument('-m', dest='mod', type=str) + + # arguments to enforce system environment. + parser.add_argument('--default-env-file', action='append') + parser.add_argument('--default-env', action='append') + parser.add_argument('--env-file', action='append') + parser.add_argument('--env', '-e', action='append') + + return parser + + def _run_path(self, file): + return runpy.run_path(file, run_name='__main__') + + def _run_module(self, mod): + return runpy.run_module(mod, run_name='__main__') + + def read(self, *, file, mod, **options): + + """ + + get_default_services( + filename, context.get(DEFAULT_SERVICES_ATTR)() if DEFAULT_SERVICES_ATTR in context else None + ) + + """ + + _graph, _options = None, None + + def _record(graph, **options): + nonlocal _graph, _options + _graph, _options = graph, options + + with _override_runner(_record), _override_environment(): + if file: + self._run_path(file) + elif mod: + self._run_module(mod) + else: + raise RuntimeError('No target provided.') + + if _graph is None: + raise RuntimeError('Could not find graph.') + + + return _graph, _options + + def handle(self, *args, **options): + pass + + def entrypoint(args=None): parser = argparse.ArgumentParser() parser.add_argument('--debug', '-D', action='store_true') @@ -17,7 +104,15 @@ def entrypoint(args=None): def register_extension(ext, commands=commands): try: parser = subparsers.add_parser(ext.name) - commands[ext.name] = ext.plugin(parser) + if isinstance(ext.plugin, type) and issubclass(ext.plugin, BaseCommand): + # current way, class based. + cmd = ext.plugin() + cmd.add_arguments(parser) + cmd.__name__ = ext.name + commands[ext.name] = cmd.handle + else: + # old school, function based. + commands[ext.name] = ext.plugin(parser) except Exception: logger.exception('Error while loading command {}.'.format(ext.name)) @@ -33,3 +128,70 @@ def entrypoint(args=None): logger.debug('Command: ' + args['command'] + ' Arguments: ' + repr(args)) commands[args.pop('command')](**args) + + +@contextmanager +def _override_runner(runner): + import bonobo + _runner_backup = bonobo.run + try: + bonobo.run = runner + yield runner + finally: + bonobo.run = _runner_backup + + +@contextmanager +def _override_environment(root_dir=None, **options): + yield + return + if default_env_file: + for f in default_env_file: + env_file_path = str(env_dir.joinpath(f)) + load_dotenv(env_file_path) + if default_env: + for e in default_env: + set_env_var(e) + if env_file: + for f in env_file: + env_file_path = str(env_dir.joinpath(f)) + load_dotenv(env_file_path, override=True) + if env: + for e in env: + set_env_var(e, override=True) + + +def get_default_services(filename, services=None): + dirname = os.path.dirname(filename) + services_filename = os.path.join(dirname, DEFAULT_SERVICES_FILENAME) + if os.path.exists(services_filename): + with open(services_filename) as file: + code = compile(file.read(), services_filename, 'exec') + context = { + '__name__': '__services__', + '__file__': services_filename, + } + exec(code, context) + + return { + **context[DEFAULT_SERVICES_ATTR](), + **(services or {}), + } + return services or {} + + +def set_env_var(e, override=False): + __escape_decoder = codecs.getdecoder('unicode_escape') + ename, evalue = e.split('=', 1) + + def decode_escaped(escaped): + return __escape_decoder(escaped)[0] + + if len(evalue) > 0: + if evalue[0] == evalue[len(evalue) - 1] in ['"', "'"]: + evalue = decode_escaped(evalue[1:-1]) + + if override: + os.environ[ename] = evalue + else: + os.environ.setdefault(ename, evalue) \ No newline at end of file diff --git a/bonobo/commands/convert.py b/bonobo/commands/convert.py index e9039fd..918d81e 100644 --- a/bonobo/commands/convert.py +++ b/bonobo/commands/convert.py @@ -1,83 +1,75 @@ import bonobo +from bonobo.commands import BaseCommand from bonobo.registry import READER, WRITER, default_registry from bonobo.util.resolvers import _resolve_transformations, _resolve_options -def execute( - 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 [])) +class ConvertCommand(BaseCommand): + def add_arguments(self, parser): + parser.add_argument('input-filename', help='Input filename.') + parser.add_argument('output-filename', help='Output filename.') + parser.add_argument( + '--' + READER, + '-r', + help='Choose the reader factory if it cannot be detected from extension, or if detection is wrong.' + ) + parser.add_argument( + '--' + WRITER, + '-w', + help= + 'Choose the writer factory if it cannot be detected from extension, or if detection is wrong (use - for console pretty print).' + ) + parser.add_argument( + '--transformation', + '-t', + dest='transformation', + action='append', + help='Add a transformation between input and output (can be used multiple times, order is preserved).', + ) + parser.add_argument( + '--option', + '-O', + dest='option', + action='append', + help='Add a named option to both reader and writer factories (i.e. foo="bar").', + ) + parser.add_argument( + '--' + READER + '-option', + '-' + READER[0].upper(), + dest=READER + '_option', + action='append', + help='Add a named option to the reader factory.', + ) + parser.add_argument( + '--' + WRITER + '-option', + '-' + WRITER[0].upper(), + dest=WRITER + '_option', + action='append', + help='Add a named option to the writer factory.', + ) - if output_filename == '-': - writer_factory = bonobo.PrettyPrinter - else: - writer_factory = default_registry.get_writer_factory_for(output_filename, format=writer) - writer_options = _resolve_options((option or []) + (writer_option or [])) + def handle(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 [])) - transformations = _resolve_transformations(transformation) + if output_filename == '-': + writer_factory = bonobo.PrettyPrinter + else: + writer_factory = default_registry.get_writer_factory_for(output_filename, format=writer) + writer_options = _resolve_options((option or []) + (writer_option or [])) - graph = bonobo.Graph() - graph.add_chain( - reader_factory(input_filename, **reader_options), - *transformations, - writer_factory(output_filename, **writer_options), - ) + transformations = _resolve_transformations(transformation) - return bonobo.run( - graph, services={ - 'fs': bonobo.open_fs(), - } - ) + graph = bonobo.Graph() + graph.add_chain( + reader_factory(input_filename, **reader_options), + *transformations, + writer_factory(output_filename, **writer_options), + ) - -def register(parser): - parser.add_argument('input-filename', help='Input filename.') - parser.add_argument('output-filename', help='Output filename.') - parser.add_argument( - '--' + READER, - '-r', - help='Choose the reader factory if it cannot be detected from extension, or if detection is wrong.' - ) - parser.add_argument( - '--' + WRITER, - '-w', - help= - 'Choose the writer factory if it cannot be detected from extension, or if detection is wrong (use - for console pretty print).' - ) - parser.add_argument( - '--transformation', - '-t', - dest='transformation', - action='append', - help='Add a transformation between input and output (can be used multiple times, order is preserved).', - ) - parser.add_argument( - '--option', - '-O', - dest='option', - action='append', - help='Add a named option to both reader and writer factories (i.e. foo="bar").', - ) - parser.add_argument( - '--' + READER + '-option', - '-' + READER[0].upper(), - dest=READER + '_option', - action='append', - help='Add a named option to the reader factory.', - ) - parser.add_argument( - '--' + WRITER + '-option', - '-' + WRITER[0].upper(), - dest=WRITER + '_option', - action='append', - help='Add a named option to the writer factory.', - ) - return execute + return bonobo.run( + graph, services={ + 'fs': bonobo.open_fs(), + } + ) diff --git a/bonobo/commands/download.py b/bonobo/commands/download.py index fd51951..9333db4 100644 --- a/bonobo/commands/download.py +++ b/bonobo/commands/download.py @@ -4,36 +4,31 @@ import re import requests import bonobo +from bonobo.commands import BaseCommand EXAMPLES_BASE_URL = 'https://raw.githubusercontent.com/python-bonobo/bonobo/master/bonobo/examples/' """The URL to our git repository, in raw mode.""" -def _write_response(response, fout): - """Read the response and write it to the output stream in chunks.""" - for chunk in response.iter_content(io.DEFAULT_BUFFER_SIZE): - fout.write(chunk) +class DownloadCommand(BaseCommand): + def handle(self, *, path, **options): + path = path.lstrip('/') + if not path.startswith('examples'): + raise ValueError('Download command currently supports examples only') + examples_path = re.sub('^examples/', '', path) + output_path = bonobo.get_examples_path(examples_path) + with _open_url(EXAMPLES_BASE_URL + examples_path) as response, open(output_path, 'wb') as fout: + for chunk in response.iter_content(io.DEFAULT_BUFFER_SIZE): + fout.write(chunk) + self.logger.info('Download saved to {}'.format(output_path)) + + def add_arguments(self, parser): + parser.add_argument('path', help='The relative path of the thing to download.') def _open_url(url): """Open a HTTP connection to the URL and return a file-like object.""" response = requests.get(url, stream=True) if response.status_code != 200: - raise IOError('unable to download {}, HTTP {}'.format(url, response.status_code)) + raise IOError('Unable to download {}, HTTP {}'.format(url, response.status_code)) return response - - -def execute(path, *args, **kwargs): - path = path.lstrip('/') - if not path.startswith('examples'): - raise ValueError('download command currently supports examples only') - examples_path = re.sub('^examples/', '', path) - output_path = bonobo.get_examples_path(examples_path) - with _open_url(EXAMPLES_BASE_URL + examples_path) as response, open(output_path, 'wb') as fout: - _write_response(response, fout) - print('saved to {}'.format(output_path)) - - -def register(parser): - parser.add_argument('path', help='The relative path of the thing to download.') - return execute diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index e69156c..c0c50f1 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -1,28 +1,33 @@ import os -def execute(name, branch): - try: - from cookiecutter.main import cookiecutter - except ImportError as exc: - raise ImportError( - 'You must install "cookiecutter" to use this command.\n\n $ pip install cookiecutter\n' - ) from exc +from jinja2 import Environment, FileSystemLoader - overwrite_if_exists = False - project_path = os.path.join(os.getcwd(), name) - if os.path.isdir(project_path) and not os.listdir(project_path): - overwrite_if_exists = True - - return cookiecutter( - 'https://github.com/python-bonobo/cookiecutter-bonobo.git', - extra_context={'name': name}, - no_input=True, - checkout=branch, - overwrite_if_exists=overwrite_if_exists - ) +from bonobo.commands import BaseCommand -def register(parser): - parser.add_argument('name') - parser.add_argument('--branch', '-b', default='master') - return execute +class InitCommand(BaseCommand): + TEMPLATES = {'job'} + TEMPLATES_PATH = os.path.join(os.path.dirname(__file__), 'templates') + + def add_arguments(self, parser): + parser.add_argument('template', choices=self.TEMPLATES) + parser.add_argument('filename') + parser.add_argument('--force', '-f', default=False, action='store_true') + + def handle(self, *, template, filename, force=False): + template_name = template + name, ext = os.path.splitext(filename) + if ext != '.py': + raise ValueError('Filenames should end with ".py".') + + loader = FileSystemLoader(self.TEMPLATES_PATH) + env = Environment(loader=loader) + template = env.get_template(template_name + '.py-tpl') + + if os.path.exists(filename) and not force: + raise FileExistsError('Target filename already exists, use --force to override.') + + with open(filename, 'w+') as f: + f.write(template.render(name=name)) + + self.logger.info('Generated {} using template {!r}.'.format(filename, template_name)) diff --git a/bonobo/commands/inspect.py b/bonobo/commands/inspect.py index 1ab6b5b..9a802d9 100644 --- a/bonobo/commands/inspect.py +++ b/bonobo/commands/inspect.py @@ -1,40 +1,21 @@ -import json +from bonobo.commands import BaseGraphCommand -from bonobo.commands.run import read, register_generic_run_arguments -from bonobo.constants import BEGIN -from bonobo.util.objects import get_name - -OUTPUT_GRAPHVIZ = 'graphviz' +OUTPUT_GRAPH = 'graphviz' -def _ident(graph, i): - escaped_index = str(i) - escaped_name = json.dumps(get_name(graph[i])) - return '{{{} [label={}]}}'.format(escaped_index, escaped_name) +class InspectCommand(BaseGraphCommand): + def add_arguments(self, parser): + super(InspectCommand, self).add_arguments(parser) + parser.add_argument('--graph', '-g', dest='output', action='store_const', const=OUTPUT_GRAPH) + def handle(self, output=None, **options): + if output is None: + raise ValueError('Output type must be provided (try --graph/-g).') -def execute(*, output, **kwargs): - graph, plugins, services = read(**kwargs) + graph, params = self.read(**options) - if output == OUTPUT_GRAPHVIZ: - print('digraph {') - print(' rankdir = LR;') - print(' "BEGIN" [shape="point"];') + if output == OUTPUT_GRAPH: + print(graph._repr_dot_()) + else: + raise NotImplementedError('Output type not implemented.') - for i in graph.outputs_of(BEGIN): - print(' "BEGIN" -> ' + _ident(graph, i) + ';') - - for ix in graph.topologically_sorted_indexes: - for iy in graph.outputs_of(ix): - print(' {} -> {};'.format(_ident(graph, ix), _ident(graph, iy))) - - print('}') - else: - raise NotImplementedError('Output type not implemented.') - - -def register(parser): - register_generic_run_arguments(parser) - parser.add_argument('--graph', '-g', dest='output', action='store_const', const=OUTPUT_GRAPHVIZ) - parser.set_defaults(output=OUTPUT_GRAPHVIZ) - return execute diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index be84d18..799816f 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -1,38 +1,60 @@ -import codecs import os -import sys -from importlib.util import spec_from_file_location, module_from_spec -from pathlib import Path - -from dotenv import load_dotenv import bonobo -from bonobo.constants import DEFAULT_SERVICES_ATTR, DEFAULT_SERVICES_FILENAME - -DEFAULT_GRAPH_FILENAMES = ( - '__main__.py', - 'main.py', -) -DEFAULT_GRAPH_ATTR = 'get_graph' +from bonobo.commands import BaseGraphCommand -def get_default_services(filename, services=None): - dirname = os.path.dirname(filename) - services_filename = os.path.join(dirname, DEFAULT_SERVICES_FILENAME) - if os.path.exists(services_filename): - with open(services_filename) as file: - code = compile(file.read(), services_filename, 'exec') - context = { - '__name__': '__bonobo__', - '__file__': services_filename, - } - exec(code, context) +class RunCommand(BaseGraphCommand): + install = False - return { - **context[DEFAULT_SERVICES_ATTR](), - **(services or {}), - } - return services or {} + def add_arguments(self, parser): + super(RunCommand, self).add_arguments(parser) + + verbosity_group = parser.add_mutually_exclusive_group() + verbosity_group.add_argument('--quiet', '-q', action='store_true') + verbosity_group.add_argument('--verbose', '-v', action='store_true') + + parser.add_argument('--install', '-I', action='store_true') + + def _run_path(self, file): + if self.install: + if os.path.isdir(file): + requirements = os.path.join(file, 'requirements.txt') + else: + requirements = os.path.join(os.path.dirname(file), 'requirements.txt') + _install_requirements(requirements) + + return super()._run_path(file) + + def _run_module(self, mod): + if self.install: + raise RuntimeError('--install behaviour when running a module is not defined.') + + return super()._run_module(mod) + + def handle(self, *args, quiet=False, verbose=False, install=False, **options): + from bonobo import settings + + settings.QUIET.set_if_true(quiet) + settings.DEBUG.set_if_true(verbose) + self.install = install + + graph, params = self.read(**options) + + params['plugins'] = set(params.pop('plugins', ())).union(set(options.pop('plugins', ()))) + + return bonobo.run(graph, **params) + + +def register_generic_run_arguments(parser, required=True): + """ + Only there for backward compatibility with third party extensions. + TODO: This should be deprecated (using the @deprecated decorator) in 0.7, and removed in 0.8 or 0.9. + """ + dummy_command = BaseGraphCommand() + dummy_command.required = required + dummy_command.add_arguments(parser) + return parser def _install_requirements(requirements): @@ -47,138 +69,3 @@ def _install_requirements(requirements): pip.utils.pkg_resources = importlib.reload(pip.utils.pkg_resources) import site importlib.reload(site) - - -def read( - filename, - module, - install=False, - quiet=False, - verbose=False, - default_env_file=None, - default_env=None, - env_file=None, - env=None -): - import runpy - from bonobo import Graph, settings - - if quiet: - settings.QUIET.set(True) - - if verbose: - settings.DEBUG.set(True) - - if filename: - if os.path.isdir(filename): - if install: - requirements = os.path.join(filename, 'requirements.txt') - _install_requirements(requirements) - - pathname = filename - for filename in DEFAULT_GRAPH_FILENAMES: - filename = os.path.join(pathname, filename) - if os.path.exists(filename): - break - if not os.path.exists(filename): - raise IOError('Could not find entrypoint (candidates: {}).'.format(', '.join(DEFAULT_GRAPH_FILENAMES))) - elif install: - requirements = os.path.join(os.path.dirname(filename), 'requirements.txt') - _install_requirements(requirements) - spec = spec_from_file_location('__bonobo__', filename) - main = sys.modules['__bonobo__'] = module_from_spec(spec) - main.__path__ = [os.path.dirname(filename)] - main.__package__ = '__bonobo__' - spec.loader.exec_module(main) - context = main.__dict__ - elif module: - context = runpy.run_module(module, run_name='__bonobo__') - filename = context['__file__'] - else: - raise RuntimeError('UNEXPECTED: argparse should not allow this.') - - env_dir = Path(filename).parent or Path(module).parent - if default_env_file: - for f in default_env_file: - env_file_path = str(env_dir.joinpath(f)) - load_dotenv(env_file_path) - if default_env: - for e in default_env: - set_env_var(e) - if env_file: - for f in env_file: - env_file_path = str(env_dir.joinpath(f)) - load_dotenv(env_file_path, override=True) - if env: - for e in env: - set_env_var(e, override=True) - - graphs = dict((k, v) for k, v in context.items() if isinstance(v, Graph)) - - assert len(graphs) == 1, ( - 'Having zero or more than one graph definition in one file is unsupported for now, ' - 'but it is something that will be implemented in the future.\n\nExpected: 1, got: {}.' - ).format(len(graphs)) - - graph = list(graphs.values())[0] - plugins = [] - services = get_default_services( - filename, context.get(DEFAULT_SERVICES_ATTR)() if DEFAULT_SERVICES_ATTR in context else None - ) - - return graph, plugins, services - - -def set_env_var(e, override=False): - __escape_decoder = codecs.getdecoder('unicode_escape') - ename, evalue = e.split('=', 1) - - def decode_escaped(escaped): - return __escape_decoder(escaped)[0] - - if len(evalue) > 0: - if evalue[0] == evalue[len(evalue) - 1] in ['"', "'"]: - evalue = decode_escaped(evalue[1:-1]) - - if override: - os.environ[ename] = evalue - else: - os.environ.setdefault(ename, evalue) - - -def execute( - filename, - module, - install=False, - quiet=False, - verbose=False, - default_env_file=None, - default_env=None, - env_file=None, - env=None -): - graph, plugins, services = read( - filename, module, install, quiet, verbose, default_env_file, default_env, env_file, env - ) - - return bonobo.run(graph, plugins=plugins, services=services) - - -def register_generic_run_arguments(parser, required=True): - source_group = parser.add_mutually_exclusive_group(required=required) - source_group.add_argument('filename', nargs='?', type=str) - source_group.add_argument('--module', '-m', type=str) - parser.add_argument('--default-env-file', action='append') - parser.add_argument('--default-env', action='append') - parser.add_argument('--env-file', action='append') - parser.add_argument('--env', '-e', action='append') - return parser - - -def register(parser): - parser = register_generic_run_arguments(parser) - verbosity_group = parser.add_mutually_exclusive_group() - verbosity_group.add_argument('--quiet', '-q', action='store_true') - verbosity_group.add_argument('--verbose', '-v', action='store_true') - parser.add_argument('--install', '-I', action='store_true') - return execute diff --git a/bonobo/commands/templates/default.py-tpl b/bonobo/commands/templates/default.py-tpl new file mode 100644 index 0000000..1d8d6a5 --- /dev/null +++ b/bonobo/commands/templates/default.py-tpl @@ -0,0 +1,50 @@ +import bonobo + +def extract(): + """Placeholder, change, rename, remove... """ + yield 'hello' + yield 'world' + + +def transform(*args): + """Placeholder, change, rename, remove... """ + yield tuple( + map(str.title, args) + ) + + +def load(*args): + """Placeholder, change, rename, remove... """ + print(*args) + + +def get_graph(): + """ + This function builds the graph that needs to be executed. + + :return: bonobo.Graph + + """ + graph = bonobo.Graph() + graph.add_chain(extract, transform, load) + + return graph + + +def get_services(): + """ + This function builds the services dictionary, which is a simple dict of names-to-implementation used by bonobo + for runtime injection. + + It will be used on top of the defaults provided by bonobo (fs, http, ...). You can override those defaults, or just + let the framework define them. You can also define your own services and naming is up to you. + + :return: dict + """ + return {} + + +# The __main__ block actually execute the graph. +if __name__ == '__main__': + # Although you're not required to use it, bonobo's graph related commands will hook to this call (inspect, run, ...). + bonobo.run(get_graph(), services=get_services()) diff --git a/bonobo/commands/version.py b/bonobo/commands/version.py index 6d4f3e7..3e3239a 100644 --- a/bonobo/commands/version.py +++ b/bonobo/commands/version.py @@ -1,4 +1,30 @@ -def format_version(mod, *, name=None, quiet=False): +from bonobo.commands import BaseCommand + + +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)) + + def add_arguments(self, parser): + parser.add_argument('--all', '-a', action='store_true') + parser.add_argument('--quiet', '-q', action='count') + + +def _format_version(mod, *, name=None, quiet=False): from bonobo.util.pkgs import bonobo_packages args = { 'name': name or mod.__name__, @@ -14,27 +40,3 @@ def format_version(mod, *, name=None, quiet=False): return '{version}'.format(**args) raise RuntimeError('Hard to be so quiet...') - - -def execute(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)) - - -def register(parser): - parser.add_argument('--all', '-a', action='store_true') - parser.add_argument('--quiet', '-q', action='count') - return execute diff --git a/bonobo/examples/datasets/coffeeshops.py b/bonobo/examples/datasets/coffeeshops.py index dc3db52..fd754ef 100644 --- a/bonobo/examples/datasets/coffeeshops.py +++ b/bonobo/examples/datasets/coffeeshops.py @@ -14,7 +14,7 @@ Extracts a list of parisian bars where you can buy a coffee for a reasonable pri """ import bonobo -from bonobo.commands.run import get_default_services +from bonobo.commands import get_default_services from bonobo.ext.opendatasoft import OpenDataSoftAPI filename = 'coffeeshops.txt' diff --git a/bonobo/examples/datasets/fablabs.py b/bonobo/examples/datasets/fablabs.py index 986aea9..d03775b 100644 --- a/bonobo/examples/datasets/fablabs.py +++ b/bonobo/examples/datasets/fablabs.py @@ -19,7 +19,7 @@ import json from colorama import Fore, Style import bonobo -from bonobo.commands.run import get_default_services +from bonobo.commands import get_default_services from bonobo.ext.opendatasoft import OpenDataSoftAPI try: diff --git a/bonobo/examples/files/csv_handlers.py b/bonobo/examples/files/csv_handlers.py index 33412c3..555bc67 100644 --- a/bonobo/examples/files/csv_handlers.py +++ b/bonobo/examples/files/csv_handlers.py @@ -1,5 +1,5 @@ import bonobo -from bonobo.commands.run import get_default_services +from bonobo.commands import get_default_services graph = bonobo.Graph( bonobo.CsvReader('datasets/coffeeshops.txt', headers=('item', )), diff --git a/bonobo/examples/files/json_handlers.py b/bonobo/examples/files/json_handlers.py index 27dc38e..f1818cd 100644 --- a/bonobo/examples/files/json_handlers.py +++ b/bonobo/examples/files/json_handlers.py @@ -1,6 +1,6 @@ import bonobo from bonobo import Bag -from bonobo.commands.run import get_default_services +from bonobo.commands import get_default_services def get_fields(**row): diff --git a/bonobo/examples/files/pickle_handlers.py b/bonobo/examples/files/pickle_handlers.py index 71a2b9a..ed2ecd4 100644 --- a/bonobo/examples/files/pickle_handlers.py +++ b/bonobo/examples/files/pickle_handlers.py @@ -28,7 +28,7 @@ messages categorized as spam, and (3) prints the output. ''' import bonobo -from bonobo.commands.run import get_default_services +from bonobo.commands import get_default_services from fs.tarfs import TarFS diff --git a/bonobo/examples/files/text_handlers.py b/bonobo/examples/files/text_handlers.py index 6ca6ef8..abbae1a 100644 --- a/bonobo/examples/files/text_handlers.py +++ b/bonobo/examples/files/text_handlers.py @@ -1,5 +1,5 @@ import bonobo -from bonobo.commands.run import get_default_services +from bonobo.commands import get_default_services def skip_comments(line): diff --git a/bonobo/ext/django.py b/bonobo/ext/django.py index 06f31a7..d35d131 100644 --- a/bonobo/ext/django.py +++ b/bonobo/ext/django.py @@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand, OutputWrapper import bonobo import bonobo.util -from bonobo.commands.run import get_default_services +from bonobo.commands import get_default_services from bonobo.ext.console import ConsoleOutputPlugin from bonobo.util.term import CLEAR_EOL diff --git a/bonobo/settings.py b/bonobo/settings.py index ef4be2d..05d2089 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -51,6 +51,12 @@ class Setting: raise ValidationError('Invalid value {!r} for setting {}.'.format(value, self.name)) self.value = value + def set_if_true(self, value): + """Sets the value to true if it is actually true. May sound strange but the main usage is enforcing some + settings from command line.""" + if value: + self.set(True) + def get(self): try: return self.value diff --git a/bonobo/structs/graphs.py b/bonobo/structs/graphs.py index fe7c1df..e89b7e7 100644 --- a/bonobo/structs/graphs.py +++ b/bonobo/structs/graphs.py @@ -1,6 +1,8 @@ +import json from copy import copy from bonobo.constants import BEGIN +from bonobo.util import get_name class Graph: @@ -110,6 +112,24 @@ class Graph: self._topologcally_sorted_indexes_cache = tuple(filter(lambda i: type(i) is int, reversed(order))) return self._topologcally_sorted_indexes_cache + def _repr_dot_(self): + src = [ + 'digraph {', + ' rankdir = LR;', + ' "BEGIN" [shape="point"];', + ] + + for i in self.outputs_of(BEGIN): + src.append(' "BEGIN" -> ' + _get_graphviz_node_id(self, i) + ';') + + for ix in self.topologically_sorted_indexes: + for iy in self.outputs_of(ix): + src.append(' {} -> {};'.format(_get_graphviz_node_id(self, ix), _get_graphviz_node_id(self, iy))) + + src.append('}') + + return '\n'.join(src) + 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. """ @@ -126,3 +146,9 @@ class Graph: return self.nodes.index(mixed) raise ValueError('Cannot find node matching {!r}.'.format(mixed)) + + +def _get_graphviz_node_id(graph, i): + escaped_index = str(i) + escaped_name = json.dumps(get_name(graph[i])) + return '{{{} [label={}]}}'.format(escaped_index, escaped_name) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4e005a7..553fefc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -27,7 +27,7 @@ pytz==2017.2 requests==2.18.4 six==1.11.0 snowballstemmer==1.2.1 -sphinx==1.6.4 +sphinx==1.6.5 sphinxcontrib-websupport==1.0.1 termcolor==1.1.0 urllib3==1.22 diff --git a/requirements-docker.txt b/requirements-docker.txt index 54bac73..d6f1160 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -3,6 +3,7 @@ appdirs==1.4.3 bonobo-docker==0.5.0 certifi==2017.7.27.1 chardet==3.0.4 +click==6.7 colorama==0.3.9 docker-pycreds==0.2.1 docker==2.3.0 @@ -12,6 +13,7 @@ packaging==16.8 pbr==3.1.1 psutil==5.4.0 pyparsing==2.2.0 +python-dotenv==0.7.1 pytz==2017.2 requests==2.18.4 six==1.11.0 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 2ad75ab..4e1d024 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -16,7 +16,7 @@ jupyter-console==5.2.0 jupyter-core==4.3.0 jupyter==1.0.0 markupsafe==1.0 -mistune==0.7.4 +mistune==0.8 nbconvert==5.3.1 nbformat==4.4.0 notebook==5.2.0 diff --git a/requirements.txt b/requirements.txt index 7384e3f..0d69f1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,8 @@ click==6.7 colorama==0.3.9 fs==2.0.12 idna==2.6 +jinja2==2.9.6 +markupsafe==1.0 packaging==16.8 pbr==3.1.1 psutil==5.4.0 diff --git a/setup.py b/setup.py index 29c6255..0c01b43 100644 --- a/setup.py +++ b/setup.py @@ -53,8 +53,9 @@ setup( packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, install_requires=[ - 'colorama (>= 0.3, < 1.0)', 'fs (>= 2.0, < 3.0)', 'packaging (>= 16, < 17)', 'psutil (>= 5.2, < 6.0)', - 'python-dotenv (>= 0.7.1, < 1.0)', 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.21, < 2.0)' + 'colorama (>= 0.3, < 0.4)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'packaging (>= 16, < 17)', + 'psutil (>= 5.4, < 6.0)', 'python-dotenv (>= 0.7, < 0.8)', 'requests (>= 2.0, < 3.0)', + 'stevedore (>= 1.27, < 1.28)' ], extras_require={ 'dev': [ @@ -67,9 +68,9 @@ setup( }, entry_points={ 'bonobo.commands': [ - 'convert = bonobo.commands.convert:register', 'init = bonobo.commands.init:register', - 'inspect = bonobo.commands.inspect:register', 'run = bonobo.commands.run:register', - 'version = bonobo.commands.version:register', 'download = bonobo.commands.download:register' + 'convert = bonobo.commands.convert:ConvertCommand', 'init = bonobo.commands.init:InitCommand', + 'inspect = bonobo.commands.inspect:InspectCommand', 'run = bonobo.commands.run:RunCommand', + 'version = bonobo.commands.version:VersionCommand', 'download = bonobo.commands.download:DownloadCommand' ], 'console_scripts': ['bonobo = bonobo.commands:entrypoint'] }, From c770287466c377f8b9c3c823df5faf95fd3e5188 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 29 Oct 2017 23:46:39 +0100 Subject: [PATCH 062/145] [core] still refactoring env-related stuff towards using __main__ blocks (but with argparser, if needed). --- Projectfile | 7 ++- bonobo/_api.py | 61 +++++++++++++++++++++++- bonobo/_version.py | 2 +- bonobo/commands/__init__.py | 93 ++++++++++++++++--------------------- bonobo/commands/init.py | 4 +- bonobo/commands/run.py | 4 +- tests/test_commands.py | 31 ------------- 7 files changed, 110 insertions(+), 92 deletions(-) diff --git a/Projectfile b/Projectfile index d730328..eb230d9 100644 --- a/Projectfile +++ b/Projectfile @@ -54,12 +54,15 @@ python.add_requirements( 'pytest-timeout >=1,<2', ], docker=[ - 'bonobo-docker', + 'bonobo-docker >=0.5.0', ], jupyter=[ 'jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0,<7', - ] + ], + sqlalchemy=[ + 'bonobo-sqlalchemy >=0.5.1', + ], ) # vim: ft=python: diff --git a/bonobo/_api.py b/bonobo/_api.py index fb1ef78..43e9a29 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,3 +1,7 @@ +import argparse +from contextlib import contextmanager + +import os from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ PickleReader, PickleWriter, PrettyPrinter, RateLimited, Tee, arg0_to_kwargs, count, identity, kwargs_to_arg0, noop from bonobo.nodes import LdjsonReader, LdjsonWriter @@ -19,7 +23,60 @@ def register_api_group(*args): @register_api -def run(graph, *, plugins=None, services=None, **options): +def get_argument_parser(parser=None): + if parser is None: + import argparse + parser = argparse.ArgumentParser() + + parser.add_argument('--default-env-file', action='append') + parser.add_argument('--default-env', action='append') + parser.add_argument('--env-file', action='append') + parser.add_argument('--env', '-e', action='append') + + return parser + + +@register_api +@contextmanager +def parse_args(parser, *, args=None, namespace=None): + options = parser.parse_args(args=args, namespace=namespace) + + with patch_environ(options) as options: + yield options + + +@register_api +@contextmanager +def patch_environ(options): + from dotenv import load_dotenv + from bonobo.commands import set_env_var + + options = options if isinstance(options, dict) else options.__dict__ + + default_env_file = options.pop('default_env_file', []) + default_env = options.pop('default_env', []) + env_file = options.pop('env_file', []) + env = options.pop('env', []) + + if default_env_file: + for f in default_env_file: + load_dotenv(os.path.join(os.getcwd(), f)) + if default_env: + for e in default_env: + set_env_var(e) + if env_file: + for f in env_file: + load_dotenv(os.path.join(os.getcwd(), f), override=True) + if env: + for e in env: + set_env_var(e, override=True) + + yield options + ## TODO XXX put it back !!! + + +@register_api +def run(graph, *, plugins=None, services=None, strategy=None): """ Main entry point of bonobo. It takes a graph and creates all the necessary plumbery around to execute it. @@ -39,7 +96,7 @@ def run(graph, *, plugins=None, services=None, **options): :param dict services: The implementations of services this graph will use. :return bonobo.execution.graph.GraphExecutionContext: """ - strategy = create_strategy(options.pop('strategy', None)) + strategy = create_strategy(strategy) plugins = plugins or [] diff --git a/bonobo/_version.py b/bonobo/_version.py index 93b60a1..ebc2ff2 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.5.1' +__version__ = '0.6-dev' diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index be877d7..c5d4908 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -3,10 +3,10 @@ import codecs import os import os.path import runpy +import sys from contextlib import contextmanager -from functools import partial -from bonobo import settings, logging +from bonobo import settings, logging, get_argument_parser, patch_environ from bonobo.constants import DEFAULT_SERVICES_FILENAME, DEFAULT_SERVICES_ATTR from bonobo.util import get_name @@ -44,11 +44,8 @@ class BaseGraphCommand(BaseCommand): source_group.add_argument('file', nargs='?', type=str) source_group.add_argument('-m', dest='mod', type=str) - # arguments to enforce system environment. - parser.add_argument('--default-env-file', action='append') - parser.add_argument('--default-env', action='append') - parser.add_argument('--env-file', action='append') - parser.add_argument('--env', '-e', action='append') + # add arguments to enforce system environment. + parser = get_argument_parser(parser) return parser @@ -58,34 +55,30 @@ class BaseGraphCommand(BaseCommand): def _run_module(self, mod): return runpy.run_module(mod, run_name='__main__') - def read(self, *, file, mod, **options): - - """ - - get_default_services( - filename, context.get(DEFAULT_SERVICES_ATTR)() if DEFAULT_SERVICES_ATTR in context else None - ) - - """ - + def read(self, *, file, mod, args=None, **options): _graph, _options = None, None def _record(graph, **options): nonlocal _graph, _options _graph, _options = graph, options - with _override_runner(_record), _override_environment(): - if file: - self._run_path(file) - elif mod: - self._run_module(mod) - else: - raise RuntimeError('No target provided.') + with _override_runner(_record), patch_environ(options): + _argv = sys.argv + try: + if file: + sys.argv = [file] + list(args) if args else [file] + self._run_path(file) + elif mod: + sys.argv = [mod, *(args or ())] + self._run_module(mod) + else: + raise RuntimeError('No target provided.') + finally: + sys.argv = _argv if _graph is None: raise RuntimeError('Could not find graph.') - return _graph, _options def handle(self, *args, **options): @@ -120,45 +113,41 @@ def entrypoint(args=None): mgr = ExtensionManager(namespace='bonobo.commands') mgr.map(register_extension) - args = parser.parse_args(args).__dict__ - if args.pop('debug', False): + parsed_args, remaining = parser.parse_known_args(args) + parsed_args = parsed_args.__dict__ + + if parsed_args.pop('debug', False): settings.DEBUG.set(True) settings.LOGGING_LEVEL.set(logging.DEBUG) logging.set_level(settings.LOGGING_LEVEL.get()) - logger.debug('Command: ' + args['command'] + ' Arguments: ' + repr(args)) - commands[args.pop('command')](**args) + logger.debug('Command: ' + parsed_args['command'] + ' Arguments: ' + repr(parsed_args)) + + # Get command handler + command = commands[parsed_args.pop('command')] + + if len(remaining): + command(_remaining_args=remaining, **parsed_args) + else: + command(**parsed_args) @contextmanager def _override_runner(runner): import bonobo - _runner_backup = bonobo.run + _get_argument_parser = bonobo.get_argument_parser + _run = bonobo.run try: + def get_argument_parser(parser=None): + return parser or argparse.ArgumentParser() + + bonobo.get_argument_parser = get_argument_parser bonobo.run = runner + yield runner finally: - bonobo.run = _runner_backup - - -@contextmanager -def _override_environment(root_dir=None, **options): - yield - return - if default_env_file: - for f in default_env_file: - env_file_path = str(env_dir.joinpath(f)) - load_dotenv(env_file_path) - if default_env: - for e in default_env: - set_env_var(e) - if env_file: - for f in env_file: - env_file_path = str(env_dir.joinpath(f)) - load_dotenv(env_file_path, override=True) - if env: - for e in env: - set_env_var(e, override=True) + bonobo.get_argument_parser = _get_argument_parser + bonobo.run = _run def get_default_services(filename, services=None): @@ -194,4 +183,4 @@ def set_env_var(e, override=False): if override: os.environ[ename] = evalue else: - os.environ.setdefault(ename, evalue) \ No newline at end of file + os.environ.setdefault(ename, evalue) diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index c0c50f1..6c6c2ff 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -6,13 +6,13 @@ from bonobo.commands import BaseCommand class InitCommand(BaseCommand): - TEMPLATES = {'job'} + TEMPLATES = {'default'} TEMPLATES_PATH = os.path.join(os.path.dirname(__file__), 'templates') def add_arguments(self, parser): - parser.add_argument('template', choices=self.TEMPLATES) parser.add_argument('filename') parser.add_argument('--force', '-f', default=False, action='store_true') + parser.add_argument('--template', '-t', choices=self.TEMPLATES, default='default') def handle(self, *, template, filename, force=False): template_name = template diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 799816f..514bb5d 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -32,14 +32,14 @@ class RunCommand(BaseGraphCommand): return super()._run_module(mod) - def handle(self, *args, quiet=False, verbose=False, install=False, **options): + def handle(self, quiet=False, verbose=False, install=False, _remaining_args=None, **options): from bonobo import settings settings.QUIET.set_if_true(quiet) settings.DEBUG.set_if_true(verbose) self.install = install - graph, params = self.read(**options) + graph, params = self.read(args=_remaining_args, **options) params['plugins'] = set(params.pop('plugins', ())).union(set(options.pop('plugins', ()))) diff --git a/tests/test_commands.py b/tests/test_commands.py index c78fa5f..fe55f87 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -12,7 +12,6 @@ from cookiecutter.exceptions import OutputDirExistsException from bonobo import __main__, __version__, get_examples_path from bonobo.commands import entrypoint -from bonobo.commands.run import DEFAULT_GRAPH_FILENAMES from bonobo.commands.download import EXAMPLES_BASE_URL @@ -72,36 +71,6 @@ def test_no_command(runner): assert 'error: the following arguments are required: command' in err -@all_runners -def test_init(runner, tmpdir): - name = 'project' - tmpdir.chdir() - runner('init', name) - assert os.path.isdir(name) - assert set(os.listdir(name)) & set(DEFAULT_GRAPH_FILENAMES) - -@single_runner -def test_init_in_empty_then_nonempty_directory(runner, tmpdir): - name = 'project' - tmpdir.chdir() - os.mkdir(name) - - # run in empty dir - runner('init', name) - assert set(os.listdir(name)) & set(DEFAULT_GRAPH_FILENAMES) - - # run in non empty dir - with pytest.raises(OutputDirExistsException): - runner('init', name) - - -@single_runner -def test_init_within_empty_directory(runner, tmpdir): - tmpdir.chdir() - runner('init', '.') - assert set(os.listdir()) & set(DEFAULT_GRAPH_FILENAMES) - - @all_runners def test_run(runner): out, err = runner('run', '--quiet', get_examples_path('types/strings.py')) From b6c7d598dc8f047efe8cb7b0edbf693cc1705e03 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 30 Oct 2017 08:28:18 +0100 Subject: [PATCH 063/145] =?UTF-8?q?[core]=20Simplification:=20as=20truthfu?= =?UTF-8?q?lly=20stated=20by=20Maik=20at=20Pycon.DE=20sprint=20=C2=ABlets?= =?UTF-8?q?=20try=20not=20to=20turn=20python=20into=20javascript=C2=BB.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bonobo/commands/__init__.py | 19 ------------------- bonobo/util/__init__.py | 2 -- bonobo/util/python.py | 31 ------------------------------- 3 files changed, 52 deletions(-) delete mode 100644 bonobo/util/python.py diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index c5d4908..56d615d 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -150,25 +150,6 @@ def _override_runner(runner): bonobo.run = _run -def get_default_services(filename, services=None): - dirname = os.path.dirname(filename) - services_filename = os.path.join(dirname, DEFAULT_SERVICES_FILENAME) - if os.path.exists(services_filename): - with open(services_filename) as file: - code = compile(file.read(), services_filename, 'exec') - context = { - '__name__': '__services__', - '__file__': services_filename, - } - exec(code, context) - - return { - **context[DEFAULT_SERVICES_ATTR](), - **(services or {}), - } - return services or {} - - def set_env_var(e, override=False): __escape_decoder = codecs.getdecoder('unicode_escape') ename, evalue = e.split('=', 1) diff --git a/bonobo/util/__init__.py b/bonobo/util/__init__.py index 4ef136e..27e50b2 100644 --- a/bonobo/util/__init__.py +++ b/bonobo/util/__init__.py @@ -15,7 +15,6 @@ from bonobo.util.inspect import ( istype, ) from bonobo.util.objects import (get_name, get_attribute_or_create, ValueHolder) -from bonobo.util.python import require # Bonobo's util API __all__ = [ @@ -35,5 +34,4 @@ __all__ = [ 'ismethod', 'isoption', 'istype', - 'require', ] diff --git a/bonobo/util/python.py b/bonobo/util/python.py deleted file mode 100644 index 8648f16..0000000 --- a/bonobo/util/python.py +++ /dev/null @@ -1,31 +0,0 @@ -import inspect -import os -import runpy - - -class _RequiredModule: - def __init__(self, dct): - self.__dict__ = dct - - -class _RequiredModulesRegistry(dict): - @property - def pathname(self): - return os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.stack()[2][0]))) - - def require(self, name): - if name not in self: - bits = name.split('.') - filename = os.path.join(self.pathname, *bits[:-1], bits[-1] + '.py') - self[name] = _RequiredModule(runpy.run_path(filename, run_name=name)) - return self[name] - - -class WorkingDirectoryModulesRegistry(_RequiredModulesRegistry): - @property - def pathname(self): - return os.getcwd() - - -registry = _RequiredModulesRegistry() -require = registry.require From 40a745fe083f620b98fe72b112ee3b5cc48026c2 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 12:46:03 +0100 Subject: [PATCH 064/145] Module registry reimported as it is needed for "bonobo convert". --- bonobo/_version.py | 2 +- bonobo/structs/inputs.py | 1 - bonobo/util/__init__.py | 5 ++++- bonobo/util/collections.py | 6 +++--- bonobo/util/resolvers.py | 23 +++++++++++++++++++++-- tests/util/test_python.py | 6 ------ 6 files changed, 29 insertions(+), 14 deletions(-) delete mode 100644 tests/util/test_python.py diff --git a/bonobo/_version.py b/bonobo/_version.py index ebc2ff2..2724bac 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.6-dev' +__version__ = '0.6.dev0' diff --git a/bonobo/structs/inputs.py b/bonobo/structs/inputs.py index 7cfe12f..9b3cd14 100644 --- a/bonobo/structs/inputs.py +++ b/bonobo/structs/inputs.py @@ -15,7 +15,6 @@ # limitations under the License. from abc import ABCMeta, abstractmethod - from queue import Queue from bonobo.constants import BEGIN, END diff --git a/bonobo/util/__init__.py b/bonobo/util/__init__.py index 27e50b2..586fe3b 100644 --- a/bonobo/util/__init__.py +++ b/bonobo/util/__init__.py @@ -1,4 +1,4 @@ -from bonobo.util.collections import sortedlist, ensure_tuple +from bonobo.util.collections import ensure_tuple, sortedlist, tuplize from bonobo.util.compat import deprecated, deprecated_alias from bonobo.util.inspect import ( inspect_node, @@ -21,6 +21,7 @@ __all__ = [ 'ValueHolder', 'deprecated', 'deprecated_alias', + 'ensure_tuple', 'get_attribute_or_create', 'get_name', 'inspect_node', @@ -34,4 +35,6 @@ __all__ = [ 'ismethod', 'isoption', 'istype', + 'sortedlist', + 'tuplize', ] diff --git a/bonobo/util/collections.py b/bonobo/util/collections.py index d53a7da..31765c4 100644 --- a/bonobo/util/collections.py +++ b/bonobo/util/collections.py @@ -22,9 +22,9 @@ def ensure_tuple(tuple_or_mixed): def tuplize(generator): - """ Takes a generator and make it a tuple-returning function. As a side - effect, it can also decorate any iterator-returning function to force - return value to be a tuple. + """ + Decorates a generator and make it a tuple-returning function. As a side effect, it can also decorate any + iterator-returning function to force return value to be a tuple. >>> tuplized_lambda = tuplize(lambda: [1, 2, 3]) >>> tuplized_lambda() diff --git a/bonobo/util/resolvers.py b/bonobo/util/resolvers.py index 0590fc7..c4a1a90 100644 --- a/bonobo/util/resolvers.py +++ b/bonobo/util/resolvers.py @@ -4,10 +4,29 @@ This package is considered private, and should only be used within bonobo. """ import json +import os +import runpy import bonobo from bonobo.util.collections import tuplize -from bonobo.util.python import WorkingDirectoryModulesRegistry + + +class _RequiredModule: + def __init__(self, dct): + self.__dict__ = dct + + +class _ModulesRegistry(dict): + @property + def pathname(self): + return os.getcwd() + + def require(self, name): + if name not in self: + bits = name.split('.') + filename = os.path.join(self.pathname, *bits[:-1], bits[-1] + '.py') + self[name] = _RequiredModule(runpy.run_path(filename, run_name=name)) + return self[name] def _parse_option(option): @@ -52,7 +71,7 @@ def _resolve_transformations(transformations): :param transformations: tuple(str) :return: tuple(object) """ - registry = WorkingDirectoryModulesRegistry() + registry = _ModulesRegistry() for t in transformations: try: mod, attr = t.split(':', 1) diff --git a/tests/util/test_python.py b/tests/util/test_python.py deleted file mode 100644 index 6b1b591..0000000 --- a/tests/util/test_python.py +++ /dev/null @@ -1,6 +0,0 @@ -from bonobo.util.python import require - - -def test_require(): - dummy = require('requireable.dummy') - assert dummy.foo == 'bar' From 69bb3cb09161e88319a90710e42a48f3677e39d4 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 13:12:19 +0100 Subject: [PATCH 065/145] Switch logger setup to mondrian (deps). --- Projectfile | 9 ++++++-- docs/roadmap.rst | 54 ------------------------------------------------ 2 files changed, 7 insertions(+), 56 deletions(-) delete mode 100644 docs/roadmap.rst diff --git a/Projectfile b/Projectfile index eb230d9..bb22b87 100644 --- a/Projectfile +++ b/Projectfile @@ -40,16 +40,15 @@ python.setup( ) python.add_requirements( - 'colorama >=0.3,<0.4', 'fs >=2.0,<2.1', 'jinja2 >=2.9,<2.10', + 'mondrian >=0.2,<0.3', 'packaging >=16,<17', 'psutil >=5.4,<6.0', 'python-dotenv >=0.7,<0.8', 'requests >=2.0,<3.0', 'stevedore >=1.27,<1.28', dev=[ - 'cookiecutter >=1.5,<1.6', 'pytest-sugar >=0.8,<0.9', 'pytest-timeout >=1,<2', ], @@ -65,4 +64,10 @@ python.add_requirements( ], ) +# Following requirements are not enforced, because some dependencies enforce them so we don't want to break +# the packaging in case it changes in dep. +python.add_requirements( + 'colorama >=0.3', +) + # vim: ft=python: diff --git a/docs/roadmap.rst b/docs/roadmap.rst deleted file mode 100644 index 182cf71..0000000 --- a/docs/roadmap.rst +++ /dev/null @@ -1,54 +0,0 @@ -Internal roadmap notes -====================== - -Things that should be thought about and/or implemented, but that I don't know where to store. - -Graph and node level plugins -:::::::::::::::::::::::::::: - - * Enhancers or node-level plugins - * Graph level plugins - * Documentation - -Command line interface and environment -:::::::::::::::::::::::::::::::::::::: - -* How do we manage environment ? .env ? -* How do we configure plugins ? - -Services and Processors -::::::::::::::::::::::: - -* ContextProcessors not clean (a bit better, but still not in love with the api) - -Next... -::::::: - -* Release process specialised for bonobo. With changelog production, etc. -* Document how to upgrade version, like, minor need change badges, etc. -* Windows console looks crappy. -* bonobo init --with sqlalchemy,docker; cookiecutter? -* logger, vebosity level - - -External libs that looks good -::::::::::::::::::::::::::::: - -* dask.distributed -* mediator (event dispatcher) - -Version 0.4 -::::::::::: - -* SQLAlchemy 101 - -Design decisions -:::::::::::::::: - -* initialize / finalize better than start / stop ? - -Minor stuff -::::::::::: - -* Should we include datasets in the repo or not? As they may change, grow, and even eventually have licenses we can't use, - it's probably best if we don't. \ No newline at end of file From e6596cf3f361537b686528c398019a8f0487e2e7 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 13:19:24 +0100 Subject: [PATCH 066/145] Removes cookiecutter. --- docs/changelog.rst | 3 +++ docs/extension/jupyter.rst | 2 -- docs/install.rst | 14 ++++++++------ docs/reference/commands.rst | 10 ---------- docs/tutorial/tut01.rst | 22 ++++++++++++---------- tests/test_commands.py | 3 +-- 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a222414..66a5a05 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,9 @@ Changelog Unreleased :::::::::: +* Cookiecutter usage is removed. Linked to the fact that bonobo now use either a single file (up to you to get python + imports working as you want) or a regular fully fledged python package, we do not need it anymore. + New features ------------ diff --git a/docs/extension/jupyter.rst b/docs/extension/jupyter.rst index 6c3385f..ed01d3b 100644 --- a/docs/extension/jupyter.rst +++ b/docs/extension/jupyter.rst @@ -4,8 +4,6 @@ Jupyter Extension There is a builtin plugin that integrates (somewhat minimallistically, for now) bonobo within jupyter notebooks, so you can read the execution status of a graph within a nice (ok, not so nice) html/javascript widget. -See https://github.com/jupyter-widgets/widget-cookiecutter for the base template used. - Installation :::::::::::: diff --git a/docs/install.rst b/docs/install.rst index c006c88..56f18ae 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -5,16 +5,18 @@ Installation Create an ETL project ::::::::::::::::::::: -Creating a project and starting to write code should take less than a minute: +Let's create a job. .. code-block:: shell-session - $ pip install --upgrade bonobo cookiecutter - $ bonobo init my-etl-project - $ bonobo run my-etl-project + $ pip install --upgrade bonobo + $ bonobo create my-etl.py + $ python my-etl.py -Once you bootstrapped a project, you can start editing the default example transformation by editing -`my-etl-project/main.py`. Now, you can head to :doc:`tutorial/index`. +This job only uses one python file, and you can run it using the python interpreter. For bigger jobs or jobs that +relates to multiple files, you should create a python package. + +Now, you can head to :doc:`tutorial/index`. Other installation options diff --git a/docs/reference/commands.rst b/docs/reference/commands.rst index 674d549..ade63be 100644 --- a/docs/reference/commands.rst +++ b/docs/reference/commands.rst @@ -16,16 +16,6 @@ Syntax: `bonobo convert [-r reader] input_filename [-w writer] output_filename` to read from csv and write to csv too (or other format) but adding a geocoder filter that would add some fields. -Bonobo Init -::::::::::: - -Create an empty project, ready to use bonobo. - -Syntax: `bonobo init` - -Requires `cookiecutter`. - - Bonobo Inspect :::::::::::::: diff --git a/docs/tutorial/tut01.rst b/docs/tutorial/tut01.rst index 3d6f9eb..836ddad 100644 --- a/docs/tutorial/tut01.rst +++ b/docs/tutorial/tut01.rst @@ -1,8 +1,7 @@ Let's get started! ================== -To begin with Bonobo, you need to install it in a working python 3.5+ environment, and you'll also need cookiecutter -to bootstrap your project. +To get started with Bonobo, you need to install it in a working python 3.5+ environment: .. code-block:: shell-session @@ -14,21 +13,24 @@ See :doc:`/install` for more options. Create an empty project ::::::::::::::::::::::: -Your ETL code will live in ETL projects, which are basically a bunch of files, including python code, that bonobo -can run. +Your ETL code will live in standard python files and packages. .. code-block:: shell-session - $ bonobo init tutorial + $ bonobo create tutorial.py -This will create a `tutorial` directory (`content description here `_). +This will create a simple example job in a `tutorial.py` file. -To run this project, use: +Now, try to execute it: .. code-block:: shell-session - $ bonobo run tutorial + $ python tutorial.py +Congratulations, you just ran your first ETL job! + + +.. todo:: XXX **CHANGES NEEDED BELOW THIS POINTS BEFORE 0.6** XXX Write a first transformation :::::::::::::::::::::::::::: @@ -131,9 +133,9 @@ Rewrite it using builtins There is a much simpler way to describe an equivalent graph: .. literalinclude:: ../../bonobo/examples/tutorials/tut01e02.py - :language: python +:language: python -The `extract()` generator has been replaced by a list, as Bonobo will interpret non-callable iterables as a no-input + The `extract()` generator has been replaced by a list, as Bonobo will interpret non-callable iterables as a no-input generator. This example is also available in :mod:`bonobo.examples.tutorials.tut01e02`, and you can also run it as a module: diff --git a/tests/test_commands.py b/tests/test_commands.py index fe55f87..64d530b 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -4,11 +4,10 @@ import os import runpy import sys from contextlib import redirect_stdout, redirect_stderr -from unittest.mock import patch, Mock +from unittest.mock import patch import pkg_resources import pytest -from cookiecutter.exceptions import OutputDirExistsException from bonobo import __main__, __version__, get_examples_path from bonobo.commands import entrypoint From e06b616251afca480321c43129e69cac3b28a746 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 18:46:45 +0100 Subject: [PATCH 067/145] Refactoring the runner to go more towards standard python, also adds the ability to use bonobo argument parser from standard python execution. --- Projectfile | 4 +- bonobo/_api.py | 79 ++--- bonobo/commands/__init__.py | 136 +------- bonobo/commands/base.py | 129 +++++++ bonobo/commands/convert.py | 13 +- bonobo/commands/inspect.py | 22 +- bonobo/commands/run.py | 23 +- bonobo/examples/environ.py | 28 ++ bonobo/examples/tutorials/tut02e02_write.py | 4 +- bonobo/examples/types/__main__.py | 8 +- bonobo/examples/types/strings.py | 12 +- bonobo/execution/node.py | 6 +- bonobo/nodes/io/json.py | 1 + bonobo/util/environ.py | 154 +++++++++ tests/features/test_not_modified.py | 12 +- tests/io/test_csv.py | 4 +- tests/io/test_json.py | 18 +- tests/test_commands.py | 354 +++++++------------- 18 files changed, 537 insertions(+), 470 deletions(-) create mode 100644 bonobo/commands/base.py create mode 100644 bonobo/examples/environ.py create mode 100644 bonobo/util/environ.py diff --git a/Projectfile b/Projectfile index bb22b87..3a10d0b 100644 --- a/Projectfile +++ b/Projectfile @@ -66,8 +66,6 @@ python.add_requirements( # Following requirements are not enforced, because some dependencies enforce them so we don't want to break # the packaging in case it changes in dep. -python.add_requirements( - 'colorama >=0.3', -) +python.add_requirements('colorama >=0.3', ) # vim: ft=python: diff --git a/bonobo/_api.py b/bonobo/_api.py index 43e9a29..f1e5da5 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,13 +1,10 @@ -import argparse -from contextlib import contextmanager - -import os from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ PickleReader, PickleWriter, PrettyPrinter, RateLimited, Tee, arg0_to_kwargs, count, identity, kwargs_to_arg0, noop from bonobo.nodes import LdjsonReader, LdjsonWriter from bonobo.strategies import create_strategy from bonobo.structs import Bag, ErrorBag, Graph, Token from bonobo.util import get_name +from bonobo.util.environ import parse_args, get_argument_parser __all__ = [] @@ -22,59 +19,6 @@ def register_api_group(*args): register_api(attr) -@register_api -def get_argument_parser(parser=None): - if parser is None: - import argparse - parser = argparse.ArgumentParser() - - parser.add_argument('--default-env-file', action='append') - parser.add_argument('--default-env', action='append') - parser.add_argument('--env-file', action='append') - parser.add_argument('--env', '-e', action='append') - - return parser - - -@register_api -@contextmanager -def parse_args(parser, *, args=None, namespace=None): - options = parser.parse_args(args=args, namespace=namespace) - - with patch_environ(options) as options: - yield options - - -@register_api -@contextmanager -def patch_environ(options): - from dotenv import load_dotenv - from bonobo.commands import set_env_var - - options = options if isinstance(options, dict) else options.__dict__ - - default_env_file = options.pop('default_env_file', []) - default_env = options.pop('default_env', []) - env_file = options.pop('env_file', []) - env = options.pop('env', []) - - if default_env_file: - for f in default_env_file: - load_dotenv(os.path.join(os.getcwd(), f)) - if default_env: - for e in default_env: - set_env_var(e) - if env_file: - for f in env_file: - load_dotenv(os.path.join(os.getcwd(), f), override=True) - if env: - for e in env: - set_env_var(e, override=True) - - yield options - ## TODO XXX put it back !!! - - @register_api def run(graph, *, plugins=None, services=None, strategy=None): """ @@ -126,6 +70,24 @@ def run(graph, *, plugins=None, services=None, strategy=None): return strategy.execute(graph, plugins=plugins, services=services) +def _inspect_as_graph(graph): + return graph._repr_dot_() + + +_inspect_formats = {'graph': _inspect_as_graph} + + +@register_api +def inspect(graph, *, format): + if not format in _inspect_formats: + raise NotImplementedError( + 'Output format {} not implemented. Choices are: {}.'.format( + format, ', '.join(sorted(_inspect_formats.keys())) + ) + ) + print(_inspect_formats[format](graph)) + + # bonobo.structs register_api_group(Bag, ErrorBag, Graph, Token) @@ -205,3 +167,6 @@ def get_examples_path(*pathsegments): @register_api def open_examples_fs(*pathsegments): return open_fs(get_examples_path(*pathsegments)) + + +register_api_group(get_argument_parser, parse_args) diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index 56d615d..cd9559c 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -1,91 +1,19 @@ import argparse -import codecs -import os -import os.path -import runpy -import sys -from contextlib import contextmanager -from bonobo import settings, logging, get_argument_parser, patch_environ -from bonobo.constants import DEFAULT_SERVICES_FILENAME, DEFAULT_SERVICES_ATTR -from bonobo.util import get_name +from bonobo import settings, logging +from bonobo.commands.base import BaseCommand, BaseGraphCommand logger = logging.get_logger() -class BaseCommand: - @property - def logger(self): - try: - return self._logger - except AttributeError: - self._logger = logging.get_logger(get_name(self)) - return self._logger - - def add_arguments(self, parser): - """ - Entry point for subclassed commands to add custom arguments. - """ - pass - - def handle(self, *args, **options): - """ - The actual logic of the command. Subclasses must implement this method. - """ - raise NotImplementedError('Subclasses of BaseCommand must provide a handle() method') - - -class BaseGraphCommand(BaseCommand): - required = True - - def add_arguments(self, parser): - # target arguments (cannot provide both). - source_group = parser.add_mutually_exclusive_group(required=self.required) - source_group.add_argument('file', nargs='?', type=str) - source_group.add_argument('-m', dest='mod', type=str) - - # add arguments to enforce system environment. - parser = get_argument_parser(parser) - - return parser - - def _run_path(self, file): - return runpy.run_path(file, run_name='__main__') - - def _run_module(self, mod): - return runpy.run_module(mod, run_name='__main__') - - def read(self, *, file, mod, args=None, **options): - _graph, _options = None, None - - def _record(graph, **options): - nonlocal _graph, _options - _graph, _options = graph, options - - with _override_runner(_record), patch_environ(options): - _argv = sys.argv - try: - if file: - sys.argv = [file] + list(args) if args else [file] - self._run_path(file) - elif mod: - sys.argv = [mod, *(args or ())] - self._run_module(mod) - else: - raise RuntimeError('No target provided.') - finally: - sys.argv = _argv - - if _graph is None: - raise RuntimeError('Could not find graph.') - - return _graph, _options - - def handle(self, *args, **options): - pass - - def entrypoint(args=None): + """ + Main callable for "bonobo" entrypoint. + + Will load commands from "bonobo.commands" entrypoints, using stevedore. + + """ + parser = argparse.ArgumentParser() parser.add_argument('--debug', '-D', action='store_true') @@ -113,8 +41,7 @@ def entrypoint(args=None): mgr = ExtensionManager(namespace='bonobo.commands') mgr.map(register_extension) - parsed_args, remaining = parser.parse_known_args(args) - parsed_args = parsed_args.__dict__ + parsed_args = parser.parse_args(args).__dict__ if parsed_args.pop('debug', False): settings.DEBUG.set(True) @@ -123,45 +50,6 @@ def entrypoint(args=None): logger.debug('Command: ' + parsed_args['command'] + ' Arguments: ' + repr(parsed_args)) - # Get command handler + # Get command handler, execute, rince. command = commands[parsed_args.pop('command')] - - if len(remaining): - command(_remaining_args=remaining, **parsed_args) - else: - command(**parsed_args) - - -@contextmanager -def _override_runner(runner): - import bonobo - _get_argument_parser = bonobo.get_argument_parser - _run = bonobo.run - try: - def get_argument_parser(parser=None): - return parser or argparse.ArgumentParser() - - bonobo.get_argument_parser = get_argument_parser - bonobo.run = runner - - yield runner - finally: - bonobo.get_argument_parser = _get_argument_parser - bonobo.run = _run - - -def set_env_var(e, override=False): - __escape_decoder = codecs.getdecoder('unicode_escape') - ename, evalue = e.split('=', 1) - - def decode_escaped(escaped): - return __escape_decoder(escaped)[0] - - if len(evalue) > 0: - if evalue[0] == evalue[len(evalue) - 1] in ['"', "'"]: - evalue = decode_escaped(evalue[1:-1]) - - if override: - os.environ[ename] = evalue - else: - os.environ.setdefault(ename, evalue) + command(**parsed_args) diff --git a/bonobo/commands/base.py b/bonobo/commands/base.py new file mode 100644 index 0000000..43513b0 --- /dev/null +++ b/bonobo/commands/base.py @@ -0,0 +1,129 @@ +import argparse +import runpy +import sys +from contextlib import contextmanager + +import bonobo.util.environ +from bonobo import logging +from bonobo.util.environ import get_argument_parser, parse_args +from bonobo.util import get_name + + +class BaseCommand: + """ + Base class for CLI commands. + + """ + + @property + def logger(self): + try: + return self._logger + except AttributeError: + self._logger = logging.get_logger(get_name(self)) + return self._logger + + def add_arguments(self, parser): + """ + Entry point for subclassed commands to add custom arguments. + """ + pass + + def handle(self, *args, **options): + """ + The actual logic of the command. Subclasses must implement this method. + """ + raise NotImplementedError('Subclasses of BaseCommand must provide a handle() method') + + +class BaseGraphCommand(BaseCommand): + """ + Base class for CLI commands that depends on a graph definition, either from a file or from a module. + + """ + required = True + handler = None + + def add_arguments(self, parser): + # target arguments (cannot provide both). + source_group = parser.add_mutually_exclusive_group(required=self.required) + source_group.add_argument('file', nargs='?', type=str) + source_group.add_argument('-m', dest='mod', type=str) + + # add arguments to enforce system environment. + parser = get_argument_parser(parser) + + return parser + + def parse_options(self, **options): + return options + + def handle(self, file, mod, **options): + options = self.parse_options(**options) + with self.read(file, mod, **options) as (graph, graph_execution_options, options): + return self.do_handle(graph, **graph_execution_options, **options) + + def do_handle(self, graph, **options): + if not self.handler: + raise RuntimeError('{} has no handler defined.'.format(get_name(self))) + return self.handler(graph, **options) + + @contextmanager + def read(self, file, mod, **options): + _graph, _graph_execution_options = None, None + + def _record(graph, **graph_execution_options): + nonlocal _graph, _graph_execution_options + _graph, _graph_execution_options = graph, graph_execution_options + + with _override_runner(_record), parse_args(options) as options: + _argv = sys.argv + try: + if file: + sys.argv = [file] + self._run_path(file) + elif mod: + sys.argv = [mod] + self._run_module(mod) + else: + raise RuntimeError('No target provided.') + finally: + sys.argv = _argv + + if _graph is None: + raise RuntimeError('Could not find graph.') + + yield _graph, _graph_execution_options, options + + def _run_path(self, file): + return runpy.run_path(file, run_name='__main__') + + def _run_module(self, mod): + return runpy.run_module(mod, run_name='__main__') + + +@contextmanager +def _override_runner(runner): + """ + Context manager that monkey patches `bonobo.run` function with our current command logic. + + :param runner: the callable that will handle the `run()` logic. + """ + import bonobo + + _get_argument_parser = bonobo.util.environ.get_argument_parser + _run = bonobo.run + try: + # Original get_argument_parser would create or update an argument parser with environment options, but here we + # already had them parsed so let's patch with something that creates an empty one instead. + def get_argument_parser(parser=None): + return parser or argparse.ArgumentParser() + + bonobo.util.environ.get_argument_parser = get_argument_parser + bonobo.run = runner + + yield runner + finally: + # Restore our saved values. + bonobo.util.environ.get_argument_parser = _get_argument_parser + bonobo.run = _run diff --git a/bonobo/commands/convert.py b/bonobo/commands/convert.py index 918d81e..faf175c 100644 --- a/bonobo/commands/convert.py +++ b/bonobo/commands/convert.py @@ -48,8 +48,17 @@ class ConvertCommand(BaseCommand): help='Add a named option to the writer factory.', ) - def handle(self, input_filename, output_filename, reader=None, reader_option=None, writer=None, writer_option=None, - option=None, transformation=None): + def handle( + 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 [])) diff --git a/bonobo/commands/inspect.py b/bonobo/commands/inspect.py index 9a802d9..0e6dcd4 100644 --- a/bonobo/commands/inspect.py +++ b/bonobo/commands/inspect.py @@ -1,21 +1,15 @@ +import bonobo from bonobo.commands import BaseGraphCommand -OUTPUT_GRAPH = 'graphviz' - class InspectCommand(BaseGraphCommand): + handler = staticmethod(bonobo.inspect) + def add_arguments(self, parser): super(InspectCommand, self).add_arguments(parser) - parser.add_argument('--graph', '-g', dest='output', action='store_const', const=OUTPUT_GRAPH) - - def handle(self, output=None, **options): - if output is None: - raise ValueError('Output type must be provided (try --graph/-g).') - - graph, params = self.read(**options) - - if output == OUTPUT_GRAPH: - print(graph._repr_dot_()) - else: - raise NotImplementedError('Output type not implemented.') + parser.add_argument('--graph', '-g', dest='format', action='store_const', const='graph') + def parse_options(self, **options): + if not options.get('format'): + raise RuntimeError('You must provide a format (try --graph).') + return options diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 514bb5d..ce76bfc 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -6,6 +6,7 @@ from bonobo.commands import BaseGraphCommand class RunCommand(BaseGraphCommand): install = False + handler = staticmethod(bonobo.run) def add_arguments(self, parser): super(RunCommand, self).add_arguments(parser) @@ -16,7 +17,15 @@ class RunCommand(BaseGraphCommand): parser.add_argument('--install', '-I', action='store_true') + def parse_options(self, *, quiet=False, verbose=False, install=False, **options): + from bonobo import settings + settings.QUIET.set_if_true(quiet) + settings.DEBUG.set_if_true(verbose) + self.install = install + return options + def _run_path(self, file): + # add install logic if self.install: if os.path.isdir(file): requirements = os.path.join(file, 'requirements.txt') @@ -27,24 +36,12 @@ class RunCommand(BaseGraphCommand): return super()._run_path(file) def _run_module(self, mod): + # install not implemented for a module, not sure it even make sense. if self.install: raise RuntimeError('--install behaviour when running a module is not defined.') return super()._run_module(mod) - def handle(self, quiet=False, verbose=False, install=False, _remaining_args=None, **options): - from bonobo import settings - - settings.QUIET.set_if_true(quiet) - settings.DEBUG.set_if_true(verbose) - self.install = install - - graph, params = self.read(args=_remaining_args, **options) - - params['plugins'] = set(params.pop('plugins', ())).union(set(options.pop('plugins', ()))) - - return bonobo.run(graph, **params) - def register_generic_run_arguments(parser, required=True): """ diff --git a/bonobo/examples/environ.py b/bonobo/examples/environ.py new file mode 100644 index 0000000..ea1b39b --- /dev/null +++ b/bonobo/examples/environ.py @@ -0,0 +1,28 @@ +import os + +import bonobo + + +def extract_environ(): + yield from sorted(os.environ.items()) + + +def get_graph(): + """ + This function builds the graph that needs to be executed. + + :return: bonobo.Graph + + """ + graph = bonobo.Graph() + graph.add_chain(extract_environ, print) + + return graph + + +# The __main__ block actually execute the graph. +if __name__ == '__main__': + parser = bonobo.get_argument_parser() + parser.add_argument('-v', action='append', dest='vars') + with bonobo.parse_args(parser): + bonobo.run(get_graph()) diff --git a/bonobo/examples/tutorials/tut02e02_write.py b/bonobo/examples/tutorials/tut02e02_write.py index c4b065d..a33a11b 100644 --- a/bonobo/examples/tutorials/tut02e02_write.py +++ b/bonobo/examples/tutorials/tut02e02_write.py @@ -8,9 +8,7 @@ def split_one(line): graph = bonobo.Graph( bonobo.FileReader('coffeeshops.txt'), split_one, - bonobo.JsonWriter( - 'coffeeshops.json', fs='fs.output' - ), + bonobo.JsonWriter('coffeeshops.json', fs='fs.output'), ) diff --git a/bonobo/examples/types/__main__.py b/bonobo/examples/types/__main__.py index 3d1549f..ccda1a9 100644 --- a/bonobo/examples/types/__main__.py +++ b/bonobo/examples/types/__main__.py @@ -1,3 +1,7 @@ -from bonobo.util.python import require +import bonobo +from bonobo.examples.types.strings import get_graph -graph = require('strings').graph +if __name__ == '__main__': + parser = bonobo.get_argument_parser() + with bonobo.parse_args(parser): + bonobo.run(get_graph()) diff --git a/bonobo/examples/types/strings.py b/bonobo/examples/types/strings.py index 1903151..2fa765f 100644 --- a/bonobo/examples/types/strings.py +++ b/bonobo/examples/types/strings.py @@ -14,7 +14,7 @@ Example on how to use symple python strings to communicate between transformatio """ from random import randint -from bonobo import Graph +import bonobo def extract(): @@ -31,9 +31,11 @@ def load(s: str): print(s) -graph = Graph(extract, transform, load) +def get_graph(): + return bonobo.Graph(extract, transform, load) + if __name__ == '__main__': - from bonobo import run - - run(graph) + parser = bonobo.get_argument_parser() + with bonobo.parse_args(parser): + bonobo.run(get_graph()) diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index 22582e6..e727531 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -138,7 +138,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): def isflag(param): - return isinstance(param, Token) and param in (NOT_MODIFIED,) + return isinstance(param, Token) and param in (NOT_MODIFIED, ) def split_tokens(output): @@ -150,11 +150,11 @@ def split_tokens(output): """ if isinstance(output, Token): # just a flag - return (output,), () + return (output, ), () if not istuple(output): # no flag - return (), (output,) + return (), (output, ) i = 0 while isflag(output[i]): diff --git a/bonobo/nodes/io/json.py b/bonobo/nodes/io/json.py index 404cdcb..bbb89ad 100644 --- a/bonobo/nodes/io/json.py +++ b/bonobo/nodes/io/json.py @@ -58,6 +58,7 @@ class LdjsonReader(FileReader): class LdjsonWriter(FileWriter): """Write a stream of JSON objects, one object per line.""" + def write(self, fs, file, lineno, **row): lineno += 1 # class-level variable file.write(json.dumps(row) + '\n') diff --git a/bonobo/util/environ.py b/bonobo/util/environ.py new file mode 100644 index 0000000..16f7c9c --- /dev/null +++ b/bonobo/util/environ.py @@ -0,0 +1,154 @@ +import argparse +import codecs +import os +import re +import warnings +from contextlib import contextmanager + +__escape_decoder = codecs.getdecoder('unicode_escape') +__posix_variable = re.compile('\$\{[^\}]*\}') + + +def parse_var(var): + name, value = var.split('=', 1) + + def decode_escaped(escaped): + return __escape_decoder(escaped)[0] + + if len(value) > 1: + c = value[0] + + if c in ['"', "'"] and value[-1] == c: + value = decode_escaped(value[1:-1]) + + return name, value + + +def load_env_from_file(filename): + """ + Read an env file into a collection of (name, value) tuples. + """ + if not os.path.exists(filename): + raise FileNotFoundError('Environment file {} does not exist.'.format(filename)) + + with open(filename) as f: + for lineno, line in enumerate(f): + line = line.strip() + if not line or line.startswith('#'): + continue + if '=' not in line: + raise SyntaxError('Invalid environment file syntax in {} at line {}.'.format(filename, lineno + 1)) + + name, value = parse_var(line) + + yield name, value + + +_parser = None + + +def get_argument_parser(parser=None): + """ + Creates an argument parser with arguments to override the system environment. + + :api: bonobo.get_argument_parser + + :param _parser: + :return: + """ + if parser is None: + import argparse + parser = argparse.ArgumentParser() + + # Store globally to be able to warn the user about the fact he's probably wrong not to pass a parser to + # parse_args(), later. + global _parser + _parser = parser + + _parser.add_argument('--default-env-file', '-E', action='append') + _parser.add_argument('--default-env', action='append') + _parser.add_argument('--env-file', action='append') + _parser.add_argument('--env', '-e', action='append') + + return _parser + + +@contextmanager +def parse_args(mixed=None): + """ + Context manager to extract and apply environment related options from the provided argparser result. + + A dictionnary with unknown options will be yielded, so the remaining options can be used by the caller. + + :api: bonobo.patch_environ + + :param mixed: ArgumentParser instance, Namespace, or dict. + :return: + """ + + if mixed is None: + global _parser + if _parser is not None: + warnings.warn( + 'You are calling bonobo.parse_args() without a parser argument, but it looks like you created a parser before. You probably want to pass your parser to this call, or if creating a new parser here is really what you want to do, please create a new one explicitely to silence this warning.' + ) + # use the api from bonobo namespace, in case a command patched it. + import bonobo + mixed = bonobo.get_argument_parser() + + if isinstance(mixed, argparse.ArgumentParser): + options = mixed.parse_args() + else: + options = mixed + + if not isinstance(options, dict): + options = options.__dict__ + + # make a copy so we don't polute our parent variables. + options = dict(options) + + # storage for values before patch. + _backup = {} + + # Priority order: --env > --env-file > system > --default-env > --default-env-file + # + # * The code below is reading default-env before default-env-file as if the first sets something, default-env-file + # won't override it. + # * Then, env-file is read from before env, as the behaviour will be the oposite (env will override a var even if + # env-file sets something.) + try: + # Set default environment + for name, value in map(parse_var, options.pop('default_env', []) or []): + if not name in os.environ: + if not name in _backup: + _backup[name] = os.environ.get(name, None) + os.environ[name] = value + + # Read and set default environment from file(s) + for filename in options.pop('default_env_file', []) or []: + for name, value in load_env_from_file(filename): + if not name in os.environ: + if not name in _backup: + _backup[name] = os.environ.get(name, None) + os.environ[name] = value + + # Read and set environment from file(s) + for filename in options.pop('env_file', []) or []: + for name, value in load_env_from_file(filename): + if not name in _backup: + _backup[name] = os.environ.get(name, None) + os.environ[name] = value + + # Set environment + for name, value in map(parse_var, options.pop('env', []) or []): + if not name in _backup: + _backup[name] = os.environ.get(name, None) + os.environ[name] = value + + yield options + finally: + for name, value in _backup.items(): + if value is None: + del os.environ[name] + else: + os.environ[name] = value diff --git a/tests/features/test_not_modified.py b/tests/features/test_not_modified.py index ddc537b..5b1b673 100644 --- a/tests/features/test_not_modified.py +++ b/tests/features/test_not_modified.py @@ -9,8 +9,12 @@ def useless(*args, **kwargs): def test_not_modified(): input_messages = [ ('foo', 'bar'), - {'foo': 'bar'}, - ('foo', {'bar': 'baz'}), + { + 'foo': 'bar' + }, + ('foo', { + 'bar': 'baz' + }), (), ] @@ -18,7 +22,3 @@ def test_not_modified(): context.write_sync(*input_messages) assert context.get_buffer() == input_messages - - - - diff --git a/tests/io/test_csv.py b/tests/io/test_csv.py index 473d243..291c241 100644 --- a/tests/io/test_csv.py +++ b/tests/io/test_csv.py @@ -51,11 +51,9 @@ def test_read_csv_from_file_kwargs(tmpdir): 'a': 'a foo', 'b': 'b foo', 'c': 'c foo', - }, - { + }, { 'a': 'a bar', 'b': 'b bar', 'c': 'c bar', } ] - diff --git a/tests/io/test_json.py b/tests/io/test_json.py index a3b25f5..b72a3de 100644 --- a/tests/io/test_json.py +++ b/tests/io/test_json.py @@ -20,10 +20,10 @@ def test_write_json_ioformat_arg0(tmpdir): @pytest.mark.parametrize('add_kwargs', ( - {}, - { - 'ioformat': settings.IOFORMAT_KWARGS, - }, + {}, + { + 'ioformat': settings.IOFORMAT_KWARGS, + }, )) def test_write_json_kwargs(tmpdir, add_kwargs): fs, filename, services = json_tester.get_services_for_writer(tmpdir) @@ -41,8 +41,7 @@ stream_json_tester.input_data = '''{"foo": "bar"}\n{"baz": "boz"}''' 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: + with BufferingNodeExecutionContext(LdjsonReader(filename), services=services) as context: context.write_sync(tuple()) actual = context.get_buffer() @@ -53,10 +52,11 @@ def test_read_stream_json(tmpdir): def test_write_stream_json(tmpdir): fs, filename, services = stream_json_tester.get_services_for_reader(tmpdir) - with BufferingNodeExecutionContext(LdjsonWriter(filename), - services=services) as context: + with BufferingNodeExecutionContext(LdjsonWriter(filename), services=services) as context: context.write_sync( - {'foo': 'bar'}, + { + 'foo': 'bar' + }, {'baz': 'boz'}, ) diff --git a/tests/test_commands.py b/tests/test_commands.py index 64d530b..4bfc8ba 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -45,7 +45,6 @@ def runner_module(args): all_runners = pytest.mark.parametrize('runner', [runner_entrypoint, runner_module]) -single_runner = pytest.mark.parametrize('runner', [runner_module]) def test_entrypoint(): @@ -158,238 +157,141 @@ def test_download_fails_non_example(runner): runner('download', '/something/entirely/different.txt') -@all_runners -class TestDefaultEnvFile(object): - def test_run_file_with_default_env_file(self, runner): - out, err = runner( - 'run', '--quiet', '--default-env-file', '.env_one', - get_examples_path('environment/env_files/get_passed_env_file.py') - ) - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'sweetpassword' - assert out[2] != 'marzo' +@pytest.fixture +def env1(tmpdir): + env_file = tmpdir.join('.env_one') + env_file.write('\n'.join(( + 'SECRET=unknown', + 'PASSWORD=sweet', + 'PATH=first', + ))) + return str(env_file) - def test_run_file_with_multiple_default_env_files(self, runner): - out, err = runner( - 'run', '--quiet', '--default-env-file', '.env_one', '--default-env-file', '.env_two', - get_examples_path('environment/env_files/get_passed_env_file.py') - ) - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'sweetpassword' - assert out[2] != 'marzo' - def test_run_module_with_default_env_file(self, runner): - out, err = runner( - 'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', '--default-env-file', - '.env_one' - ) - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'sweetpassword' - assert out[2] != 'marzo' +@pytest.fixture +def env2(tmpdir): + env_file = tmpdir.join('.env_two') + env_file.write('\n'.join(( + 'PASSWORD=bitter', + "PATH='second'", + ))) + return str(env_file) - def test_run_module_with_multiple_default_env_files(self, runner): - out, err = runner( - 'run', - '--quiet', + +all_environ_targets = pytest.mark.parametrize( + 'target', [ + (get_examples_path('environ.py'), ), + ( '-m', - 'bonobo.examples.environment.env_files.get_passed_env_file', - '--default-env-file', - '.env_one', - '--default-env-file', - '.env_two', - ) - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'sweetpassword' - assert out[2] != 'marzo' + 'bonobo.examples.environ', + ), + ] +) @all_runners -class TestEnvFile(object): - def test_run_file_with_file(self, runner): - out, err = runner( - 'run', - '--quiet', - get_examples_path('environment/env_files/get_passed_env_file.py'), - '--env-file', - '.env_one', +@all_environ_targets +class EnvironmentTestCase(): + def run_quiet(self, runner, *args): + return runner('run', '--quiet', *args) + + def run_environ(self, runner, *args, environ=None): + _environ = {'PATH': '/usr/bin'} + if environ: + _environ.update(environ) + + with patch.dict('os.environ', _environ, clear=True): + out, err = self.run_quiet(runner, *args) + assert 'SECRET' not in os.environ + assert 'PASSWORD' not in os.environ + if 'PATH' in _environ: + assert 'PATH' in os.environ + assert os.environ['PATH'] == _environ['PATH'] + + assert err == '' + return dict(map(lambda line: line.split(' ', 1), filter(None, out.split('\n')))) + + +class TestDefaultEnvFile(EnvironmentTestCase): + def test_run_with_default_env_file(self, runner, target, env1): + env = self.run_environ(runner, *target, '--default-env-file', env1) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'sweet' + assert env.get('PATH') == '/usr/bin' + + def test_run_with_multiple_default_env_files(self, runner, target, env1, env2): + env = self.run_environ(runner, *target, '--default-env-file', env1, '--default-env-file', env2) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'sweet' + assert env.get('PATH') == '/usr/bin' + + env = self.run_environ(runner, *target, '--default-env-file', env2, '--default-env-file', env1) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'bitter' + assert env.get('PATH') == '/usr/bin' + + +class TestEnvFile(EnvironmentTestCase): + def test_run_with_file(self, runner, target, env1): + env = self.run_environ(runner, *target, '--env-file', env1) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'sweet' + assert env.get('PATH') == 'first' + + def test_run_with_multiple_files(self, runner, target, env1, env2): + env = self.run_environ(runner, *target, '--env-file', env1, '--env-file', env2) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'bitter' + assert env.get('PATH') == 'second' + + env = self.run_environ(runner, *target, '--env-file', env2, '--env-file', env1) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'sweet' + assert env.get('PATH') == 'first' + + +class TestEnvFileCombinations(EnvironmentTestCase): + def test_run_with_both_env_files(self, runner, target, env1, env2): + env = self.run_environ(runner, *target, '--default-env-file', env1, '--env-file', env2) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'bitter' + assert env.get('PATH') == 'second' + + def test_run_with_both_env_files_then_overrides(self, runner, target, env1, env2): + env = self.run_environ( + runner, *target, '--default-env-file', env1, '--env-file', env2, '--env', 'PASSWORD=mine', '--env', + 'SECRET=s3cr3t' ) - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'sweetpassword' - assert out[2] == 'marzo' + assert env.get('SECRET') == 's3cr3t' + assert env.get('PASSWORD') == 'mine' + assert env.get('PATH') == 'second' - def test_run_file_with_multiple_files(self, runner): - out, err = runner( - 'run', - '--quiet', - get_examples_path('environment/env_files/get_passed_env_file.py'), - '--env-file', - '.env_one', - '--env-file', - '.env_two', + +class TestEnvVars(EnvironmentTestCase): + def test_run_no_env(self, runner, target): + env = self.run_environ(runner, *target, environ={'USER': 'romain'}) + assert env.get('USER') == 'romain' + + def test_run_env(self, runner, target): + env = self.run_environ(runner, *target, '--env', 'USER=serious', environ={'USER': 'romain'}) + assert env.get('USER') == 'serious' + + def test_run_env_mixed(self, runner, target): + env = self.run_environ(runner, *target, '--env', 'ONE=1', '--env', 'TWO="2"', environ={'USER': 'romain'}) + assert env.get('USER') == 'romain' + assert env.get('ONE') == '1' + assert env.get('TWO') == '2' + + def test_run_default_env(self, runner, target): + env = self.run_environ(runner, *target, '--default-env', 'USER=clown') + assert env.get('USER') == 'clown' + + env = self.run_environ(runner, *target, '--default-env', 'USER=clown', environ={'USER': 'romain'}) + assert env.get('USER') == 'romain' + + env = self.run_environ( + runner, *target, '--env', 'USER=serious', '--default-env', 'USER=clown', environ={ + 'USER': 'romain' + } ) - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'not_sweet_password' - assert out[2] == 'abril' - - def test_run_module_with_file(self, runner): - out, err = runner( - 'run', - '--quiet', - '-m', - 'bonobo.examples.environment.env_files.get_passed_env_file', - '--env-file', - '.env_one', - ) - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'sweetpassword' - assert out[2] == 'marzo' - - def test_run_module_with_multiple_files(self, runner): - out, err = runner( - 'run', - '--quiet', - '-m', - 'bonobo.examples.environment.env_files.get_passed_env_file', - '--env-file', - '.env_one', - '--env-file', - '.env_two', - ) - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'not_sweet_password' - assert out[2] == 'abril' - - -@all_runners -class TestEnvFileCombinations: - def test_run_file_with_default_env_file_and_env_file(self, runner): - out, err = runner( - 'run', - '--quiet', - get_examples_path('environment/env_files/get_passed_env_file.py'), - '--default-env-file', - '.env_one', - '--env-file', - '.env_two', - ) - out = out.split('\n') - assert out[0] == '321' - assert out[1] == 'not_sweet_password' - assert out[2] == 'abril' - - def test_run_file_with_default_env_file_and_env_file_and_env_vars(self, runner): - out, err = runner( - 'run', - '--quiet', - get_examples_path('environment/env_files/get_passed_env_file.py'), - '--default-env-file', - '.env_one', - '--env-file', - '.env_two', - '--env', - 'TEST_USER_PASSWORD=SWEETpassWORD', - '--env', - 'MY_SECRET=444', - ) - out = out.split('\n') - assert out[0] == '444' - assert out[1] == 'SWEETpassWORD' - assert out[2] == 'abril' - - -@all_runners -class TestDefaultEnvVars: - def test_run_file_with_default_env_var(self, runner): - out, err = runner( - 'run', '--quiet', - get_examples_path('environment/env_vars/get_passed_env.py'), '--default-env', 'USER=clowncity', '--env', - 'USER=ted' - ) - out = out.split('\n') - assert out[0] == 'user' - assert out[1] == 'number' - assert out[2] == 'string' - assert out[3] != 'clowncity' - - def test_run_file_with_default_env_vars(self, runner): - out, err = runner( - 'run', '--quiet', - get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123', '--env', - 'ENV_TEST_USER=cwandrews', '--default-env', "ENV_TEST_STRING='my_test_string'" - ) - out = out.split('\n') - assert out[0] == 'cwandrews' - assert out[1] == '123' - assert out[2] == 'my_test_string' - - def test_run_module_with_default_env_var(self, runner): - out, err = runner( - 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', - 'ENV_TEST_NUMBER=123', '--default-env', 'ENV_TEST_STRING=string' - ) - out = out.split('\n') - assert out[0] == 'cwandrews' - assert out[1] == '123' - assert out[2] != 'string' - - def test_run_module_with_default_env_vars(self, runner): - out, err = runner( - 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', - 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', '--default-env', "ENV_TEST_STRING='string'" - ) - out = out.split('\n') - assert out[0] == 'cwandrews' - assert out[1] == '123' - assert out[2] != 'string' - - -@all_runners -class TestEnvVars: - def test_run_file_with_env_var(self, runner): - out, err = runner( - 'run', '--quiet', - get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123' - ) - out = out.split('\n') - assert out[0] != 'test_user' - assert out[1] == '123' - assert out[2] == 'my_test_string' - - def test_run_file_with_env_vars(self, runner): - out, err = runner( - 'run', '--quiet', - get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123', '--env', - 'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'" - ) - out = out.split('\n') - assert out[0] == 'cwandrews' - assert out[1] == '123' - assert out[2] == 'my_test_string' - - def test_run_module_with_env_var(self, runner): - out, err = runner( - 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', - 'ENV_TEST_NUMBER=123' - ) - out = out.split('\n') - assert out[0] == 'cwandrews' - assert out[1] == '123' - assert out[2] == 'my_test_string' - - def test_run_module_with_env_vars(self, runner): - out, err = runner( - 'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env', - 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'" - ) - out = out.split('\n') - assert out[0] == 'cwandrews' - assert out[1] == '123' - assert out[2] == 'my_test_string' + assert env.get('USER') == 'serious' From ac6cba02cb472fdadf90da7f1b7dd32cee5f6366 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 18:48:53 +0100 Subject: [PATCH 068/145] Remove files in examples that are not used anymore. --- .../examples/environment/env_files/.env_one | 3 --- .../examples/environment/env_files/.env_two | 2 -- .../environment/env_files/__init__.py | 0 .../env_files/get_passed_env_file.py | 23 ----------------- .../examples/environment/env_vars/__init__.py | 0 .../environment/env_vars/get_passed_env.py | 25 ------------------- 6 files changed, 53 deletions(-) delete mode 100644 bonobo/examples/environment/env_files/.env_one delete mode 100644 bonobo/examples/environment/env_files/.env_two delete mode 100644 bonobo/examples/environment/env_files/__init__.py delete mode 100644 bonobo/examples/environment/env_files/get_passed_env_file.py delete mode 100644 bonobo/examples/environment/env_vars/__init__.py delete mode 100644 bonobo/examples/environment/env_vars/get_passed_env.py diff --git a/bonobo/examples/environment/env_files/.env_one b/bonobo/examples/environment/env_files/.env_one deleted file mode 100644 index 65f2b17..0000000 --- a/bonobo/examples/environment/env_files/.env_one +++ /dev/null @@ -1,3 +0,0 @@ -MY_SECRET=321 -TEST_USER_PASSWORD=sweetpassword -PATH=marzo \ No newline at end of file diff --git a/bonobo/examples/environment/env_files/.env_two b/bonobo/examples/environment/env_files/.env_two deleted file mode 100644 index 672d6d2..0000000 --- a/bonobo/examples/environment/env_files/.env_two +++ /dev/null @@ -1,2 +0,0 @@ -TEST_USER_PASSWORD=not_sweet_password -PATH='abril' \ No newline at end of file diff --git a/bonobo/examples/environment/env_files/__init__.py b/bonobo/examples/environment/env_files/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bonobo/examples/environment/env_files/get_passed_env_file.py b/bonobo/examples/environment/env_files/get_passed_env_file.py deleted file mode 100644 index bb45540..0000000 --- a/bonobo/examples/environment/env_files/get_passed_env_file.py +++ /dev/null @@ -1,23 +0,0 @@ -import os - -import bonobo - - -def extract(): - my_secret = os.getenv('MY_SECRET') - test_user_password = os.getenv('TEST_USER_PASSWORD') - path = os.getenv('PATH') - - yield my_secret - yield test_user_password - yield path - - -def load(s: str): - print(s) - - -graph = bonobo.Graph(extract, load) - -if __name__ == '__main__': - bonobo.run(graph) diff --git a/bonobo/examples/environment/env_vars/__init__.py b/bonobo/examples/environment/env_vars/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bonobo/examples/environment/env_vars/get_passed_env.py b/bonobo/examples/environment/env_vars/get_passed_env.py deleted file mode 100644 index e0c6c45..0000000 --- a/bonobo/examples/environment/env_vars/get_passed_env.py +++ /dev/null @@ -1,25 +0,0 @@ -import os - -import bonobo - - -def extract(): - env_test_user = os.getenv('ENV_TEST_USER', 'user') - env_test_number = os.getenv('ENV_TEST_NUMBER', 'number') - env_test_string = os.getenv('ENV_TEST_STRING', 'string') - env_user = os.getenv('USER') - - yield env_test_user - yield env_test_number - yield env_test_string - yield env_user - - -def load(s: str): - print(s) - - -graph = bonobo.Graph(extract, load) - -if __name__ == '__main__': - bonobo.run(graph) From b5a8af3efe5fd159e9a33b54d2ae561406b45811 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 18:49:29 +0100 Subject: [PATCH 069/145] Remove unused argument. --- bonobo/examples/environ.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bonobo/examples/environ.py b/bonobo/examples/environ.py index ea1b39b..17d4e36 100644 --- a/bonobo/examples/environ.py +++ b/bonobo/examples/environ.py @@ -23,6 +23,5 @@ def get_graph(): # The __main__ block actually execute the graph. if __name__ == '__main__': parser = bonobo.get_argument_parser() - parser.add_argument('-v', action='append', dest='vars') with bonobo.parse_args(parser): bonobo.run(get_graph()) From 5cc514e490bc2c3527e39e16f00ce93f2def2c1a Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 18:53:27 +0100 Subject: [PATCH 070/145] Update dependencies, remove python-dotenv. --- Makefile | 2 +- Projectfile | 1 - requirements-dev.txt | 12 ++---------- requirements-docker.txt | 4 +--- requirements-jupyter.txt | 4 ++-- requirements-sqlalchemy.txt | 18 ++++++++++++++++++ requirements.txt | 5 ++--- setup.py | 15 +++++++-------- 8 files changed, 33 insertions(+), 28 deletions(-) create mode 100644 requirements-sqlalchemy.txt diff --git a/Makefile b/Makefile index bb68335..89f16e8 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4a5 on 2017-10-29. +# Generated by Medikit 0.4a9 on 2017-11-01. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index 3a10d0b..c51ac38 100644 --- a/Projectfile +++ b/Projectfile @@ -45,7 +45,6 @@ python.add_requirements( 'mondrian >=0.2,<0.3', 'packaging >=16,<17', 'psutil >=5.4,<6.0', - 'python-dotenv >=0.7,<0.8', 'requests >=2.0,<3.0', 'stevedore >=1.27,<1.28', dev=[ diff --git a/requirements-dev.txt b/requirements-dev.txt index 553fefc..8000471 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,29 +1,21 @@ -e .[dev] alabaster==0.7.10 -arrow==0.10.0 babel==2.5.1 -binaryornot==0.4.4 certifi==2017.7.27.1 chardet==3.0.4 -click==6.7 -cookiecutter==1.5.1 coverage==4.4.1 docutils==0.14 -future==0.16.0 idna==2.6 imagesize==0.7.1 -jinja2-time==0.2.0 jinja2==2.9.6 markupsafe==1.0 -poyo==0.4.1 py==1.4.34 pygments==2.2.0 pytest-cov==2.5.1 pytest-sugar==0.8.0 pytest-timeout==1.2.0 pytest==3.2.3 -python-dateutil==2.6.1 -pytz==2017.2 +pytz==2017.3 requests==2.18.4 six==1.11.0 snowballstemmer==1.2.1 @@ -31,4 +23,4 @@ sphinx==1.6.5 sphinxcontrib-websupport==1.0.1 termcolor==1.1.0 urllib3==1.22 -whichcraft==0.4.1 +yapf==0.19.0 diff --git a/requirements-docker.txt b/requirements-docker.txt index d6f1160..8eb4059 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -3,7 +3,6 @@ appdirs==1.4.3 bonobo-docker==0.5.0 certifi==2017.7.27.1 chardet==3.0.4 -click==6.7 colorama==0.3.9 docker-pycreds==0.2.1 docker==2.3.0 @@ -13,8 +12,7 @@ packaging==16.8 pbr==3.1.1 psutil==5.4.0 pyparsing==2.2.0 -python-dotenv==0.7.1 -pytz==2017.2 +pytz==2017.3 requests==2.18.4 six==1.11.0 stevedore==1.27.1 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 4e1d024..1978875 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -13,7 +13,7 @@ jinja2==2.9.6 jsonschema==2.6.0 jupyter-client==5.1.0 jupyter-console==5.2.0 -jupyter-core==4.3.0 +jupyter-core==4.4.0 jupyter==1.0.0 markupsafe==1.0 mistune==0.8 @@ -28,7 +28,7 @@ prompt-toolkit==1.0.15 ptyprocess==0.5.2 pygments==2.2.0 python-dateutil==2.6.1 -pyzmq==16.0.2 +pyzmq==16.0.3 qtconsole==4.3.1 simplegeneric==0.8.1 six==1.11.0 diff --git a/requirements-sqlalchemy.txt b/requirements-sqlalchemy.txt new file mode 100644 index 0000000..7083f9e --- /dev/null +++ b/requirements-sqlalchemy.txt @@ -0,0 +1,18 @@ +-e .[sqlalchemy] +appdirs==1.4.3 +bonobo-sqlalchemy==0.5.1 +certifi==2017.7.27.1 +chardet==3.0.4 +colorama==0.3.9 +fs==2.0.12 +idna==2.6 +packaging==16.8 +pbr==3.1.1 +psutil==5.4.0 +pyparsing==2.2.0 +pytz==2017.3 +requests==2.18.4 +six==1.11.0 +sqlalchemy==1.1.14 +stevedore==1.27.1 +urllib3==1.22 diff --git a/requirements.txt b/requirements.txt index 0d69f1a..0b8ada0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,18 +2,17 @@ appdirs==1.4.3 certifi==2017.7.27.1 chardet==3.0.4 -click==6.7 colorama==0.3.9 fs==2.0.12 idna==2.6 jinja2==2.9.6 markupsafe==1.0 +mondrian==0.2.0 packaging==16.8 pbr==3.1.1 psutil==5.4.0 pyparsing==2.2.0 -python-dotenv==0.7.1 -pytz==2017.2 +pytz==2017.3 requests==2.18.4 six==1.11.0 stevedore==1.27.1 diff --git a/setup.py b/setup.py index 0c01b43..2b949c0 100644 --- a/setup.py +++ b/setup.py @@ -53,18 +53,17 @@ setup( packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, install_requires=[ - 'colorama (>= 0.3, < 0.4)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'packaging (>= 16, < 17)', - 'psutil (>= 5.4, < 6.0)', 'python-dotenv (>= 0.7, < 0.8)', 'requests (>= 2.0, < 3.0)', - 'stevedore (>= 1.27, < 1.28)' + 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian (>= 0.2, < 0.3)', + 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6.0)', 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.27, < 1.28)' ], extras_require={ 'dev': [ - 'cookiecutter (>= 1.5, < 1.6)', 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', - 'pytest-cov (>= 2.5, < 3.0)', 'pytest-sugar (>= 0.8, < 0.9)', 'pytest-timeout (>= 1, < 2)', - 'sphinx (>= 1.6, < 2.0)' + 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', 'pytest-cov (>= 2.5, < 3.0)', + 'pytest-sugar (>= 0.8, < 0.9)', 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)', 'yapf' ], - 'docker': ['bonobo-docker'], - 'jupyter': ['ipywidgets (>= 6.0.0, < 7)', 'jupyter (>= 1.0, < 1.1)'] + 'docker': ['bonobo-docker (>= 0.5.0)'], + 'jupyter': ['ipywidgets (>= 6.0.0, < 7)', 'jupyter (>= 1.0, < 1.1)'], + 'sqlalchemy': ['bonobo-sqlalchemy (>= 0.5.1)'] }, entry_points={ 'bonobo.commands': [ From ddd84c4f505e2e51b0a05c0e9d02e48851a7397d Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 18:56:44 +0100 Subject: [PATCH 071/145] [examples] comments. --- bonobo/examples/environ.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bonobo/examples/environ.py b/bonobo/examples/environ.py index 17d4e36..280d2e1 100644 --- a/bonobo/examples/environ.py +++ b/bonobo/examples/environ.py @@ -1,26 +1,26 @@ +""" +This transformation extracts the environment and prints it, sorted alphabetically, one item per line. + +Used in the bonobo tests around environment management. + +""" import os import bonobo def extract_environ(): + """Yield all the system environment.""" yield from sorted(os.environ.items()) def get_graph(): - """ - This function builds the graph that needs to be executed. - - :return: bonobo.Graph - - """ graph = bonobo.Graph() graph.add_chain(extract_environ, print) return graph -# The __main__ block actually execute the graph. if __name__ == '__main__': parser = bonobo.get_argument_parser() with bonobo.parse_args(parser): From 23404d06ddd06a545fa62df68c8782109337c242 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 19:02:48 +0100 Subject: [PATCH 072/145] Still cleaning up. --- bonobo/events.py | 3 --- bonobo/examples/__init__.py | 23 ----------------------- tests/test_commands.py | 10 ++++++++++ 3 files changed, 10 insertions(+), 26 deletions(-) delete mode 100644 bonobo/events.py diff --git a/bonobo/events.py b/bonobo/events.py deleted file mode 100644 index 9a0cbba..0000000 --- a/bonobo/events.py +++ /dev/null @@ -1,3 +0,0 @@ -ON_START = 'bonobo.on_start' -ON_TICK = 'bonobo.on_tick' -ON_STOP = 'bonobo.on_stop' diff --git a/bonobo/examples/__init__.py b/bonobo/examples/__init__.py index 49b1544..e69de29 100644 --- a/bonobo/examples/__init__.py +++ b/bonobo/examples/__init__.py @@ -1,23 +0,0 @@ -def require(package, requirement=None): - requirement = requirement or package - - try: - return __import__(package) - except ImportError: - from colorama import Fore, Style - print( - Fore.YELLOW, - 'This example requires the {!r} package. Install it using:'. - format(requirement), - Style.RESET_ALL, - sep='' - ) - print() - print( - Fore.YELLOW, - ' $ pip install {!s}'.format(requirement), - Style.RESET_ALL, - sep='' - ) - print() - raise diff --git a/tests/test_commands.py b/tests/test_commands.py index 4bfc8ba..8a2c9b8 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -119,6 +119,16 @@ def test_version(runner): assert out.startswith('bonobo ') assert __version__ in out + out, err = runner('version', '-q') + out = out.strip() + assert out.startswith('bonobo ') + assert __version__ in out + + out, err = runner('version', '-qq') + out = out.strip() + assert not out.startswith('bonobo ') + assert __version__ in out + @all_runners def test_download_works_for_examples(runner): From 8e85fa04e222df4a83122e71d3df8a50c7c8e576 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 19:45:35 +0100 Subject: [PATCH 073/145] Adds the ability to initialize a package from bonobo init. --- bonobo/commands/__init__.py | 9 ++++++- bonobo/commands/init.py | 49 +++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index cd9559c..2f1c7cd 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -1,7 +1,9 @@ import argparse +import traceback from bonobo import settings, logging from bonobo.commands.base import BaseCommand, BaseGraphCommand +from bonobo.util.errors import print_error logger = logging.get_logger() @@ -52,4 +54,9 @@ def entrypoint(args=None): # Get command handler, execute, rince. command = commands[parsed_args.pop('command')] - command(**parsed_args) + + try: + command(**parsed_args) + except Exception as exc: + print_error(exc, traceback.format_exc()) + return 255 diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index 6c6c2ff..e5d11d2 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -12,9 +12,12 @@ class InitCommand(BaseCommand): def add_arguments(self, parser): parser.add_argument('filename') parser.add_argument('--force', '-f', default=False, action='store_true') - parser.add_argument('--template', '-t', choices=self.TEMPLATES, default='default') - def handle(self, *, template, filename, force=False): + target_group = parser.add_mutually_exclusive_group(required=True) + target_group.add_argument('--template', '-t', choices=self.TEMPLATES, default='default') + target_group.add_argument('--package', '-p', action='store_true', default=False) + + def create_file_from_template(self, *, template, filename): template_name = template name, ext = os.path.splitext(filename) if ext != '.py': @@ -24,10 +27,46 @@ class InitCommand(BaseCommand): env = Environment(loader=loader) template = env.get_template(template_name + '.py-tpl') - if os.path.exists(filename) and not force: - raise FileExistsError('Target filename already exists, use --force to override.') - with open(filename, 'w+') as f: f.write(template.render(name=name)) self.logger.info('Generated {} using template {!r}.'.format(filename, template_name)) + + def create_package(self, *, filename): + name, ext = os.path.splitext(filename) + if ext != '': + raise ValueError('Package names should not have an extension.') + + try: + import medikit.commands + except ImportError as exc: + raise ImportError( + 'To initialize a package, you need to install medikit (pip install --upgrade medikit).') from exc + + package_name = os.path.basename(filename) + medikit.commands.handle_init(os.path.join(os.getcwd(), filename, 'Projectfile'), name=package_name, + requirements=['bonobo']) + + self.logger.info('Generated "{}" package with medikit.'.format(package_name)) + self.create_file_from_template(template='default', filename=os.path.join(filename, package_name, '__main__.py')) + + print('Your "{}" package has been created.'.format(package_name)) + print() + print('Install it...') + print() + print(' pip install --editable {}'.format(filename)) + print() + print('Then maybe run the example...') + print() + print(' python -m {}'.format(package_name)) + print() + print('Enjoy!') + + def handle(self, *, template, filename, package=False, force=False): + if os.path.exists(filename) and not force: + raise FileExistsError('Target filename already exists, use --force to override.') + + if package: + self.create_package(filename=filename) + else: + self.create_file_from_template(template=template, filename=filename) From 7035cc01e058115ce9d903c0f1e44e7d4f8c384b Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 19:52:34 +0100 Subject: [PATCH 074/145] Adds argument parser support in default template. --- bonobo/commands/templates/default.py-tpl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bonobo/commands/templates/default.py-tpl b/bonobo/commands/templates/default.py-tpl index 1d8d6a5..c72efb5 100644 --- a/bonobo/commands/templates/default.py-tpl +++ b/bonobo/commands/templates/default.py-tpl @@ -1,5 +1,6 @@ import bonobo + def extract(): """Placeholder, change, rename, remove... """ yield 'hello' @@ -46,5 +47,6 @@ def get_services(): # The __main__ block actually execute the graph. if __name__ == '__main__': - # Although you're not required to use it, bonobo's graph related commands will hook to this call (inspect, run, ...). - bonobo.run(get_graph(), services=get_services()) + parser = bonobo.get_argument_parser() + with bonobo.parse_args(parser): + bonobo.run(get_graph(), services=get_services()) From 58923f4a84f16e5a53ccb8736361ac762aed7182 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 1 Nov 2017 19:57:51 +0100 Subject: [PATCH 075/145] [logging] Switching to mondrian, who got all our formating code. --- bonobo/commands/__init__.py | 10 +++-- bonobo/commands/base.py | 6 +-- bonobo/logging.py | 86 ------------------------------------- 3 files changed, 9 insertions(+), 93 deletions(-) delete mode 100644 bonobo/logging.py diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index 2f1c7cd..c015d78 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -1,12 +1,12 @@ import argparse import traceback +import logging +import mondrian -from bonobo import settings, logging +from bonobo import settings from bonobo.commands.base import BaseCommand, BaseGraphCommand from bonobo.util.errors import print_error -logger = logging.get_logger() - def entrypoint(args=None): """ @@ -16,6 +16,8 @@ def entrypoint(args=None): """ + logger = mondrian.getLogger() + parser = argparse.ArgumentParser() parser.add_argument('--debug', '-D', action='store_true') @@ -48,7 +50,7 @@ def entrypoint(args=None): if parsed_args.pop('debug', False): settings.DEBUG.set(True) settings.LOGGING_LEVEL.set(logging.DEBUG) - logging.set_level(settings.LOGGING_LEVEL.get()) + logger.setLevel(settings.LOGGING_LEVEL.get()) logger.debug('Command: ' + parsed_args['command'] + ' Arguments: ' + repr(parsed_args)) diff --git a/bonobo/commands/base.py b/bonobo/commands/base.py index 43513b0..da2967f 100644 --- a/bonobo/commands/base.py +++ b/bonobo/commands/base.py @@ -1,12 +1,12 @@ import argparse +import logging import runpy import sys from contextlib import contextmanager import bonobo.util.environ -from bonobo import logging -from bonobo.util.environ import get_argument_parser, parse_args from bonobo.util import get_name +from bonobo.util.environ import get_argument_parser, parse_args class BaseCommand: @@ -20,7 +20,7 @@ class BaseCommand: try: return self._logger except AttributeError: - self._logger = logging.get_logger(get_name(self)) + self._logger = logging.getLogger(get_name(self)) return self._logger def add_arguments(self, parser): diff --git a/bonobo/logging.py b/bonobo/logging.py deleted file mode 100644 index 071fcd3..0000000 --- a/bonobo/logging.py +++ /dev/null @@ -1,86 +0,0 @@ -import logging -import sys -import textwrap -from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING - -from colorama import Fore, Style - -from bonobo import settings -from bonobo.util.term import CLEAR_EOL - -iswindows = (sys.platform == 'win32') - - -def get_format(): - yield '{b}[%(fg)s%(levelname)s{b}][{w}' - yield '{b}][{w}'.join(('%(spent)04d', '%(name)s')) - yield '{b}]' - yield ' %(fg)s%(message)s{r}' - if not iswindows: - yield CLEAR_EOL - - -colors = { - 'b': '' if iswindows else Fore.BLACK, - 'w': '' if iswindows else Fore.LIGHTBLACK_EX, - 'r': '' if iswindows else Style.RESET_ALL, -} -format = (''.join(get_format())).format(**colors) - - -class Filter(logging.Filter): - def filter(self, record): - record.spent = record.relativeCreated // 1000 - if iswindows: - record.fg = '' - elif record.levelname == 'DEBG': - record.fg = Fore.LIGHTBLACK_EX - elif record.levelname == 'INFO': - record.fg = Fore.LIGHTWHITE_EX - elif record.levelname == 'WARN': - record.fg = Fore.LIGHTYELLOW_EX - elif record.levelname == 'ERR ': - record.fg = Fore.LIGHTRED_EX - elif record.levelname == 'CRIT': - record.fg = Fore.RED - else: - record.fg = Fore.LIGHTWHITE_EX - return True - - -class Formatter(logging.Formatter): - def formatException(self, ei): - tb = super().formatException(ei) - if iswindows: - return textwrap.indent(tb, ' | ') - else: - return textwrap.indent(tb, Fore.BLACK + ' | ' + Fore.WHITE) - - -def setup(level): - logging.addLevelName(DEBUG, 'DEBG') - logging.addLevelName(INFO, 'INFO') - logging.addLevelName(WARNING, 'WARN') - logging.addLevelName(ERROR, 'ERR ') - logging.addLevelName(CRITICAL, 'CRIT') - handler = logging.StreamHandler(sys.stderr) - handler.setFormatter(Formatter(format)) - handler.addFilter(Filter()) - root = logging.getLogger() - root.addHandler(handler) - root.setLevel(level) - - -def set_level(level): - logging.getLogger().setLevel(level) - - -def get_logger(name='bonobo'): - return logging.getLogger(name) - - -# Compatibility with python logging -getLogger = get_logger - -# Setup formating and level. -setup(level=settings.LOGGING_LEVEL.get()) From fb86bc950716973888fc509e0a2582253f5f5343 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 2 Nov 2017 00:08:09 +0100 Subject: [PATCH 076/145] [errors] Move error handling in transformations to use mondrian. --- Makefile | 2 +- Projectfile | 2 +- bonobo/__init__.py | 1 + bonobo/_api.py | 7 +++++-- bonobo/commands/__init__.py | 16 +++++++--------- bonobo/commands/download.py | 1 - bonobo/commands/init.py | 10 ++++++---- bonobo/execution/__init__.py | 4 +++- bonobo/execution/base.py | 19 +++++++++++++------ bonobo/execution/node.py | 6 ++++-- bonobo/ext/django.py | 1 - bonobo/settings.py | 1 + requirements.txt | 2 +- setup.py | 2 +- tests/test_commands.py | 2 +- 15 files changed, 45 insertions(+), 31 deletions(-) diff --git a/Makefile b/Makefile index 89f16e8..14825bf 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4a9 on 2017-11-01. +# Generated by Medikit 0.4a10 on 2017-11-01. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index c51ac38..3a48797 100644 --- a/Projectfile +++ b/Projectfile @@ -42,7 +42,7 @@ python.setup( python.add_requirements( 'fs >=2.0,<2.1', 'jinja2 >=2.9,<2.10', - 'mondrian >=0.2,<0.3', + 'mondrian >=0.3,<0.4', 'packaging >=16,<17', 'psutil >=5.4,<6.0', 'requests >=2.0,<3.0', diff --git a/bonobo/__init__.py b/bonobo/__init__.py index 3c15c18..0ac9bc3 100644 --- a/bonobo/__init__.py +++ b/bonobo/__init__.py @@ -9,6 +9,7 @@ import sys assert (sys.version_info >= (3, 5)), 'Python 3.5+ is required to use Bonobo.' + from bonobo._api import * from bonobo._api import __all__ from bonobo._version import __version__ diff --git a/bonobo/_api.py b/bonobo/_api.py index f1e5da5..d4dbda2 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -40,7 +40,6 @@ def run(graph, *, plugins=None, services=None, strategy=None): :param dict services: The implementations of services this graph will use. :return bonobo.execution.graph.GraphExecutionContext: """ - strategy = create_strategy(strategy) plugins = plugins or [] @@ -49,6 +48,10 @@ def run(graph, *, plugins=None, services=None, strategy=None): if not settings.QUIET.get(): # pragma: no cover if _is_interactive_console(): + import mondrian + mondrian.setup() + mondrian.setupExceptHook() + from bonobo.ext.console import ConsoleOutputPlugin if ConsoleOutputPlugin not in plugins: plugins.append(ConsoleOutputPlugin) @@ -67,7 +70,7 @@ def run(graph, *, plugins=None, services=None, strategy=None): if JupyterOutputPlugin not in plugins: plugins.append(JupyterOutputPlugin) - return strategy.execute(graph, plugins=plugins, services=services) + return create_strategy(strategy).execute(graph, plugins=plugins, services=services) def _inspect_as_graph(graph): diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index c015d78..39cfa05 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -1,11 +1,9 @@ import argparse -import traceback import logging -import mondrian +import mondrian from bonobo import settings from bonobo.commands.base import BaseCommand, BaseGraphCommand -from bonobo.util.errors import print_error def entrypoint(args=None): @@ -16,7 +14,10 @@ def entrypoint(args=None): """ - logger = mondrian.getLogger() + mondrian.setup() + mondrian.setupExceptHook() + + logger = logging.getLogger() parser = argparse.ArgumentParser() parser.add_argument('--debug', '-D', action='store_true') @@ -56,9 +57,6 @@ def entrypoint(args=None): # Get command handler, execute, rince. command = commands[parsed_args.pop('command')] + command(**parsed_args) - try: - command(**parsed_args) - except Exception as exc: - print_error(exc, traceback.format_exc()) - return 255 + return 0 diff --git a/bonobo/commands/download.py b/bonobo/commands/download.py index 9333db4..96b1c2f 100644 --- a/bonobo/commands/download.py +++ b/bonobo/commands/download.py @@ -12,7 +12,6 @@ EXAMPLES_BASE_URL = 'https://raw.githubusercontent.com/python-bonobo/bonobo/mast class DownloadCommand(BaseCommand): def handle(self, *, path, **options): - path = path.lstrip('/') if not path.startswith('examples'): raise ValueError('Download command currently supports examples only') examples_path = re.sub('^examples/', '', path) diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index e5d11d2..8c50b16 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -13,7 +13,7 @@ class InitCommand(BaseCommand): parser.add_argument('filename') parser.add_argument('--force', '-f', default=False, action='store_true') - target_group = parser.add_mutually_exclusive_group(required=True) + target_group = parser.add_mutually_exclusive_group(required=False) target_group.add_argument('--template', '-t', choices=self.TEMPLATES, default='default') target_group.add_argument('--package', '-p', action='store_true', default=False) @@ -41,11 +41,13 @@ class InitCommand(BaseCommand): import medikit.commands except ImportError as exc: raise ImportError( - 'To initialize a package, you need to install medikit (pip install --upgrade medikit).') from exc + 'To initialize a package, you need to install medikit (pip install --upgrade medikit).' + ) from exc package_name = os.path.basename(filename) - medikit.commands.handle_init(os.path.join(os.getcwd(), filename, 'Projectfile'), name=package_name, - requirements=['bonobo']) + medikit.commands.handle_init( + os.path.join(os.getcwd(), filename, 'Projectfile'), name=package_name, requirements=['bonobo'] + ) self.logger.info('Generated "{}" package with medikit.'.format(package_name)) self.create_file_from_template(template='default', filename=os.path.join(filename, package_name, '__main__.py')) diff --git a/bonobo/execution/__init__.py b/bonobo/execution/__init__.py index b8a83dd..eea436a 100644 --- a/bonobo/execution/__init__.py +++ b/bonobo/execution/__init__.py @@ -1 +1,3 @@ -from bonobo.execution.graph import GraphExecutionContext, NodeExecutionContext, PluginExecutionContext +import logging + +logger = logging.getLogger(__name__) diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py index b9bce36..d469631 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/base.py @@ -1,12 +1,17 @@ -import traceback +import logging from contextlib import contextmanager +from logging import WARNING, ERROR from time import sleep +import sys + +import mondrian + from bonobo.config import create_container from bonobo.config.processors import ContextCurrifier from bonobo.util import isconfigurabletype -from bonobo.util.errors import print_error from bonobo.util.objects import Wrapper, get_name +from bonobo.execution import logger @contextmanager @@ -14,7 +19,7 @@ def recoverable(error_handler): try: yield except Exception as exc: # pylint: disable=broad-except - error_handler(exc, traceback.format_exc()) + error_handler(*sys.exc_info(), level=ERROR) @contextmanager @@ -22,7 +27,7 @@ def unrecoverable(error_handler): try: yield except Exception as exc: # pylint: disable=broad-except - error_handler(exc, traceback.format_exc()) + error_handler(*sys.exc_info(), level=ERROR) raise # raise unrecoverableerror from x ? @@ -101,8 +106,10 @@ class LoopingExecutionContext(Wrapper): finally: self._stopped = True - def handle_error(self, exc, trace): - return print_error(exc, trace, context=self.wrapped) + def handle_error(self, exctype, exc, tb): + mondrian.excepthook( + exctype, exc, tb, level=WARNING, context='{} in {}'.format(exctype.__name__, get_name(self)), logger=logger + ) def _get_initial_context(self): if self.parent: diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index e727531..7781a78 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -3,6 +3,8 @@ from queue import Empty from time import sleep from types import GeneratorType +import sys + from bonobo.constants import NOT_MODIFIED, BEGIN, END from bonobo.errors import InactiveReadableError, UnrecoverableError from bonobo.execution.base import LoopingExecutionContext @@ -101,11 +103,11 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): sleep(self.PERIOD) continue except UnrecoverableError as exc: - self.handle_error(exc, traceback.format_exc()) + self.handle_error(*sys.exc_info()) self.input.shutdown() break except Exception as exc: # pylint: disable=broad-except - self.handle_error(exc, traceback.format_exc()) + self.handle_error(*sys.exc_info()) def step(self): # Pull data from the first available input channel. diff --git a/bonobo/ext/django.py b/bonobo/ext/django.py index d35d131..d9d17f7 100644 --- a/bonobo/ext/django.py +++ b/bonobo/ext/django.py @@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, OutputWrapper import bonobo import bonobo.util -from bonobo.commands import get_default_services from bonobo.ext.console import ConsoleOutputPlugin from bonobo.util.term import CLEAR_EOL diff --git a/bonobo/settings.py b/bonobo/settings.py index 05d2089..fdc4412 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -1,4 +1,5 @@ import logging + import os from bonobo.errors import ValidationError diff --git a/requirements.txt b/requirements.txt index 0b8ada0..35ab601 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ fs==2.0.12 idna==2.6 jinja2==2.9.6 markupsafe==1.0 -mondrian==0.2.0 +mondrian==0.3.0 packaging==16.8 pbr==3.1.1 psutil==5.4.0 diff --git a/setup.py b/setup.py index 2b949c0..9bfecb1 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ setup( packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, install_requires=[ - 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian (>= 0.2, < 0.3)', + 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian (>= 0.3, < 0.4)', 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6.0)', 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.27, < 1.28)' ], extras_require={ diff --git a/tests/test_commands.py b/tests/test_commands.py index 8a2c9b8..e7e3523 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -164,7 +164,7 @@ def test_download_works_for_examples(runner): @all_runners def test_download_fails_non_example(runner): with pytest.raises(ValueError): - runner('download', '/something/entirely/different.txt') + runner('download', 'something/entirely/different.txt') @pytest.fixture From 23542dc675b251418e1aa7ac2473a12831634038 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 2 Nov 2017 00:17:03 +0100 Subject: [PATCH 077/145] Removing old error handler. --- bonobo/strategies/executor.py | 18 +++++++++------- bonobo/util/errors.py | 39 ----------------------------------- 2 files changed, 11 insertions(+), 46 deletions(-) delete mode 100644 bonobo/util/errors.py diff --git a/bonobo/strategies/executor.py b/bonobo/strategies/executor.py index 3bfabc6..24ca154 100644 --- a/bonobo/strategies/executor.py +++ b/bonobo/strategies/executor.py @@ -1,11 +1,15 @@ import time + +import sys + +import mondrian import traceback from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor +from bonobo.util import get_name from bonobo.constants import BEGIN, END from bonobo.strategies.base import Strategy from bonobo.structs.bags import Bag -from bonobo.util.errors import print_error class ExecutorStrategy(Strategy): @@ -47,16 +51,16 @@ class ExecutorStrategy(Strategy): def _runner(): try: node.start() - except Exception as exc: - print_error(exc, traceback.format_exc(), context=node, method='start') + except Exception: + mondrian.excepthook(*sys.exc_info(), context='Could not start node {}.'.format(get_name(node))) node.input.on_end() else: node.loop() try: node.stop() - except Exception as exc: - print_error(exc, traceback.format_exc(), context=node, method='stop') + except Exception: + mondrian.excepthook(*sys.exc_info(), context='Could not stop node {}.'.format(get_name(node))) futures.append(executor.submit(_runner)) @@ -68,8 +72,8 @@ class ExecutorStrategy(Strategy): with plugin: try: plugin.loop() - except Exception as exc: - print_error(exc, traceback.format_exc(), context=plugin) + except Exception: + mondrian.excepthook(*sys.exc_info(), context='In plugin loop for {}...'.format(get_name(plugin))) futures.append(executor.submit(_runner)) diff --git a/bonobo/util/errors.py b/bonobo/util/errors.py deleted file mode 100644 index cae2789..0000000 --- a/bonobo/util/errors.py +++ /dev/null @@ -1,39 +0,0 @@ -import sys -from textwrap import indent - - -def _get_error_message(exc): - if hasattr(exc, '__str__'): - message = str(exc) - return message[0].upper() + message[1:] - return '\n'.join(exc.args), - - -def print_error(exc, trace, context=None, method=None): - """ - Error handler. Whatever happens in a plugin or component, if it looks like an exception, taste like an exception - or somehow make me think it is an exception, I'll handle it. - - :param exc: the culprit - :param trace: Hercule Poirot's logbook. - :return: to hell - """ - - from colorama import Fore, Style - - prefix = '{}{} | {}'.format(Fore.RED, Style.BRIGHT, Style.RESET_ALL) - - print( - Style.BRIGHT, - Fore.RED, - type(exc).__name__, - ' (in {}{})'.format(type(context).__name__, '.{}()'.format(method) if method else '') if context else '', - Style.RESET_ALL, - '\n', - indent(_get_error_message(exc), prefix + Style.BRIGHT), - Style.RESET_ALL, - sep='', - file=sys.stderr, - ) - print(prefix, file=sys.stderr) - print(indent(trace, prefix, predicate=lambda line: True), file=sys.stderr) From 9037d946546878076419ff7404698fc14f3d791c Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 2 Nov 2017 07:25:38 +0100 Subject: [PATCH 078/145] Fix imports. --- bonobo/execution/__init__.py | 2 ++ bonobo/execution/base.py | 7 ++----- bonobo/strategies/executor.py | 4 +++- bonobo/util/testing.py | 2 +- tests/io/test_csv.py | 3 +-- tests/test_basicusage.py | 5 +++-- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/bonobo/execution/__init__.py b/bonobo/execution/__init__.py index eea436a..43ffbf3 100644 --- a/bonobo/execution/__init__.py +++ b/bonobo/execution/__init__.py @@ -1,3 +1,5 @@ import logging logger = logging.getLogger(__name__) + +__all__ = [] diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py index d469631..08375a8 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/base.py @@ -1,17 +1,14 @@ -import logging +import sys from contextlib import contextmanager from logging import WARNING, ERROR from time import sleep -import sys - import mondrian - from bonobo.config import create_container from bonobo.config.processors import ContextCurrifier +from bonobo.execution import logger from bonobo.util import isconfigurabletype from bonobo.util.objects import Wrapper, get_name -from bonobo.execution import logger @contextmanager diff --git a/bonobo/strategies/executor.py b/bonobo/strategies/executor.py index 24ca154..8c27d40 100644 --- a/bonobo/strategies/executor.py +++ b/bonobo/strategies/executor.py @@ -73,7 +73,9 @@ class ExecutorStrategy(Strategy): try: plugin.loop() except Exception: - mondrian.excepthook(*sys.exc_info(), context='In plugin loop for {}...'.format(get_name(plugin))) + mondrian.excepthook( + *sys.exc_info(), context='In plugin loop for {}...'.format(get_name(plugin)) + ) futures.append(executor.submit(_runner)) diff --git a/bonobo/util/testing.py b/bonobo/util/testing.py index 6fc7d60..7dc8f38 100644 --- a/bonobo/util/testing.py +++ b/bonobo/util/testing.py @@ -1,7 +1,7 @@ from contextlib import contextmanager from bonobo import open_fs, Token -from bonobo.execution import GraphExecutionContext +from bonobo.execution.graph import GraphExecutionContext from bonobo.execution.node import NodeExecutionContext diff --git a/tests/io/test_csv.py b/tests/io/test_csv.py index 291c241..1c4c6cc 100644 --- a/tests/io/test_csv.py +++ b/tests/io/test_csv.py @@ -1,7 +1,6 @@ import pytest -from bonobo import Bag, CsvReader, CsvWriter, settings -from bonobo.constants import BEGIN, END +from bonobo import CsvReader, CsvWriter, settings from bonobo.execution.node import NodeExecutionContext from bonobo.util.testing import FilesystemTester, BufferingNodeExecutionContext diff --git a/tests/test_basicusage.py b/tests/test_basicusage.py index 58a1212..f002d36 100644 --- a/tests/test_basicusage.py +++ b/tests/test_basicusage.py @@ -1,8 +1,9 @@ +from unittest.mock import patch + import pytest import bonobo -from bonobo.execution import GraphExecutionContext -from unittest.mock import patch +from bonobo.execution.graph import GraphExecutionContext @pytest.mark.timeout(2) From d988d304742c5d41028859289a0cd31b787820bf Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 2 Nov 2017 08:25:30 +0100 Subject: [PATCH 079/145] Update to mondrian 0.4a0. --- Makefile | 2 +- Projectfile | 2 +- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 14825bf..8baafb1 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4a10 on 2017-11-01. +# Generated by Medikit 0.4a10 on 2017-11-02. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index 3a48797..a91bfe5 100644 --- a/Projectfile +++ b/Projectfile @@ -42,7 +42,7 @@ python.setup( python.add_requirements( 'fs >=2.0,<2.1', 'jinja2 >=2.9,<2.10', - 'mondrian >=0.3,<0.4', + 'mondrian ==0.4a0', 'packaging >=16,<17', 'psutil >=5.4,<6.0', 'requests >=2.0,<3.0', diff --git a/requirements.txt b/requirements.txt index 35ab601..82f5cdb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ fs==2.0.12 idna==2.6 jinja2==2.9.6 markupsafe==1.0 -mondrian==0.3.0 +mondrian==0.4a0 packaging==16.8 pbr==3.1.1 psutil==5.4.0 diff --git a/setup.py b/setup.py index 9bfecb1..219e0e6 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ setup( packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, install_requires=[ - 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian (>= 0.3, < 0.4)', + 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian (== 0.4a0)', 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6.0)', 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.27, < 1.28)' ], extras_require={ From 6bd1130e34021888d63cdc840e109254b1ce5f39 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 11:20:15 +0100 Subject: [PATCH 080/145] [core] Refactoring to use an event dispatcher in the main thread. Plugins now run in the main thread, instead of their own threads, and the API changed to use an event dispatcher approach instead of a static class interface. --- Makefile | 2 +- Projectfile | 3 +- bonobo/_api.py | 10 ++- bonobo/commands/__init__.py | 4 +- bonobo/examples/clock.py | 26 +++++++ bonobo/execution/base.py | 17 ++++- bonobo/execution/events.py | 13 ++++ bonobo/execution/graph.py | 52 ++++++++++--- bonobo/execution/node.py | 54 +++++++++----- bonobo/execution/plugin.py | 27 ++----- bonobo/ext/django.py | 2 +- bonobo/{plugins.py => plugins/__init__.py} | 13 +++- bonobo/{ext => plugins}/console.py | 86 +++++++++++++--------- bonobo/strategies/executor.py | 64 +++++----------- bonobo/strategies/util.py | 1 - requirements.txt | 3 +- setup.py | 4 +- 17 files changed, 233 insertions(+), 148 deletions(-) create mode 100644 bonobo/examples/clock.py create mode 100644 bonobo/execution/events.py rename bonobo/{plugins.py => plugins/__init__.py} (67%) rename bonobo/{ext => plugins}/console.py (83%) delete mode 100644 bonobo/strategies/util.py diff --git a/Makefile b/Makefile index 8baafb1..483467d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4a10 on 2017-11-02. +# Generated by Medikit 0.4a10 on 2017-11-03. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index a91bfe5..7aa05b5 100644 --- a/Projectfile +++ b/Projectfile @@ -42,11 +42,12 @@ python.setup( python.add_requirements( 'fs >=2.0,<2.1', 'jinja2 >=2.9,<2.10', - 'mondrian ==0.4a0', + 'mondrian ==0.4a1', 'packaging >=16,<17', 'psutil >=5.4,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.27,<1.28', + 'whistle ==1.0a3', dev=[ 'pytest-sugar >=0.8,<0.9', 'pytest-timeout >=1,<2', diff --git a/bonobo/_api.py b/bonobo/_api.py index d4dbda2..9a82f41 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -49,10 +49,9 @@ def run(graph, *, plugins=None, services=None, strategy=None): if not settings.QUIET.get(): # pragma: no cover if _is_interactive_console(): import mondrian - mondrian.setup() - mondrian.setupExceptHook() + mondrian.setup(excepthook=True) - from bonobo.ext.console import ConsoleOutputPlugin + from bonobo.plugins.console import ConsoleOutputPlugin if ConsoleOutputPlugin not in plugins: plugins.append(ConsoleOutputPlugin) @@ -70,7 +69,10 @@ def run(graph, *, plugins=None, services=None, strategy=None): if JupyterOutputPlugin not in plugins: plugins.append(JupyterOutputPlugin) - return create_strategy(strategy).execute(graph, plugins=plugins, services=services) + import logging + logging.getLogger().setLevel(settings.LOGGING_LEVEL.get()) + strategy = create_strategy(strategy) + return strategy.execute(graph, plugins=plugins, services=services) def _inspect_as_graph(graph): diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index 39cfa05..f42d5c6 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -14,9 +14,7 @@ def entrypoint(args=None): """ - mondrian.setup() - mondrian.setupExceptHook() - + mondrian.setup(excepthook=True) logger = logging.getLogger() parser = argparse.ArgumentParser() diff --git a/bonobo/examples/clock.py b/bonobo/examples/clock.py new file mode 100644 index 0000000..765f077 --- /dev/null +++ b/bonobo/examples/clock.py @@ -0,0 +1,26 @@ +import bonobo +import datetime +import time + + +def extract(): + """Placeholder, change, rename, remove... """ + for x in range(60): + if x: + time.sleep(1) + yield datetime.datetime.now() + + +def get_graph(): + graph = bonobo.Graph() + graph.add_chain( + extract, + print, + ) + + return graph + +if __name__ == '__main__': + parser = bonobo.get_argument_parser() + with bonobo.parse_args(parser): + bonobo.run(get_graph()) diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py index 08375a8..74dd89b 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/base.py @@ -29,8 +29,7 @@ def unrecoverable(error_handler): class LoopingExecutionContext(Wrapper): - alive = True - PERIOD = 0.25 + PERIOD = 0.5 @property def started(self): @@ -40,6 +39,19 @@ class LoopingExecutionContext(Wrapper): def stopped(self): return self._stopped + @property + def alive(self): + return self._started and not self._stopped + + @property + def status(self): + """One character status for this node. """ + if not self.started: + return ' ' + if not self.stopped: + return '+' + return '-' + def __init__(self, wrapped, parent, services=None): super().__init__(wrapped) @@ -84,7 +96,6 @@ class LoopingExecutionContext(Wrapper): """Generic loop. A bit boring. """ while self.alive: self.step() - sleep(self.PERIOD) def step(self): """Left as an exercise for the children.""" diff --git a/bonobo/execution/events.py b/bonobo/execution/events.py new file mode 100644 index 0000000..036e879 --- /dev/null +++ b/bonobo/execution/events.py @@ -0,0 +1,13 @@ +from whistle import Event + +START = 'execution.start' +STARTED = 'execution.started' +TICK = 'execution.tick' +STOP = 'execution.stop' +STOPPED = 'execution.stopped' +KILL = 'execution.kill' + + +class ExecutionEvent(Event): + def __init__(self, graph_context): + self.graph_context = graph_context diff --git a/bonobo/execution/graph.py b/bonobo/execution/graph.py index 77e01fa..deaa150 100644 --- a/bonobo/execution/graph.py +++ b/bonobo/execution/graph.py @@ -1,8 +1,11 @@ -import time from functools import partial +from time import sleep + +from whistle import EventDispatcher from bonobo.config import create_container from bonobo.constants import BEGIN, END +from bonobo.execution import events from bonobo.execution.node import NodeExecutionContext from bonobo.execution.plugin import PluginExecutionContext @@ -11,6 +14,8 @@ class GraphExecutionContext: NodeExecutionContextType = NodeExecutionContext PluginExecutionContextType = PluginExecutionContext + TICK_PERIOD = 0.25 + @property def started(self): return any(node.started for node in self.nodes) @@ -23,7 +28,8 @@ class GraphExecutionContext: def alive(self): return any(node.alive for node in self.nodes) - def __init__(self, graph, plugins=None, services=None): + def __init__(self, graph, plugins=None, services=None, dispatcher=None): + self.dispatcher = dispatcher or EventDispatcher() self.graph = graph self.nodes = [self.create_node_execution_context_for(node) for node in self.graph] self.plugins = [self.create_plugin_execution_context_for(plugin) for plugin in plugins or ()] @@ -53,6 +59,8 @@ class GraphExecutionContext: return self.NodeExecutionContextType(node, parent=self) def create_plugin_execution_context_for(self, plugin): + if isinstance(plugin, type): + plugin = plugin() return self.PluginExecutionContextType(plugin, parent=self) def write(self, *messages): @@ -63,23 +71,45 @@ class GraphExecutionContext: for message in messages: self[i].write(message) + def dispatch(self, name): + self.dispatcher.dispatch(name, events.ExecutionEvent(self)) + def start(self, starter=None): + self.register_plugins() + self.dispatch(events.START) + self.tick() for node in self.nodes: if starter is None: node.start() else: starter(node) + self.dispatch(events.STARTED) - def start_plugins(self, starter=None): - for plugin in self.plugins: - if starter is None: - plugin.start() - else: - starter(plugin) + def tick(self): + self.dispatch(events.TICK) + sleep(self.TICK_PERIOD) + + def kill(self): + self.dispatch(events.KILL) + for node_context in self.nodes: + node_context.kill() + self.tick() def stop(self, stopper=None): - for node in self.nodes: + self.dispatch(events.STOP) + for node_context in self.nodes: if stopper is None: - node.stop() + node_context.stop() else: - stopper(node) + stopper(node_context) + self.tick() + self.dispatch(events.STOPPED) + self.unregister_plugins() + + def register_plugins(self): + for plugin_context in self.plugins: + plugin_context.register() + + def unregister_plugins(self): + for plugin_context in self.plugins: + plugin_context.unregister() diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index 7781a78..fdb0c9f 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -1,10 +1,9 @@ -import traceback +import sys +import threading from queue import Empty from time import sleep from types import GeneratorType -import sys - from bonobo.constants import NOT_MODIFIED, BEGIN, END from bonobo.errors import InactiveReadableError, UnrecoverableError from bonobo.execution.base import LoopingExecutionContext @@ -22,13 +21,8 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): """ @property - def alive(self): - """todo check if this is right, and where it is used""" - return self._started and not self._stopped - - @property - def alive_str(self): - return '+' if self.alive else '-' + def killed(self): + return self._killed def __init__(self, wrapped, parent=None, services=None, _input=None, _outputs=None): LoopingExecutionContext.__init__(self, wrapped, parent=parent, services=services) @@ -36,13 +30,19 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): self.input = _input or Input() self.outputs = _outputs or [] + self._killed = False def __str__(self): - return self.alive_str + ' ' + self.__name__ + self.get_statistics_as_string(prefix=' ') + return self.__name__ + self.get_statistics_as_string(prefix=' ') def __repr__(self): name, type_name = get_name(self), get_name(type(self)) - return '<{}({}{}){}>'.format(type_name, self.alive_str, name, self.get_statistics_as_string(prefix=' ')) + return '<{}({}{}){}>'.format(type_name, self.status, name, self.get_statistics_as_string(prefix=' ')) + + def get_flags_as_string(self): + if self.killed: + return '[killed]' + return '' def write(self, *messages): """ @@ -92,22 +92,26 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): return row def loop(self): - while True: + while not self._killed: try: self.step() except KeyboardInterrupt: - raise + self.handle_error(*sys.exc_info()) + break except InactiveReadableError: break except Empty: sleep(self.PERIOD) continue - except UnrecoverableError as exc: + except UnrecoverableError: self.handle_error(*sys.exc_info()) self.input.shutdown() break - except Exception as exc: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except self.handle_error(*sys.exc_info()) + except BaseException: + self.handle_error(*sys.exc_info()) + break def step(self): # Pull data from the first available input channel. @@ -119,6 +123,15 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): # todo add timer self.handle_results(input_bag, input_bag.apply(self._stack)) + def kill(self): + if not self.started: + raise RuntimeError('Cannot kill a node context that has not started yet.') + + if self.stopped: + raise RuntimeError('Cannot kill a node context that has already stopped.') + + self._killed = True + def handle_results(self, input_bag, results): # self._exec_time += timer.duration # Put data onto output channels @@ -126,6 +139,9 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): if isinstance(results, GeneratorType): while True: try: + # if kill flag was step, stop iterating. + if self._killed: + break result = next(results) except StopIteration: break @@ -140,7 +156,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): def isflag(param): - return isinstance(param, Token) and param in (NOT_MODIFIED, ) + return isinstance(param, Token) and param in (NOT_MODIFIED,) def split_tokens(output): @@ -152,11 +168,11 @@ def split_tokens(output): """ if isinstance(output, Token): # just a flag - return (output, ), () + return (output,), () if not istuple(output): # no flag - return (), (output, ) + return (), (output,) i = 0 while isflag(output[i]): diff --git a/bonobo/execution/plugin.py b/bonobo/execution/plugin.py index 3379fc0..f552724 100644 --- a/bonobo/execution/plugin.py +++ b/bonobo/execution/plugin.py @@ -2,25 +2,12 @@ from bonobo.execution.base import LoopingExecutionContext, recoverable class PluginExecutionContext(LoopingExecutionContext): - PERIOD = 0.5 + @property + def dispatcher(self): + return self.parent.dispatcher - def __init__(self, wrapped, parent): - # Instanciate plugin. This is not yet considered stable, as at some point we may need a way to configure - # plugins, for example if it depends on an external service. - super().__init__(wrapped(self), parent) + def register(self): + return self.wrapped.register(self.dispatcher) - def start(self): - super().start() - - with recoverable(self.handle_error): - self.wrapped.on_start() - - def shutdown(self): - if self.started: - with recoverable(self.handle_error): - self.wrapped.on_stop() - self.alive = False - - def step(self): - with recoverable(self.handle_error): - self.wrapped.on_tick() + def unregister(self): + return self.wrapped.unregister(self.dispatcher) diff --git a/bonobo/ext/django.py b/bonobo/ext/django.py index d9d17f7..60b583c 100644 --- a/bonobo/ext/django.py +++ b/bonobo/ext/django.py @@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand, OutputWrapper import bonobo import bonobo.util -from bonobo.ext.console import ConsoleOutputPlugin +from bonobo.plugins.console import ConsoleOutputPlugin from bonobo.util.term import CLEAR_EOL diff --git a/bonobo/plugins.py b/bonobo/plugins/__init__.py similarity index 67% rename from bonobo/plugins.py rename to bonobo/plugins/__init__.py index 7a0f5d1..897b687 100644 --- a/bonobo/plugins.py +++ b/bonobo/plugins/__init__.py @@ -10,5 +10,14 @@ class Plugin: """ - def __init__(self, context): - self.context = context + def register(self, dispatcher): + """ + :param dispatcher: whistle.EventDispatcher + """ + pass + + def unregister(self, dispatcher): + """ + :param dispatcher: whistle.EventDispatcher + """ + pass diff --git a/bonobo/ext/console.py b/bonobo/plugins/console.py similarity index 83% rename from bonobo/ext/console.py rename to bonobo/plugins/console.py index 0e6abb3..814894b 100644 --- a/bonobo/ext/console.py +++ b/bonobo/plugins/console.py @@ -2,38 +2,14 @@ import io import sys from contextlib import redirect_stdout, redirect_stderr -from colorama import Style, Fore, init - -init(wrap=True) +from colorama import Style, Fore, init as initialize_colorama_output_wrappers from bonobo import settings +from bonobo.execution import events from bonobo.plugins import Plugin from bonobo.util.term import CLEAR_EOL, MOVE_CURSOR_UP - -class IOBuffer(): - """ - The role of IOBuffer is to overcome the problem of multiple threads wanting to write to stdout at the same time. It - works a bit like a videogame: there are two buffers, one that is used to write, and one which is used to read from. - On each cycle, we swap the buffers, and the console plugin handle output of the one which is not anymore "active". - - """ - - def __init__(self): - self.current = io.StringIO() - self.write = self.current.write - - def switch(self): - previous = self.current - self.current = io.StringIO() - self.write = self.current.write - try: - return previous.getvalue() - finally: - previous.close() - - def flush(self): - self.current.flush() +initialize_colorama_output_wrappers(wrap=True) class ConsoleOutputPlugin(Plugin): @@ -60,13 +36,24 @@ class ConsoleOutputPlugin(Plugin): # Whether we're on windows, or a real operating system. iswindows = (sys.platform == 'win32') - def on_start(self): + def __init__(self): + self.isatty = self._stdout.isatty() + + def register(self, dispatcher): + dispatcher.add_listener(events.START, self.setup) + dispatcher.add_listener(events.TICK, self.tick) + dispatcher.add_listener(events.STOPPED, self.teardown) + + def unregister(self, dispatcher): + dispatcher.remove_listener(events.STOPPED, self.teardown) + dispatcher.remove_listener(events.TICK, self.tick) + dispatcher.remove_listener(events.START, self.setup) + + def setup(self, event): self.prefix = '' self.counter = 0 self._append_cache = '' - self.isatty = self._stdout.isatty() - self.stdout = IOBuffer() self.redirect_stdout = redirect_stdout(self._stdout if self.iswindows else self.stdout) self.redirect_stdout.__enter__() @@ -75,14 +62,14 @@ class ConsoleOutputPlugin(Plugin): self.redirect_stderr = redirect_stderr(self._stderr if self.iswindows else self.stderr) self.redirect_stderr.__enter__() - def on_tick(self): + def tick(self, event): if self.isatty and not self.iswindows: - self._write(self.context.parent, rewind=True) + self._write(event.graph_context, rewind=True) else: pass # not a tty, or windows, so we'll ignore stats output - def on_stop(self): - self._write(self.context.parent, rewind=False) + def teardown(self, event): + self._write(event.graph_context, rewind=False) self.redirect_stderr.__exit__(None, None, None) self.redirect_stdout.__exit__(None, None, None) @@ -113,6 +100,8 @@ class ConsoleOutputPlugin(Plugin): name_suffix, ' ', node.get_statistics_as_string(), + ' ', + node.get_flags_as_string(), Style.RESET_ALL, ' ', ) @@ -128,6 +117,8 @@ class ConsoleOutputPlugin(Plugin): name_suffix, ' ', node.get_statistics_as_string(), + ' ', + node.get_flags_as_string(), Style.RESET_ALL, ' ', ) @@ -166,7 +157,32 @@ class ConsoleOutputPlugin(Plugin): self.counter += 1 +class IOBuffer(): + """ + The role of IOBuffer is to overcome the problem of multiple threads wanting to write to stdout at the same time. It + works a bit like a videogame: there are two buffers, one that is used to write, and one which is used to read from. + On each cycle, we swap the buffers, and the console plugin handle output of the one which is not anymore "active". + + """ + + def __init__(self): + self.current = io.StringIO() + self.write = self.current.write + + def switch(self): + previous = self.current + self.current = io.StringIO() + self.write = self.current.write + try: + return previous.getvalue() + finally: + previous.close() + + def flush(self): + self.current.flush() + + def memory_usage(): import os, psutil process = psutil.Process(os.getpid()) - return process.memory_info()[0] / float(2**20) + return process.memory_info()[0] / float(2 ** 20) diff --git a/bonobo/strategies/executor.py b/bonobo/strategies/executor.py index 8c27d40..e5ffdc0 100644 --- a/bonobo/strategies/executor.py +++ b/bonobo/strategies/executor.py @@ -1,10 +1,8 @@ -import time - +import functools +import logging import sys - -import mondrian -import traceback -from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor +from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor, wait, FIRST_EXCEPTION +from time import sleep from bonobo.util import get_name from bonobo.constants import BEGIN, END @@ -27,60 +25,38 @@ class ExecutorStrategy(Strategy): context = self.create_graph_execution_context(graph, **kwargs) context.write(BEGIN, Bag(), END) - executor = self.create_executor() - futures = [] - context.start_plugins(self.get_plugin_starter(executor, futures)) - context.start(self.get_starter(executor, futures)) + with self.create_executor() as executor: + context.start(self.get_starter(executor, futures)) - while context.alive: - time.sleep(0.1) + while context.alive: + try: + context.tick() + except KeyboardInterrupt: + logging.getLogger(__name__).warning('KeyboardInterrupt received. Trying to terminate the nodes gracefully.') + context.kill() + break - for plugin_context in context.plugins: - plugin_context.shutdown() - - context.stop() - - executor.shutdown() + context.stop() return context def get_starter(self, executor, futures): def starter(node): + @functools.wraps(node) def _runner(): try: - node.start() - except Exception: - mondrian.excepthook(*sys.exc_info(), context='Could not start node {}.'.format(get_name(node))) - node.input.on_end() - else: - node.loop() - - try: - node.stop() - except Exception: - mondrian.excepthook(*sys.exc_info(), context='Could not stop node {}.'.format(get_name(node))) + with node: + node.loop() + except BaseException as exc: + logging.getLogger(__name__).info('Got {} in {} runner.'.format(get_name(exc), node), + exc_info=sys.exc_info()) futures.append(executor.submit(_runner)) return starter - def get_plugin_starter(self, executor, futures): - def plugin_starter(plugin): - def _runner(): - with plugin: - try: - plugin.loop() - except Exception: - mondrian.excepthook( - *sys.exc_info(), context='In plugin loop for {}...'.format(get_name(plugin)) - ) - - futures.append(executor.submit(_runner)) - - return plugin_starter - class ThreadPoolExecutorStrategy(ExecutorStrategy): executor_factory = ThreadPoolExecutor diff --git a/bonobo/strategies/util.py b/bonobo/strategies/util.py deleted file mode 100644 index 8b13789..0000000 --- a/bonobo/strategies/util.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/requirements.txt b/requirements.txt index 82f5cdb..f579bdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ fs==2.0.12 idna==2.6 jinja2==2.9.6 markupsafe==1.0 -mondrian==0.4a0 +mondrian==0.3.0 packaging==16.8 pbr==3.1.1 psutil==5.4.0 @@ -17,3 +17,4 @@ requests==2.18.4 six==1.11.0 stevedore==1.27.1 urllib3==1.22 +whistle==1.0a3 diff --git a/setup.py b/setup.py index 219e0e6..b914b72 100644 --- a/setup.py +++ b/setup.py @@ -53,8 +53,8 @@ setup( packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, install_requires=[ - 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian (== 0.4a0)', - 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6.0)', 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.27, < 1.28)' + 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian', 'packaging (>= 16, < 17)', + 'psutil (>= 5.4, < 6.0)', 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.27, < 1.28)', 'whistle (== 1.0a3)' ], extras_require={ 'dev': [ From 5f300225a858a25a37445b9305353675c9573534 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 12:10:28 +0100 Subject: [PATCH 081/145] Update requirements with first whistle stable. --- Makefile | 2 +- Projectfile | 2 +- requirements-jupyter.txt | 2 +- requirements-sqlalchemy.txt | 2 +- requirements.txt | 4 ++-- setup.py | 5 +++-- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 483467d..ccd73ba 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4a10 on 2017-11-03. +# Generated by Medikit 0.4a11 on 2017-11-04. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index 7aa05b5..d0e800e 100644 --- a/Projectfile +++ b/Projectfile @@ -47,7 +47,7 @@ python.add_requirements( 'psutil >=5.4,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.27,<1.28', - 'whistle ==1.0a3', + 'whistle >=1.0,<1.1', dev=[ 'pytest-sugar >=0.8,<0.9', 'pytest-timeout >=1,<2', diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 1978875..2c499ad 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -19,7 +19,7 @@ markupsafe==1.0 mistune==0.8 nbconvert==5.3.1 nbformat==4.4.0 -notebook==5.2.0 +notebook==5.2.1 pandocfilters==1.4.2 parso==0.1.0 pexpect==4.2.1 diff --git a/requirements-sqlalchemy.txt b/requirements-sqlalchemy.txt index 7083f9e..d33c754 100644 --- a/requirements-sqlalchemy.txt +++ b/requirements-sqlalchemy.txt @@ -13,6 +13,6 @@ pyparsing==2.2.0 pytz==2017.3 requests==2.18.4 six==1.11.0 -sqlalchemy==1.1.14 +sqlalchemy==1.1.15 stevedore==1.27.1 urllib3==1.22 diff --git a/requirements.txt b/requirements.txt index f579bdc..dd9696d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ fs==2.0.12 idna==2.6 jinja2==2.9.6 markupsafe==1.0 -mondrian==0.3.0 +mondrian==0.4a1 packaging==16.8 pbr==3.1.1 psutil==5.4.0 @@ -17,4 +17,4 @@ requests==2.18.4 six==1.11.0 stevedore==1.27.1 urllib3==1.22 -whistle==1.0a3 +whistle==1.0.0 diff --git a/setup.py b/setup.py index b914b72..d622505 100644 --- a/setup.py +++ b/setup.py @@ -53,8 +53,9 @@ setup( packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, install_requires=[ - 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian', 'packaging (>= 16, < 17)', - 'psutil (>= 5.4, < 6.0)', 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.27, < 1.28)', 'whistle (== 1.0a3)' + 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian (== 0.4a1)', + 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6.0)', 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.27, < 1.28)', + 'whistle (>= 1.0, < 1.1)' ], extras_require={ 'dev': [ From 1108b319dbf31f801b45f61b2c01b27f6c9bbb09 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 12:15:18 +0100 Subject: [PATCH 082/145] Adds 3.7-dev target to travis runner. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 3eb10b2..27eebb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - 3.5-dev - 3.6 - 3.6-dev + - 3.7-dev - nightly install: - make install-dev From 017bb4a9a6e0fda84da5980db1b548479b6aa2cf Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 12:24:30 +0100 Subject: [PATCH 083/145] Adds a test for default file init command. --- bonobo/examples/clock.py | 5 +++-- bonobo/execution/node.py | 6 +++--- bonobo/plugins/console.py | 2 +- bonobo/strategies/executor.py | 9 ++++++--- tests/test_commands.py | 11 +++++++++++ 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/bonobo/examples/clock.py b/bonobo/examples/clock.py index 765f077..1977cba 100644 --- a/bonobo/examples/clock.py +++ b/bonobo/examples/clock.py @@ -14,12 +14,13 @@ def extract(): def get_graph(): graph = bonobo.Graph() graph.add_chain( - extract, - print, + extract, + print, ) return graph + if __name__ == '__main__': parser = bonobo.get_argument_parser() with bonobo.parse_args(parser): diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index fdb0c9f..7771812 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -156,7 +156,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): def isflag(param): - return isinstance(param, Token) and param in (NOT_MODIFIED,) + return isinstance(param, Token) and param in (NOT_MODIFIED, ) def split_tokens(output): @@ -168,11 +168,11 @@ def split_tokens(output): """ if isinstance(output, Token): # just a flag - return (output,), () + return (output, ), () if not istuple(output): # no flag - return (), (output,) + return (), (output, ) i = 0 while isflag(output[i]): diff --git a/bonobo/plugins/console.py b/bonobo/plugins/console.py index 814894b..0548d68 100644 --- a/bonobo/plugins/console.py +++ b/bonobo/plugins/console.py @@ -185,4 +185,4 @@ class IOBuffer(): def memory_usage(): import os, psutil process = psutil.Process(os.getpid()) - return process.memory_info()[0] / float(2 ** 20) + return process.memory_info()[0] / float(2**20) diff --git a/bonobo/strategies/executor.py b/bonobo/strategies/executor.py index e5ffdc0..49c5d4a 100644 --- a/bonobo/strategies/executor.py +++ b/bonobo/strategies/executor.py @@ -34,7 +34,9 @@ class ExecutorStrategy(Strategy): try: context.tick() except KeyboardInterrupt: - logging.getLogger(__name__).warning('KeyboardInterrupt received. Trying to terminate the nodes gracefully.') + logging.getLogger(__name__).warning( + 'KeyboardInterrupt received. Trying to terminate the nodes gracefully.' + ) context.kill() break @@ -50,8 +52,9 @@ class ExecutorStrategy(Strategy): with node: node.loop() except BaseException as exc: - logging.getLogger(__name__).info('Got {} in {} runner.'.format(get_name(exc), node), - exc_info=sys.exc_info()) + logging.getLogger(__name__).info( + 'Got {} in {} runner.'.format(get_name(exc), node), exc_info=sys.exc_info() + ) futures.append(executor.submit(_runner)) diff --git a/tests/test_commands.py b/tests/test_commands.py index e7e3523..2877648 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -112,6 +112,17 @@ def test_install_requirements_for_file(runner): install_mock.assert_called_once_with(os.path.join(dirname, 'requirements.txt')) +@all_runners +def test_init_file(runner, tmpdir): + target = tmpdir.join('foo.py') + runner('init', str(target)) + assert os.path.exists(target) + + out, err = runner('run', str(target)) + assert out.replace('\n', ' ').strip() == 'Hello World' + assert not err + + @all_runners def test_version(runner): out, err = runner('version') From 0b8168f7daa7619779c29e1ff78d26b2188b97c8 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 12:29:46 +0100 Subject: [PATCH 084/145] [tests] Fix path usage for python 3.5 --- tests/test_commands.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 2877648..64f3363 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -115,10 +115,11 @@ def test_install_requirements_for_file(runner): @all_runners def test_init_file(runner, tmpdir): target = tmpdir.join('foo.py') - runner('init', str(target)) - assert os.path.exists(target) + target_filename = str(target) + runner('init', target_filename) + assert os.path.exists(target_filename) - out, err = runner('run', str(target)) + out, err = runner('run', target_filename) assert out.replace('\n', ' ').strip() == 'Hello World' assert not err @@ -201,7 +202,7 @@ def env2(tmpdir): all_environ_targets = pytest.mark.parametrize( 'target', [ - (get_examples_path('environ.py'), ), + (get_examples_path('environ.py'),), ( '-m', 'bonobo.examples.environ', From 2c9729c4cadeafe1472b9492419b61e417b60ca2 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 13:01:43 +0100 Subject: [PATCH 085/145] Remove the sleep() in tick() that causes a minimum execution time of 2*PERIOD, more explicit status display and a small test case for console plugin. --- bonobo/execution/graph.py | 9 +++--- bonobo/execution/node.py | 2 ++ bonobo/plugins/console.py | 57 +++++++++++++---------------------- tests/plugins/test_console.py | 36 ++++++++++++++++++++++ 4 files changed, 64 insertions(+), 40 deletions(-) create mode 100644 tests/plugins/test_console.py diff --git a/bonobo/execution/graph.py b/bonobo/execution/graph.py index deaa150..0c5bc36 100644 --- a/bonobo/execution/graph.py +++ b/bonobo/execution/graph.py @@ -77,7 +77,7 @@ class GraphExecutionContext: def start(self, starter=None): self.register_plugins() self.dispatch(events.START) - self.tick() + self.tick(pause=False) for node in self.nodes: if starter is None: node.start() @@ -85,9 +85,10 @@ class GraphExecutionContext: starter(node) self.dispatch(events.STARTED) - def tick(self): + def tick(self, pause=True): self.dispatch(events.TICK) - sleep(self.TICK_PERIOD) + if pause: + sleep(self.TICK_PERIOD) def kill(self): self.dispatch(events.KILL) @@ -102,7 +103,7 @@ class GraphExecutionContext: node_context.stop() else: stopper(node_context) - self.tick() + self.tick(pause=False) self.dispatch(events.STOPPED) self.unregister_plugins() diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index 7771812..daa035f 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -42,6 +42,8 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): def get_flags_as_string(self): if self.killed: return '[killed]' + if self.stopped: + return '[done]' return '' def write(self, *messages): diff --git a/bonobo/plugins/console.py b/bonobo/plugins/console.py index 0548d68..dc511b7 100644 --- a/bonobo/plugins/console.py +++ b/bonobo/plugins/console.py @@ -30,7 +30,7 @@ class ConsoleOutputPlugin(Plugin): _stdout = sys.stdout _stderr = sys.stderr - # When the plugin is started, we'll set the real value of this. + # When the plugin is instanciated, we'll set the real value of this. isatty = False # Whether we're on windows, or a real operating system. @@ -50,6 +50,10 @@ class ConsoleOutputPlugin(Plugin): dispatcher.remove_listener(events.START, self.setup) def setup(self, event): + # TODO this wont work if one instance is registered with more than one context. + # Two options: + # - move state to context + # - forbid registering more than once self.prefix = '' self.counter = 0 self._append_cache = '' @@ -88,41 +92,22 @@ class ConsoleOutputPlugin(Plugin): for i in context.graph.topologically_sorted_indexes: node = context[i] name_suffix = '({})'.format(i) if settings.DEBUG.get() else '' - if node.alive: - _line = ''.join( - ( - ' ', - alive_color, - '+', - Style.RESET_ALL, - ' ', - node.name, - name_suffix, - ' ', - node.get_statistics_as_string(), - ' ', - node.get_flags_as_string(), - Style.RESET_ALL, - ' ', - ) - ) - else: - _line = ''.join( - ( - ' ', - dead_color, - '-', - ' ', - node.name, - name_suffix, - ' ', - node.get_statistics_as_string(), - ' ', - node.get_flags_as_string(), - Style.RESET_ALL, - ' ', - ) + + 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, + ' ', ) + ) print(prefix + _line + CLEAR_EOL, file=self._stderr) if append: @@ -185,4 +170,4 @@ class IOBuffer(): def memory_usage(): import os, psutil process = psutil.Process(os.getpid()) - return process.memory_info()[0] / float(2**20) + return process.memory_info()[0] / float(2 ** 20) diff --git a/tests/plugins/test_console.py b/tests/plugins/test_console.py new file mode 100644 index 0000000..4a34f7a --- /dev/null +++ b/tests/plugins/test_console.py @@ -0,0 +1,36 @@ +from unittest.mock import MagicMock + +import bonobo +from bonobo.execution import events +from bonobo.execution.graph import GraphExecutionContext +from bonobo.plugins.console import ConsoleOutputPlugin +from whistle import EventDispatcher + + +def test_register_unregister(): + plugin = ConsoleOutputPlugin() + dispatcher = EventDispatcher() + + plugin.register(dispatcher) + assert plugin.setup in dispatcher.get_listeners(events.START) + assert plugin.tick in dispatcher.get_listeners(events.TICK) + assert plugin.teardown in dispatcher.get_listeners(events.STOPPED) + plugin.unregister(dispatcher) + assert plugin.setup not in dispatcher.get_listeners(events.START) + assert plugin.tick not in dispatcher.get_listeners(events.TICK) + assert plugin.teardown not in dispatcher.get_listeners(events.STOPPED) + + +def test_one_pass(): + plugin = ConsoleOutputPlugin() + dispatcher = EventDispatcher() + plugin.register(dispatcher) + + graph = bonobo.Graph() + context = MagicMock(spec=GraphExecutionContext(graph)) + + dispatcher.dispatch(events.START, events.ExecutionEvent(context)) + dispatcher.dispatch(events.TICK, events.ExecutionEvent(context)) + dispatcher.dispatch(events.STOPPED, events.ExecutionEvent(context)) + + plugin.unregister(dispatcher) From 28884231b32d5e7659fff0287c4ee765b7f1b712 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 13:13:47 +0100 Subject: [PATCH 086/145] [core] Moves bonobo.execution context related package to new bonobo.execution.contexts package, also moves bonobo.strategies to new bonobo.execution.strategies package, so everything related to execution is now contained under the bonobo.execution package. --- bonobo/_api.py | 10 +++++----- bonobo/execution/contexts/__init__.py | 0 bonobo/execution/{ => contexts}/base.py | 1 - bonobo/execution/{ => contexts}/graph.py | 7 +++---- bonobo/execution/{ => contexts}/node.py | 3 +-- bonobo/execution/{ => contexts}/plugin.py | 2 +- bonobo/{ => execution}/strategies/__init__.py | 8 ++++---- bonobo/{ => execution}/strategies/base.py | 2 +- bonobo/{ => execution}/strategies/executor.py | 9 ++++----- bonobo/{ => execution}/strategies/naive.py | 2 +- bonobo/plugins/console.py | 2 +- bonobo/util/testing.py | 4 ++-- tests/execution/test_node.py | 2 +- tests/io/test_csv.py | 2 +- tests/io/test_file.py | 2 +- tests/io/test_json.py | 2 +- tests/io/test_pickle.py | 2 +- tests/plugins/test_console.py | 2 +- tests/test_basicusage.py | 2 +- tests/test_commands.py | 2 +- tests/test_execution.py | 4 ++-- 21 files changed, 33 insertions(+), 37 deletions(-) create mode 100644 bonobo/execution/contexts/__init__.py rename bonobo/execution/{ => contexts}/base.py (99%) rename bonobo/execution/{ => contexts}/graph.py (96%) rename bonobo/execution/{ => contexts}/node.py (98%) rename bonobo/execution/{ => contexts}/plugin.py (80%) rename bonobo/{ => execution}/strategies/__init__.py (76%) rename bonobo/{ => execution}/strategies/base.py (89%) rename bonobo/{ => execution}/strategies/executor.py (94%) rename bonobo/{ => execution}/strategies/naive.py (91%) diff --git a/bonobo/_api.py b/bonobo/_api.py index 9a82f41..816dfda 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,7 +1,7 @@ +from bonobo.execution.strategies import create_strategy from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ PickleReader, PickleWriter, PrettyPrinter, RateLimited, Tee, arg0_to_kwargs, count, identity, kwargs_to_arg0, noop from bonobo.nodes import LdjsonReader, LdjsonWriter -from bonobo.strategies import create_strategy from bonobo.structs import Bag, ErrorBag, Graph, Token from bonobo.util import get_name from bonobo.util.environ import parse_args, get_argument_parser @@ -35,7 +35,7 @@ def run(graph, *, plugins=None, services=None, strategy=None): You'll probably want to provide a services dictionary mapping service names to service instances. :param Graph graph: The :class:`Graph` to execute. - :param str strategy: The :class:`bonobo.strategies.base.Strategy` to use. + :param str strategy: The :class:`bonobo.execution.strategies.base.Strategy` to use. :param list plugins: The list of plugins to enhance execution. :param dict services: The implementations of services this graph will use. :return bonobo.execution.graph.GraphExecutionContext: @@ -93,10 +93,10 @@ def inspect(graph, *, format): print(_inspect_formats[format](graph)) -# bonobo.structs +# data structures register_api_group(Bag, ErrorBag, Graph, Token) -# bonobo.strategies +# execution strategies register_api(create_strategy) @@ -125,7 +125,7 @@ def open_fs(fs_url=None, *args, **kwargs): return _open_fs(expanduser(str(fs_url)), *args, **kwargs) -# bonobo.nodes +# standard transformations register_api_group( CsvReader, CsvWriter, diff --git a/bonobo/execution/contexts/__init__.py b/bonobo/execution/contexts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bonobo/execution/base.py b/bonobo/execution/contexts/base.py similarity index 99% rename from bonobo/execution/base.py rename to bonobo/execution/contexts/base.py index 74dd89b..39598bc 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/contexts/base.py @@ -1,7 +1,6 @@ import sys from contextlib import contextmanager from logging import WARNING, ERROR -from time import sleep import mondrian from bonobo.config import create_container diff --git a/bonobo/execution/graph.py b/bonobo/execution/contexts/graph.py similarity index 96% rename from bonobo/execution/graph.py rename to bonobo/execution/contexts/graph.py index 0c5bc36..55dbf7e 100644 --- a/bonobo/execution/graph.py +++ b/bonobo/execution/contexts/graph.py @@ -1,13 +1,12 @@ from functools import partial from time import sleep -from whistle import EventDispatcher - from bonobo.config import create_container from bonobo.constants import BEGIN, END from bonobo.execution import events -from bonobo.execution.node import NodeExecutionContext -from bonobo.execution.plugin import PluginExecutionContext +from bonobo.execution.contexts.node import NodeExecutionContext +from bonobo.execution.contexts.plugin import PluginExecutionContext +from whistle import EventDispatcher class GraphExecutionContext: diff --git a/bonobo/execution/node.py b/bonobo/execution/contexts/node.py similarity index 98% rename from bonobo/execution/node.py rename to bonobo/execution/contexts/node.py index daa035f..8511825 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/contexts/node.py @@ -1,12 +1,11 @@ import sys -import threading from queue import Empty from time import sleep from types import GeneratorType from bonobo.constants import NOT_MODIFIED, BEGIN, END from bonobo.errors import InactiveReadableError, UnrecoverableError -from bonobo.execution.base import LoopingExecutionContext +from bonobo.execution.contexts.base import LoopingExecutionContext from bonobo.structs.bags import Bag from bonobo.structs.inputs import Input from bonobo.structs.tokens import Token diff --git a/bonobo/execution/plugin.py b/bonobo/execution/contexts/plugin.py similarity index 80% rename from bonobo/execution/plugin.py rename to bonobo/execution/contexts/plugin.py index f552724..524c2e1 100644 --- a/bonobo/execution/plugin.py +++ b/bonobo/execution/contexts/plugin.py @@ -1,4 +1,4 @@ -from bonobo.execution.base import LoopingExecutionContext, recoverable +from bonobo.execution.contexts.base import LoopingExecutionContext class PluginExecutionContext(LoopingExecutionContext): diff --git a/bonobo/strategies/__init__.py b/bonobo/execution/strategies/__init__.py similarity index 76% rename from bonobo/strategies/__init__.py rename to bonobo/execution/strategies/__init__.py index 1420da6..1c5d50a 100644 --- a/bonobo/strategies/__init__.py +++ b/bonobo/execution/strategies/__init__.py @@ -1,5 +1,5 @@ -from bonobo.strategies.executor import ProcessPoolExecutorStrategy, ThreadPoolExecutorStrategy -from bonobo.strategies.naive import NaiveStrategy +from bonobo.execution.strategies.executor import ProcessPoolExecutorStrategy, ThreadPoolExecutorStrategy +from bonobo.execution.strategies.naive import NaiveStrategy __all__ = [ 'create_strategy', @@ -21,8 +21,8 @@ def create_strategy(name=None): :param name: :return: Strategy """ - from bonobo.strategies.base import Strategy import logging + from bonobo.execution.strategies.base import Strategy if isinstance(name, Strategy): return name @@ -39,4 +39,4 @@ def create_strategy(name=None): 'Invalid strategy {}. Available choices: {}.'.format(repr(name), ', '.join(sorted(STRATEGIES.keys()))) ) from exc - return factory() \ No newline at end of file + return factory() diff --git a/bonobo/strategies/base.py b/bonobo/execution/strategies/base.py similarity index 89% rename from bonobo/strategies/base.py rename to bonobo/execution/strategies/base.py index 47f7db4..0a8d2a5 100644 --- a/bonobo/strategies/base.py +++ b/bonobo/execution/strategies/base.py @@ -1,4 +1,4 @@ -from bonobo.execution.graph import GraphExecutionContext +from bonobo.execution.contexts.graph import GraphExecutionContext class Strategy: diff --git a/bonobo/strategies/executor.py b/bonobo/execution/strategies/executor.py similarity index 94% rename from bonobo/strategies/executor.py rename to bonobo/execution/strategies/executor.py index 49c5d4a..ebaba32 100644 --- a/bonobo/strategies/executor.py +++ b/bonobo/execution/strategies/executor.py @@ -1,13 +1,12 @@ import functools import logging import sys -from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor, wait, FIRST_EXCEPTION -from time import sleep +from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor -from bonobo.util import get_name -from bonobo.constants import BEGIN, END -from bonobo.strategies.base import Strategy 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 class ExecutorStrategy(Strategy): diff --git a/bonobo/strategies/naive.py b/bonobo/execution/strategies/naive.py similarity index 91% rename from bonobo/strategies/naive.py rename to bonobo/execution/strategies/naive.py index 20477c1..bd581ff 100644 --- a/bonobo/strategies/naive.py +++ b/bonobo/execution/strategies/naive.py @@ -1,5 +1,5 @@ from bonobo.constants import BEGIN, END -from bonobo.strategies.base import Strategy +from bonobo.execution.strategies.base import Strategy from bonobo.structs.bags import Bag diff --git a/bonobo/plugins/console.py b/bonobo/plugins/console.py index dc511b7..d5f9914 100644 --- a/bonobo/plugins/console.py +++ b/bonobo/plugins/console.py @@ -170,4 +170,4 @@ class IOBuffer(): def memory_usage(): import os, psutil process = psutil.Process(os.getpid()) - return process.memory_info()[0] / float(2 ** 20) + return process.memory_info()[0] / float(2**20) diff --git a/bonobo/util/testing.py b/bonobo/util/testing.py index 7dc8f38..6dd2c8a 100644 --- a/bonobo/util/testing.py +++ b/bonobo/util/testing.py @@ -1,8 +1,8 @@ from contextlib import contextmanager from bonobo import open_fs, Token -from bonobo.execution.graph import GraphExecutionContext -from bonobo.execution.node import NodeExecutionContext +from bonobo.execution.contexts.graph import GraphExecutionContext +from bonobo.execution.contexts.node import NodeExecutionContext @contextmanager diff --git a/tests/execution/test_node.py b/tests/execution/test_node.py index fef385c..34a8ae1 100644 --- a/tests/execution/test_node.py +++ b/tests/execution/test_node.py @@ -1,5 +1,5 @@ from bonobo import Bag, Graph -from bonobo.strategies import NaiveStrategy +from bonobo.execution.strategies import NaiveStrategy from bonobo.util.testing import BufferingNodeExecutionContext, BufferingGraphExecutionContext diff --git a/tests/io/test_csv.py b/tests/io/test_csv.py index 1c4c6cc..b0b91c5 100644 --- a/tests/io/test_csv.py +++ b/tests/io/test_csv.py @@ -1,7 +1,7 @@ import pytest from bonobo import CsvReader, CsvWriter, settings -from bonobo.execution.node import NodeExecutionContext +from bonobo.execution.contexts.node import NodeExecutionContext from bonobo.util.testing import FilesystemTester, BufferingNodeExecutionContext csv_tester = FilesystemTester('csv') diff --git a/tests/io/test_file.py b/tests/io/test_file.py index d7645e7..5fc2823 100644 --- a/tests/io/test_file.py +++ b/tests/io/test_file.py @@ -2,7 +2,7 @@ import pytest from bonobo import Bag, FileReader, FileWriter from bonobo.constants import BEGIN, END -from bonobo.execution.node import NodeExecutionContext +from bonobo.execution.contexts.node import NodeExecutionContext from bonobo.util.testing import BufferingNodeExecutionContext, FilesystemTester txt_tester = FilesystemTester('txt') diff --git a/tests/io/test_json.py b/tests/io/test_json.py index b72a3de..b5b0781 100644 --- a/tests/io/test_json.py +++ b/tests/io/test_json.py @@ -2,7 +2,7 @@ import pytest from bonobo import JsonReader, JsonWriter, settings from bonobo import LdjsonReader, LdjsonWriter -from bonobo.execution.node import NodeExecutionContext +from bonobo.execution.contexts.node import NodeExecutionContext from bonobo.util.testing import FilesystemTester, BufferingNodeExecutionContext json_tester = FilesystemTester('json') diff --git a/tests/io/test_pickle.py b/tests/io/test_pickle.py index 1f95309..8416a9f 100644 --- a/tests/io/test_pickle.py +++ b/tests/io/test_pickle.py @@ -3,7 +3,7 @@ import pickle import pytest from bonobo import Bag, PickleReader, PickleWriter -from bonobo.execution.node import NodeExecutionContext +from bonobo.execution.contexts.node import NodeExecutionContext from bonobo.util.testing import BufferingNodeExecutionContext, FilesystemTester pickle_tester = FilesystemTester('pkl', mode='wb') diff --git a/tests/plugins/test_console.py b/tests/plugins/test_console.py index 4a34f7a..543d341 100644 --- a/tests/plugins/test_console.py +++ b/tests/plugins/test_console.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock import bonobo from bonobo.execution import events -from bonobo.execution.graph import GraphExecutionContext +from bonobo.execution.contexts.graph import GraphExecutionContext from bonobo.plugins.console import ConsoleOutputPlugin from whistle import EventDispatcher diff --git a/tests/test_basicusage.py b/tests/test_basicusage.py index f002d36..7772af3 100644 --- a/tests/test_basicusage.py +++ b/tests/test_basicusage.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest import bonobo -from bonobo.execution.graph import GraphExecutionContext +from bonobo.execution.contexts.graph import GraphExecutionContext @pytest.mark.timeout(2) diff --git a/tests/test_commands.py b/tests/test_commands.py index 64f3363..255bae7 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -202,7 +202,7 @@ def env2(tmpdir): all_environ_targets = pytest.mark.parametrize( 'target', [ - (get_examples_path('environ.py'),), + (get_examples_path('environ.py'), ), ( '-m', 'bonobo.examples.environ', diff --git a/tests/test_execution.py b/tests/test_execution.py index 6fb33e4..84f40c5 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -1,7 +1,7 @@ from bonobo.config.processors import ContextProcessor from bonobo.constants import BEGIN, END -from bonobo.execution.graph import GraphExecutionContext -from bonobo.strategies import NaiveStrategy +from bonobo.execution.contexts.graph import GraphExecutionContext +from bonobo.execution.strategies import NaiveStrategy from bonobo.structs import Bag, Graph From 83fc1743fcdeb23eba5a7e571533a9b7da0dd114 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 13:20:53 +0100 Subject: [PATCH 087/145] Small changes in events, and associated tests. --- bonobo/execution/events.py | 4 ++-- bonobo/plugins/console.py | 8 ++++---- tests/execution/{ => contexts}/test_node.py | 0 tests/execution/test_events.py | 17 +++++++++++++++++ 4 files changed, 23 insertions(+), 6 deletions(-) rename tests/execution/{ => contexts}/test_node.py (100%) create mode 100644 tests/execution/test_events.py diff --git a/bonobo/execution/events.py b/bonobo/execution/events.py index 036e879..3bf3986 100644 --- a/bonobo/execution/events.py +++ b/bonobo/execution/events.py @@ -9,5 +9,5 @@ KILL = 'execution.kill' class ExecutionEvent(Event): - def __init__(self, graph_context): - self.graph_context = graph_context + def __init__(self, context): + self.context = context diff --git a/bonobo/plugins/console.py b/bonobo/plugins/console.py index d5f9914..584244c 100644 --- a/bonobo/plugins/console.py +++ b/bonobo/plugins/console.py @@ -68,12 +68,12 @@ class ConsoleOutputPlugin(Plugin): def tick(self, event): if self.isatty and not self.iswindows: - self._write(event.graph_context, rewind=True) + self._write(event.context, rewind=True) else: pass # not a tty, or windows, so we'll ignore stats output def teardown(self, event): - self._write(event.graph_context, rewind=False) + self._write(event.context, rewind=False) self.redirect_stderr.__exit__(None, None, None) self.redirect_stdout.__exit__(None, None, None) @@ -127,7 +127,7 @@ class ConsoleOutputPlugin(Plugin): print(CLEAR_EOL, file=self._stderr) print(MOVE_CURSOR_UP(t_cnt + 2), file=self._stderr) - def _write(self, graph_context, rewind): + def _write(self, context, rewind): if settings.PROFILE.get(): if self.counter % 10 and self._append_cache: append = self._append_cache @@ -138,7 +138,7 @@ class ConsoleOutputPlugin(Plugin): ) else: append = () - self.write(graph_context, prefix=self.prefix, append=append, rewind=rewind) + self.write(context, prefix=self.prefix, append=append, rewind=rewind) self.counter += 1 diff --git a/tests/execution/test_node.py b/tests/execution/contexts/test_node.py similarity index 100% rename from tests/execution/test_node.py rename to tests/execution/contexts/test_node.py diff --git a/tests/execution/test_events.py b/tests/execution/test_events.py new file mode 100644 index 0000000..8abeb57 --- /dev/null +++ b/tests/execution/test_events.py @@ -0,0 +1,17 @@ +from unittest.mock import Mock + +from bonobo.execution import events + + +def test_names(): + # This test looks useless, but as it's becoming the pliugin API, I want to make sure that nothing changes here, or + # notice it otherwise. + for name in 'start', 'started', 'tick', 'stop', 'stopped', 'kill': + event_name = getattr(events, name.upper()) + assert event_name == '.'.join(('execution', name)) + +def test_event_object(): + # Same logic as above. + c = Mock() + e = events.ExecutionEvent(c) + assert e.context is c From 25e919ab969772adc47a5c92ba3e08da110eaf87 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 13:36:54 +0100 Subject: [PATCH 088/145] [tests] adds node context lifecycle test.( --- tests/execution/contexts/test_node.py | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/execution/contexts/test_node.py b/tests/execution/contexts/test_node.py index 34a8ae1..648743b 100644 --- a/tests/execution/contexts/test_node.py +++ b/tests/execution/contexts/test_node.py @@ -1,4 +1,9 @@ +from unittest.mock import MagicMock + +import pytest + from bonobo import Bag, Graph +from bonobo.execution.contexts.node import NodeExecutionContext from bonobo.execution.strategies import NaiveStrategy from bonobo.util.testing import BufferingNodeExecutionContext, BufferingGraphExecutionContext @@ -179,3 +184,46 @@ def test_node_tuple_dict(): assert len(output) == 2 assert output[0] == ('foo', 'bar', {'id': 1}) assert output[1] == ('foo', 'baz', {'id': 2}) + +def test_node_lifecycle_natural(): + func = MagicMock() + + ctx = NodeExecutionContext(func) + assert not any((ctx.started, ctx.stopped, ctx.killed, ctx.alive)) + + # cannot stop before start + with pytest.raises(RuntimeError): + ctx.stop() + assert not any((ctx.started, ctx.stopped, ctx.killed, ctx.alive)) + + # turn the key + ctx.start() + assert all((ctx.started, ctx.alive)) and not any((ctx.stopped, ctx.killed)) + + ctx.stop() + assert all((ctx.started, ctx.stopped)) and not any((ctx.alive, ctx.killed)) + +def test_node_lifecycle_with_kill(): + func = MagicMock() + + ctx = NodeExecutionContext(func) + assert not any((ctx.started, ctx.stopped, ctx.killed, ctx.alive)) + + # cannot kill before start + with pytest.raises(RuntimeError): + ctx.kill() + assert not any((ctx.started, ctx.stopped, ctx.killed, ctx.alive)) + + # turn the key + ctx.start() + assert all((ctx.started, ctx.alive)) and not any((ctx.stopped, ctx.killed)) + + ctx.kill() + assert all((ctx.started, ctx.killed, ctx.alive)) and not ctx.stopped + + ctx.stop() + assert all((ctx.started, ctx.killed, ctx.stopped)) and not ctx.alive + + + + From 0b969d31e0b008fdbf8d86ecc8294f72eb26a3af Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 14:55:08 +0100 Subject: [PATCH 089/145] Adds basic test for convert command. --- bonobo/commands/convert.py | 4 +- bonobo/execution/contexts/base.py | 42 ++-- bonobo/execution/contexts/node.py | 16 +- bonobo/execution/strategies/executor.py | 19 +- bonobo/nodes/io/base.py | 3 + bonobo/util/environ.py | 10 + bonobo/util/resolvers.py | 1 + bonobo/util/testing.py | 78 +++++- tests/commands/test_clibasics.py | 25 ++ tests/commands/test_convert.py | 13 + tests/commands/test_download.py | 44 ++++ tests/commands/test_init.py | 15 ++ tests/commands/test_run.py | 48 ++++ tests/commands/test_run_environ.py | 109 ++++++++ tests/commands/test_version.py | 20 ++ tests/execution/contexts/test_node.py | 6 +- tests/execution/test_events.py | 1 + tests/test_commands.py | 319 ------------------------ 18 files changed, 420 insertions(+), 353 deletions(-) create mode 100644 tests/commands/test_clibasics.py create mode 100644 tests/commands/test_convert.py create mode 100644 tests/commands/test_download.py create mode 100644 tests/commands/test_init.py create mode 100644 tests/commands/test_run.py create mode 100644 tests/commands/test_run_environ.py create mode 100644 tests/commands/test_version.py delete mode 100644 tests/test_commands.py diff --git a/bonobo/commands/convert.py b/bonobo/commands/convert.py index faf175c..198dce0 100644 --- a/bonobo/commands/convert.py +++ b/bonobo/commands/convert.py @@ -6,8 +6,8 @@ from bonobo.util.resolvers import _resolve_transformations, _resolve_options class ConvertCommand(BaseCommand): def add_arguments(self, parser): - parser.add_argument('input-filename', help='Input filename.') - parser.add_argument('output-filename', help='Output filename.') + parser.add_argument('input_filename', help='Input filename.') + parser.add_argument('output_filename', help='Output filename.') parser.add_argument( '--' + READER, '-r', diff --git a/bonobo/execution/contexts/base.py b/bonobo/execution/contexts/base.py index 39598bc..3ca580a 100644 --- a/bonobo/execution/contexts/base.py +++ b/bonobo/execution/contexts/base.py @@ -1,3 +1,4 @@ +import logging import sys from contextlib import contextmanager from logging import WARNING, ERROR @@ -38,6 +39,10 @@ class LoopingExecutionContext(Wrapper): def stopped(self): return self._stopped + @property + def defunct(self): + return self._defunct + @property def alive(self): return self._started and not self._stopped @@ -45,6 +50,8 @@ class LoopingExecutionContext(Wrapper): @property def status(self): """One character status for this node. """ + if self._defunct: + return '!' if not self.started: return ' ' if not self.stopped: @@ -65,7 +72,7 @@ class LoopingExecutionContext(Wrapper): else: self.services = None - self._started, self._stopped = False, False + self._started, self._stopped, self._defunct = False, False, False self._stack = None def __enter__(self): @@ -81,15 +88,17 @@ class LoopingExecutionContext(Wrapper): self._started = True - self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context()) - 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...' - ) - - self._stack.setup(self) + try: + self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context()) + 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...' + ) + self._stack.setup(self) + except Exception: + return self.fatal(sys.exc_info()) def loop(self): """Generic loop. A bit boring. """ @@ -113,14 +122,17 @@ class LoopingExecutionContext(Wrapper): finally: self._stopped = True - def handle_error(self, exctype, exc, tb): - mondrian.excepthook( - exctype, exc, tb, level=WARNING, context='{} in {}'.format(exctype.__name__, get_name(self)), logger=logger - ) - def _get_initial_context(self): if self.parent: return self.parent.services.args_for(self.wrapped) if self.services: return self.services.args_for(self.wrapped) return () + + def handle_error(self, exctype, exc, tb, *, level=logging.ERROR): + logging.getLogger(__name__).log(level, repr(self), exc_info=(exctype, exc, tb)) + + def fatal(self, exc_info): + self._defunct = True + self.input.shutdown() + self.handle_error(*exc_info, level=logging.CRITICAL) diff --git a/bonobo/execution/contexts/node.py b/bonobo/execution/contexts/node.py index 8511825..db2c39a 100644 --- a/bonobo/execution/contexts/node.py +++ b/bonobo/execution/contexts/node.py @@ -1,3 +1,4 @@ +import logging import sys from queue import Empty from time import sleep @@ -12,6 +13,7 @@ from bonobo.structs.tokens import Token from bonobo.util import get_name, iserrorbag, isloopbackbag, isbag, istuple from bonobo.util.compat import deprecated_alias from bonobo.util.statistics import WithStatistics +from mondrian import term class NodeExecutionContext(WithStatistics, LoopingExecutionContext): @@ -39,10 +41,12 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): return '<{}({}{}){}>'.format(type_name, self.status, name, self.get_statistics_as_string(prefix=' ')) def get_flags_as_string(self): + if self._defunct: + return term.red('[defunct]') if self.killed: - return '[killed]' + return term.lightred('[killed]') if self.stopped: - return '[done]' + return term.lightblack('[done]') return '' def write(self, *messages): @@ -92,13 +96,13 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): self.increment('in') return row + def should_loop(self): + return not any((self.defunct, self.killed)) + def loop(self): - while not self._killed: + while self.should_loop(): try: self.step() - except KeyboardInterrupt: - self.handle_error(*sys.exc_info()) - break except InactiveReadableError: break except Empty: diff --git a/bonobo/execution/strategies/executor.py b/bonobo/execution/strategies/executor.py index ebaba32..ebbaef1 100644 --- a/bonobo/execution/strategies/executor.py +++ b/bonobo/execution/strategies/executor.py @@ -27,7 +27,11 @@ class ExecutorStrategy(Strategy): futures = [] with self.create_executor() as executor: - context.start(self.get_starter(executor, futures)) + try: + context.start(self.get_starter(executor, futures)) + except: + logging.getLogger(__name__ + ).warning('KeyboardInterrupt received. Trying to terminate the nodes gracefully.') while context.alive: try: @@ -50,12 +54,17 @@ class ExecutorStrategy(Strategy): try: with node: node.loop() - except BaseException as exc: - logging.getLogger(__name__).info( - 'Got {} in {} runner.'.format(get_name(exc), node), exc_info=sys.exc_info() + except: + logging.getLogger(__name__).critical( + 'Uncaught exception in node execution for {}.'.format(node), exc_info=True ) + node.shutdown() + node.stop() - futures.append(executor.submit(_runner)) + try: + futures.append(executor.submit(_runner)) + except: + logging.getLogger(__name__).critical('futures.append', exc_info=sys.exc_info()) return starter diff --git a/bonobo/nodes/io/base.py b/bonobo/nodes/io/base.py index af9e609..db0bc80 100644 --- a/bonobo/nodes/io/base.py +++ b/bonobo/nodes/io/base.py @@ -1,4 +1,7 @@ +from fs.errors import ResourceNotFound + from bonobo.config import Configurable, ContextProcessor, Option, Service +from bonobo.errors import UnrecoverableError class FileHandler(Configurable): diff --git a/bonobo/util/environ.py b/bonobo/util/environ.py index 16f7c9c..b344d29 100644 --- a/bonobo/util/environ.py +++ b/bonobo/util/environ.py @@ -152,3 +152,13 @@ def parse_args(mixed=None): del os.environ[name] else: os.environ[name] = value + + +@contextmanager +def change_working_directory(path): + old_dir = os.getcwd() + os.chdir(str(path)) + try: + yield + finally: + os.chdir(old_dir) diff --git a/bonobo/util/resolvers.py b/bonobo/util/resolvers.py index c4a1a90..60934d8 100644 --- a/bonobo/util/resolvers.py +++ b/bonobo/util/resolvers.py @@ -72,6 +72,7 @@ def _resolve_transformations(transformations): :return: tuple(object) """ registry = _ModulesRegistry() + transformations = transformations or [] for t in transformations: try: mod, attr = t.split(':', 1) diff --git a/bonobo/util/testing.py b/bonobo/util/testing.py index 6dd2c8a..9044715 100644 --- a/bonobo/util/testing.py +++ b/bonobo/util/testing.py @@ -1,6 +1,15 @@ -from contextlib import contextmanager +import functools +import io +import os +import runpy +import sys +from contextlib import contextmanager, redirect_stdout, redirect_stderr +from unittest.mock import patch -from bonobo import open_fs, Token +import pytest + +from bonobo import open_fs, Token, __main__, get_examples_path +from bonobo.commands import entrypoint from bonobo.execution.contexts.graph import GraphExecutionContext from bonobo.execution.contexts.node import NodeExecutionContext @@ -64,3 +73,68 @@ class BufferingGraphExecutionContext(BufferingContext, GraphExecutionContext): def create_node_execution_context_for(self, node): return self.NodeExecutionContextType(node, parent=self, buffer=self.buffer) + + +def runner(f): + @functools.wraps(f) + def wrapped_runner(*args, catch_errors=False): + with redirect_stdout(io.StringIO()) as stdout, redirect_stderr(io.StringIO()) as stderr: + try: + f(list(args)) + except BaseException as exc: + if not catch_errors: + raise + elif isinstance(catch_errors, BaseException) and not isinstance(exc, catch_errors): + raise + return stdout.getvalue(), stderr.getvalue(), exc + return stdout.getvalue(), stderr.getvalue() + + return wrapped_runner + + +@runner +def runner_entrypoint(args): + """ Run bonobo using the python command entrypoint directly (bonobo.commands.entrypoint). """ + return entrypoint(args) + + +@runner +def runner_module(args): + """ Run bonobo using the bonobo.__main__ file, which is equivalent as doing "python -m bonobo ...".""" + with patch.object(sys, 'argv', ['bonobo', *args]): + return runpy.run_path(__main__.__file__, run_name='__main__') + + +all_runners = pytest.mark.parametrize('runner', [runner_entrypoint, runner_module]) +all_environ_targets = pytest.mark.parametrize( + 'target', [ + (get_examples_path('environ.py'), ), + ( + '-m', + 'bonobo.examples.environ', + ), + ] +) + + +@all_runners +@all_environ_targets +class EnvironmentTestCase(): + def run_quiet(self, runner, *args): + return runner('run', '--quiet', *args) + + def run_environ(self, runner, *args, environ=None): + _environ = {'PATH': '/usr/bin'} + if environ: + _environ.update(environ) + + with patch.dict('os.environ', _environ, clear=True): + out, err = self.run_quiet(runner, *args) + assert 'SECRET' not in os.environ + assert 'PASSWORD' not in os.environ + if 'PATH' in _environ: + assert 'PATH' in os.environ + assert os.environ['PATH'] == _environ['PATH'] + + assert err == '' + return dict(map(lambda line: line.split(' ', 1), filter(None, out.split('\n')))) diff --git a/tests/commands/test_clibasics.py b/tests/commands/test_clibasics.py new file mode 100644 index 0000000..1fc292b --- /dev/null +++ b/tests/commands/test_clibasics.py @@ -0,0 +1,25 @@ +import pkg_resources + +from bonobo.util.testing import all_runners + + +def test_entrypoint(): + commands = {} + + for command in pkg_resources.iter_entry_points('bonobo.commands'): + commands[command.name] = command + + assert not { + 'convert', + 'init', + 'inspect', + 'run', + 'version', + }.difference(set(commands)) + + +@all_runners +def test_no_command(runner): + _, err, exc = runner(catch_errors=True) + assert type(exc) == SystemExit + assert 'error: the following arguments are required: command' in err \ No newline at end of file diff --git a/tests/commands/test_convert.py b/tests/commands/test_convert.py new file mode 100644 index 0000000..ea0c3c4 --- /dev/null +++ b/tests/commands/test_convert.py @@ -0,0 +1,13 @@ +from bonobo.util.environ import change_working_directory +from bonobo.util.testing import all_runners + + +@all_runners +def test_convert(runner, tmpdir): + csv_content = 'id;name\n1;Romain' + tmpdir.join('in.csv').write(csv_content) + + with change_working_directory(tmpdir): + runner('convert', 'in.csv', 'out.csv') + + assert tmpdir.join('out.csv').read().strip() == csv_content diff --git a/tests/commands/test_download.py b/tests/commands/test_download.py new file mode 100644 index 0000000..83b0ef4 --- /dev/null +++ b/tests/commands/test_download.py @@ -0,0 +1,44 @@ +import io +from unittest.mock import patch + +import pytest + +from bonobo.commands.download import EXAMPLES_BASE_URL +from bonobo.util.testing import all_runners + + +@all_runners +def test_download_works_for_examples(runner): + expected_bytes = b'hello world' + + class MockResponse(object): + def __init__(self): + self.status_code = 200 + + def iter_content(self, *args, **kwargs): + return [expected_bytes] + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + pass + + fout = io.BytesIO() + fout.close = lambda: None + + with patch('bonobo.commands.download._open_url') as mock_open_url, \ + patch('bonobo.commands.download.open') as mock_open: + mock_open_url.return_value = MockResponse() + mock_open.return_value = fout + runner('download', 'examples/datasets/coffeeshops.txt') + expected_url = EXAMPLES_BASE_URL + 'datasets/coffeeshops.txt' + mock_open_url.assert_called_once_with(expected_url) + + assert fout.getvalue() == expected_bytes + + +@all_runners +def test_download_fails_non_example(runner): + with pytest.raises(ValueError): + runner('download', 'something/entirely/different.txt') \ No newline at end of file diff --git a/tests/commands/test_init.py b/tests/commands/test_init.py new file mode 100644 index 0000000..a551a9a --- /dev/null +++ b/tests/commands/test_init.py @@ -0,0 +1,15 @@ +import os + +from bonobo.util.testing import all_runners + + +@all_runners +def test_init_file(runner, tmpdir): + target = tmpdir.join('foo.py') + target_filename = str(target) + runner('init', target_filename) + assert os.path.exists(target_filename) + + out, err = runner('run', target_filename) + assert out.replace('\n', ' ').strip() == 'Hello World' + assert not err \ No newline at end of file diff --git a/tests/commands/test_run.py b/tests/commands/test_run.py new file mode 100644 index 0000000..69e4f94 --- /dev/null +++ b/tests/commands/test_run.py @@ -0,0 +1,48 @@ +import os +from unittest.mock import patch + +from bonobo import get_examples_path +from bonobo.util.testing import all_runners + + +@all_runners +def test_run(runner): + out, err = runner('run', '--quiet', get_examples_path('types/strings.py')) + out = out.split('\n') + assert out[0].startswith('Foo ') + assert out[1].startswith('Bar ') + assert out[2].startswith('Baz ') + + +@all_runners +def test_run_module(runner): + out, err = runner('run', '--quiet', '-m', 'bonobo.examples.types.strings') + out = out.split('\n') + assert out[0].startswith('Foo ') + assert out[1].startswith('Bar ') + assert out[2].startswith('Baz ') + + +@all_runners +def test_run_path(runner): + out, err = runner('run', '--quiet', get_examples_path('types')) + out = out.split('\n') + assert out[0].startswith('Foo ') + assert out[1].startswith('Bar ') + assert out[2].startswith('Baz ') + + +@all_runners +def test_install_requirements_for_dir(runner): + dirname = get_examples_path('types') + with patch('bonobo.commands.run._install_requirements') as install_mock: + runner('run', '--install', dirname) + install_mock.assert_called_once_with(os.path.join(dirname, 'requirements.txt')) + + +@all_runners +def test_install_requirements_for_file(runner): + dirname = get_examples_path('types') + with patch('bonobo.commands.run._install_requirements') as install_mock: + runner('run', '--install', os.path.join(dirname, 'strings.py')) + install_mock.assert_called_once_with(os.path.join(dirname, 'requirements.txt')) diff --git a/tests/commands/test_run_environ.py b/tests/commands/test_run_environ.py new file mode 100644 index 0000000..1d966be --- /dev/null +++ b/tests/commands/test_run_environ.py @@ -0,0 +1,109 @@ +import pytest + +from bonobo.util.testing import EnvironmentTestCase + + +@pytest.fixture +def env1(tmpdir): + env_file = tmpdir.join('.env_one') + env_file.write('\n'.join(( + 'SECRET=unknown', + 'PASSWORD=sweet', + 'PATH=first', + ))) + return str(env_file) + + +@pytest.fixture +def env2(tmpdir): + env_file = tmpdir.join('.env_two') + env_file.write('\n'.join(( + 'PASSWORD=bitter', + "PATH='second'", + ))) + return str(env_file) + + +class TestDefaultEnvFile(EnvironmentTestCase): + def test_run_with_default_env_file(self, runner, target, env1): + env = self.run_environ(runner, *target, '--default-env-file', env1) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'sweet' + assert env.get('PATH') == '/usr/bin' + + def test_run_with_multiple_default_env_files(self, runner, target, env1, env2): + env = self.run_environ(runner, *target, '--default-env-file', env1, '--default-env-file', env2) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'sweet' + assert env.get('PATH') == '/usr/bin' + + env = self.run_environ(runner, *target, '--default-env-file', env2, '--default-env-file', env1) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'bitter' + assert env.get('PATH') == '/usr/bin' + + +class TestEnvFile(EnvironmentTestCase): + def test_run_with_file(self, runner, target, env1): + env = self.run_environ(runner, *target, '--env-file', env1) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'sweet' + assert env.get('PATH') == 'first' + + def test_run_with_multiple_files(self, runner, target, env1, env2): + env = self.run_environ(runner, *target, '--env-file', env1, '--env-file', env2) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'bitter' + assert env.get('PATH') == 'second' + + env = self.run_environ(runner, *target, '--env-file', env2, '--env-file', env1) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'sweet' + assert env.get('PATH') == 'first' + + +class TestEnvFileCombinations(EnvironmentTestCase): + def test_run_with_both_env_files(self, runner, target, env1, env2): + env = self.run_environ(runner, *target, '--default-env-file', env1, '--env-file', env2) + assert env.get('SECRET') == 'unknown' + assert env.get('PASSWORD') == 'bitter' + assert env.get('PATH') == 'second' + + def test_run_with_both_env_files_then_overrides(self, runner, target, env1, env2): + env = self.run_environ( + runner, *target, '--default-env-file', env1, '--env-file', env2, '--env', 'PASSWORD=mine', '--env', + 'SECRET=s3cr3t' + ) + assert env.get('SECRET') == 's3cr3t' + assert env.get('PASSWORD') == 'mine' + assert env.get('PATH') == 'second' + + +class TestEnvVars(EnvironmentTestCase): + def test_run_no_env(self, runner, target): + env = self.run_environ(runner, *target, environ={'USER': 'romain'}) + assert env.get('USER') == 'romain' + + def test_run_env(self, runner, target): + env = self.run_environ(runner, *target, '--env', 'USER=serious', environ={'USER': 'romain'}) + assert env.get('USER') == 'serious' + + def test_run_env_mixed(self, runner, target): + env = self.run_environ(runner, *target, '--env', 'ONE=1', '--env', 'TWO="2"', environ={'USER': 'romain'}) + assert env.get('USER') == 'romain' + assert env.get('ONE') == '1' + assert env.get('TWO') == '2' + + def test_run_default_env(self, runner, target): + env = self.run_environ(runner, *target, '--default-env', 'USER=clown') + assert env.get('USER') == 'clown' + + env = self.run_environ(runner, *target, '--default-env', 'USER=clown', environ={'USER': 'romain'}) + assert env.get('USER') == 'romain' + + env = self.run_environ( + runner, *target, '--env', 'USER=serious', '--default-env', 'USER=clown', environ={ + 'USER': 'romain' + } + ) + assert env.get('USER') == 'serious' diff --git a/tests/commands/test_version.py b/tests/commands/test_version.py new file mode 100644 index 0000000..1ee893f --- /dev/null +++ b/tests/commands/test_version.py @@ -0,0 +1,20 @@ +from bonobo import __version__ +from bonobo.util.testing import all_runners + + +@all_runners +def test_version(runner): + out, err = runner('version') + out = out.strip() + assert out.startswith('bonobo ') + assert __version__ in out + + out, err = runner('version', '-q') + out = out.strip() + assert out.startswith('bonobo ') + assert __version__ in out + + out, err = runner('version', '-qq') + out = out.strip() + assert not out.startswith('bonobo ') + assert __version__ in out \ No newline at end of file diff --git a/tests/execution/contexts/test_node.py b/tests/execution/contexts/test_node.py index 648743b..ef29c6e 100644 --- a/tests/execution/contexts/test_node.py +++ b/tests/execution/contexts/test_node.py @@ -185,6 +185,7 @@ def test_node_tuple_dict(): assert output[0] == ('foo', 'bar', {'id': 1}) assert output[1] == ('foo', 'baz', {'id': 2}) + def test_node_lifecycle_natural(): func = MagicMock() @@ -203,6 +204,7 @@ def test_node_lifecycle_natural(): ctx.stop() assert all((ctx.started, ctx.stopped)) and not any((ctx.alive, ctx.killed)) + def test_node_lifecycle_with_kill(): func = MagicMock() @@ -223,7 +225,3 @@ def test_node_lifecycle_with_kill(): ctx.stop() assert all((ctx.started, ctx.killed, ctx.stopped)) and not ctx.alive - - - - diff --git a/tests/execution/test_events.py b/tests/execution/test_events.py index 8abeb57..6fbc405 100644 --- a/tests/execution/test_events.py +++ b/tests/execution/test_events.py @@ -10,6 +10,7 @@ def test_names(): event_name = getattr(events, name.upper()) assert event_name == '.'.join(('execution', name)) + def test_event_object(): # Same logic as above. c = Mock() diff --git a/tests/test_commands.py b/tests/test_commands.py deleted file mode 100644 index 255bae7..0000000 --- a/tests/test_commands.py +++ /dev/null @@ -1,319 +0,0 @@ -import functools -import io -import os -import runpy -import sys -from contextlib import redirect_stdout, redirect_stderr -from unittest.mock import patch - -import pkg_resources -import pytest - -from bonobo import __main__, __version__, get_examples_path -from bonobo.commands import entrypoint -from bonobo.commands.download import EXAMPLES_BASE_URL - - -def runner(f): - @functools.wraps(f) - def wrapped_runner(*args, catch_errors=False): - with redirect_stdout(io.StringIO()) as stdout, redirect_stderr(io.StringIO()) as stderr: - try: - f(list(args)) - except BaseException as exc: - if not catch_errors: - raise - elif isinstance(catch_errors, BaseException) and not isinstance(exc, catch_errors): - raise - return stdout.getvalue(), stderr.getvalue(), exc - return stdout.getvalue(), stderr.getvalue() - - return wrapped_runner - - -@runner -def runner_entrypoint(args): - """ Run bonobo using the python command entrypoint directly (bonobo.commands.entrypoint). """ - return entrypoint(args) - - -@runner -def runner_module(args): - """ Run bonobo using the bonobo.__main__ file, which is equivalent as doing "python -m bonobo ...".""" - with patch.object(sys, 'argv', ['bonobo', *args]): - return runpy.run_path(__main__.__file__, run_name='__main__') - - -all_runners = pytest.mark.parametrize('runner', [runner_entrypoint, runner_module]) - - -def test_entrypoint(): - commands = {} - - for command in pkg_resources.iter_entry_points('bonobo.commands'): - commands[command.name] = command - - assert not { - 'convert', - 'init', - 'inspect', - 'run', - 'version', - }.difference(set(commands)) - - -@all_runners -def test_no_command(runner): - _, err, exc = runner(catch_errors=True) - assert type(exc) == SystemExit - assert 'error: the following arguments are required: command' in err - - -@all_runners -def test_run(runner): - out, err = runner('run', '--quiet', get_examples_path('types/strings.py')) - out = out.split('\n') - assert out[0].startswith('Foo ') - assert out[1].startswith('Bar ') - assert out[2].startswith('Baz ') - - -@all_runners -def test_run_module(runner): - out, err = runner('run', '--quiet', '-m', 'bonobo.examples.types.strings') - out = out.split('\n') - assert out[0].startswith('Foo ') - assert out[1].startswith('Bar ') - assert out[2].startswith('Baz ') - - -@all_runners -def test_run_path(runner): - out, err = runner('run', '--quiet', get_examples_path('types')) - out = out.split('\n') - assert out[0].startswith('Foo ') - assert out[1].startswith('Bar ') - assert out[2].startswith('Baz ') - - -@all_runners -def test_install_requirements_for_dir(runner): - dirname = get_examples_path('types') - with patch('bonobo.commands.run._install_requirements') as install_mock: - runner('run', '--install', dirname) - install_mock.assert_called_once_with(os.path.join(dirname, 'requirements.txt')) - - -@all_runners -def test_install_requirements_for_file(runner): - dirname = get_examples_path('types') - with patch('bonobo.commands.run._install_requirements') as install_mock: - runner('run', '--install', os.path.join(dirname, 'strings.py')) - install_mock.assert_called_once_with(os.path.join(dirname, 'requirements.txt')) - - -@all_runners -def test_init_file(runner, tmpdir): - target = tmpdir.join('foo.py') - target_filename = str(target) - runner('init', target_filename) - assert os.path.exists(target_filename) - - out, err = runner('run', target_filename) - assert out.replace('\n', ' ').strip() == 'Hello World' - assert not err - - -@all_runners -def test_version(runner): - out, err = runner('version') - out = out.strip() - assert out.startswith('bonobo ') - assert __version__ in out - - out, err = runner('version', '-q') - out = out.strip() - assert out.startswith('bonobo ') - assert __version__ in out - - out, err = runner('version', '-qq') - out = out.strip() - assert not out.startswith('bonobo ') - assert __version__ in out - - -@all_runners -def test_download_works_for_examples(runner): - expected_bytes = b'hello world' - - class MockResponse(object): - def __init__(self): - self.status_code = 200 - - def iter_content(self, *args, **kwargs): - return [expected_bytes] - - def __enter__(self): - return self - - def __exit__(self, *args, **kwargs): - pass - - fout = io.BytesIO() - fout.close = lambda: None - - with patch('bonobo.commands.download._open_url') as mock_open_url, \ - patch('bonobo.commands.download.open') as mock_open: - mock_open_url.return_value = MockResponse() - mock_open.return_value = fout - runner('download', 'examples/datasets/coffeeshops.txt') - expected_url = EXAMPLES_BASE_URL + 'datasets/coffeeshops.txt' - mock_open_url.assert_called_once_with(expected_url) - - assert fout.getvalue() == expected_bytes - - -@all_runners -def test_download_fails_non_example(runner): - with pytest.raises(ValueError): - runner('download', 'something/entirely/different.txt') - - -@pytest.fixture -def env1(tmpdir): - env_file = tmpdir.join('.env_one') - env_file.write('\n'.join(( - 'SECRET=unknown', - 'PASSWORD=sweet', - 'PATH=first', - ))) - return str(env_file) - - -@pytest.fixture -def env2(tmpdir): - env_file = tmpdir.join('.env_two') - env_file.write('\n'.join(( - 'PASSWORD=bitter', - "PATH='second'", - ))) - return str(env_file) - - -all_environ_targets = pytest.mark.parametrize( - 'target', [ - (get_examples_path('environ.py'), ), - ( - '-m', - 'bonobo.examples.environ', - ), - ] -) - - -@all_runners -@all_environ_targets -class EnvironmentTestCase(): - def run_quiet(self, runner, *args): - return runner('run', '--quiet', *args) - - def run_environ(self, runner, *args, environ=None): - _environ = {'PATH': '/usr/bin'} - if environ: - _environ.update(environ) - - with patch.dict('os.environ', _environ, clear=True): - out, err = self.run_quiet(runner, *args) - assert 'SECRET' not in os.environ - assert 'PASSWORD' not in os.environ - if 'PATH' in _environ: - assert 'PATH' in os.environ - assert os.environ['PATH'] == _environ['PATH'] - - assert err == '' - return dict(map(lambda line: line.split(' ', 1), filter(None, out.split('\n')))) - - -class TestDefaultEnvFile(EnvironmentTestCase): - def test_run_with_default_env_file(self, runner, target, env1): - env = self.run_environ(runner, *target, '--default-env-file', env1) - assert env.get('SECRET') == 'unknown' - assert env.get('PASSWORD') == 'sweet' - assert env.get('PATH') == '/usr/bin' - - def test_run_with_multiple_default_env_files(self, runner, target, env1, env2): - env = self.run_environ(runner, *target, '--default-env-file', env1, '--default-env-file', env2) - assert env.get('SECRET') == 'unknown' - assert env.get('PASSWORD') == 'sweet' - assert env.get('PATH') == '/usr/bin' - - env = self.run_environ(runner, *target, '--default-env-file', env2, '--default-env-file', env1) - assert env.get('SECRET') == 'unknown' - assert env.get('PASSWORD') == 'bitter' - assert env.get('PATH') == '/usr/bin' - - -class TestEnvFile(EnvironmentTestCase): - def test_run_with_file(self, runner, target, env1): - env = self.run_environ(runner, *target, '--env-file', env1) - assert env.get('SECRET') == 'unknown' - assert env.get('PASSWORD') == 'sweet' - assert env.get('PATH') == 'first' - - def test_run_with_multiple_files(self, runner, target, env1, env2): - env = self.run_environ(runner, *target, '--env-file', env1, '--env-file', env2) - assert env.get('SECRET') == 'unknown' - assert env.get('PASSWORD') == 'bitter' - assert env.get('PATH') == 'second' - - env = self.run_environ(runner, *target, '--env-file', env2, '--env-file', env1) - assert env.get('SECRET') == 'unknown' - assert env.get('PASSWORD') == 'sweet' - assert env.get('PATH') == 'first' - - -class TestEnvFileCombinations(EnvironmentTestCase): - def test_run_with_both_env_files(self, runner, target, env1, env2): - env = self.run_environ(runner, *target, '--default-env-file', env1, '--env-file', env2) - assert env.get('SECRET') == 'unknown' - assert env.get('PASSWORD') == 'bitter' - assert env.get('PATH') == 'second' - - def test_run_with_both_env_files_then_overrides(self, runner, target, env1, env2): - env = self.run_environ( - runner, *target, '--default-env-file', env1, '--env-file', env2, '--env', 'PASSWORD=mine', '--env', - 'SECRET=s3cr3t' - ) - assert env.get('SECRET') == 's3cr3t' - assert env.get('PASSWORD') == 'mine' - assert env.get('PATH') == 'second' - - -class TestEnvVars(EnvironmentTestCase): - def test_run_no_env(self, runner, target): - env = self.run_environ(runner, *target, environ={'USER': 'romain'}) - assert env.get('USER') == 'romain' - - def test_run_env(self, runner, target): - env = self.run_environ(runner, *target, '--env', 'USER=serious', environ={'USER': 'romain'}) - assert env.get('USER') == 'serious' - - def test_run_env_mixed(self, runner, target): - env = self.run_environ(runner, *target, '--env', 'ONE=1', '--env', 'TWO="2"', environ={'USER': 'romain'}) - assert env.get('USER') == 'romain' - assert env.get('ONE') == '1' - assert env.get('TWO') == '2' - - def test_run_default_env(self, runner, target): - env = self.run_environ(runner, *target, '--default-env', 'USER=clown') - assert env.get('USER') == 'clown' - - env = self.run_environ(runner, *target, '--default-env', 'USER=clown', environ={'USER': 'romain'}) - assert env.get('USER') == 'romain' - - env = self.run_environ( - runner, *target, '--env', 'USER=serious', '--default-env', 'USER=clown', environ={ - 'USER': 'romain' - } - ) - assert env.get('USER') == 'serious' From 8439a535fec408d9db80a7c62ffdecaedb57e836 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 15:01:04 +0100 Subject: [PATCH 090/145] Moves timer to statistics utilities. --- bonobo/util/statistics.py | 21 +++++++++++++++++++++ bonobo/util/time.py | 21 --------------------- 2 files changed, 21 insertions(+), 21 deletions(-) delete mode 100644 bonobo/util/time.py diff --git a/bonobo/util/statistics.py b/bonobo/util/statistics.py index 5d71a0f..2f9c5c2 100644 --- a/bonobo/util/statistics.py +++ b/bonobo/util/statistics.py @@ -13,6 +13,7 @@ # without warranties or conditions of any kind, either express or implied. # see the license for the specific language governing permissions and # limitations under the license. +import time class WithStatistics: @@ -29,3 +30,23 @@ class WithStatistics: def increment(self, name): self.statistics[name] += 1 + + +class Timer: + """ + Context manager used to time execution of stuff. + """ + + def __enter__(self): + self.__start = time.time() + + def __exit__(self, type=None, value=None, traceback=None): + # Error handling here + self.__finish = time.time() + + @property + def duration(self): + return self.__finish - self.__start + + def __str__(self): + return str(int(self.duration * 1000) / 1000.0) + 's' diff --git a/bonobo/util/time.py b/bonobo/util/time.py deleted file mode 100644 index 14de016..0000000 --- a/bonobo/util/time.py +++ /dev/null @@ -1,21 +0,0 @@ -import time - - -class Timer: - """ - Context manager used to time execution of stuff. - """ - - def __enter__(self): - self.__start = time.time() - - def __exit__(self, type=None, value=None, traceback=None): - # Error handling here - self.__finish = time.time() - - @property - def duration(self): - return self.__finish - self.__start - - def __str__(self): - return str(int(self.duration * 1000) / 1000.0) + 's' From a901731fffd9b5196f113fdf93bfd00a72d9a146 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 4 Nov 2017 15:17:51 +0100 Subject: [PATCH 091/145] Switch to stable mondrian. --- Makefile | 2 +- Projectfile | 2 +- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index ccd73ba..8ae38cd 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4a11 on 2017-11-04. +# Generated by Medikit 0.4.1 on 2017-11-04. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index d0e800e..ca6a0f4 100644 --- a/Projectfile +++ b/Projectfile @@ -42,7 +42,7 @@ python.setup( python.add_requirements( 'fs >=2.0,<2.1', 'jinja2 >=2.9,<2.10', - 'mondrian ==0.4a1', + 'mondrian >=0.4,<0.5', 'packaging >=16,<17', 'psutil >=5.4,<6.0', 'requests >=2.0,<3.0', diff --git a/requirements.txt b/requirements.txt index dd9696d..3a5c70d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ fs==2.0.12 idna==2.6 jinja2==2.9.6 markupsafe==1.0 -mondrian==0.4a1 +mondrian==0.4.0 packaging==16.8 pbr==3.1.1 psutil==5.4.0 diff --git a/setup.py b/setup.py index d622505..d9eb59b 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ setup( packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, install_requires=[ - 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian (== 0.4a1)', + 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian (>= 0.4, < 0.5)', 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6.0)', 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.27, < 1.28)', 'whistle (>= 1.0, < 1.1)' ], From 674f9348e7d8c40c0a15e9d3a5c7f9041a6697a7 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 5 Nov 2017 14:45:30 +0100 Subject: [PATCH 092/145] Skip failing order test for python 3.5 (temporary). --- tests/commands/test_convert.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/commands/test_convert.py b/tests/commands/test_convert.py index ea0c3c4..ed6f9e2 100644 --- a/tests/commands/test_convert.py +++ b/tests/commands/test_convert.py @@ -1,7 +1,13 @@ +import sys + +import pytest + from bonobo.util.environ import change_working_directory from bonobo.util.testing import all_runners +@pytest.mark.skipif(sys.version_info < (3, 6), + reason="python 3.5 does not preserve kwargs order and this cant pass for now") @all_runners def test_convert(runner, tmpdir): csv_content = 'id;name\n1;Romain' From 56c26ea26cd63ad83ea997cb6fbc75ac62ee522e Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 5 Nov 2017 14:54:01 +0100 Subject: [PATCH 093/145] Fix default logging level, adds options to default template. --- bonobo/commands/__init__.py | 1 + bonobo/commands/templates/default.py-tpl | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index f42d5c6..a482b53 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -16,6 +16,7 @@ def entrypoint(args=None): mondrian.setup(excepthook=True) logger = logging.getLogger() + logger.setLevel(settings.LOGGING_LEVEL.get()) parser = argparse.ArgumentParser() parser.add_argument('--debug', '-D', action='store_true') diff --git a/bonobo/commands/templates/default.py-tpl b/bonobo/commands/templates/default.py-tpl index c72efb5..eaea053 100644 --- a/bonobo/commands/templates/default.py-tpl +++ b/bonobo/commands/templates/default.py-tpl @@ -19,7 +19,7 @@ def load(*args): print(*args) -def get_graph(): +def get_graph(**options): """ This function builds the graph that needs to be executed. @@ -32,7 +32,7 @@ def get_graph(): return graph -def get_services(): +def get_services(**options): """ This function builds the services dictionary, which is a simple dict of names-to-implementation used by bonobo for runtime injection. @@ -48,5 +48,8 @@ def get_services(): # The __main__ block actually execute the graph. if __name__ == '__main__': parser = bonobo.get_argument_parser() - with bonobo.parse_args(parser): - bonobo.run(get_graph(), services=get_services()) + with bonobo.parse_args(parser) as options: + bonobo.run( + get_graph(**options), + services=get_services(**options) + ) From eb393331cdf4136af2c1955f532350528ba43103 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 5 Nov 2017 14:59:25 +0100 Subject: [PATCH 094/145] Adds a "bare" template, containing the very minimum you want to have in 90% of cases. --- bonobo/commands/init.py | 2 +- bonobo/commands/templates/bare.py-tpl | 15 +++++++++++++++ tests/commands/test_init.py | 16 +++++++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 bonobo/commands/templates/bare.py-tpl diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index 8c50b16..6d4b217 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -6,7 +6,7 @@ from bonobo.commands import BaseCommand class InitCommand(BaseCommand): - TEMPLATES = {'default'} + TEMPLATES = {'bare', 'default'} TEMPLATES_PATH = os.path.join(os.path.dirname(__file__), 'templates') def add_arguments(self, parser): diff --git a/bonobo/commands/templates/bare.py-tpl b/bonobo/commands/templates/bare.py-tpl new file mode 100644 index 0000000..1ca3019 --- /dev/null +++ b/bonobo/commands/templates/bare.py-tpl @@ -0,0 +1,15 @@ +import bonobo + + +def get_graph(**options): + graph = bonobo.Graph() + return graph + + +def get_services(**options): + return {} + + +if __name__ == '__main__': + with bonobo.parse_args() as options: + bonobo.run(get_graph(**options), services=get_services(**options)) diff --git a/tests/commands/test_init.py b/tests/commands/test_init.py index a551a9a..626f5e8 100644 --- a/tests/commands/test_init.py +++ b/tests/commands/test_init.py @@ -1,5 +1,8 @@ import os +import pytest + +from bonobo.commands.init import InitCommand from bonobo.util.testing import all_runners @@ -12,4 +15,15 @@ def test_init_file(runner, tmpdir): out, err = runner('run', target_filename) assert out.replace('\n', ' ').strip() == 'Hello World' - assert not err \ No newline at end of file + assert not err + + +@all_runners +@pytest.mark.parametrize('template', InitCommand.TEMPLATES) +def test_init_file_templates(runner, template, tmpdir): + target = tmpdir.join('foo.py') + target_filename = str(target) + runner('init', target_filename) + assert os.path.exists(target_filename) + out, err = runner('run', target_filename) + assert not err From 8f3c4252b4f5ea28b0f6924fa3e17bd7362a2619 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 5 Nov 2017 19:41:27 +0100 Subject: [PATCH 095/145] Working on the new version of the tutorial. Only Step1 implemented. --- bonobo/_api.py | 23 ++- docs/_static/custom.css | 18 ++- docs/_templates/base.html | 11 +- docs/conf.py | 9 ++ docs/tutorial/1-init.rst | 258 ++++++++++++++++++++++++++++++++++ docs/tutorial/2-jobs.rst | 12 ++ docs/tutorial/3-files.rst | 12 ++ docs/tutorial/4-services.rst | 210 +++++++++++++++++++++++++++ docs/tutorial/5-packaging.rst | 11 ++ docs/tutorial/django.rst | 3 + docs/tutorial/index.rst | 54 ++++--- docs/tutorial/notebooks.rst | 4 + docs/tutorial/sqlalchemy.rst | 4 + 13 files changed, 586 insertions(+), 43 deletions(-) create mode 100644 docs/tutorial/1-init.rst create mode 100644 docs/tutorial/2-jobs.rst create mode 100644 docs/tutorial/3-files.rst create mode 100644 docs/tutorial/4-services.rst create mode 100644 docs/tutorial/5-packaging.rst create mode 100644 docs/tutorial/django.rst create mode 100644 docs/tutorial/notebooks.rst create mode 100644 docs/tutorial/sqlalchemy.rst diff --git a/bonobo/_api.py b/bonobo/_api.py index 816dfda..af92868 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -10,16 +10,33 @@ __all__ = [] def register_api(x, __all__=__all__): + """Register a function as being part of Bonobo's API, then returns the original function.""" __all__.append(get_name(x)) return x +def register_graph_api(x, __all__=__all__): + """ + Register a function as being part of Bonobo's API, after checking that its signature contains the right parameters + to work correctly, then returns the original function. + """ + from inspect import signature + parameters = list(signature(x).parameters) + required_parameters = {'plugins', 'services', 'strategy'} + assert parameters[0] == 'graph', 'First parameter of a graph api function must be "graph".' + assert required_parameters.intersection( + parameters) == required_parameters, 'Graph api functions must define the following parameters: ' + ', '.join( + sorted(required_parameters)) + + return register_api(x, __all__=__all__) + + def register_api_group(*args): for attr in args: register_api(attr) -@register_api +@register_graph_api def run(graph, *, plugins=None, services=None, strategy=None): """ Main entry point of bonobo. It takes a graph and creates all the necessary plumbery around to execute it. @@ -82,8 +99,8 @@ def _inspect_as_graph(graph): _inspect_formats = {'graph': _inspect_as_graph} -@register_api -def inspect(graph, *, format): +@register_graph_api +def inspect(graph, *, plugins=None, services=None, strategy=None, format): if not format in _inspect_formats: raise NotImplementedError( 'Output format {} not implemented. Choices are: {}.'.format( diff --git a/docs/_static/custom.css b/docs/_static/custom.css index f658da9..fa608d1 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,3 +1,19 @@ svg { border: 2px solid green -} \ No newline at end of file +} + +div.related { + width: 940px; + margin: 30px auto 0 auto; +} + +@media screen and (max-width: 875px) { + div.related { + visibility: hidden; + display: none; + } +} + +.brand { + font-family: 'Ubuntu', 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; +} diff --git a/docs/_templates/base.html b/docs/_templates/base.html index f8ad58a..27ca438 100644 --- a/docs/_templates/base.html +++ b/docs/_templates/base.html @@ -4,17 +4,8 @@ {%- block extrahead %} {{ super() }} + {% endblock %} {%- block footer %} diff --git a/docs/conf.py b/docs/conf.py index 93895a8..07d0424 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -186,3 +186,12 @@ epub_exclude_files = ['search.html'] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} + +rst_epilog = """ +.. |bonobo| replace:: **Bonobo** + +.. |longversion| replace:: v.{version} + +""".format( + version = version, +) diff --git a/docs/tutorial/1-init.rst b/docs/tutorial/1-init.rst new file mode 100644 index 0000000..780d34d --- /dev/null +++ b/docs/tutorial/1-init.rst @@ -0,0 +1,258 @@ +Part 1: Let's get started! +========================== + +To get started with |bonobo|, you need to install it in a working python 3.5+ environment (you should use a +`virtualenv `_). + +.. code-block:: shell-session + + $ pip install bonobo + +Check that the installation worked, and that you're using a version that matches this tutorial (written for bonobo +|longversion|). + +.. code-block:: shell-session + + $ bonobo version + +See :doc:`/install` for more options. + + +Create an ETL job +::::::::::::::::: + +Since Bonobo 0.6, it's easy to bootstrap a simple ETL job using just one file. + +We'll start here, and the later stages of the tutorial will guide you toward refactoring this to a python package. + +.. code-block:: shell-session + + $ bonobo init tutorial.py + +This will create a simple job in a `tutorial.py` file. Let's run it: + +.. code-block:: shell-session + + $ python tutorial.py + Hello + World + - extract in=1 out=2 [done] + - transform in=2 out=2 [done] + - load in=2 [done] + +If you have a similar result, then congratulations! You just ran your first |bonobo| ETL job. + + +Inspect your graph +:::::::::::::::::: + +The basic building blocks of |bonobo| are **transformations** and **graphs**. + +**Transformations** are simple python callables (like functions) that handle a transformation step for a line of data. + +**Graphs** are a set of transformations, with directional links between them to define the data-flow that will happen +at runtime. + +To inspect the graph of your first transformation (you must install graphviz first to do so), run: + +.. code-block:: shell-session + + $ bonobo inspect --graph tutorial.py | dot -Tpng -o tutorial.png + +Open the generated `tutorial.png` file to have a quick look at the graph. + +.. graphviz:: + + digraph { + rankdir = LR; + "BEGIN" [shape="point"]; + "BEGIN" -> {0 [label="extract"]}; + {0 [label="extract"]} -> {1 [label="transform"]}; + {1 [label="transform"]} -> {2 [label="load"]}; + } + +You can easily understand here the structure of your graph. For such a simple graph, it's pretty much useless, but as +you'll write more complex transformations, it will be helpful. + + +Read the Code +::::::::::::: + +Before we write our own job, let's look at the code we have in `tutorial.py`. + + +Import +------ + +.. code-block:: python + + import bonobo + + +The highest level APIs of |bonobo| are all contained within the top level **bonobo** namespace. + +If you're a beginner with the library, stick to using only those APIs (they also are the most stable APIs). + +If you're an advanced user (and you'll be one quite soon), you can safely use second level APIs. + +The third level APIs are considered private, and you should not use them unless you're hacking on |bonobo| directly. + + +Extract +------- + +.. code-block:: python + + def extract(): + yield 'hello' + yield 'world' + +This is a first transformation, written as a python generator, that will send some strings, one after the other, to its +output. + +Transformations that take no input and yields a variable number of outputs are usually called **extractors**. You'll +encounter a few different types, either purely generating the data (like here), using an external service (a +database, for example) or using some filesystem (which is considered an external service too). + +Extractors do not need to have its input connected to anything, and will be called exactly once when the graph is +executed. + + +Transform +--------- + +.. code-block:: python + + def transform(*args): + yield tuple( + map(str.title, args) + ) + +This is a second transformation. It will get called a bunch of times, once for each input row it gets, and apply some +logic on the input to generate the output. + +This is the most **generic** case. For each input row, you can generate zero, one or many lines of output for each line +of input. + + +Load +---- + +.. code-block:: python + + def load(*args): + print(*args) + +This is the third and last transformation in our "hello world" example. It will apply some logic to each row, and have +absolutely no output. + +Transformations that take input and yields nothing are also called **loaders**. Like extractors, you'll encounter +different types, to work with various external systems. + +Please note that as a convenience mean and because the cost is marginal, most builtin `loaders` will send their +inputs to their output, so you can easily chain more than one loader, or apply more transformations after a given +loader was applied. + + +Graph Factory +------------- + +.. code-block:: python + + def get_graph(**options): + graph = bonobo.Graph() + graph.add_chain(extract, transform, load) + return graph + +All our transformations were defined above, but nothing ties them together, for now. + +This "graph factory" function is in charge of the creation and configuration of a :class:`bonobo.Graph` instance, that +will be executed later. + +By no mean is |bonobo| limited to simple graphs like this one. You can add as many chains as you want, and each chain +can contain as many nodes as you want. + + +Services Factory +---------------- + +.. code-block:: python + + def get_services(**options): + return {} + +This is the "services factory", that we'll use later to connect to external systems. Let's skip this one, for now. + +(we'll dive into this topic in :doc:`4-services`) + + +Main Block +---------- + +.. code-block:: python + + if __name__ == '__main__': + parser = bonobo.get_argument_parser() + with bonobo.parse_args(parser) as options: + bonobo.run( + get_graph(**options), + services=get_services(**options) + ) + +Here, the real thing happens. + +Without diving into too much details for now, using the :func:`bonobo.parse_args` context manager will allow our job to +be configurable, later, and although we don't really need it right now, it does not harm neither. + +Reading the output +:::::::::::::::::: + +Let's run this job once again: + +.. code-block:: shell-session + + $ python tutorial.py + Hello + World + - extract in=1 out=2 [done] + - transform in=2 out=2 [done] + - load in=2 [done] + +The console output contains two things. + +* First, it contains the real output of your job (what was :func:`print`-ed to `sys.stdout`). +* Second, it displays the execution status (on `sys.stderr`). Each line contains a "status" character, the node name, + numbers and a human readable status. This status will evolve in real time, and allows to understand a job's progress + while it's running. + + * Status character: + + * “ ” means that the node was not yet started. + * “`-`” means that the node finished its execution. + * “`+`” means that the node is currently running. + * “`!`” means that the node had problems running. + + * Numerical statistics: + + * “`in=...`” shows the input lines count, also known as the amount of calls to your transformation. + * “`out=...`” shows the output lines count. + * “`read=...`” shows the count of reads applied to an external system, if the transformation supports it. + * “`write=...`” shows the count of writes applied to an external system, if the transformation supports it. + * “`err=...`” shows the count of exceptions that happened while running the transformation. Note that exception will abort + a call, but the execution will move to the next row. + + +Moving forward +:::::::::::::: + +That's all for this first step. + +You now know: + +* How to create a new job file. +* How to inspect the content of a job file. +* What should go in a job file. +* How to execute a job file. +* How to read the console output. + +**Next: :doc:`2-jobs`** diff --git a/docs/tutorial/2-jobs.rst b/docs/tutorial/2-jobs.rst new file mode 100644 index 0000000..c3a6c8b --- /dev/null +++ b/docs/tutorial/2-jobs.rst @@ -0,0 +1,12 @@ +Part 2: Writing ETL Jobs +======================== + + +Moving forward +:::::::::::::: + +You now know: + +* How to ... + +**Next: :doc:`3-files`** diff --git a/docs/tutorial/3-files.rst b/docs/tutorial/3-files.rst new file mode 100644 index 0000000..adcc334 --- /dev/null +++ b/docs/tutorial/3-files.rst @@ -0,0 +1,12 @@ +Part 3: Working with Files +========================== + + +Moving forward +:::::::::::::: + +You now know: + +* How to ... + +**Next: :doc:`4-services`** diff --git a/docs/tutorial/4-services.rst b/docs/tutorial/4-services.rst new file mode 100644 index 0000000..e39f15b --- /dev/null +++ b/docs/tutorial/4-services.rst @@ -0,0 +1,210 @@ +Part 4: Services and Configurables +================================== + +.. note:: + + This section lacks completeness, sorry for that (but you can still read it!). + +In the last section, we used a few new tools. + +Class-based transformations and configurables +::::::::::::::::::::::::::::::::::::::::::::: + +Bonobo is a bit dumb. If something is callable, it considers it can be used as a transformation, and it's up to the +user to provide callables that logically fits in a graph. + +You can use plain python objects with a `__call__()` method, and it ill just work. + +As a lot of transformations needs common machinery, there is a few tools to quickly build transformations, most of +them requiring your class to subclass :class:`bonobo.config.Configurable`. + +Configurables allows to use the following features: + +* You can add **Options** (using the :class:`bonobo.config.Option` descriptor). Options can be positional, or keyword + based, can have a default value and will be consumed from the constructor arguments. + + .. code-block:: python + + from bonobo.config import Configurable, Option + + class PrefixIt(Configurable): + prefix = Option(str, positional=True, default='>>>') + + def call(self, row): + return self.prefix + ' ' + row + + prefixer = PrefixIt('$') + +* You can add **Services** (using the :class:`bonobo.config.Service` descriptor). Services are a subclass of + :class:`bonobo.config.Option`, sharing the same basics, but specialized in the definition of "named services" that + will be resolved at runtime (a.k.a for which we will provide an implementation at runtime). We'll dive more into that + in the next section + + .. code-block:: python + + from bonobo.config import Configurable, Option, Service + + class HttpGet(Configurable): + url = Option(default='https://jsonplaceholder.typicode.com/users') + http = Service('http.client') + + def call(self, http): + resp = http.get(self.url) + + for row in resp.json(): + yield row + + http_get = HttpGet() + + +* You can add **Methods** (using the :class:`bonobo.config.Method` descriptor). :class:`bonobo.config.Method` is a + subclass of :class:`bonobo.config.Option` that allows to pass callable parameters, either to the class constructor, + or using the class as a decorator. + + .. code-block:: python + + from bonobo.config import Configurable, Method + + class Applier(Configurable): + apply = Method() + + def call(self, row): + return self.apply(row) + + @Applier + def Prefixer(self, row): + return 'Hello, ' + row + + prefixer = Prefixer() + +* You can add **ContextProcessors**, which are an advanced feature we won't introduce here. If you're familiar with + pytest, you can think of them as pytest fixtures, execution wise. + +Services +:::::::: + +The motivation behind services is mostly separation of concerns, testability and deployability. + +Usually, your transformations will depend on services (like a filesystem, an http client, a database, a rest api, ...). +Those services can very well be hardcoded in the transformations, but there is two main drawbacks: + +* You won't be able to change the implementation depending on the current environment (development laptop versus + production servers, bug-hunting session versus execution, etc.) +* You won't be able to test your transformations without testing the associated services. + +To overcome those caveats of hardcoding things, we define Services in the configurable, which are basically +string-options of the service names, and we provide an implementation at the last moment possible. + +There are two ways of providing implementations: + +* Either file-wide, by providing a `get_services()` function that returns a dict of named implementations (we did so + with filesystems in the previous step, :doc:`tut02`) +* Either directory-wide, by providing a `get_services()` function in a specially named `_services.py` file. + +The first is simpler if you only have one transformation graph in one file, the second allows to group coherent +transformations together in a directory and share the implementations. + +Let's see how to use it, starting from the previous service example: + +.. code-block:: python + + from bonobo.config import Configurable, Option, Service + + class HttpGet(Configurable): + url = Option(default='https://jsonplaceholder.typicode.com/users') + http = Service('http.client') + + def call(self, http): + resp = http.get(self.url) + + for row in resp.json(): + yield row + +We defined an "http.client" service, that obviously should have a `get()` method, returning responses that have a +`json()` method. + +Let's provide two implementations for that. The first one will be using `requests `_, +that coincidally satisfies the described interface: + +.. code-block:: python + + import bonobo + import requests + + def get_services(): + return { + 'http.client': requests + } + + graph = bonobo.Graph( + HttpGet(), + print, + ) + +If you run this code, you should see some mock data returned by the webservice we called (assuming it's up and you can +reach it). + +Now, the second implementation will replace that with a mock, used for testing purposes: + +.. code-block:: python + + class HttpResponseStub: + def json(self): + return [ + {'id': 1, 'name': 'Leanne Graham', 'username': 'Bret', 'email': 'Sincere@april.biz', 'address': {'street': 'Kulas Light', 'suite': 'Apt. 556', 'city': 'Gwenborough', 'zipcode': '92998-3874', 'geo': {'lat': '-37.3159', 'lng': '81.1496'}}, 'phone': '1-770-736-8031 x56442', 'website': 'hildegard.org', 'company': {'name': 'Romaguera-Crona', 'catchPhrase': 'Multi-layered client-server neural-net', 'bs': 'harness real-time e-markets'}}, + {'id': 2, 'name': 'Ervin Howell', 'username': 'Antonette', 'email': 'Shanna@melissa.tv', 'address': {'street': 'Victor Plains', 'suite': 'Suite 879', 'city': 'Wisokyburgh', 'zipcode': '90566-7771', 'geo': {'lat': '-43.9509', 'lng': '-34.4618'}}, 'phone': '010-692-6593 x09125', 'website': 'anastasia.net', 'company': {'name': 'Deckow-Crist', 'catchPhrase': 'Proactive didactic contingency', 'bs': 'synergize scalable supply-chains'}}, + ] + + class HttpStub: + def get(self, url): + return HttpResponseStub() + + def get_services(): + return { + 'http.client': HttpStub() + } + + graph = bonobo.Graph( + HttpGet(), + print, + ) + +The `Graph` definition staying the exact same, you can easily substitute the `_services.py` file depending on your +environment (the way you're doing this is out of bonobo scope and heavily depends on your usual way of managing +configuration files on different platforms). + +Starting with bonobo 0.5 (not yet released), you will be able to use service injections with function-based +transformations too, using the `bonobo.config.requires` decorator to mark a dependency. + +.. code-block:: python + + from bonobo.config import requires + + @requires('http.client') + def http_get(http): + resp = http.get('https://jsonplaceholder.typicode.com/users') + + for row in resp.json(): + yield row + + +Read more +::::::::: + +* :doc:`/guide/services` +* :doc:`/reference/api_config` + +Next +:::: + +:doc:`tut04`. + + +Moving forward +:::::::::::::: + +You now know: + +* How to ... + +**Next: :doc:`5-packaging`** diff --git a/docs/tutorial/5-packaging.rst b/docs/tutorial/5-packaging.rst new file mode 100644 index 0000000..bf4537b --- /dev/null +++ b/docs/tutorial/5-packaging.rst @@ -0,0 +1,11 @@ +Part 5: Projects and Packaging +============================== + + +Moving forward +:::::::::::::: + +You now know: + +* How to ... + diff --git a/docs/tutorial/django.rst b/docs/tutorial/django.rst new file mode 100644 index 0000000..1be4f52 --- /dev/null +++ b/docs/tutorial/django.rst @@ -0,0 +1,3 @@ +Working with Django +=================== + diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 4ba99c2..111c543 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -17,47 +17,43 @@ Bonobo uses simple python and should be quick and easy to learn. Tutorial :::::::: -.. note:: +.. toctree:: + :maxdepth: 1 - Good documentation is not easy to write. We do our best to make it better and better. + 1-init + 2-jobs + 3-files + 4-services + 5-packaging - Although all content here should be accurate, you may feel a lack of completeness, for which we plead guilty and - apologize. - - If you're stuck, please come and ask on our `slack channel `_, we'll figure - something out. - - If you're not stuck but had trouble understanding something, please consider contributing to the docs (via GitHub - pull requests). +More +:::: .. toctree:: - :maxdepth: 2 - - tut01 - tut02 - tut03 - tut04 + :maxdepth: 1 + django + notebooks + sqlalchemy What's next? :::::::::::: -Read a few examples -------------------- +* :doc:`The Bonobo Guide <../guide/index>` +* :doc:`Extensions <../extension/index>` -* :doc:`../reference/examples` -Read about best development practices -------------------------------------- +We're there! +:::::::::::: -* :doc:`../guide/index` -* :doc:`../guide/purity` +Good documentation is not easy to write. -Read about integrating external tools with bonobo -------------------------------------------------- +Although all content here should be accurate, you may feel a lack of completeness, for which we plead guilty and +apologize. -* :doc:`../extension/docker`: run transformation graphs in isolated containers. -* :doc:`../extension/jupyter`: run transformations within jupyter notebooks. -* :doc:`../extension/selenium`: crawl the web using a real browser and work with the gathered data. -* :doc:`../extension/sqlalchemy`: everything you need to interract with SQL databases. +If you're stuck, please come to the `Bonobo Slack Channel `_ and we'll figure it +out. + +If you're not stuck but had trouble understanding something, please consider contributing to the docs (using GitHub +pull requests). diff --git a/docs/tutorial/notebooks.rst b/docs/tutorial/notebooks.rst new file mode 100644 index 0000000..ed59121 --- /dev/null +++ b/docs/tutorial/notebooks.rst @@ -0,0 +1,4 @@ +Working with Jupyter Notebooks +============================== + + diff --git a/docs/tutorial/sqlalchemy.rst b/docs/tutorial/sqlalchemy.rst new file mode 100644 index 0000000..359fbd5 --- /dev/null +++ b/docs/tutorial/sqlalchemy.rst @@ -0,0 +1,4 @@ +Working with SQL Databases +========================== + + From 2b712f5c4ec03f4c2aad418b4061968b5262a151 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 5 Nov 2017 19:50:38 +0100 Subject: [PATCH 096/145] Update conda conf so readthedocs can maybe build. --- config/conda.yml | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/config/conda.yml b/config/conda.yml index 09b92de..2af0f49 100644 --- a/config/conda.yml +++ b/config/conda.yml @@ -1,14 +1,28 @@ name: py35 dependencies: - pip=9.0.1=py35_0 -- python=3.5.2=0 -- setuptools=20.3=py35_0 -- wheel=0.29.0=py35_0 +- python=3.5.4=0 +- setuptools=36.6.0=py35_0 +- wheel=0.30.0=py35_0 - pip: - - colorama ==0.3.9 - - fs ==2.0.3 - - psutil ==5.2.2 - - requests ==2.13.0 - - stevedore ==1.21.0 + - appdirs==1.4.3 + - certifi==2017.7.27.1 + - chardet==3.0.4 + - colorama==0.3.9 + - fs==2.0.12 + - idna==2.6 + - jinja2==2.9.6 + - markupsafe==1.0 + - mondrian==0.4.0 + - packaging==16.8 + - pbr==3.1.1 + - psutil==5.4.0 + - pyparsing==2.2.0 + - pytz==2017.3 + - requests==2.18.4 + - six==1.11.0 + - stevedore==1.27.1 + - urllib3==1.22 + - whistle==1.0.0 # for examples - pycountry ==17.9.23 From afbc2efce0e5d01402020ff19bb9d9b56d39ec97 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 5 Nov 2017 20:04:36 +0100 Subject: [PATCH 097/145] Trying to understand conda... --- config/conda.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/conda.yml b/config/conda.yml index 2af0f49..81d72c7 100644 --- a/config/conda.yml +++ b/config/conda.yml @@ -1,9 +1,9 @@ name: py35 dependencies: -- pip=9.0.1=py35_0 -- python=3.5.4=0 -- setuptools=36.6.0=py35_0 -- wheel=0.30.0=py35_0 +- pip=9.0.1 +- python=3.5 +- setuptools=36.5.0 +- wheel=0.29.0 - pip: - appdirs==1.4.3 - certifi==2017.7.27.1 From de449304fbfa208b034aaddf55ac2426e9377141 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 5 Nov 2017 20:06:09 +0100 Subject: [PATCH 098/145] Trying to understand conda... --- config/conda.yml => readthedocs-conda.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config/conda.yml => readthedocs-conda.yml (100%) diff --git a/config/conda.yml b/readthedocs-conda.yml similarity index 100% rename from config/conda.yml rename to readthedocs-conda.yml From 1a53c89efc47478666e0cb6efdc290bf30657b87 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 5 Nov 2017 20:06:34 +0100 Subject: [PATCH 099/145] Trying to understand conda... --- readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs.yml b/readthedocs.yml index 8a7f195..c77e2a7 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -1,5 +1,5 @@ conda: - file: config/conda.yml + file: readthedocs-conda.yml python: extra_requirements: - dev From 1d2916480a20fd800763d12852ad664031face43 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Tue, 7 Nov 2017 08:00:59 +0100 Subject: [PATCH 100/145] Adds argument parser support to django extension. --- bonobo/ext/django.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/bonobo/ext/django.py b/bonobo/ext/django.py index 60b583c..797df01 100644 --- a/bonobo/ext/django.py +++ b/bonobo/ext/django.py @@ -1,17 +1,21 @@ from logging import getLogger -from colorama import Fore, Back, Style -from django.core.management.base import BaseCommand, OutputWrapper - import bonobo import bonobo.util from bonobo.plugins.console import ConsoleOutputPlugin from bonobo.util.term import CLEAR_EOL +from colorama import Fore, Back, Style +from django.core.management.base import BaseCommand, OutputWrapper class ETLCommand(BaseCommand): GraphType = bonobo.Graph + def create_parser(self, prog_name, subcommand): + return bonobo.get_argument_parser( + super().create_parser(prog_name, subcommand) + ) + def create_or_update(self, model, *, defaults=None, save=True, **kwargs): """ Create or update a django model instance. @@ -63,10 +67,12 @@ class ETLCommand(BaseCommand): self.stderr = OutputWrapper(ConsoleOutputPlugin._stderr, ending=CLEAR_EOL + '\n') self.stderr.style_func = lambda x: Fore.LIGHTRED_EX + Back.RED + '!' + Style.RESET_ALL + ' ' + x - result = bonobo.run( - self.get_graph(*args, **options), - services=self.get_services(), - ) - self.stdout = _stdout_backup + with bonobo.parse_args(options) as options: + result = bonobo.run( + self.get_graph(*args, **options), + services=self.get_services(), + ) + + self.stdout, self.stderr = _stdout_backup, _stderr_backup return '\nReturn Value: ' + str(result) From b0c8440e3543a6a1ac86ae5ddd3274e613e835b1 Mon Sep 17 00:00:00 2001 From: CW Andrews Date: Sat, 11 Nov 2017 17:33:01 -0500 Subject: [PATCH 101/145] Update graphs.rst Rewording and other English fixes for readability. --- docs/guide/graphs.rst | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/guide/graphs.rst b/docs/guide/graphs.rst index 1c753a1..3d2d4b1 100644 --- a/docs/guide/graphs.rst +++ b/docs/guide/graphs.rst @@ -1,9 +1,8 @@ Graphs ====== -Graphs are the glue that ties transformations together. It's the only data-structure bonobo can execute directly. Graphs -must be acyclic, and can contain as much nodes as your system can handle. Although this number can be rather high in -theory, extreme practical cases usually do not exceed hundreds of nodes (and this is already extreme, really). +Graphs are the glue that ties transformations together. They are the only data-structure bonobo can execute directly. Graphs +must be acyclic, and can contain as many nodes as your system can handle. However, although in theory the number of nodes can be rather high, practical use cases usually do not exceed more than a few hundred nodes and only then in extreme cases. Definitions @@ -50,7 +49,7 @@ Non-linear graphs Divergences / forks ------------------- -To create two or more divergent data streams ("fork"), you should specify `_input` kwarg to `add_chain`. +To create two or more divergent data streams ("forks"), you should specify the `_input` kwarg to `add_chain`. .. code-block:: python @@ -74,12 +73,12 @@ Resulting graph: "b" -> "f" -> "g"; } -.. note:: Both branch will receive the same data, at the same time. +.. note:: Both branches will receive the same data and at the same time. -Convergences / merges +Convergence / merges --------------------- -To merge two data streams ("merge"), you can use the `_output` kwarg to `add_chain`, or use named nodes (see below). +To merge two data streams, you can use the `_output` kwarg to `add_chain`, or use named nodes (see below). .. code-block:: python @@ -88,7 +87,7 @@ To merge two data streams ("merge"), you can use the `_output` kwarg to `add_cha graph = bonobo.Graph() - # Here we mark _input to None, so normalize won't get the "begin" impulsion. + # Here we set _input to None, so normalize won't start on its own but only after it receives input from the other chains. graph.add_chain(normalize, store, _input=None) # Add two different chains @@ -122,7 +121,7 @@ Resulting graph: Named nodes ::::::::::: -Using above code to create convergences can lead to hard to read code, because you have to define the "target" stream +Using above code to create convergences often leads to code which is hard to read, because you have to define the "target" stream before the streams that logically goes to the beginning of the transformation graph. To overcome that, one can use "named" nodes: @@ -194,7 +193,7 @@ You can also run a python module: $ bonobo run -m my.own.etlmod -In each case, bonobo's CLI will look for an instance of :class:`bonobo.Graph` in your file/module, create the plumbery +In each case, bonobo's CLI will look for an instance of :class:`bonobo.Graph` in your file/module, create the plumbing needed to execute it, and run it. If you're in an interactive terminal context, it will use :class:`bonobo.ext.console.ConsoleOutputPlugin` for display. From 40be9c71da165e44c932152a482232c59703cfe2 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 06:20:46 +0100 Subject: [PATCH 102/145] Moves django extension to contrib module. --- bonobo/contrib/__init__.py | 0 bonobo/contrib/django/__init__.py | 7 +++ .../django.py => contrib/django/commands.py} | 62 ++++++------------- bonobo/contrib/django/utils.py | 23 +++++++ 4 files changed, 50 insertions(+), 42 deletions(-) create mode 100644 bonobo/contrib/__init__.py create mode 100644 bonobo/contrib/django/__init__.py rename bonobo/{ext/django.py => contrib/django/commands.py} (65%) create mode 100644 bonobo/contrib/django/utils.py diff --git a/bonobo/contrib/__init__.py b/bonobo/contrib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bonobo/contrib/django/__init__.py b/bonobo/contrib/django/__init__.py new file mode 100644 index 0000000..d8bd00a --- /dev/null +++ b/bonobo/contrib/django/__init__.py @@ -0,0 +1,7 @@ +from .utils import create_or_update +from .commands import ETLCommand + +__all__ = [ + 'ETLCommand', + 'create_or_update', +] diff --git a/bonobo/ext/django.py b/bonobo/contrib/django/commands.py similarity index 65% rename from bonobo/ext/django.py rename to bonobo/contrib/django/commands.py index 797df01..11ec680 100644 --- a/bonobo/ext/django.py +++ b/bonobo/contrib/django/commands.py @@ -1,54 +1,16 @@ from logging import getLogger import bonobo -import bonobo.util from bonobo.plugins.console import ConsoleOutputPlugin from bonobo.util.term import CLEAR_EOL from colorama import Fore, Back, Style -from django.core.management.base import BaseCommand, OutputWrapper +from django.core.management import BaseCommand +from django.core.management.base import OutputWrapper + +from .utils import create_or_update class ETLCommand(BaseCommand): - GraphType = bonobo.Graph - - def create_parser(self, prog_name, subcommand): - return bonobo.get_argument_parser( - super().create_parser(prog_name, subcommand) - ) - - def create_or_update(self, model, *, defaults=None, save=True, **kwargs): - """ - Create or update a django model instance. - - :param model: - :param defaults: - :param kwargs: - :return: object, created, updated - - """ - obj, created = model._default_manager.get_or_create(defaults=defaults, **kwargs) - - updated = False - if not created: - for k, v in defaults.items(): - if getattr(obj, k) != v: - setattr(obj, k, v) - updated = True - - if updated and save: - obj.save() - - return obj, created, updated - - def get_graph(self, *args, **options): - def not_implemented(): - raise NotImplementedError('You must implement {}.get_graph() method.'.format(self)) - - return self.GraphType(not_implemented) - - def get_services(self): - return {} - @property def logger(self): try: @@ -57,6 +19,22 @@ class ETLCommand(BaseCommand): self._logger = getLogger(type(self).__module__) return self._logger + create_or_update = staticmethod(create_or_update) + + def create_parser(self, prog_name, subcommand): + return bonobo.get_argument_parser( + super().create_parser(prog_name, subcommand) + ) + + def get_graph(self, *args, **options): + def not_implemented(): + raise NotImplementedError('You must implement {}.get_graph() method.'.format(self)) + + return bonobo.Graph(not_implemented) + + def get_services(self): + return {} + def info(self, *args, **kwargs): self.logger.info(*args, **kwargs) diff --git a/bonobo/contrib/django/utils.py b/bonobo/contrib/django/utils.py new file mode 100644 index 0000000..56f1201 --- /dev/null +++ b/bonobo/contrib/django/utils.py @@ -0,0 +1,23 @@ +def create_or_update(model, *, defaults=None, save=True, **kwargs): + """ + Create or update a django model instance. + + :param model: + :param defaults: + :param kwargs: + :return: object, created, updated + + """ + obj, created = model._default_manager.get_or_create(defaults=defaults, **kwargs) + + updated = False + if not created: + for k, v in defaults.items(): + if getattr(obj, k) != v: + setattr(obj, k, v) + updated = True + + if updated and save: + obj.save() + + return obj, created, updated From a4d49a0f323e37c3ac467fe55659ba1f1dd48efb Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 06:21:05 +0100 Subject: [PATCH 103/145] Moves google extension to contrib module. --- bonobo/{ext/google.py => contrib/google/__init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bonobo/{ext/google.py => contrib/google/__init__.py} (100%) diff --git a/bonobo/ext/google.py b/bonobo/contrib/google/__init__.py similarity index 100% rename from bonobo/ext/google.py rename to bonobo/contrib/google/__init__.py From 780199551ca1cc9c1ae829eaf2745f6b6e353ca4 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 06:21:28 +0100 Subject: [PATCH 104/145] Moves ODS extension to contrib module. --- bonobo/{ext/opendatasoft.py => contrib/opendatasoft/__init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bonobo/{ext/opendatasoft.py => contrib/opendatasoft/__init__.py} (100%) diff --git a/bonobo/ext/opendatasoft.py b/bonobo/contrib/opendatasoft/__init__.py similarity index 100% rename from bonobo/ext/opendatasoft.py rename to bonobo/contrib/opendatasoft/__init__.py From 96fd4533e19eff4ffac661af1bba517a4a11fb7d Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 06:22:28 +0100 Subject: [PATCH 105/145] Adds a __getattr__ dunder to ValueHolder to enable getting attributes, and especially method calls, on contained objects. --- bonobo/util/objects.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bonobo/util/objects.py b/bonobo/util/objects.py index e4dd29e..209f4db 100644 --- a/bonobo/util/objects.py +++ b/bonobo/util/objects.py @@ -142,10 +142,10 @@ class ValueHolder: return divmod(other, self._value) def __pow__(self, other): - return self._value**other + return self._value ** other def __rpow__(self, other): - return other**self._value + return other ** self._value def __ipow__(self, other): self._value **= other @@ -225,6 +225,9 @@ class ValueHolder: def __setitem__(self, key, value): self._value[key] = value + def __getattr__(self, item): + return getattr(self._value, item) + def get_attribute_or_create(obj, attr, default): try: From f6093e308d8a798b086b676ce0beed5357168b73 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 06:23:19 +0100 Subject: [PATCH 106/145] IOFormats: if no kwargs, then try with one positional argument. --- bonobo/nodes/io/json.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/bonobo/nodes/io/json.py b/bonobo/nodes/io/json.py index bbb89ad..6c2b4b2 100644 --- a/bonobo/nodes/io/json.py +++ b/bonobo/nodes/io/json.py @@ -35,13 +35,14 @@ class JsonWriter(FileWriter, JsonHandler): yield file.write(self.suffix) - def write(self, fs, file, lineno, **row): + def write(self, fs, file, lineno, arg0=None, **kwargs): """ 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 return NOT_MODIFIED @@ -59,7 +60,19 @@ class LdjsonReader(FileReader): class LdjsonWriter(FileWriter): """Write a stream of JSON objects, one object per line.""" - def write(self, fs, file, lineno, **row): - lineno += 1 # class-level variable + 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 + + +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 From 4bea3f7dadd9265f7fdc75365fcb149d693bf431 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 06:24:29 +0100 Subject: [PATCH 107/145] Fix examples with new module paths. --- bonobo/examples/datasets/coffeeshops.py | 2 +- bonobo/examples/datasets/fablabs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bonobo/examples/datasets/coffeeshops.py b/bonobo/examples/datasets/coffeeshops.py index fd754ef..80f2d8d 100644 --- a/bonobo/examples/datasets/coffeeshops.py +++ b/bonobo/examples/datasets/coffeeshops.py @@ -15,7 +15,7 @@ Extracts a list of parisian bars where you can buy a coffee for a reasonable pri import bonobo from bonobo.commands import get_default_services -from bonobo.ext.opendatasoft import OpenDataSoftAPI +from bonobo.contrib.opendatasoft import OpenDataSoftAPI filename = 'coffeeshops.txt' diff --git a/bonobo/examples/datasets/fablabs.py b/bonobo/examples/datasets/fablabs.py index d03775b..71b7da3 100644 --- a/bonobo/examples/datasets/fablabs.py +++ b/bonobo/examples/datasets/fablabs.py @@ -20,7 +20,7 @@ from colorama import Fore, Style import bonobo from bonobo.commands import get_default_services -from bonobo.ext.opendatasoft import OpenDataSoftAPI +from bonobo.contrib.opendatasoft import OpenDataSoftAPI try: import pycountry From d1481fbfe85b4281667ca9d282cfb6d0334139c8 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 09:08:05 +0100 Subject: [PATCH 108/145] Moves jupyter extension to both bonobo.contrib.jupyter (for the jupyter widget) and to bonobo.plugins (for the executor-side plugin). --- .gitignore | 2 +- Makefile | 4 +- Projectfile | 19 ++++----- bonobo/_api.py | 2 +- bonobo/contrib/jupyter/__init__.py | 15 +++++++ bonobo/contrib/jupyter/js/.gitignore | 1 + bonobo/contrib/jupyter/js/README.rst | 19 +++++++++ .../{ext => contrib}/jupyter/js/dist/index.js | 8 ++-- bonobo/contrib/jupyter/js/dist/index.js.map | 1 + .../{ext => contrib}/jupyter/js/package.json | 0 .../{ext => contrib}/jupyter/js/src/bonobo.js | 8 ++-- .../{ext => contrib}/jupyter/js/src/embed.js | 0 .../jupyter/js/src/extension.js | 0 .../{ext => contrib}/jupyter/js/src/index.js | 0 .../jupyter/js/webpack.config.js | 0 bonobo/{ext => contrib}/jupyter/js/yarn.lock | 0 .../jupyter/static/extension.js | 0 .../{ext => contrib}/jupyter/static/index.js | 8 ++-- bonobo/contrib/jupyter/static/index.js.map | 1 + bonobo/{ext => contrib}/jupyter/widget.py | 0 bonobo/execution/contexts/node.py | 8 ++++ bonobo/execution/strategies/executor.py | 4 +- bonobo/ext/__init__.py | 1 - bonobo/ext/jupyter/__init__.py | 10 ----- bonobo/ext/jupyter/js/README.md | 19 --------- bonobo/ext/jupyter/js/dist/index.js.map | 1 - bonobo/ext/jupyter/plugin.py | 26 ------------- bonobo/ext/jupyter/static/index.js.map | 1 - bonobo/plugins/__init__.py | 7 +++- bonobo/plugins/jupyter.py | 35 +++++++++++++++++ bonobo/structs/graphs.py | 39 ++++++++++++------- docs/_templates/sidebarlogo.html | 4 +- docs/conf.py | 2 +- docs/tutorial/1-init.rst | 4 +- requirements-dev.txt | 8 ++-- requirements-docker.txt | 6 +-- requirements-jupyter.txt | 10 ++--- requirements-sqlalchemy.txt | 6 +-- requirements.txt | 9 +++-- setup.py | 16 ++++++-- tests/commands/test_convert.py | 5 ++- tests/ext/test_ods.py | 2 +- 42 files changed, 183 insertions(+), 128 deletions(-) create mode 100644 bonobo/contrib/jupyter/__init__.py create mode 100644 bonobo/contrib/jupyter/js/.gitignore create mode 100644 bonobo/contrib/jupyter/js/README.rst rename bonobo/{ext => contrib}/jupyter/js/dist/index.js (99%) create mode 100644 bonobo/contrib/jupyter/js/dist/index.js.map rename bonobo/{ext => contrib}/jupyter/js/package.json (100%) rename bonobo/{ext => contrib}/jupyter/js/src/bonobo.js (70%) rename bonobo/{ext => contrib}/jupyter/js/src/embed.js (100%) rename bonobo/{ext => contrib}/jupyter/js/src/extension.js (100%) rename bonobo/{ext => contrib}/jupyter/js/src/index.js (100%) rename bonobo/{ext => contrib}/jupyter/js/webpack.config.js (100%) rename bonobo/{ext => contrib}/jupyter/js/yarn.lock (100%) rename bonobo/{ext => contrib}/jupyter/static/extension.js (100%) rename bonobo/{ext => contrib}/jupyter/static/index.js (99%) create mode 100644 bonobo/contrib/jupyter/static/index.js.map rename bonobo/{ext => contrib}/jupyter/widget.py (100%) delete mode 100644 bonobo/ext/__init__.py delete mode 100644 bonobo/ext/jupyter/__init__.py delete mode 100644 bonobo/ext/jupyter/js/README.md delete mode 100644 bonobo/ext/jupyter/js/dist/index.js.map delete mode 100644 bonobo/ext/jupyter/plugin.py delete mode 100644 bonobo/ext/jupyter/static/index.js.map create mode 100644 bonobo/plugins/jupyter.py diff --git a/.gitignore b/.gitignore index f16c58c..db473d4 100644 --- a/.gitignore +++ b/.gitignore @@ -23,8 +23,8 @@ .python-version /.idea /.release +/bonobo/contrib/jupyter/js/node_modules/ /bonobo/examples/work_in_progress/ -/bonobo/ext/jupyter/js/node_modules/ /build/ /coverage.xml /dist/ diff --git a/Makefile b/Makefile index 8ae38cd..445b863 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4.1 on 2017-11-04. +# Generated by Medikit 0.4.1 on 2017-11-12. # All changes will be overriden. PACKAGE ?= bonobo @@ -10,6 +10,7 @@ PYTHON_REQUIREMENTS_DEV_FILE ?= requirements-dev.txt QUICK ?= PIP ?= $(PYTHON_DIRNAME)/pip PIP_INSTALL_OPTIONS ?= +VERSION ?= $(shell git describe 2>/dev/null || git rev-parse --short HEAD) PYTEST ?= $(PYTHON_DIRNAME)/pytest PYTEST_OPTIONS ?= --capture=no --cov=$(PACKAGE) --cov-report html SPHINX_BUILD ?= $(PYTHON_DIRNAME)/sphinx-build @@ -18,7 +19,6 @@ SPHINX_SOURCEDIR ?= docs SPHINX_BUILDDIR ?= $(SPHINX_SOURCEDIR)/_build YAPF ?= $(PYTHON) -m yapf YAPF_OPTIONS ?= -rip -VERSION ?= $(shell git describe 2>/dev/null || echo dev) .PHONY: $(SPHINX_SOURCEDIR) clean format install install-dev test update update-requirements diff --git a/Projectfile b/Projectfile index ca6a0f4..be2e0c8 100644 --- a/Projectfile +++ b/Projectfile @@ -18,9 +18,9 @@ python.setup( data_files=[ ( 'share/jupyter/nbextensions/bonobo-jupyter', [ - 'bonobo/ext/jupyter/static/extension.js', - 'bonobo/ext/jupyter/static/index.js', - 'bonobo/ext/jupyter/static/index.js.map', + 'bonobo/contrib/jupyter/static/extension.js', + 'bonobo/contrib/jupyter/static/index.js', + 'bonobo/contrib/jupyter/static/index.js.map', ] ), ], @@ -41,23 +41,24 @@ python.setup( python.add_requirements( 'fs >=2.0,<2.1', - 'jinja2 >=2.9,<2.10', + 'graphviz >=0.8,<0.9', + 'jinja2 >=2.9,<3', 'mondrian >=0.4,<0.5', 'packaging >=16,<17', - 'psutil >=5.4,<6.0', - 'requests >=2.0,<3.0', + 'psutil >=5.4,<6', + 'requests >=2,<3', 'stevedore >=1.27,<1.28', 'whistle >=1.0,<1.1', dev=[ - 'pytest-sugar >=0.8,<0.9', + 'pytest-sugar >=0.9,<0.10', 'pytest-timeout >=1,<2', ], docker=[ 'bonobo-docker >=0.5.0', ], jupyter=[ - 'jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0,<7', + 'jupyter >=1.0,<1.1', ], sqlalchemy=[ 'bonobo-sqlalchemy >=0.5.1', @@ -66,6 +67,6 @@ python.add_requirements( # Following requirements are not enforced, because some dependencies enforce them so we don't want to break # the packaging in case it changes in dep. -python.add_requirements('colorama >=0.3', ) +python.add_requirements('colorama >=0.3') # vim: ft=python: diff --git a/bonobo/_api.py b/bonobo/_api.py index af92868..925e6ce 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -74,7 +74,7 @@ def run(graph, *, plugins=None, services=None, strategy=None): if _is_jupyter_notebook(): try: - from bonobo.ext.jupyter import JupyterOutputPlugin + from bonobo.contrib.jupyter import JupyterOutputPlugin except ImportError: import logging logging.warning( diff --git a/bonobo/contrib/jupyter/__init__.py b/bonobo/contrib/jupyter/__init__.py new file mode 100644 index 0000000..7e5f892 --- /dev/null +++ b/bonobo/contrib/jupyter/__init__.py @@ -0,0 +1,15 @@ +from bonobo.plugins.jupyter import JupyterOutputPlugin + + +def _jupyter_nbextension_paths(): + return [{ + 'section': 'notebook', + 'src': 'static', + 'dest': 'bonobo-jupyter', + 'require': 'bonobo-jupyter/extension' + }] + + +__all__ = [ + 'JupyterOutputPlugin', +] diff --git a/bonobo/contrib/jupyter/js/.gitignore b/bonobo/contrib/jupyter/js/.gitignore new file mode 100644 index 0000000..07e6e47 --- /dev/null +++ b/bonobo/contrib/jupyter/js/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/bonobo/contrib/jupyter/js/README.rst b/bonobo/contrib/jupyter/js/README.rst new file mode 100644 index 0000000..ff9dda1 --- /dev/null +++ b/bonobo/contrib/jupyter/js/README.rst @@ -0,0 +1,19 @@ +Bonobo within Jupyter +===================== + +Install +------- + +.. code-block:: shell-session + + yarn install + + +Watch mode (for development) +---------------------------- + +.. code-block:: shell-session + + yarn run webpack --watch + + diff --git a/bonobo/ext/jupyter/js/dist/index.js b/bonobo/contrib/jupyter/js/dist/index.js similarity index 99% rename from bonobo/ext/jupyter/js/dist/index.js rename to bonobo/contrib/jupyter/js/dist/index.js index 075db9f..e790612 100644 --- a/bonobo/ext/jupyter/js/dist/index.js +++ b/bonobo/contrib/jupyter/js/dist/index.js @@ -69,7 +69,7 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return // When serialiazing entire widget state for embedding, only values different from the // defaults will be specified. - var BonoboModel = widgets.DOMWidgetModel.extend({ + const BonoboModel = widgets.DOMWidgetModel.extend({ defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, { _model_name: 'BonoboModel', _view_name: 'BonoboView', @@ -81,7 +81,7 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return // Custom View. Renders the widget model. - var BonoboView = widgets.DOMWidgetView.extend({ + const BonoboView = widgets.DOMWidgetView.extend({ render: function () { this.value_changed(); this.model.on('change:value', this.value_changed, this); @@ -89,7 +89,9 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return value_changed: function () { this.$el.html( - this.model.get('value').join('
') + '
' + this.model.get('value').map((key, i) => { + return `` + }).join('\n') + '
${key.status}${key.name}${key.stats}${key.flags}
' ); }, }); diff --git a/bonobo/contrib/jupyter/js/dist/index.js.map b/bonobo/contrib/jupyter/js/dist/index.js.map new file mode 100644 index 0000000..9b05ba5 --- /dev/null +++ b/bonobo/contrib/jupyter/js/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack/bootstrap 86b6cf150e2c47113a10","webpack:///./src/embed.js","webpack:///./src/bonobo.js","webpack:///external \"jupyter-js-widgets\"","webpack:///./~/underscore/underscore.js","webpack:///./package.json"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;;;;;;ACRA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA,iEAAgE,yBAAyB;AACzF,mCAAkC,WAAW,WAAW,SAAS,WAAW,UAAU,WAAW,UAAU;AAC3G,cAAa;AACb;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;;;;;;;ACzCA,gD;;;;;;ACAA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;AACH;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA,wBAAuB,OAAO;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uCAAsC,YAAY;AAClD;AACA;AACA,MAAK;AACL;AACA,wCAAuC,YAAY;AACnD;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA6B,gBAAgB;AAC7C;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA,qDAAoD;AACpD,IAAG;;AAEH;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA,2CAA0C;AAC1C,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,6DAA4D,YAAY;AACxE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA,sBAAqB,gBAAgB;AACrC;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,8CAA6C,YAAY;AACzD;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAsD;AACtD;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAS;AACT;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,0BAA0B;AACpE;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA,sBAAqB,cAAc;AACnC;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,sBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAe,YAAY;AAC3B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,QAAO,eAAe;AACtB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,sBAAqB,eAAe;AACpC;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAsB;AACtB;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA,oBAAmB;AACnB;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,6CAA4C,mBAAmB;AAC/D;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,sDAAqD;AACrD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;AACA;;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8EAA6E;AAC7E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA,sCAAqC;AACrC;AACA;AACA;;AAEA;AACA;AACA;AACA,2BAA0B;AAC1B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,oBAAmB,OAAO;AAC1B;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,gBAAe;AACf,eAAc;AACd,eAAc;AACd,iBAAgB;AAChB,iBAAgB;AAChB,iBAAgB;AAChB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA4B;;AAE5B;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA,QAAO;AACP,sBAAqB;AACrB;;AAEA;AACA;AACA,MAAK;AACL,kBAAiB;;AAEjB;AACA,mDAAkD,EAAE,iBAAiB;;AAErE;AACA,yBAAwB,8BAA8B;AACtD,4BAA2B;;AAE3B;AACA;AACA,MAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,mDAAkD,iBAAiB;;AAEnE;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,EAAC;;;;;;;AC3gDD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA,G","file":"index.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"https://unpkg.com/jupyter-widget-example@0.0.1/dist/\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 86b6cf150e2c47113a10","// Entry point for the unpkg bundle containing custom model definitions.\n//\n// It differs from the notebook bundle in that it does not need to define a\n// dynamic baseURL for the static assets and may load some css that would\n// already be loaded by the notebook otherwise.\n\n// Export widget models and views, and the npm package version number.\nmodule.exports = require('./bonobo.js');\nmodule.exports['version'] = require('../package.json').version;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/embed.js\n// module id = 0\n// module chunks = 0","var widgets = require('jupyter-js-widgets');\nvar _ = require('underscore');\n\n// Custom Model. Custom widgets models must at least provide default values\n// for model attributes, including `_model_name`, `_view_name`, `_model_module`\n// and `_view_module` when different from the base class.\n//\n// When serialiazing entire widget state for embedding, only values different from the\n// defaults will be specified.\n\nconst BonoboModel = widgets.DOMWidgetModel.extend({\n defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, {\n _model_name: 'BonoboModel',\n _view_name: 'BonoboView',\n _model_module: 'bonobo',\n _view_module: 'bonobo',\n value: []\n })\n});\n\n\n// Custom View. Renders the widget model.\nconst BonoboView = widgets.DOMWidgetView.extend({\n render: function () {\n this.value_changed();\n this.model.on('change:value', this.value_changed, this);\n },\n\n value_changed: function () {\n this.$el.html(\n '
' + this.model.get('value').map((key, i) => {\n return ``\n }).join('\\n') + '
${key.status}${key.name}${key.stats}${key.flags}
'\n );\n },\n});\n\n\nmodule.exports = {\n BonoboModel: BonoboModel,\n BonoboView: BonoboView\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/bonobo.js\n// module id = 1\n// module chunks = 0","module.exports = __WEBPACK_EXTERNAL_MODULE_2__;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"jupyter-js-widgets\"\n// module id = 2\n// module chunks = 0","// Underscore.js 1.8.3\n// http://underscorejs.org\n// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n// Underscore may be freely distributed under the MIT license.\n\n(function() {\n\n // Baseline setup\n // --------------\n\n // Establish the root object, `window` in the browser, or `exports` on the server.\n var root = this;\n\n // Save the previous value of the `_` variable.\n var previousUnderscore = root._;\n\n // Save bytes in the minified (but not gzipped) version:\n var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n // Create quick reference variables for speed access to core prototypes.\n var\n push = ArrayProto.push,\n slice = ArrayProto.slice,\n toString = ObjProto.toString,\n hasOwnProperty = ObjProto.hasOwnProperty;\n\n // All **ECMAScript 5** native function implementations that we hope to use\n // are declared here.\n var\n nativeIsArray = Array.isArray,\n nativeKeys = Object.keys,\n nativeBind = FuncProto.bind,\n nativeCreate = Object.create;\n\n // Naked function reference for surrogate-prototype-swapping.\n var Ctor = function(){};\n\n // Create a safe reference to the Underscore object for use below.\n var _ = function(obj) {\n if (obj instanceof _) return obj;\n if (!(this instanceof _)) return new _(obj);\n this._wrapped = obj;\n };\n\n // Export the Underscore object for **Node.js**, with\n // backwards-compatibility for the old `require()` API. If we're in\n // the browser, add `_` as a global object.\n if (typeof exports !== 'undefined') {\n if (typeof module !== 'undefined' && module.exports) {\n exports = module.exports = _;\n }\n exports._ = _;\n } else {\n root._ = _;\n }\n\n // Current version.\n _.VERSION = '1.8.3';\n\n // Internal function that returns an efficient (for current engines) version\n // of the passed-in callback, to be repeatedly applied in other Underscore\n // functions.\n var optimizeCb = function(func, context, argCount) {\n if (context === void 0) return func;\n switch (argCount == null ? 3 : argCount) {\n case 1: return function(value) {\n return func.call(context, value);\n };\n case 2: return function(value, other) {\n return func.call(context, value, other);\n };\n case 3: return function(value, index, collection) {\n return func.call(context, value, index, collection);\n };\n case 4: return function(accumulator, value, index, collection) {\n return func.call(context, accumulator, value, index, collection);\n };\n }\n return function() {\n return func.apply(context, arguments);\n };\n };\n\n // A mostly-internal function to generate callbacks that can be applied\n // to each element in a collection, returning the desired result — either\n // identity, an arbitrary callback, a property matcher, or a property accessor.\n var cb = function(value, context, argCount) {\n if (value == null) return _.identity;\n if (_.isFunction(value)) return optimizeCb(value, context, argCount);\n if (_.isObject(value)) return _.matcher(value);\n return _.property(value);\n };\n _.iteratee = function(value, context) {\n return cb(value, context, Infinity);\n };\n\n // An internal function for creating assigner functions.\n var createAssigner = function(keysFunc, undefinedOnly) {\n return function(obj) {\n var length = arguments.length;\n if (length < 2 || obj == null) return obj;\n for (var index = 1; index < length; index++) {\n var source = arguments[index],\n keys = keysFunc(source),\n l = keys.length;\n for (var i = 0; i < l; i++) {\n var key = keys[i];\n if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];\n }\n }\n return obj;\n };\n };\n\n // An internal function for creating a new object that inherits from another.\n var baseCreate = function(prototype) {\n if (!_.isObject(prototype)) return {};\n if (nativeCreate) return nativeCreate(prototype);\n Ctor.prototype = prototype;\n var result = new Ctor;\n Ctor.prototype = null;\n return result;\n };\n\n var property = function(key) {\n return function(obj) {\n return obj == null ? void 0 : obj[key];\n };\n };\n\n // Helper for collection methods to determine whether a collection\n // should be iterated as an array or as an object\n // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength\n // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094\n var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;\n var getLength = property('length');\n var isArrayLike = function(collection) {\n var length = getLength(collection);\n return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;\n };\n\n // Collection Functions\n // --------------------\n\n // The cornerstone, an `each` implementation, aka `forEach`.\n // Handles raw objects in addition to array-likes. Treats all\n // sparse array-likes as if they were dense.\n _.each = _.forEach = function(obj, iteratee, context) {\n iteratee = optimizeCb(iteratee, context);\n var i, length;\n if (isArrayLike(obj)) {\n for (i = 0, length = obj.length; i < length; i++) {\n iteratee(obj[i], i, obj);\n }\n } else {\n var keys = _.keys(obj);\n for (i = 0, length = keys.length; i < length; i++) {\n iteratee(obj[keys[i]], keys[i], obj);\n }\n }\n return obj;\n };\n\n // Return the results of applying the iteratee to each element.\n _.map = _.collect = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n results = Array(length);\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n results[index] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Create a reducing function iterating left or right.\n function createReduce(dir) {\n // Optimized iterator function as using arguments.length\n // in the main function will deoptimize the, see #1991.\n function iterator(obj, iteratee, memo, keys, index, length) {\n for (; index >= 0 && index < length; index += dir) {\n var currentKey = keys ? keys[index] : index;\n memo = iteratee(memo, obj[currentKey], currentKey, obj);\n }\n return memo;\n }\n\n return function(obj, iteratee, memo, context) {\n iteratee = optimizeCb(iteratee, context, 4);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n index = dir > 0 ? 0 : length - 1;\n // Determine the initial value if none is provided.\n if (arguments.length < 3) {\n memo = obj[keys ? keys[index] : index];\n index += dir;\n }\n return iterator(obj, iteratee, memo, keys, index, length);\n };\n }\n\n // **Reduce** builds up a single result from a list of values, aka `inject`,\n // or `foldl`.\n _.reduce = _.foldl = _.inject = createReduce(1);\n\n // The right-associative version of reduce, also known as `foldr`.\n _.reduceRight = _.foldr = createReduce(-1);\n\n // Return the first value which passes a truth test. Aliased as `detect`.\n _.find = _.detect = function(obj, predicate, context) {\n var key;\n if (isArrayLike(obj)) {\n key = _.findIndex(obj, predicate, context);\n } else {\n key = _.findKey(obj, predicate, context);\n }\n if (key !== void 0 && key !== -1) return obj[key];\n };\n\n // Return all the elements that pass a truth test.\n // Aliased as `select`.\n _.filter = _.select = function(obj, predicate, context) {\n var results = [];\n predicate = cb(predicate, context);\n _.each(obj, function(value, index, list) {\n if (predicate(value, index, list)) results.push(value);\n });\n return results;\n };\n\n // Return all the elements for which a truth test fails.\n _.reject = function(obj, predicate, context) {\n return _.filter(obj, _.negate(cb(predicate)), context);\n };\n\n // Determine whether all of the elements match a truth test.\n // Aliased as `all`.\n _.every = _.all = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (!predicate(obj[currentKey], currentKey, obj)) return false;\n }\n return true;\n };\n\n // Determine if at least one element in the object matches a truth test.\n // Aliased as `any`.\n _.some = _.any = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (predicate(obj[currentKey], currentKey, obj)) return true;\n }\n return false;\n };\n\n // Determine if the array or object contains a given item (using `===`).\n // Aliased as `includes` and `include`.\n _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n if (typeof fromIndex != 'number' || guard) fromIndex = 0;\n return _.indexOf(obj, item, fromIndex) >= 0;\n };\n\n // Invoke a method (with arguments) on every item in a collection.\n _.invoke = function(obj, method) {\n var args = slice.call(arguments, 2);\n var isFunc = _.isFunction(method);\n return _.map(obj, function(value) {\n var func = isFunc ? method : value[method];\n return func == null ? func : func.apply(value, args);\n });\n };\n\n // Convenience version of a common use case of `map`: fetching a property.\n _.pluck = function(obj, key) {\n return _.map(obj, _.property(key));\n };\n\n // Convenience version of a common use case of `filter`: selecting only objects\n // containing specific `key:value` pairs.\n _.where = function(obj, attrs) {\n return _.filter(obj, _.matcher(attrs));\n };\n\n // Convenience version of a common use case of `find`: getting the first object\n // containing specific `key:value` pairs.\n _.findWhere = function(obj, attrs) {\n return _.find(obj, _.matcher(attrs));\n };\n\n // Return the maximum element (or element-based computation).\n _.max = function(obj, iteratee, context) {\n var result = -Infinity, lastComputed = -Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value > result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed > lastComputed || computed === -Infinity && result === -Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Return the minimum element (or element-based computation).\n _.min = function(obj, iteratee, context) {\n var result = Infinity, lastComputed = Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value < result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed < lastComputed || computed === Infinity && result === Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Shuffle a collection, using the modern version of the\n // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).\n _.shuffle = function(obj) {\n var set = isArrayLike(obj) ? obj : _.values(obj);\n var length = set.length;\n var shuffled = Array(length);\n for (var index = 0, rand; index < length; index++) {\n rand = _.random(0, index);\n if (rand !== index) shuffled[index] = shuffled[rand];\n shuffled[rand] = set[index];\n }\n return shuffled;\n };\n\n // Sample **n** random values from a collection.\n // If **n** is not specified, returns a single random element.\n // The internal `guard` argument allows it to work with `map`.\n _.sample = function(obj, n, guard) {\n if (n == null || guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n return obj[_.random(obj.length - 1)];\n }\n return _.shuffle(obj).slice(0, Math.max(0, n));\n };\n\n // Sort the object's values by a criterion produced by an iteratee.\n _.sortBy = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n return _.pluck(_.map(obj, function(value, index, list) {\n return {\n value: value,\n index: index,\n criteria: iteratee(value, index, list)\n };\n }).sort(function(left, right) {\n var a = left.criteria;\n var b = right.criteria;\n if (a !== b) {\n if (a > b || a === void 0) return 1;\n if (a < b || b === void 0) return -1;\n }\n return left.index - right.index;\n }), 'value');\n };\n\n // An internal function used for aggregate \"group by\" operations.\n var group = function(behavior) {\n return function(obj, iteratee, context) {\n var result = {};\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index) {\n var key = iteratee(value, index, obj);\n behavior(result, value, key);\n });\n return result;\n };\n };\n\n // Groups the object's values by a criterion. Pass either a string attribute\n // to group by, or a function that returns the criterion.\n _.groupBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key].push(value); else result[key] = [value];\n });\n\n // Indexes the object's values by a criterion, similar to `groupBy`, but for\n // when you know that your index values will be unique.\n _.indexBy = group(function(result, value, key) {\n result[key] = value;\n });\n\n // Counts instances of an object that group by a certain criterion. Pass\n // either a string attribute to count by, or a function that returns the\n // criterion.\n _.countBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key]++; else result[key] = 1;\n });\n\n // Safely create a real, live array from anything iterable.\n _.toArray = function(obj) {\n if (!obj) return [];\n if (_.isArray(obj)) return slice.call(obj);\n if (isArrayLike(obj)) return _.map(obj, _.identity);\n return _.values(obj);\n };\n\n // Return the number of elements in an object.\n _.size = function(obj) {\n if (obj == null) return 0;\n return isArrayLike(obj) ? obj.length : _.keys(obj).length;\n };\n\n // Split a collection into two arrays: one whose elements all satisfy the given\n // predicate, and one whose elements all do not satisfy the predicate.\n _.partition = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var pass = [], fail = [];\n _.each(obj, function(value, key, obj) {\n (predicate(value, key, obj) ? pass : fail).push(value);\n });\n return [pass, fail];\n };\n\n // Array Functions\n // ---------------\n\n // Get the first element of an array. Passing **n** will return the first N\n // values in the array. Aliased as `head` and `take`. The **guard** check\n // allows it to work with `_.map`.\n _.first = _.head = _.take = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[0];\n return _.initial(array, array.length - n);\n };\n\n // Returns everything but the last entry of the array. Especially useful on\n // the arguments object. Passing **n** will return all the values in\n // the array, excluding the last N.\n _.initial = function(array, n, guard) {\n return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));\n };\n\n // Get the last element of an array. Passing **n** will return the last N\n // values in the array.\n _.last = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[array.length - 1];\n return _.rest(array, Math.max(0, array.length - n));\n };\n\n // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.\n // Especially useful on the arguments object. Passing an **n** will return\n // the rest N values in the array.\n _.rest = _.tail = _.drop = function(array, n, guard) {\n return slice.call(array, n == null || guard ? 1 : n);\n };\n\n // Trim out all falsy values from an array.\n _.compact = function(array) {\n return _.filter(array, _.identity);\n };\n\n // Internal implementation of a recursive `flatten` function.\n var flatten = function(input, shallow, strict, startIndex) {\n var output = [], idx = 0;\n for (var i = startIndex || 0, length = getLength(input); i < length; i++) {\n var value = input[i];\n if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {\n //flatten current level of array or arguments object\n if (!shallow) value = flatten(value, shallow, strict);\n var j = 0, len = value.length;\n output.length += len;\n while (j < len) {\n output[idx++] = value[j++];\n }\n } else if (!strict) {\n output[idx++] = value;\n }\n }\n return output;\n };\n\n // Flatten out an array, either recursively (by default), or just one level.\n _.flatten = function(array, shallow) {\n return flatten(array, shallow, false);\n };\n\n // Return a version of the array that does not contain the specified value(s).\n _.without = function(array) {\n return _.difference(array, slice.call(arguments, 1));\n };\n\n // Produce a duplicate-free version of the array. If the array has already\n // been sorted, you have the option of using a faster algorithm.\n // Aliased as `unique`.\n _.uniq = _.unique = function(array, isSorted, iteratee, context) {\n if (!_.isBoolean(isSorted)) {\n context = iteratee;\n iteratee = isSorted;\n isSorted = false;\n }\n if (iteratee != null) iteratee = cb(iteratee, context);\n var result = [];\n var seen = [];\n for (var i = 0, length = getLength(array); i < length; i++) {\n var value = array[i],\n computed = iteratee ? iteratee(value, i, array) : value;\n if (isSorted) {\n if (!i || seen !== computed) result.push(value);\n seen = computed;\n } else if (iteratee) {\n if (!_.contains(seen, computed)) {\n seen.push(computed);\n result.push(value);\n }\n } else if (!_.contains(result, value)) {\n result.push(value);\n }\n }\n return result;\n };\n\n // Produce an array that contains the union: each distinct element from all of\n // the passed-in arrays.\n _.union = function() {\n return _.uniq(flatten(arguments, true, true));\n };\n\n // Produce an array that contains every item shared between all the\n // passed-in arrays.\n _.intersection = function(array) {\n var result = [];\n var argsLength = arguments.length;\n for (var i = 0, length = getLength(array); i < length; i++) {\n var item = array[i];\n if (_.contains(result, item)) continue;\n for (var j = 1; j < argsLength; j++) {\n if (!_.contains(arguments[j], item)) break;\n }\n if (j === argsLength) result.push(item);\n }\n return result;\n };\n\n // Take the difference between one array and a number of other arrays.\n // Only the elements present in just the first array will remain.\n _.difference = function(array) {\n var rest = flatten(arguments, true, true, 1);\n return _.filter(array, function(value){\n return !_.contains(rest, value);\n });\n };\n\n // Zip together multiple lists into a single array -- elements that share\n // an index go together.\n _.zip = function() {\n return _.unzip(arguments);\n };\n\n // Complement of _.zip. Unzip accepts an array of arrays and groups\n // each array's elements on shared indices\n _.unzip = function(array) {\n var length = array && _.max(array, getLength).length || 0;\n var result = Array(length);\n\n for (var index = 0; index < length; index++) {\n result[index] = _.pluck(array, index);\n }\n return result;\n };\n\n // Converts lists into objects. Pass either a single array of `[key, value]`\n // pairs, or two parallel arrays of the same length -- one of keys, and one of\n // the corresponding values.\n _.object = function(list, values) {\n var result = {};\n for (var i = 0, length = getLength(list); i < length; i++) {\n if (values) {\n result[list[i]] = values[i];\n } else {\n result[list[i][0]] = list[i][1];\n }\n }\n return result;\n };\n\n // Generator function to create the findIndex and findLastIndex functions\n function createPredicateIndexFinder(dir) {\n return function(array, predicate, context) {\n predicate = cb(predicate, context);\n var length = getLength(array);\n var index = dir > 0 ? 0 : length - 1;\n for (; index >= 0 && index < length; index += dir) {\n if (predicate(array[index], index, array)) return index;\n }\n return -1;\n };\n }\n\n // Returns the first index on an array-like that passes a predicate test\n _.findIndex = createPredicateIndexFinder(1);\n _.findLastIndex = createPredicateIndexFinder(-1);\n\n // Use a comparator function to figure out the smallest index at which\n // an object should be inserted so as to maintain order. Uses binary search.\n _.sortedIndex = function(array, obj, iteratee, context) {\n iteratee = cb(iteratee, context, 1);\n var value = iteratee(obj);\n var low = 0, high = getLength(array);\n while (low < high) {\n var mid = Math.floor((low + high) / 2);\n if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;\n }\n return low;\n };\n\n // Generator function to create the indexOf and lastIndexOf functions\n function createIndexFinder(dir, predicateFind, sortedIndex) {\n return function(array, item, idx) {\n var i = 0, length = getLength(array);\n if (typeof idx == 'number') {\n if (dir > 0) {\n i = idx >= 0 ? idx : Math.max(idx + length, i);\n } else {\n length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;\n }\n } else if (sortedIndex && idx && length) {\n idx = sortedIndex(array, item);\n return array[idx] === item ? idx : -1;\n }\n if (item !== item) {\n idx = predicateFind(slice.call(array, i, length), _.isNaN);\n return idx >= 0 ? idx + i : -1;\n }\n for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {\n if (array[idx] === item) return idx;\n }\n return -1;\n };\n }\n\n // Return the position of the first occurrence of an item in an array,\n // or -1 if the item is not included in the array.\n // If the array is large and already in sort order, pass `true`\n // for **isSorted** to use binary search.\n _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);\n _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);\n\n // Generate an integer Array containing an arithmetic progression. A port of\n // the native Python `range()` function. See\n // [the Python documentation](http://docs.python.org/library/functions.html#range).\n _.range = function(start, stop, step) {\n if (stop == null) {\n stop = start || 0;\n start = 0;\n }\n step = step || 1;\n\n var length = Math.max(Math.ceil((stop - start) / step), 0);\n var range = Array(length);\n\n for (var idx = 0; idx < length; idx++, start += step) {\n range[idx] = start;\n }\n\n return range;\n };\n\n // Function (ahem) Functions\n // ------------------\n\n // Determines whether to execute a function as a constructor\n // or a normal function with the provided arguments\n var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {\n if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);\n var self = baseCreate(sourceFunc.prototype);\n var result = sourceFunc.apply(self, args);\n if (_.isObject(result)) return result;\n return self;\n };\n\n // Create a function bound to a given object (assigning `this`, and arguments,\n // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if\n // available.\n _.bind = function(func, context) {\n if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');\n var args = slice.call(arguments, 2);\n var bound = function() {\n return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));\n };\n return bound;\n };\n\n // Partially apply a function by creating a version that has had some of its\n // arguments pre-filled, without changing its dynamic `this` context. _ acts\n // as a placeholder, allowing any combination of arguments to be pre-filled.\n _.partial = function(func) {\n var boundArgs = slice.call(arguments, 1);\n var bound = function() {\n var position = 0, length = boundArgs.length;\n var args = Array(length);\n for (var i = 0; i < length; i++) {\n args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];\n }\n while (position < arguments.length) args.push(arguments[position++]);\n return executeBound(func, bound, this, this, args);\n };\n return bound;\n };\n\n // Bind a number of an object's methods to that object. Remaining arguments\n // are the method names to be bound. Useful for ensuring that all callbacks\n // defined on an object belong to it.\n _.bindAll = function(obj) {\n var i, length = arguments.length, key;\n if (length <= 1) throw new Error('bindAll must be passed function names');\n for (i = 1; i < length; i++) {\n key = arguments[i];\n obj[key] = _.bind(obj[key], obj);\n }\n return obj;\n };\n\n // Memoize an expensive function by storing its results.\n _.memoize = function(func, hasher) {\n var memoize = function(key) {\n var cache = memoize.cache;\n var address = '' + (hasher ? hasher.apply(this, arguments) : key);\n if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);\n return cache[address];\n };\n memoize.cache = {};\n return memoize;\n };\n\n // Delays a function for the given number of milliseconds, and then calls\n // it with the arguments supplied.\n _.delay = function(func, wait) {\n var args = slice.call(arguments, 2);\n return setTimeout(function(){\n return func.apply(null, args);\n }, wait);\n };\n\n // Defers a function, scheduling it to run after the current call stack has\n // cleared.\n _.defer = _.partial(_.delay, _, 1);\n\n // Returns a function, that, when invoked, will only be triggered at most once\n // during a given window of time. Normally, the throttled function will run\n // as much as it can, without ever going more than once per `wait` duration;\n // but if you'd like to disable the execution on the leading edge, pass\n // `{leading: false}`. To disable execution on the trailing edge, ditto.\n _.throttle = function(func, wait, options) {\n var context, args, result;\n var timeout = null;\n var previous = 0;\n if (!options) options = {};\n var later = function() {\n previous = options.leading === false ? 0 : _.now();\n timeout = null;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n };\n return function() {\n var now = _.now();\n if (!previous && options.leading === false) previous = now;\n var remaining = wait - (now - previous);\n context = this;\n args = arguments;\n if (remaining <= 0 || remaining > wait) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n previous = now;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n } else if (!timeout && options.trailing !== false) {\n timeout = setTimeout(later, remaining);\n }\n return result;\n };\n };\n\n // Returns a function, that, as long as it continues to be invoked, will not\n // be triggered. The function will be called after it stops being called for\n // N milliseconds. If `immediate` is passed, trigger the function on the\n // leading edge, instead of the trailing.\n _.debounce = function(func, wait, immediate) {\n var timeout, args, context, timestamp, result;\n\n var later = function() {\n var last = _.now() - timestamp;\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last);\n } else {\n timeout = null;\n if (!immediate) {\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n }\n }\n };\n\n return function() {\n context = this;\n args = arguments;\n timestamp = _.now();\n var callNow = immediate && !timeout;\n if (!timeout) timeout = setTimeout(later, wait);\n if (callNow) {\n result = func.apply(context, args);\n context = args = null;\n }\n\n return result;\n };\n };\n\n // Returns the first function passed as an argument to the second,\n // allowing you to adjust arguments, run code before and after, and\n // conditionally execute the original function.\n _.wrap = function(func, wrapper) {\n return _.partial(wrapper, func);\n };\n\n // Returns a negated version of the passed-in predicate.\n _.negate = function(predicate) {\n return function() {\n return !predicate.apply(this, arguments);\n };\n };\n\n // Returns a function that is the composition of a list of functions, each\n // consuming the return value of the function that follows.\n _.compose = function() {\n var args = arguments;\n var start = args.length - 1;\n return function() {\n var i = start;\n var result = args[start].apply(this, arguments);\n while (i--) result = args[i].call(this, result);\n return result;\n };\n };\n\n // Returns a function that will only be executed on and after the Nth call.\n _.after = function(times, func) {\n return function() {\n if (--times < 1) {\n return func.apply(this, arguments);\n }\n };\n };\n\n // Returns a function that will only be executed up to (but not including) the Nth call.\n _.before = function(times, func) {\n var memo;\n return function() {\n if (--times > 0) {\n memo = func.apply(this, arguments);\n }\n if (times <= 1) func = null;\n return memo;\n };\n };\n\n // Returns a function that will be executed at most one time, no matter how\n // often you call it. Useful for lazy initialization.\n _.once = _.partial(_.before, 2);\n\n // Object Functions\n // ----------------\n\n // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.\n var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');\n var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',\n 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];\n\n function collectNonEnumProps(obj, keys) {\n var nonEnumIdx = nonEnumerableProps.length;\n var constructor = obj.constructor;\n var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;\n\n // Constructor is a special case.\n var prop = 'constructor';\n if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);\n\n while (nonEnumIdx--) {\n prop = nonEnumerableProps[nonEnumIdx];\n if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {\n keys.push(prop);\n }\n }\n }\n\n // Retrieve the names of an object's own properties.\n // Delegates to **ECMAScript 5**'s native `Object.keys`\n _.keys = function(obj) {\n if (!_.isObject(obj)) return [];\n if (nativeKeys) return nativeKeys(obj);\n var keys = [];\n for (var key in obj) if (_.has(obj, key)) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve all the property names of an object.\n _.allKeys = function(obj) {\n if (!_.isObject(obj)) return [];\n var keys = [];\n for (var key in obj) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve the values of an object's properties.\n _.values = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var values = Array(length);\n for (var i = 0; i < length; i++) {\n values[i] = obj[keys[i]];\n }\n return values;\n };\n\n // Returns the results of applying the iteratee to each element of the object\n // In contrast to _.map it returns an object\n _.mapObject = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = _.keys(obj),\n length = keys.length,\n results = {},\n currentKey;\n for (var index = 0; index < length; index++) {\n currentKey = keys[index];\n results[currentKey] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Convert an object into a list of `[key, value]` pairs.\n _.pairs = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var pairs = Array(length);\n for (var i = 0; i < length; i++) {\n pairs[i] = [keys[i], obj[keys[i]]];\n }\n return pairs;\n };\n\n // Invert the keys and values of an object. The values must be serializable.\n _.invert = function(obj) {\n var result = {};\n var keys = _.keys(obj);\n for (var i = 0, length = keys.length; i < length; i++) {\n result[obj[keys[i]]] = keys[i];\n }\n return result;\n };\n\n // Return a sorted list of the function names available on the object.\n // Aliased as `methods`\n _.functions = _.methods = function(obj) {\n var names = [];\n for (var key in obj) {\n if (_.isFunction(obj[key])) names.push(key);\n }\n return names.sort();\n };\n\n // Extend a given object with all the properties in passed-in object(s).\n _.extend = createAssigner(_.allKeys);\n\n // Assigns a given object with all the own properties in the passed-in object(s)\n // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)\n _.extendOwn = _.assign = createAssigner(_.keys);\n\n // Returns the first key on an object that passes a predicate test\n _.findKey = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = _.keys(obj), key;\n for (var i = 0, length = keys.length; i < length; i++) {\n key = keys[i];\n if (predicate(obj[key], key, obj)) return key;\n }\n };\n\n // Return a copy of the object only containing the whitelisted properties.\n _.pick = function(object, oiteratee, context) {\n var result = {}, obj = object, iteratee, keys;\n if (obj == null) return result;\n if (_.isFunction(oiteratee)) {\n keys = _.allKeys(obj);\n iteratee = optimizeCb(oiteratee, context);\n } else {\n keys = flatten(arguments, false, false, 1);\n iteratee = function(value, key, obj) { return key in obj; };\n obj = Object(obj);\n }\n for (var i = 0, length = keys.length; i < length; i++) {\n var key = keys[i];\n var value = obj[key];\n if (iteratee(value, key, obj)) result[key] = value;\n }\n return result;\n };\n\n // Return a copy of the object without the blacklisted properties.\n _.omit = function(obj, iteratee, context) {\n if (_.isFunction(iteratee)) {\n iteratee = _.negate(iteratee);\n } else {\n var keys = _.map(flatten(arguments, false, false, 1), String);\n iteratee = function(value, key) {\n return !_.contains(keys, key);\n };\n }\n return _.pick(obj, iteratee, context);\n };\n\n // Fill in a given object with default properties.\n _.defaults = createAssigner(_.allKeys, true);\n\n // Creates an object that inherits from the given prototype object.\n // If additional properties are provided then they will be added to the\n // created object.\n _.create = function(prototype, props) {\n var result = baseCreate(prototype);\n if (props) _.extendOwn(result, props);\n return result;\n };\n\n // Create a (shallow-cloned) duplicate of an object.\n _.clone = function(obj) {\n if (!_.isObject(obj)) return obj;\n return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n };\n\n // Invokes interceptor with the obj, and then returns obj.\n // The primary purpose of this method is to \"tap into\" a method chain, in\n // order to perform operations on intermediate results within the chain.\n _.tap = function(obj, interceptor) {\n interceptor(obj);\n return obj;\n };\n\n // Returns whether an object has a given set of `key:value` pairs.\n _.isMatch = function(object, attrs) {\n var keys = _.keys(attrs), length = keys.length;\n if (object == null) return !length;\n var obj = Object(object);\n for (var i = 0; i < length; i++) {\n var key = keys[i];\n if (attrs[key] !== obj[key] || !(key in obj)) return false;\n }\n return true;\n };\n\n\n // Internal recursive comparison function for `isEqual`.\n var eq = function(a, b, aStack, bStack) {\n // Identical objects are equal. `0 === -0`, but they aren't identical.\n // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).\n if (a === b) return a !== 0 || 1 / a === 1 / b;\n // A strict comparison is necessary because `null == undefined`.\n if (a == null || b == null) return a === b;\n // Unwrap any wrapped objects.\n if (a instanceof _) a = a._wrapped;\n if (b instanceof _) b = b._wrapped;\n // Compare `[[Class]]` names.\n var className = toString.call(a);\n if (className !== toString.call(b)) return false;\n switch (className) {\n // Strings, numbers, regular expressions, dates, and booleans are compared by value.\n case '[object RegExp]':\n // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')\n case '[object String]':\n // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n // equivalent to `new String(\"5\")`.\n return '' + a === '' + b;\n case '[object Number]':\n // `NaN`s are equivalent, but non-reflexive.\n // Object(NaN) is equivalent to NaN\n if (+a !== +a) return +b !== +b;\n // An `egal` comparison is performed for other numeric values.\n return +a === 0 ? 1 / +a === 1 / b : +a === +b;\n case '[object Date]':\n case '[object Boolean]':\n // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n // millisecond representations. Note that invalid dates with millisecond representations\n // of `NaN` are not equivalent.\n return +a === +b;\n }\n\n var areArrays = className === '[object Array]';\n if (!areArrays) {\n if (typeof a != 'object' || typeof b != 'object') return false;\n\n // Objects with different constructors are not equivalent, but `Object`s or `Array`s\n // from different frames are.\n var aCtor = a.constructor, bCtor = b.constructor;\n if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&\n _.isFunction(bCtor) && bCtor instanceof bCtor)\n && ('constructor' in a && 'constructor' in b)) {\n return false;\n }\n }\n // Assume equality for cyclic structures. The algorithm for detecting cyclic\n // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n\n // Initializing stack of traversed objects.\n // It's done here since we only need them for objects and arrays comparison.\n aStack = aStack || [];\n bStack = bStack || [];\n var length = aStack.length;\n while (length--) {\n // Linear search. Performance is inversely proportional to the number of\n // unique nested structures.\n if (aStack[length] === a) return bStack[length] === b;\n }\n\n // Add the first object to the stack of traversed objects.\n aStack.push(a);\n bStack.push(b);\n\n // Recursively compare objects and arrays.\n if (areArrays) {\n // Compare array lengths to determine if a deep comparison is necessary.\n length = a.length;\n if (length !== b.length) return false;\n // Deep compare the contents, ignoring non-numeric properties.\n while (length--) {\n if (!eq(a[length], b[length], aStack, bStack)) return false;\n }\n } else {\n // Deep compare objects.\n var keys = _.keys(a), key;\n length = keys.length;\n // Ensure that both objects contain the same number of properties before comparing deep equality.\n if (_.keys(b).length !== length) return false;\n while (length--) {\n // Deep compare each member\n key = keys[length];\n if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;\n }\n }\n // Remove the first object from the stack of traversed objects.\n aStack.pop();\n bStack.pop();\n return true;\n };\n\n // Perform a deep comparison to check if two objects are equal.\n _.isEqual = function(a, b) {\n return eq(a, b);\n };\n\n // Is a given array, string, or object empty?\n // An \"empty\" object has no enumerable own-properties.\n _.isEmpty = function(obj) {\n if (obj == null) return true;\n if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;\n return _.keys(obj).length === 0;\n };\n\n // Is a given value a DOM element?\n _.isElement = function(obj) {\n return !!(obj && obj.nodeType === 1);\n };\n\n // Is a given value an array?\n // Delegates to ECMA5's native Array.isArray\n _.isArray = nativeIsArray || function(obj) {\n return toString.call(obj) === '[object Array]';\n };\n\n // Is a given variable an object?\n _.isObject = function(obj) {\n var type = typeof obj;\n return type === 'function' || type === 'object' && !!obj;\n };\n\n // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.\n _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {\n _['is' + name] = function(obj) {\n return toString.call(obj) === '[object ' + name + ']';\n };\n });\n\n // Define a fallback version of the method in browsers (ahem, IE < 9), where\n // there isn't any inspectable \"Arguments\" type.\n if (!_.isArguments(arguments)) {\n _.isArguments = function(obj) {\n return _.has(obj, 'callee');\n };\n }\n\n // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,\n // IE 11 (#1621), and in Safari 8 (#1929).\n if (typeof /./ != 'function' && typeof Int8Array != 'object') {\n _.isFunction = function(obj) {\n return typeof obj == 'function' || false;\n };\n }\n\n // Is a given object a finite number?\n _.isFinite = function(obj) {\n return isFinite(obj) && !isNaN(parseFloat(obj));\n };\n\n // Is the given value `NaN`? (NaN is the only number which does not equal itself).\n _.isNaN = function(obj) {\n return _.isNumber(obj) && obj !== +obj;\n };\n\n // Is a given value a boolean?\n _.isBoolean = function(obj) {\n return obj === true || obj === false || toString.call(obj) === '[object Boolean]';\n };\n\n // Is a given value equal to null?\n _.isNull = function(obj) {\n return obj === null;\n };\n\n // Is a given variable undefined?\n _.isUndefined = function(obj) {\n return obj === void 0;\n };\n\n // Shortcut function for checking if an object has a given property directly\n // on itself (in other words, not on a prototype).\n _.has = function(obj, key) {\n return obj != null && hasOwnProperty.call(obj, key);\n };\n\n // Utility Functions\n // -----------------\n\n // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n // previous owner. Returns a reference to the Underscore object.\n _.noConflict = function() {\n root._ = previousUnderscore;\n return this;\n };\n\n // Keep the identity function around for default iteratees.\n _.identity = function(value) {\n return value;\n };\n\n // Predicate-generating functions. Often useful outside of Underscore.\n _.constant = function(value) {\n return function() {\n return value;\n };\n };\n\n _.noop = function(){};\n\n _.property = property;\n\n // Generates a function for a given object that returns a given property.\n _.propertyOf = function(obj) {\n return obj == null ? function(){} : function(key) {\n return obj[key];\n };\n };\n\n // Returns a predicate for checking whether an object has a given set of\n // `key:value` pairs.\n _.matcher = _.matches = function(attrs) {\n attrs = _.extendOwn({}, attrs);\n return function(obj) {\n return _.isMatch(obj, attrs);\n };\n };\n\n // Run a function **n** times.\n _.times = function(n, iteratee, context) {\n var accum = Array(Math.max(0, n));\n iteratee = optimizeCb(iteratee, context, 1);\n for (var i = 0; i < n; i++) accum[i] = iteratee(i);\n return accum;\n };\n\n // Return a random integer between min and max (inclusive).\n _.random = function(min, max) {\n if (max == null) {\n max = min;\n min = 0;\n }\n return min + Math.floor(Math.random() * (max - min + 1));\n };\n\n // A (possibly faster) way to get the current timestamp as an integer.\n _.now = Date.now || function() {\n return new Date().getTime();\n };\n\n // List of HTML entities for escaping.\n var escapeMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '`': '`'\n };\n var unescapeMap = _.invert(escapeMap);\n\n // Functions for escaping and unescaping strings to/from HTML interpolation.\n var createEscaper = function(map) {\n var escaper = function(match) {\n return map[match];\n };\n // Regexes for identifying a key that needs to be escaped\n var source = '(?:' + _.keys(map).join('|') + ')';\n var testRegexp = RegExp(source);\n var replaceRegexp = RegExp(source, 'g');\n return function(string) {\n string = string == null ? '' : '' + string;\n return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;\n };\n };\n _.escape = createEscaper(escapeMap);\n _.unescape = createEscaper(unescapeMap);\n\n // If the value of the named `property` is a function then invoke it with the\n // `object` as context; otherwise, return it.\n _.result = function(object, property, fallback) {\n var value = object == null ? void 0 : object[property];\n if (value === void 0) {\n value = fallback;\n }\n return _.isFunction(value) ? value.call(object) : value;\n };\n\n // Generate a unique integer id (unique within the entire client session).\n // Useful for temporary DOM ids.\n var idCounter = 0;\n _.uniqueId = function(prefix) {\n var id = ++idCounter + '';\n return prefix ? prefix + id : id;\n };\n\n // By default, Underscore uses ERB-style template delimiters, change the\n // following template settings to use alternative delimiters.\n _.templateSettings = {\n evaluate : /<%([\\s\\S]+?)%>/g,\n interpolate : /<%=([\\s\\S]+?)%>/g,\n escape : /<%-([\\s\\S]+?)%>/g\n };\n\n // When customizing `templateSettings`, if you don't want to define an\n // interpolation, evaluation or escaping regex, we need one that is\n // guaranteed not to match.\n var noMatch = /(.)^/;\n\n // Certain characters need to be escaped so that they can be put into a\n // string literal.\n var escapes = {\n \"'\": \"'\",\n '\\\\': '\\\\',\n '\\r': 'r',\n '\\n': 'n',\n '\\u2028': 'u2028',\n '\\u2029': 'u2029'\n };\n\n var escaper = /\\\\|'|\\r|\\n|\\u2028|\\u2029/g;\n\n var escapeChar = function(match) {\n return '\\\\' + escapes[match];\n };\n\n // JavaScript micro-templating, similar to John Resig's implementation.\n // Underscore templating handles arbitrary delimiters, preserves whitespace,\n // and correctly escapes quotes within interpolated code.\n // NB: `oldSettings` only exists for backwards compatibility.\n _.template = function(text, settings, oldSettings) {\n if (!settings && oldSettings) settings = oldSettings;\n settings = _.defaults({}, settings, _.templateSettings);\n\n // Combine delimiters into one regular expression via alternation.\n var matcher = RegExp([\n (settings.escape || noMatch).source,\n (settings.interpolate || noMatch).source,\n (settings.evaluate || noMatch).source\n ].join('|') + '|$', 'g');\n\n // Compile the template source, escaping string literals appropriately.\n var index = 0;\n var source = \"__p+='\";\n text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {\n source += text.slice(index, offset).replace(escaper, escapeChar);\n index = offset + match.length;\n\n if (escape) {\n source += \"'+\\n((__t=(\" + escape + \"))==null?'':_.escape(__t))+\\n'\";\n } else if (interpolate) {\n source += \"'+\\n((__t=(\" + interpolate + \"))==null?'':__t)+\\n'\";\n } else if (evaluate) {\n source += \"';\\n\" + evaluate + \"\\n__p+='\";\n }\n\n // Adobe VMs need the match returned to produce the correct offest.\n return match;\n });\n source += \"';\\n\";\n\n // If a variable is not specified, place data values in local scope.\n if (!settings.variable) source = 'with(obj||{}){\\n' + source + '}\\n';\n\n source = \"var __t,__p='',__j=Array.prototype.join,\" +\n \"print=function(){__p+=__j.call(arguments,'');};\\n\" +\n source + 'return __p;\\n';\n\n try {\n var render = new Function(settings.variable || 'obj', '_', source);\n } catch (e) {\n e.source = source;\n throw e;\n }\n\n var template = function(data) {\n return render.call(this, data, _);\n };\n\n // Provide the compiled source as a convenience for precompilation.\n var argument = settings.variable || 'obj';\n template.source = 'function(' + argument + '){\\n' + source + '}';\n\n return template;\n };\n\n // Add a \"chain\" function. Start chaining a wrapped Underscore object.\n _.chain = function(obj) {\n var instance = _(obj);\n instance._chain = true;\n return instance;\n };\n\n // OOP\n // ---------------\n // If Underscore is called as a function, it returns a wrapped object that\n // can be used OO-style. This wrapper holds altered versions of all the\n // underscore functions. Wrapped objects may be chained.\n\n // Helper function to continue chaining intermediate results.\n var result = function(instance, obj) {\n return instance._chain ? _(obj).chain() : obj;\n };\n\n // Add your own custom functions to the Underscore object.\n _.mixin = function(obj) {\n _.each(_.functions(obj), function(name) {\n var func = _[name] = obj[name];\n _.prototype[name] = function() {\n var args = [this._wrapped];\n push.apply(args, arguments);\n return result(this, func.apply(_, args));\n };\n });\n };\n\n // Add all of the Underscore functions to the wrapper object.\n _.mixin(_);\n\n // Add all mutator Array functions to the wrapper.\n _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n var obj = this._wrapped;\n method.apply(obj, arguments);\n if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];\n return result(this, obj);\n };\n });\n\n // Add all accessor Array functions to the wrapper.\n _.each(['concat', 'join', 'slice'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n return result(this, method.apply(this._wrapped, arguments));\n };\n });\n\n // Extracts the result from a wrapped and chained object.\n _.prototype.value = function() {\n return this._wrapped;\n };\n\n // Provide unwrapping proxy for some methods used in engine operations\n // such as arithmetic and JSON stringification.\n _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;\n\n _.prototype.toString = function() {\n return '' + this._wrapped;\n };\n\n // AMD registration happens at the end for compatibility with AMD loaders\n // that may not enforce next-turn semantics on modules. Even though general\n // practice for AMD registration is to be anonymous, underscore registers\n // as a named module because, like jQuery, it is a base library that is\n // popular enough to be bundled in a third party lib, but not be part of\n // an AMD load request. Those cases could generate an error when an\n // anonymous define() is called outside of a loader request.\n if (typeof define === 'function' && define.amd) {\n define('underscore', [], function() {\n return _;\n });\n }\n}.call(this));\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/underscore/underscore.js\n// module id = 3\n// module chunks = 0","module.exports = {\n\t\"name\": \"bonobo-jupyter\",\n\t\"version\": \"0.0.1\",\n\t\"description\": \"Jupyter integration for Bonobo\",\n\t\"author\": \"\",\n\t\"main\": \"src/index.js\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"\"\n\t},\n\t\"keywords\": [\n\t\t\"jupyter\",\n\t\t\"widgets\",\n\t\t\"ipython\",\n\t\t\"ipywidgets\"\n\t],\n\t\"scripts\": {\n\t\t\"prepublish\": \"webpack\",\n\t\t\"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"json-loader\": \"^0.5.4\",\n\t\t\"webpack\": \"^1.12.14\"\n\t},\n\t\"dependencies\": {\n\t\t\"jupyter-js-widgets\": \"^2.0.9\",\n\t\t\"underscore\": \"^1.8.3\"\n\t}\n};\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./package.json\n// module id = 4\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file diff --git a/bonobo/ext/jupyter/js/package.json b/bonobo/contrib/jupyter/js/package.json similarity index 100% rename from bonobo/ext/jupyter/js/package.json rename to bonobo/contrib/jupyter/js/package.json diff --git a/bonobo/ext/jupyter/js/src/bonobo.js b/bonobo/contrib/jupyter/js/src/bonobo.js similarity index 70% rename from bonobo/ext/jupyter/js/src/bonobo.js rename to bonobo/contrib/jupyter/js/src/bonobo.js index 7e75be2..78e9c71 100644 --- a/bonobo/ext/jupyter/js/src/bonobo.js +++ b/bonobo/contrib/jupyter/js/src/bonobo.js @@ -8,7 +8,7 @@ var _ = require('underscore'); // When serialiazing entire widget state for embedding, only values different from the // defaults will be specified. -var BonoboModel = widgets.DOMWidgetModel.extend({ +const BonoboModel = widgets.DOMWidgetModel.extend({ defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, { _model_name: 'BonoboModel', _view_name: 'BonoboView', @@ -20,7 +20,7 @@ var BonoboModel = widgets.DOMWidgetModel.extend({ // Custom View. Renders the widget model. -var BonoboView = widgets.DOMWidgetView.extend({ +const BonoboView = widgets.DOMWidgetView.extend({ render: function () { this.value_changed(); this.model.on('change:value', this.value_changed, this); @@ -28,7 +28,9 @@ var BonoboView = widgets.DOMWidgetView.extend({ value_changed: function () { this.$el.html( - this.model.get('value').join('
') + '
' + this.model.get('value').map((key, i) => { + return `` + }).join('\n') + '
${key.status}${key.name}${key.stats}${key.flags}
' ); }, }); diff --git a/bonobo/ext/jupyter/js/src/embed.js b/bonobo/contrib/jupyter/js/src/embed.js similarity index 100% rename from bonobo/ext/jupyter/js/src/embed.js rename to bonobo/contrib/jupyter/js/src/embed.js diff --git a/bonobo/ext/jupyter/js/src/extension.js b/bonobo/contrib/jupyter/js/src/extension.js similarity index 100% rename from bonobo/ext/jupyter/js/src/extension.js rename to bonobo/contrib/jupyter/js/src/extension.js diff --git a/bonobo/ext/jupyter/js/src/index.js b/bonobo/contrib/jupyter/js/src/index.js similarity index 100% rename from bonobo/ext/jupyter/js/src/index.js rename to bonobo/contrib/jupyter/js/src/index.js diff --git a/bonobo/ext/jupyter/js/webpack.config.js b/bonobo/contrib/jupyter/js/webpack.config.js similarity index 100% rename from bonobo/ext/jupyter/js/webpack.config.js rename to bonobo/contrib/jupyter/js/webpack.config.js diff --git a/bonobo/ext/jupyter/js/yarn.lock b/bonobo/contrib/jupyter/js/yarn.lock similarity index 100% rename from bonobo/ext/jupyter/js/yarn.lock rename to bonobo/contrib/jupyter/js/yarn.lock diff --git a/bonobo/ext/jupyter/static/extension.js b/bonobo/contrib/jupyter/static/extension.js similarity index 100% rename from bonobo/ext/jupyter/static/extension.js rename to bonobo/contrib/jupyter/static/extension.js diff --git a/bonobo/ext/jupyter/static/index.js b/bonobo/contrib/jupyter/static/index.js similarity index 99% rename from bonobo/ext/jupyter/static/index.js rename to bonobo/contrib/jupyter/static/index.js index 75e4b99..8714e24 100644 --- a/bonobo/ext/jupyter/static/index.js +++ b/bonobo/contrib/jupyter/static/index.js @@ -72,7 +72,7 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return // When serialiazing entire widget state for embedding, only values different from the // defaults will be specified. - var BonoboModel = widgets.DOMWidgetModel.extend({ + const BonoboModel = widgets.DOMWidgetModel.extend({ defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, { _model_name: 'BonoboModel', _view_name: 'BonoboView', @@ -84,7 +84,7 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return // Custom View. Renders the widget model. - var BonoboView = widgets.DOMWidgetView.extend({ + const BonoboView = widgets.DOMWidgetView.extend({ render: function () { this.value_changed(); this.model.on('change:value', this.value_changed, this); @@ -92,7 +92,9 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return value_changed: function () { this.$el.html( - this.model.get('value').join('
') + '
' + this.model.get('value').map((key, i) => { + return `` + }).join('\n') + '
${key.status}${key.name}${key.stats}${key.flags}
' ); }, }); diff --git a/bonobo/contrib/jupyter/static/index.js.map b/bonobo/contrib/jupyter/static/index.js.map new file mode 100644 index 0000000..a9ed471 --- /dev/null +++ b/bonobo/contrib/jupyter/static/index.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack/bootstrap 2cdb85ca4cf1fecca3d0","webpack:///./src/index.js","webpack:///./src/bonobo.js","webpack:///external \"jupyter-js-widgets\"","webpack:///./~/underscore/underscore.js","webpack:///./package.json"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;;;;;;ACXA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA,iEAAgE,yBAAyB;AACzF,mCAAkC,WAAW,WAAW,SAAS,WAAW,UAAU,WAAW,UAAU;AAC3G,cAAa;AACb;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;;;;;;;ACzCA,gD;;;;;;ACAA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;AACH;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA,wBAAuB,OAAO;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uCAAsC,YAAY;AAClD;AACA;AACA,MAAK;AACL;AACA,wCAAuC,YAAY;AACnD;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA6B,gBAAgB;AAC7C;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA,qDAAoD;AACpD,IAAG;;AAEH;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA,2CAA0C;AAC1C,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,6DAA4D,YAAY;AACxE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA,sBAAqB,gBAAgB;AACrC;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,8CAA6C,YAAY;AACzD;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAsD;AACtD;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAS;AACT;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,0BAA0B;AACpE;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA,sBAAqB,cAAc;AACnC;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,sBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAe,YAAY;AAC3B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,QAAO,eAAe;AACtB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,sBAAqB,eAAe;AACpC;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAsB;AACtB;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA,oBAAmB;AACnB;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,6CAA4C,mBAAmB;AAC/D;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,sDAAqD;AACrD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;AACA;;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8EAA6E;AAC7E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA,sCAAqC;AACrC;AACA;AACA;;AAEA;AACA;AACA;AACA,2BAA0B;AAC1B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,oBAAmB,OAAO;AAC1B;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,gBAAe;AACf,eAAc;AACd,eAAc;AACd,iBAAgB;AAChB,iBAAgB;AAChB,iBAAgB;AAChB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA4B;;AAE5B;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA,QAAO;AACP,sBAAqB;AACrB;;AAEA;AACA;AACA,MAAK;AACL,kBAAiB;;AAEjB;AACA,mDAAkD,EAAE,iBAAiB;;AAErE;AACA,yBAAwB,8BAA8B;AACtD,4BAA2B;;AAE3B;AACA;AACA,MAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,mDAAkD,iBAAiB;;AAEnE;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,EAAC;;;;;;;AC3gDD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA,G","file":"index.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 2cdb85ca4cf1fecca3d0","// Entry point for the notebook bundle containing custom model definitions.\n//\n// Setup notebook base URL\n//\n// Some static assets may be required by the custom widget javascript. The base\n// url for the notebook is not known at build time and is therefore computed\n// dynamically.\n__webpack_public_path__ = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/bonobo/';\n\n// Export widget models and views, and the npm package version number.\nmodule.exports = require('./bonobo.js');\nmodule.exports['version'] = require('../package.json').version;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = 0\n// module chunks = 0","var widgets = require('jupyter-js-widgets');\nvar _ = require('underscore');\n\n// Custom Model. Custom widgets models must at least provide default values\n// for model attributes, including `_model_name`, `_view_name`, `_model_module`\n// and `_view_module` when different from the base class.\n//\n// When serialiazing entire widget state for embedding, only values different from the\n// defaults will be specified.\n\nconst BonoboModel = widgets.DOMWidgetModel.extend({\n defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, {\n _model_name: 'BonoboModel',\n _view_name: 'BonoboView',\n _model_module: 'bonobo',\n _view_module: 'bonobo',\n value: []\n })\n});\n\n\n// Custom View. Renders the widget model.\nconst BonoboView = widgets.DOMWidgetView.extend({\n render: function () {\n this.value_changed();\n this.model.on('change:value', this.value_changed, this);\n },\n\n value_changed: function () {\n this.$el.html(\n '
' + this.model.get('value').map((key, i) => {\n return ``\n }).join('\\n') + '
${key.status}${key.name}${key.stats}${key.flags}
'\n );\n },\n});\n\n\nmodule.exports = {\n BonoboModel: BonoboModel,\n BonoboView: BonoboView\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/bonobo.js\n// module id = 1\n// module chunks = 0","module.exports = __WEBPACK_EXTERNAL_MODULE_2__;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"jupyter-js-widgets\"\n// module id = 2\n// module chunks = 0","// Underscore.js 1.8.3\n// http://underscorejs.org\n// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n// Underscore may be freely distributed under the MIT license.\n\n(function() {\n\n // Baseline setup\n // --------------\n\n // Establish the root object, `window` in the browser, or `exports` on the server.\n var root = this;\n\n // Save the previous value of the `_` variable.\n var previousUnderscore = root._;\n\n // Save bytes in the minified (but not gzipped) version:\n var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n // Create quick reference variables for speed access to core prototypes.\n var\n push = ArrayProto.push,\n slice = ArrayProto.slice,\n toString = ObjProto.toString,\n hasOwnProperty = ObjProto.hasOwnProperty;\n\n // All **ECMAScript 5** native function implementations that we hope to use\n // are declared here.\n var\n nativeIsArray = Array.isArray,\n nativeKeys = Object.keys,\n nativeBind = FuncProto.bind,\n nativeCreate = Object.create;\n\n // Naked function reference for surrogate-prototype-swapping.\n var Ctor = function(){};\n\n // Create a safe reference to the Underscore object for use below.\n var _ = function(obj) {\n if (obj instanceof _) return obj;\n if (!(this instanceof _)) return new _(obj);\n this._wrapped = obj;\n };\n\n // Export the Underscore object for **Node.js**, with\n // backwards-compatibility for the old `require()` API. If we're in\n // the browser, add `_` as a global object.\n if (typeof exports !== 'undefined') {\n if (typeof module !== 'undefined' && module.exports) {\n exports = module.exports = _;\n }\n exports._ = _;\n } else {\n root._ = _;\n }\n\n // Current version.\n _.VERSION = '1.8.3';\n\n // Internal function that returns an efficient (for current engines) version\n // of the passed-in callback, to be repeatedly applied in other Underscore\n // functions.\n var optimizeCb = function(func, context, argCount) {\n if (context === void 0) return func;\n switch (argCount == null ? 3 : argCount) {\n case 1: return function(value) {\n return func.call(context, value);\n };\n case 2: return function(value, other) {\n return func.call(context, value, other);\n };\n case 3: return function(value, index, collection) {\n return func.call(context, value, index, collection);\n };\n case 4: return function(accumulator, value, index, collection) {\n return func.call(context, accumulator, value, index, collection);\n };\n }\n return function() {\n return func.apply(context, arguments);\n };\n };\n\n // A mostly-internal function to generate callbacks that can be applied\n // to each element in a collection, returning the desired result — either\n // identity, an arbitrary callback, a property matcher, or a property accessor.\n var cb = function(value, context, argCount) {\n if (value == null) return _.identity;\n if (_.isFunction(value)) return optimizeCb(value, context, argCount);\n if (_.isObject(value)) return _.matcher(value);\n return _.property(value);\n };\n _.iteratee = function(value, context) {\n return cb(value, context, Infinity);\n };\n\n // An internal function for creating assigner functions.\n var createAssigner = function(keysFunc, undefinedOnly) {\n return function(obj) {\n var length = arguments.length;\n if (length < 2 || obj == null) return obj;\n for (var index = 1; index < length; index++) {\n var source = arguments[index],\n keys = keysFunc(source),\n l = keys.length;\n for (var i = 0; i < l; i++) {\n var key = keys[i];\n if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];\n }\n }\n return obj;\n };\n };\n\n // An internal function for creating a new object that inherits from another.\n var baseCreate = function(prototype) {\n if (!_.isObject(prototype)) return {};\n if (nativeCreate) return nativeCreate(prototype);\n Ctor.prototype = prototype;\n var result = new Ctor;\n Ctor.prototype = null;\n return result;\n };\n\n var property = function(key) {\n return function(obj) {\n return obj == null ? void 0 : obj[key];\n };\n };\n\n // Helper for collection methods to determine whether a collection\n // should be iterated as an array or as an object\n // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength\n // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094\n var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;\n var getLength = property('length');\n var isArrayLike = function(collection) {\n var length = getLength(collection);\n return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;\n };\n\n // Collection Functions\n // --------------------\n\n // The cornerstone, an `each` implementation, aka `forEach`.\n // Handles raw objects in addition to array-likes. Treats all\n // sparse array-likes as if they were dense.\n _.each = _.forEach = function(obj, iteratee, context) {\n iteratee = optimizeCb(iteratee, context);\n var i, length;\n if (isArrayLike(obj)) {\n for (i = 0, length = obj.length; i < length; i++) {\n iteratee(obj[i], i, obj);\n }\n } else {\n var keys = _.keys(obj);\n for (i = 0, length = keys.length; i < length; i++) {\n iteratee(obj[keys[i]], keys[i], obj);\n }\n }\n return obj;\n };\n\n // Return the results of applying the iteratee to each element.\n _.map = _.collect = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n results = Array(length);\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n results[index] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Create a reducing function iterating left or right.\n function createReduce(dir) {\n // Optimized iterator function as using arguments.length\n // in the main function will deoptimize the, see #1991.\n function iterator(obj, iteratee, memo, keys, index, length) {\n for (; index >= 0 && index < length; index += dir) {\n var currentKey = keys ? keys[index] : index;\n memo = iteratee(memo, obj[currentKey], currentKey, obj);\n }\n return memo;\n }\n\n return function(obj, iteratee, memo, context) {\n iteratee = optimizeCb(iteratee, context, 4);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n index = dir > 0 ? 0 : length - 1;\n // Determine the initial value if none is provided.\n if (arguments.length < 3) {\n memo = obj[keys ? keys[index] : index];\n index += dir;\n }\n return iterator(obj, iteratee, memo, keys, index, length);\n };\n }\n\n // **Reduce** builds up a single result from a list of values, aka `inject`,\n // or `foldl`.\n _.reduce = _.foldl = _.inject = createReduce(1);\n\n // The right-associative version of reduce, also known as `foldr`.\n _.reduceRight = _.foldr = createReduce(-1);\n\n // Return the first value which passes a truth test. Aliased as `detect`.\n _.find = _.detect = function(obj, predicate, context) {\n var key;\n if (isArrayLike(obj)) {\n key = _.findIndex(obj, predicate, context);\n } else {\n key = _.findKey(obj, predicate, context);\n }\n if (key !== void 0 && key !== -1) return obj[key];\n };\n\n // Return all the elements that pass a truth test.\n // Aliased as `select`.\n _.filter = _.select = function(obj, predicate, context) {\n var results = [];\n predicate = cb(predicate, context);\n _.each(obj, function(value, index, list) {\n if (predicate(value, index, list)) results.push(value);\n });\n return results;\n };\n\n // Return all the elements for which a truth test fails.\n _.reject = function(obj, predicate, context) {\n return _.filter(obj, _.negate(cb(predicate)), context);\n };\n\n // Determine whether all of the elements match a truth test.\n // Aliased as `all`.\n _.every = _.all = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (!predicate(obj[currentKey], currentKey, obj)) return false;\n }\n return true;\n };\n\n // Determine if at least one element in the object matches a truth test.\n // Aliased as `any`.\n _.some = _.any = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (predicate(obj[currentKey], currentKey, obj)) return true;\n }\n return false;\n };\n\n // Determine if the array or object contains a given item (using `===`).\n // Aliased as `includes` and `include`.\n _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n if (typeof fromIndex != 'number' || guard) fromIndex = 0;\n return _.indexOf(obj, item, fromIndex) >= 0;\n };\n\n // Invoke a method (with arguments) on every item in a collection.\n _.invoke = function(obj, method) {\n var args = slice.call(arguments, 2);\n var isFunc = _.isFunction(method);\n return _.map(obj, function(value) {\n var func = isFunc ? method : value[method];\n return func == null ? func : func.apply(value, args);\n });\n };\n\n // Convenience version of a common use case of `map`: fetching a property.\n _.pluck = function(obj, key) {\n return _.map(obj, _.property(key));\n };\n\n // Convenience version of a common use case of `filter`: selecting only objects\n // containing specific `key:value` pairs.\n _.where = function(obj, attrs) {\n return _.filter(obj, _.matcher(attrs));\n };\n\n // Convenience version of a common use case of `find`: getting the first object\n // containing specific `key:value` pairs.\n _.findWhere = function(obj, attrs) {\n return _.find(obj, _.matcher(attrs));\n };\n\n // Return the maximum element (or element-based computation).\n _.max = function(obj, iteratee, context) {\n var result = -Infinity, lastComputed = -Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value > result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed > lastComputed || computed === -Infinity && result === -Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Return the minimum element (or element-based computation).\n _.min = function(obj, iteratee, context) {\n var result = Infinity, lastComputed = Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value < result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed < lastComputed || computed === Infinity && result === Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Shuffle a collection, using the modern version of the\n // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).\n _.shuffle = function(obj) {\n var set = isArrayLike(obj) ? obj : _.values(obj);\n var length = set.length;\n var shuffled = Array(length);\n for (var index = 0, rand; index < length; index++) {\n rand = _.random(0, index);\n if (rand !== index) shuffled[index] = shuffled[rand];\n shuffled[rand] = set[index];\n }\n return shuffled;\n };\n\n // Sample **n** random values from a collection.\n // If **n** is not specified, returns a single random element.\n // The internal `guard` argument allows it to work with `map`.\n _.sample = function(obj, n, guard) {\n if (n == null || guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n return obj[_.random(obj.length - 1)];\n }\n return _.shuffle(obj).slice(0, Math.max(0, n));\n };\n\n // Sort the object's values by a criterion produced by an iteratee.\n _.sortBy = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n return _.pluck(_.map(obj, function(value, index, list) {\n return {\n value: value,\n index: index,\n criteria: iteratee(value, index, list)\n };\n }).sort(function(left, right) {\n var a = left.criteria;\n var b = right.criteria;\n if (a !== b) {\n if (a > b || a === void 0) return 1;\n if (a < b || b === void 0) return -1;\n }\n return left.index - right.index;\n }), 'value');\n };\n\n // An internal function used for aggregate \"group by\" operations.\n var group = function(behavior) {\n return function(obj, iteratee, context) {\n var result = {};\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index) {\n var key = iteratee(value, index, obj);\n behavior(result, value, key);\n });\n return result;\n };\n };\n\n // Groups the object's values by a criterion. Pass either a string attribute\n // to group by, or a function that returns the criterion.\n _.groupBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key].push(value); else result[key] = [value];\n });\n\n // Indexes the object's values by a criterion, similar to `groupBy`, but for\n // when you know that your index values will be unique.\n _.indexBy = group(function(result, value, key) {\n result[key] = value;\n });\n\n // Counts instances of an object that group by a certain criterion. Pass\n // either a string attribute to count by, or a function that returns the\n // criterion.\n _.countBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key]++; else result[key] = 1;\n });\n\n // Safely create a real, live array from anything iterable.\n _.toArray = function(obj) {\n if (!obj) return [];\n if (_.isArray(obj)) return slice.call(obj);\n if (isArrayLike(obj)) return _.map(obj, _.identity);\n return _.values(obj);\n };\n\n // Return the number of elements in an object.\n _.size = function(obj) {\n if (obj == null) return 0;\n return isArrayLike(obj) ? obj.length : _.keys(obj).length;\n };\n\n // Split a collection into two arrays: one whose elements all satisfy the given\n // predicate, and one whose elements all do not satisfy the predicate.\n _.partition = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var pass = [], fail = [];\n _.each(obj, function(value, key, obj) {\n (predicate(value, key, obj) ? pass : fail).push(value);\n });\n return [pass, fail];\n };\n\n // Array Functions\n // ---------------\n\n // Get the first element of an array. Passing **n** will return the first N\n // values in the array. Aliased as `head` and `take`. The **guard** check\n // allows it to work with `_.map`.\n _.first = _.head = _.take = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[0];\n return _.initial(array, array.length - n);\n };\n\n // Returns everything but the last entry of the array. Especially useful on\n // the arguments object. Passing **n** will return all the values in\n // the array, excluding the last N.\n _.initial = function(array, n, guard) {\n return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));\n };\n\n // Get the last element of an array. Passing **n** will return the last N\n // values in the array.\n _.last = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[array.length - 1];\n return _.rest(array, Math.max(0, array.length - n));\n };\n\n // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.\n // Especially useful on the arguments object. Passing an **n** will return\n // the rest N values in the array.\n _.rest = _.tail = _.drop = function(array, n, guard) {\n return slice.call(array, n == null || guard ? 1 : n);\n };\n\n // Trim out all falsy values from an array.\n _.compact = function(array) {\n return _.filter(array, _.identity);\n };\n\n // Internal implementation of a recursive `flatten` function.\n var flatten = function(input, shallow, strict, startIndex) {\n var output = [], idx = 0;\n for (var i = startIndex || 0, length = getLength(input); i < length; i++) {\n var value = input[i];\n if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {\n //flatten current level of array or arguments object\n if (!shallow) value = flatten(value, shallow, strict);\n var j = 0, len = value.length;\n output.length += len;\n while (j < len) {\n output[idx++] = value[j++];\n }\n } else if (!strict) {\n output[idx++] = value;\n }\n }\n return output;\n };\n\n // Flatten out an array, either recursively (by default), or just one level.\n _.flatten = function(array, shallow) {\n return flatten(array, shallow, false);\n };\n\n // Return a version of the array that does not contain the specified value(s).\n _.without = function(array) {\n return _.difference(array, slice.call(arguments, 1));\n };\n\n // Produce a duplicate-free version of the array. If the array has already\n // been sorted, you have the option of using a faster algorithm.\n // Aliased as `unique`.\n _.uniq = _.unique = function(array, isSorted, iteratee, context) {\n if (!_.isBoolean(isSorted)) {\n context = iteratee;\n iteratee = isSorted;\n isSorted = false;\n }\n if (iteratee != null) iteratee = cb(iteratee, context);\n var result = [];\n var seen = [];\n for (var i = 0, length = getLength(array); i < length; i++) {\n var value = array[i],\n computed = iteratee ? iteratee(value, i, array) : value;\n if (isSorted) {\n if (!i || seen !== computed) result.push(value);\n seen = computed;\n } else if (iteratee) {\n if (!_.contains(seen, computed)) {\n seen.push(computed);\n result.push(value);\n }\n } else if (!_.contains(result, value)) {\n result.push(value);\n }\n }\n return result;\n };\n\n // Produce an array that contains the union: each distinct element from all of\n // the passed-in arrays.\n _.union = function() {\n return _.uniq(flatten(arguments, true, true));\n };\n\n // Produce an array that contains every item shared between all the\n // passed-in arrays.\n _.intersection = function(array) {\n var result = [];\n var argsLength = arguments.length;\n for (var i = 0, length = getLength(array); i < length; i++) {\n var item = array[i];\n if (_.contains(result, item)) continue;\n for (var j = 1; j < argsLength; j++) {\n if (!_.contains(arguments[j], item)) break;\n }\n if (j === argsLength) result.push(item);\n }\n return result;\n };\n\n // Take the difference between one array and a number of other arrays.\n // Only the elements present in just the first array will remain.\n _.difference = function(array) {\n var rest = flatten(arguments, true, true, 1);\n return _.filter(array, function(value){\n return !_.contains(rest, value);\n });\n };\n\n // Zip together multiple lists into a single array -- elements that share\n // an index go together.\n _.zip = function() {\n return _.unzip(arguments);\n };\n\n // Complement of _.zip. Unzip accepts an array of arrays and groups\n // each array's elements on shared indices\n _.unzip = function(array) {\n var length = array && _.max(array, getLength).length || 0;\n var result = Array(length);\n\n for (var index = 0; index < length; index++) {\n result[index] = _.pluck(array, index);\n }\n return result;\n };\n\n // Converts lists into objects. Pass either a single array of `[key, value]`\n // pairs, or two parallel arrays of the same length -- one of keys, and one of\n // the corresponding values.\n _.object = function(list, values) {\n var result = {};\n for (var i = 0, length = getLength(list); i < length; i++) {\n if (values) {\n result[list[i]] = values[i];\n } else {\n result[list[i][0]] = list[i][1];\n }\n }\n return result;\n };\n\n // Generator function to create the findIndex and findLastIndex functions\n function createPredicateIndexFinder(dir) {\n return function(array, predicate, context) {\n predicate = cb(predicate, context);\n var length = getLength(array);\n var index = dir > 0 ? 0 : length - 1;\n for (; index >= 0 && index < length; index += dir) {\n if (predicate(array[index], index, array)) return index;\n }\n return -1;\n };\n }\n\n // Returns the first index on an array-like that passes a predicate test\n _.findIndex = createPredicateIndexFinder(1);\n _.findLastIndex = createPredicateIndexFinder(-1);\n\n // Use a comparator function to figure out the smallest index at which\n // an object should be inserted so as to maintain order. Uses binary search.\n _.sortedIndex = function(array, obj, iteratee, context) {\n iteratee = cb(iteratee, context, 1);\n var value = iteratee(obj);\n var low = 0, high = getLength(array);\n while (low < high) {\n var mid = Math.floor((low + high) / 2);\n if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;\n }\n return low;\n };\n\n // Generator function to create the indexOf and lastIndexOf functions\n function createIndexFinder(dir, predicateFind, sortedIndex) {\n return function(array, item, idx) {\n var i = 0, length = getLength(array);\n if (typeof idx == 'number') {\n if (dir > 0) {\n i = idx >= 0 ? idx : Math.max(idx + length, i);\n } else {\n length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;\n }\n } else if (sortedIndex && idx && length) {\n idx = sortedIndex(array, item);\n return array[idx] === item ? idx : -1;\n }\n if (item !== item) {\n idx = predicateFind(slice.call(array, i, length), _.isNaN);\n return idx >= 0 ? idx + i : -1;\n }\n for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {\n if (array[idx] === item) return idx;\n }\n return -1;\n };\n }\n\n // Return the position of the first occurrence of an item in an array,\n // or -1 if the item is not included in the array.\n // If the array is large and already in sort order, pass `true`\n // for **isSorted** to use binary search.\n _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);\n _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);\n\n // Generate an integer Array containing an arithmetic progression. A port of\n // the native Python `range()` function. See\n // [the Python documentation](http://docs.python.org/library/functions.html#range).\n _.range = function(start, stop, step) {\n if (stop == null) {\n stop = start || 0;\n start = 0;\n }\n step = step || 1;\n\n var length = Math.max(Math.ceil((stop - start) / step), 0);\n var range = Array(length);\n\n for (var idx = 0; idx < length; idx++, start += step) {\n range[idx] = start;\n }\n\n return range;\n };\n\n // Function (ahem) Functions\n // ------------------\n\n // Determines whether to execute a function as a constructor\n // or a normal function with the provided arguments\n var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {\n if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);\n var self = baseCreate(sourceFunc.prototype);\n var result = sourceFunc.apply(self, args);\n if (_.isObject(result)) return result;\n return self;\n };\n\n // Create a function bound to a given object (assigning `this`, and arguments,\n // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if\n // available.\n _.bind = function(func, context) {\n if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');\n var args = slice.call(arguments, 2);\n var bound = function() {\n return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));\n };\n return bound;\n };\n\n // Partially apply a function by creating a version that has had some of its\n // arguments pre-filled, without changing its dynamic `this` context. _ acts\n // as a placeholder, allowing any combination of arguments to be pre-filled.\n _.partial = function(func) {\n var boundArgs = slice.call(arguments, 1);\n var bound = function() {\n var position = 0, length = boundArgs.length;\n var args = Array(length);\n for (var i = 0; i < length; i++) {\n args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];\n }\n while (position < arguments.length) args.push(arguments[position++]);\n return executeBound(func, bound, this, this, args);\n };\n return bound;\n };\n\n // Bind a number of an object's methods to that object. Remaining arguments\n // are the method names to be bound. Useful for ensuring that all callbacks\n // defined on an object belong to it.\n _.bindAll = function(obj) {\n var i, length = arguments.length, key;\n if (length <= 1) throw new Error('bindAll must be passed function names');\n for (i = 1; i < length; i++) {\n key = arguments[i];\n obj[key] = _.bind(obj[key], obj);\n }\n return obj;\n };\n\n // Memoize an expensive function by storing its results.\n _.memoize = function(func, hasher) {\n var memoize = function(key) {\n var cache = memoize.cache;\n var address = '' + (hasher ? hasher.apply(this, arguments) : key);\n if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);\n return cache[address];\n };\n memoize.cache = {};\n return memoize;\n };\n\n // Delays a function for the given number of milliseconds, and then calls\n // it with the arguments supplied.\n _.delay = function(func, wait) {\n var args = slice.call(arguments, 2);\n return setTimeout(function(){\n return func.apply(null, args);\n }, wait);\n };\n\n // Defers a function, scheduling it to run after the current call stack has\n // cleared.\n _.defer = _.partial(_.delay, _, 1);\n\n // Returns a function, that, when invoked, will only be triggered at most once\n // during a given window of time. Normally, the throttled function will run\n // as much as it can, without ever going more than once per `wait` duration;\n // but if you'd like to disable the execution on the leading edge, pass\n // `{leading: false}`. To disable execution on the trailing edge, ditto.\n _.throttle = function(func, wait, options) {\n var context, args, result;\n var timeout = null;\n var previous = 0;\n if (!options) options = {};\n var later = function() {\n previous = options.leading === false ? 0 : _.now();\n timeout = null;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n };\n return function() {\n var now = _.now();\n if (!previous && options.leading === false) previous = now;\n var remaining = wait - (now - previous);\n context = this;\n args = arguments;\n if (remaining <= 0 || remaining > wait) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n previous = now;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n } else if (!timeout && options.trailing !== false) {\n timeout = setTimeout(later, remaining);\n }\n return result;\n };\n };\n\n // Returns a function, that, as long as it continues to be invoked, will not\n // be triggered. The function will be called after it stops being called for\n // N milliseconds. If `immediate` is passed, trigger the function on the\n // leading edge, instead of the trailing.\n _.debounce = function(func, wait, immediate) {\n var timeout, args, context, timestamp, result;\n\n var later = function() {\n var last = _.now() - timestamp;\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last);\n } else {\n timeout = null;\n if (!immediate) {\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n }\n }\n };\n\n return function() {\n context = this;\n args = arguments;\n timestamp = _.now();\n var callNow = immediate && !timeout;\n if (!timeout) timeout = setTimeout(later, wait);\n if (callNow) {\n result = func.apply(context, args);\n context = args = null;\n }\n\n return result;\n };\n };\n\n // Returns the first function passed as an argument to the second,\n // allowing you to adjust arguments, run code before and after, and\n // conditionally execute the original function.\n _.wrap = function(func, wrapper) {\n return _.partial(wrapper, func);\n };\n\n // Returns a negated version of the passed-in predicate.\n _.negate = function(predicate) {\n return function() {\n return !predicate.apply(this, arguments);\n };\n };\n\n // Returns a function that is the composition of a list of functions, each\n // consuming the return value of the function that follows.\n _.compose = function() {\n var args = arguments;\n var start = args.length - 1;\n return function() {\n var i = start;\n var result = args[start].apply(this, arguments);\n while (i--) result = args[i].call(this, result);\n return result;\n };\n };\n\n // Returns a function that will only be executed on and after the Nth call.\n _.after = function(times, func) {\n return function() {\n if (--times < 1) {\n return func.apply(this, arguments);\n }\n };\n };\n\n // Returns a function that will only be executed up to (but not including) the Nth call.\n _.before = function(times, func) {\n var memo;\n return function() {\n if (--times > 0) {\n memo = func.apply(this, arguments);\n }\n if (times <= 1) func = null;\n return memo;\n };\n };\n\n // Returns a function that will be executed at most one time, no matter how\n // often you call it. Useful for lazy initialization.\n _.once = _.partial(_.before, 2);\n\n // Object Functions\n // ----------------\n\n // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.\n var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');\n var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',\n 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];\n\n function collectNonEnumProps(obj, keys) {\n var nonEnumIdx = nonEnumerableProps.length;\n var constructor = obj.constructor;\n var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;\n\n // Constructor is a special case.\n var prop = 'constructor';\n if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);\n\n while (nonEnumIdx--) {\n prop = nonEnumerableProps[nonEnumIdx];\n if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {\n keys.push(prop);\n }\n }\n }\n\n // Retrieve the names of an object's own properties.\n // Delegates to **ECMAScript 5**'s native `Object.keys`\n _.keys = function(obj) {\n if (!_.isObject(obj)) return [];\n if (nativeKeys) return nativeKeys(obj);\n var keys = [];\n for (var key in obj) if (_.has(obj, key)) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve all the property names of an object.\n _.allKeys = function(obj) {\n if (!_.isObject(obj)) return [];\n var keys = [];\n for (var key in obj) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve the values of an object's properties.\n _.values = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var values = Array(length);\n for (var i = 0; i < length; i++) {\n values[i] = obj[keys[i]];\n }\n return values;\n };\n\n // Returns the results of applying the iteratee to each element of the object\n // In contrast to _.map it returns an object\n _.mapObject = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = _.keys(obj),\n length = keys.length,\n results = {},\n currentKey;\n for (var index = 0; index < length; index++) {\n currentKey = keys[index];\n results[currentKey] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Convert an object into a list of `[key, value]` pairs.\n _.pairs = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var pairs = Array(length);\n for (var i = 0; i < length; i++) {\n pairs[i] = [keys[i], obj[keys[i]]];\n }\n return pairs;\n };\n\n // Invert the keys and values of an object. The values must be serializable.\n _.invert = function(obj) {\n var result = {};\n var keys = _.keys(obj);\n for (var i = 0, length = keys.length; i < length; i++) {\n result[obj[keys[i]]] = keys[i];\n }\n return result;\n };\n\n // Return a sorted list of the function names available on the object.\n // Aliased as `methods`\n _.functions = _.methods = function(obj) {\n var names = [];\n for (var key in obj) {\n if (_.isFunction(obj[key])) names.push(key);\n }\n return names.sort();\n };\n\n // Extend a given object with all the properties in passed-in object(s).\n _.extend = createAssigner(_.allKeys);\n\n // Assigns a given object with all the own properties in the passed-in object(s)\n // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)\n _.extendOwn = _.assign = createAssigner(_.keys);\n\n // Returns the first key on an object that passes a predicate test\n _.findKey = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = _.keys(obj), key;\n for (var i = 0, length = keys.length; i < length; i++) {\n key = keys[i];\n if (predicate(obj[key], key, obj)) return key;\n }\n };\n\n // Return a copy of the object only containing the whitelisted properties.\n _.pick = function(object, oiteratee, context) {\n var result = {}, obj = object, iteratee, keys;\n if (obj == null) return result;\n if (_.isFunction(oiteratee)) {\n keys = _.allKeys(obj);\n iteratee = optimizeCb(oiteratee, context);\n } else {\n keys = flatten(arguments, false, false, 1);\n iteratee = function(value, key, obj) { return key in obj; };\n obj = Object(obj);\n }\n for (var i = 0, length = keys.length; i < length; i++) {\n var key = keys[i];\n var value = obj[key];\n if (iteratee(value, key, obj)) result[key] = value;\n }\n return result;\n };\n\n // Return a copy of the object without the blacklisted properties.\n _.omit = function(obj, iteratee, context) {\n if (_.isFunction(iteratee)) {\n iteratee = _.negate(iteratee);\n } else {\n var keys = _.map(flatten(arguments, false, false, 1), String);\n iteratee = function(value, key) {\n return !_.contains(keys, key);\n };\n }\n return _.pick(obj, iteratee, context);\n };\n\n // Fill in a given object with default properties.\n _.defaults = createAssigner(_.allKeys, true);\n\n // Creates an object that inherits from the given prototype object.\n // If additional properties are provided then they will be added to the\n // created object.\n _.create = function(prototype, props) {\n var result = baseCreate(prototype);\n if (props) _.extendOwn(result, props);\n return result;\n };\n\n // Create a (shallow-cloned) duplicate of an object.\n _.clone = function(obj) {\n if (!_.isObject(obj)) return obj;\n return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n };\n\n // Invokes interceptor with the obj, and then returns obj.\n // The primary purpose of this method is to \"tap into\" a method chain, in\n // order to perform operations on intermediate results within the chain.\n _.tap = function(obj, interceptor) {\n interceptor(obj);\n return obj;\n };\n\n // Returns whether an object has a given set of `key:value` pairs.\n _.isMatch = function(object, attrs) {\n var keys = _.keys(attrs), length = keys.length;\n if (object == null) return !length;\n var obj = Object(object);\n for (var i = 0; i < length; i++) {\n var key = keys[i];\n if (attrs[key] !== obj[key] || !(key in obj)) return false;\n }\n return true;\n };\n\n\n // Internal recursive comparison function for `isEqual`.\n var eq = function(a, b, aStack, bStack) {\n // Identical objects are equal. `0 === -0`, but they aren't identical.\n // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).\n if (a === b) return a !== 0 || 1 / a === 1 / b;\n // A strict comparison is necessary because `null == undefined`.\n if (a == null || b == null) return a === b;\n // Unwrap any wrapped objects.\n if (a instanceof _) a = a._wrapped;\n if (b instanceof _) b = b._wrapped;\n // Compare `[[Class]]` names.\n var className = toString.call(a);\n if (className !== toString.call(b)) return false;\n switch (className) {\n // Strings, numbers, regular expressions, dates, and booleans are compared by value.\n case '[object RegExp]':\n // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')\n case '[object String]':\n // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n // equivalent to `new String(\"5\")`.\n return '' + a === '' + b;\n case '[object Number]':\n // `NaN`s are equivalent, but non-reflexive.\n // Object(NaN) is equivalent to NaN\n if (+a !== +a) return +b !== +b;\n // An `egal` comparison is performed for other numeric values.\n return +a === 0 ? 1 / +a === 1 / b : +a === +b;\n case '[object Date]':\n case '[object Boolean]':\n // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n // millisecond representations. Note that invalid dates with millisecond representations\n // of `NaN` are not equivalent.\n return +a === +b;\n }\n\n var areArrays = className === '[object Array]';\n if (!areArrays) {\n if (typeof a != 'object' || typeof b != 'object') return false;\n\n // Objects with different constructors are not equivalent, but `Object`s or `Array`s\n // from different frames are.\n var aCtor = a.constructor, bCtor = b.constructor;\n if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&\n _.isFunction(bCtor) && bCtor instanceof bCtor)\n && ('constructor' in a && 'constructor' in b)) {\n return false;\n }\n }\n // Assume equality for cyclic structures. The algorithm for detecting cyclic\n // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n\n // Initializing stack of traversed objects.\n // It's done here since we only need them for objects and arrays comparison.\n aStack = aStack || [];\n bStack = bStack || [];\n var length = aStack.length;\n while (length--) {\n // Linear search. Performance is inversely proportional to the number of\n // unique nested structures.\n if (aStack[length] === a) return bStack[length] === b;\n }\n\n // Add the first object to the stack of traversed objects.\n aStack.push(a);\n bStack.push(b);\n\n // Recursively compare objects and arrays.\n if (areArrays) {\n // Compare array lengths to determine if a deep comparison is necessary.\n length = a.length;\n if (length !== b.length) return false;\n // Deep compare the contents, ignoring non-numeric properties.\n while (length--) {\n if (!eq(a[length], b[length], aStack, bStack)) return false;\n }\n } else {\n // Deep compare objects.\n var keys = _.keys(a), key;\n length = keys.length;\n // Ensure that both objects contain the same number of properties before comparing deep equality.\n if (_.keys(b).length !== length) return false;\n while (length--) {\n // Deep compare each member\n key = keys[length];\n if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;\n }\n }\n // Remove the first object from the stack of traversed objects.\n aStack.pop();\n bStack.pop();\n return true;\n };\n\n // Perform a deep comparison to check if two objects are equal.\n _.isEqual = function(a, b) {\n return eq(a, b);\n };\n\n // Is a given array, string, or object empty?\n // An \"empty\" object has no enumerable own-properties.\n _.isEmpty = function(obj) {\n if (obj == null) return true;\n if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;\n return _.keys(obj).length === 0;\n };\n\n // Is a given value a DOM element?\n _.isElement = function(obj) {\n return !!(obj && obj.nodeType === 1);\n };\n\n // Is a given value an array?\n // Delegates to ECMA5's native Array.isArray\n _.isArray = nativeIsArray || function(obj) {\n return toString.call(obj) === '[object Array]';\n };\n\n // Is a given variable an object?\n _.isObject = function(obj) {\n var type = typeof obj;\n return type === 'function' || type === 'object' && !!obj;\n };\n\n // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.\n _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {\n _['is' + name] = function(obj) {\n return toString.call(obj) === '[object ' + name + ']';\n };\n });\n\n // Define a fallback version of the method in browsers (ahem, IE < 9), where\n // there isn't any inspectable \"Arguments\" type.\n if (!_.isArguments(arguments)) {\n _.isArguments = function(obj) {\n return _.has(obj, 'callee');\n };\n }\n\n // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,\n // IE 11 (#1621), and in Safari 8 (#1929).\n if (typeof /./ != 'function' && typeof Int8Array != 'object') {\n _.isFunction = function(obj) {\n return typeof obj == 'function' || false;\n };\n }\n\n // Is a given object a finite number?\n _.isFinite = function(obj) {\n return isFinite(obj) && !isNaN(parseFloat(obj));\n };\n\n // Is the given value `NaN`? (NaN is the only number which does not equal itself).\n _.isNaN = function(obj) {\n return _.isNumber(obj) && obj !== +obj;\n };\n\n // Is a given value a boolean?\n _.isBoolean = function(obj) {\n return obj === true || obj === false || toString.call(obj) === '[object Boolean]';\n };\n\n // Is a given value equal to null?\n _.isNull = function(obj) {\n return obj === null;\n };\n\n // Is a given variable undefined?\n _.isUndefined = function(obj) {\n return obj === void 0;\n };\n\n // Shortcut function for checking if an object has a given property directly\n // on itself (in other words, not on a prototype).\n _.has = function(obj, key) {\n return obj != null && hasOwnProperty.call(obj, key);\n };\n\n // Utility Functions\n // -----------------\n\n // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n // previous owner. Returns a reference to the Underscore object.\n _.noConflict = function() {\n root._ = previousUnderscore;\n return this;\n };\n\n // Keep the identity function around for default iteratees.\n _.identity = function(value) {\n return value;\n };\n\n // Predicate-generating functions. Often useful outside of Underscore.\n _.constant = function(value) {\n return function() {\n return value;\n };\n };\n\n _.noop = function(){};\n\n _.property = property;\n\n // Generates a function for a given object that returns a given property.\n _.propertyOf = function(obj) {\n return obj == null ? function(){} : function(key) {\n return obj[key];\n };\n };\n\n // Returns a predicate for checking whether an object has a given set of\n // `key:value` pairs.\n _.matcher = _.matches = function(attrs) {\n attrs = _.extendOwn({}, attrs);\n return function(obj) {\n return _.isMatch(obj, attrs);\n };\n };\n\n // Run a function **n** times.\n _.times = function(n, iteratee, context) {\n var accum = Array(Math.max(0, n));\n iteratee = optimizeCb(iteratee, context, 1);\n for (var i = 0; i < n; i++) accum[i] = iteratee(i);\n return accum;\n };\n\n // Return a random integer between min and max (inclusive).\n _.random = function(min, max) {\n if (max == null) {\n max = min;\n min = 0;\n }\n return min + Math.floor(Math.random() * (max - min + 1));\n };\n\n // A (possibly faster) way to get the current timestamp as an integer.\n _.now = Date.now || function() {\n return new Date().getTime();\n };\n\n // List of HTML entities for escaping.\n var escapeMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '`': '`'\n };\n var unescapeMap = _.invert(escapeMap);\n\n // Functions for escaping and unescaping strings to/from HTML interpolation.\n var createEscaper = function(map) {\n var escaper = function(match) {\n return map[match];\n };\n // Regexes for identifying a key that needs to be escaped\n var source = '(?:' + _.keys(map).join('|') + ')';\n var testRegexp = RegExp(source);\n var replaceRegexp = RegExp(source, 'g');\n return function(string) {\n string = string == null ? '' : '' + string;\n return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;\n };\n };\n _.escape = createEscaper(escapeMap);\n _.unescape = createEscaper(unescapeMap);\n\n // If the value of the named `property` is a function then invoke it with the\n // `object` as context; otherwise, return it.\n _.result = function(object, property, fallback) {\n var value = object == null ? void 0 : object[property];\n if (value === void 0) {\n value = fallback;\n }\n return _.isFunction(value) ? value.call(object) : value;\n };\n\n // Generate a unique integer id (unique within the entire client session).\n // Useful for temporary DOM ids.\n var idCounter = 0;\n _.uniqueId = function(prefix) {\n var id = ++idCounter + '';\n return prefix ? prefix + id : id;\n };\n\n // By default, Underscore uses ERB-style template delimiters, change the\n // following template settings to use alternative delimiters.\n _.templateSettings = {\n evaluate : /<%([\\s\\S]+?)%>/g,\n interpolate : /<%=([\\s\\S]+?)%>/g,\n escape : /<%-([\\s\\S]+?)%>/g\n };\n\n // When customizing `templateSettings`, if you don't want to define an\n // interpolation, evaluation or escaping regex, we need one that is\n // guaranteed not to match.\n var noMatch = /(.)^/;\n\n // Certain characters need to be escaped so that they can be put into a\n // string literal.\n var escapes = {\n \"'\": \"'\",\n '\\\\': '\\\\',\n '\\r': 'r',\n '\\n': 'n',\n '\\u2028': 'u2028',\n '\\u2029': 'u2029'\n };\n\n var escaper = /\\\\|'|\\r|\\n|\\u2028|\\u2029/g;\n\n var escapeChar = function(match) {\n return '\\\\' + escapes[match];\n };\n\n // JavaScript micro-templating, similar to John Resig's implementation.\n // Underscore templating handles arbitrary delimiters, preserves whitespace,\n // and correctly escapes quotes within interpolated code.\n // NB: `oldSettings` only exists for backwards compatibility.\n _.template = function(text, settings, oldSettings) {\n if (!settings && oldSettings) settings = oldSettings;\n settings = _.defaults({}, settings, _.templateSettings);\n\n // Combine delimiters into one regular expression via alternation.\n var matcher = RegExp([\n (settings.escape || noMatch).source,\n (settings.interpolate || noMatch).source,\n (settings.evaluate || noMatch).source\n ].join('|') + '|$', 'g');\n\n // Compile the template source, escaping string literals appropriately.\n var index = 0;\n var source = \"__p+='\";\n text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {\n source += text.slice(index, offset).replace(escaper, escapeChar);\n index = offset + match.length;\n\n if (escape) {\n source += \"'+\\n((__t=(\" + escape + \"))==null?'':_.escape(__t))+\\n'\";\n } else if (interpolate) {\n source += \"'+\\n((__t=(\" + interpolate + \"))==null?'':__t)+\\n'\";\n } else if (evaluate) {\n source += \"';\\n\" + evaluate + \"\\n__p+='\";\n }\n\n // Adobe VMs need the match returned to produce the correct offest.\n return match;\n });\n source += \"';\\n\";\n\n // If a variable is not specified, place data values in local scope.\n if (!settings.variable) source = 'with(obj||{}){\\n' + source + '}\\n';\n\n source = \"var __t,__p='',__j=Array.prototype.join,\" +\n \"print=function(){__p+=__j.call(arguments,'');};\\n\" +\n source + 'return __p;\\n';\n\n try {\n var render = new Function(settings.variable || 'obj', '_', source);\n } catch (e) {\n e.source = source;\n throw e;\n }\n\n var template = function(data) {\n return render.call(this, data, _);\n };\n\n // Provide the compiled source as a convenience for precompilation.\n var argument = settings.variable || 'obj';\n template.source = 'function(' + argument + '){\\n' + source + '}';\n\n return template;\n };\n\n // Add a \"chain\" function. Start chaining a wrapped Underscore object.\n _.chain = function(obj) {\n var instance = _(obj);\n instance._chain = true;\n return instance;\n };\n\n // OOP\n // ---------------\n // If Underscore is called as a function, it returns a wrapped object that\n // can be used OO-style. This wrapper holds altered versions of all the\n // underscore functions. Wrapped objects may be chained.\n\n // Helper function to continue chaining intermediate results.\n var result = function(instance, obj) {\n return instance._chain ? _(obj).chain() : obj;\n };\n\n // Add your own custom functions to the Underscore object.\n _.mixin = function(obj) {\n _.each(_.functions(obj), function(name) {\n var func = _[name] = obj[name];\n _.prototype[name] = function() {\n var args = [this._wrapped];\n push.apply(args, arguments);\n return result(this, func.apply(_, args));\n };\n });\n };\n\n // Add all of the Underscore functions to the wrapper object.\n _.mixin(_);\n\n // Add all mutator Array functions to the wrapper.\n _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n var obj = this._wrapped;\n method.apply(obj, arguments);\n if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];\n return result(this, obj);\n };\n });\n\n // Add all accessor Array functions to the wrapper.\n _.each(['concat', 'join', 'slice'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n return result(this, method.apply(this._wrapped, arguments));\n };\n });\n\n // Extracts the result from a wrapped and chained object.\n _.prototype.value = function() {\n return this._wrapped;\n };\n\n // Provide unwrapping proxy for some methods used in engine operations\n // such as arithmetic and JSON stringification.\n _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;\n\n _.prototype.toString = function() {\n return '' + this._wrapped;\n };\n\n // AMD registration happens at the end for compatibility with AMD loaders\n // that may not enforce next-turn semantics on modules. Even though general\n // practice for AMD registration is to be anonymous, underscore registers\n // as a named module because, like jQuery, it is a base library that is\n // popular enough to be bundled in a third party lib, but not be part of\n // an AMD load request. Those cases could generate an error when an\n // anonymous define() is called outside of a loader request.\n if (typeof define === 'function' && define.amd) {\n define('underscore', [], function() {\n return _;\n });\n }\n}.call(this));\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/underscore/underscore.js\n// module id = 3\n// module chunks = 0","module.exports = {\n\t\"name\": \"bonobo-jupyter\",\n\t\"version\": \"0.0.1\",\n\t\"description\": \"Jupyter integration for Bonobo\",\n\t\"author\": \"\",\n\t\"main\": \"src/index.js\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"\"\n\t},\n\t\"keywords\": [\n\t\t\"jupyter\",\n\t\t\"widgets\",\n\t\t\"ipython\",\n\t\t\"ipywidgets\"\n\t],\n\t\"scripts\": {\n\t\t\"prepublish\": \"webpack\",\n\t\t\"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"json-loader\": \"^0.5.4\",\n\t\t\"webpack\": \"^1.12.14\"\n\t},\n\t\"dependencies\": {\n\t\t\"jupyter-js-widgets\": \"^2.0.9\",\n\t\t\"underscore\": \"^1.8.3\"\n\t}\n};\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./package.json\n// module id = 4\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file diff --git a/bonobo/ext/jupyter/widget.py b/bonobo/contrib/jupyter/widget.py similarity index 100% rename from bonobo/ext/jupyter/widget.py rename to bonobo/contrib/jupyter/widget.py diff --git a/bonobo/execution/contexts/node.py b/bonobo/execution/contexts/node.py index db2c39a..767b929 100644 --- a/bonobo/execution/contexts/node.py +++ b/bonobo/execution/contexts/node.py @@ -159,6 +159,14 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): # self._exec_count += 1 pass + def as_dict(self): + return { + 'status': self.status, + 'name': self.name, + 'stats': self.get_statistics_as_string(), + 'flags': self.get_flags_as_string(), + } + def isflag(param): return isinstance(param, Token) and param in (NOT_MODIFIED, ) diff --git a/bonobo/execution/strategies/executor.py b/bonobo/execution/strategies/executor.py index ebbaef1..ea6c08e 100644 --- a/bonobo/execution/strategies/executor.py +++ b/bonobo/execution/strategies/executor.py @@ -8,6 +8,7 @@ from bonobo.constants import BEGIN, END from bonobo.execution.strategies.base import Strategy from bonobo.util import get_name +logger = logging.getLogger(__name__) class ExecutorStrategy(Strategy): """ @@ -30,8 +31,7 @@ class ExecutorStrategy(Strategy): try: context.start(self.get_starter(executor, futures)) except: - logging.getLogger(__name__ - ).warning('KeyboardInterrupt received. Trying to terminate the nodes gracefully.') + logger.critical('Exception caught while starting execution context.', exc_info=sys.exc_info()) while context.alive: try: diff --git a/bonobo/ext/__init__.py b/bonobo/ext/__init__.py deleted file mode 100644 index 7b00775..0000000 --- a/bonobo/ext/__init__.py +++ /dev/null @@ -1 +0,0 @@ -""" Extensions, not required. """ diff --git a/bonobo/ext/jupyter/__init__.py b/bonobo/ext/jupyter/__init__.py deleted file mode 100644 index 3ae8068..0000000 --- a/bonobo/ext/jupyter/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .plugin import JupyterOutputPlugin - - -def _jupyter_nbextension_paths(): - return [{'section': 'notebook', 'src': 'static', 'dest': 'bonobo-jupyter', 'require': 'bonobo-jupyter/extension'}] - - -__all__ = [ - 'JupyterOutputPlugin', -] diff --git a/bonobo/ext/jupyter/js/README.md b/bonobo/ext/jupyter/js/README.md deleted file mode 100644 index 2c4376e..0000000 --- a/bonobo/ext/jupyter/js/README.md +++ /dev/null @@ -1,19 +0,0 @@ -Bonobo integration in Jupyter - -Package Install ---------------- - -**Prerequisites** -- [node](http://nodejs.org/) - -```bash -npm install --save bonobo-jupyter -``` - -Watch mode (for development) ----------------------------- - -```bash -./node_modules/.bin/webpack --watch -`` - diff --git a/bonobo/ext/jupyter/js/dist/index.js.map b/bonobo/ext/jupyter/js/dist/index.js.map deleted file mode 100644 index 664c7e0..0000000 --- a/bonobo/ext/jupyter/js/dist/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack:///webpack/bootstrap 0804ef1bbb84581f3e1d","webpack:///./src/embed.js","webpack:///./src/bonobo.js","webpack:///external \"jupyter-js-widgets\"","webpack:///./~/underscore/underscore.js","webpack:///./package.json"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;;;;;;ACRA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;;;;;;;ACvCA,gD;;;;;;ACAA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;AACH;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA,wBAAuB,OAAO;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uCAAsC,YAAY;AAClD;AACA;AACA,MAAK;AACL;AACA,wCAAuC,YAAY;AACnD;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA6B,gBAAgB;AAC7C;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA,qDAAoD;AACpD,IAAG;;AAEH;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA,2CAA0C;AAC1C,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,6DAA4D,YAAY;AACxE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA,sBAAqB,gBAAgB;AACrC;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,8CAA6C,YAAY;AACzD;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAsD;AACtD;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAS;AACT;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,0BAA0B;AACpE;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA,sBAAqB,cAAc;AACnC;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,sBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAe,YAAY;AAC3B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,QAAO,eAAe;AACtB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,sBAAqB,eAAe;AACpC;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAsB;AACtB;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA,oBAAmB;AACnB;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,6CAA4C,mBAAmB;AAC/D;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,sDAAqD;AACrD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;AACA;;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8EAA6E;AAC7E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA,sCAAqC;AACrC;AACA;AACA;;AAEA;AACA;AACA;AACA,2BAA0B;AAC1B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,oBAAmB,OAAO;AAC1B;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,gBAAe;AACf,eAAc;AACd,eAAc;AACd,iBAAgB;AAChB,iBAAgB;AAChB,iBAAgB;AAChB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA4B;;AAE5B;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA,QAAO;AACP,sBAAqB;AACrB;;AAEA;AACA;AACA,MAAK;AACL,kBAAiB;;AAEjB;AACA,mDAAkD,EAAE,iBAAiB;;AAErE;AACA,yBAAwB,8BAA8B;AACtD,4BAA2B;;AAE3B;AACA;AACA,MAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,mDAAkD,iBAAiB;;AAEnE;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,EAAC;;;;;;;AC3gDD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA,G","file":"index.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"https://unpkg.com/jupyter-widget-example@0.0.1/dist/\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 0804ef1bbb84581f3e1d","// Entry point for the unpkg bundle containing custom model definitions.\n//\n// It differs from the notebook bundle in that it does not need to define a\n// dynamic baseURL for the static assets and may load some css that would\n// already be loaded by the notebook otherwise.\n\n// Export widget models and views, and the npm package version number.\nmodule.exports = require('./bonobo.js');\nmodule.exports['version'] = require('../package.json').version;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/embed.js\n// module id = 0\n// module chunks = 0","var widgets = require('jupyter-js-widgets');\nvar _ = require('underscore');\n\n// Custom Model. Custom widgets models must at least provide default values\n// for model attributes, including `_model_name`, `_view_name`, `_model_module`\n// and `_view_module` when different from the base class.\n//\n// When serialiazing entire widget state for embedding, only values different from the\n// defaults will be specified.\n\nvar BonoboModel = widgets.DOMWidgetModel.extend({\n defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, {\n _model_name: 'BonoboModel',\n _view_name: 'BonoboView',\n _model_module: 'bonobo',\n _view_module: 'bonobo',\n value: []\n })\n});\n\n\n// Custom View. Renders the widget model.\nvar BonoboView = widgets.DOMWidgetView.extend({\n render: function () {\n this.value_changed();\n this.model.on('change:value', this.value_changed, this);\n },\n\n value_changed: function () {\n this.$el.html(\n this.model.get('value').join('
')\n );\n },\n});\n\n\nmodule.exports = {\n BonoboModel: BonoboModel,\n BonoboView: BonoboView\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/bonobo.js\n// module id = 1\n// module chunks = 0","module.exports = __WEBPACK_EXTERNAL_MODULE_2__;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"jupyter-js-widgets\"\n// module id = 2\n// module chunks = 0","// Underscore.js 1.8.3\n// http://underscorejs.org\n// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n// Underscore may be freely distributed under the MIT license.\n\n(function() {\n\n // Baseline setup\n // --------------\n\n // Establish the root object, `window` in the browser, or `exports` on the server.\n var root = this;\n\n // Save the previous value of the `_` variable.\n var previousUnderscore = root._;\n\n // Save bytes in the minified (but not gzipped) version:\n var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n // Create quick reference variables for speed access to core prototypes.\n var\n push = ArrayProto.push,\n slice = ArrayProto.slice,\n toString = ObjProto.toString,\n hasOwnProperty = ObjProto.hasOwnProperty;\n\n // All **ECMAScript 5** native function implementations that we hope to use\n // are declared here.\n var\n nativeIsArray = Array.isArray,\n nativeKeys = Object.keys,\n nativeBind = FuncProto.bind,\n nativeCreate = Object.create;\n\n // Naked function reference for surrogate-prototype-swapping.\n var Ctor = function(){};\n\n // Create a safe reference to the Underscore object for use below.\n var _ = function(obj) {\n if (obj instanceof _) return obj;\n if (!(this instanceof _)) return new _(obj);\n this._wrapped = obj;\n };\n\n // Export the Underscore object for **Node.js**, with\n // backwards-compatibility for the old `require()` API. If we're in\n // the browser, add `_` as a global object.\n if (typeof exports !== 'undefined') {\n if (typeof module !== 'undefined' && module.exports) {\n exports = module.exports = _;\n }\n exports._ = _;\n } else {\n root._ = _;\n }\n\n // Current version.\n _.VERSION = '1.8.3';\n\n // Internal function that returns an efficient (for current engines) version\n // of the passed-in callback, to be repeatedly applied in other Underscore\n // functions.\n var optimizeCb = function(func, context, argCount) {\n if (context === void 0) return func;\n switch (argCount == null ? 3 : argCount) {\n case 1: return function(value) {\n return func.call(context, value);\n };\n case 2: return function(value, other) {\n return func.call(context, value, other);\n };\n case 3: return function(value, index, collection) {\n return func.call(context, value, index, collection);\n };\n case 4: return function(accumulator, value, index, collection) {\n return func.call(context, accumulator, value, index, collection);\n };\n }\n return function() {\n return func.apply(context, arguments);\n };\n };\n\n // A mostly-internal function to generate callbacks that can be applied\n // to each element in a collection, returning the desired result — either\n // identity, an arbitrary callback, a property matcher, or a property accessor.\n var cb = function(value, context, argCount) {\n if (value == null) return _.identity;\n if (_.isFunction(value)) return optimizeCb(value, context, argCount);\n if (_.isObject(value)) return _.matcher(value);\n return _.property(value);\n };\n _.iteratee = function(value, context) {\n return cb(value, context, Infinity);\n };\n\n // An internal function for creating assigner functions.\n var createAssigner = function(keysFunc, undefinedOnly) {\n return function(obj) {\n var length = arguments.length;\n if (length < 2 || obj == null) return obj;\n for (var index = 1; index < length; index++) {\n var source = arguments[index],\n keys = keysFunc(source),\n l = keys.length;\n for (var i = 0; i < l; i++) {\n var key = keys[i];\n if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];\n }\n }\n return obj;\n };\n };\n\n // An internal function for creating a new object that inherits from another.\n var baseCreate = function(prototype) {\n if (!_.isObject(prototype)) return {};\n if (nativeCreate) return nativeCreate(prototype);\n Ctor.prototype = prototype;\n var result = new Ctor;\n Ctor.prototype = null;\n return result;\n };\n\n var property = function(key) {\n return function(obj) {\n return obj == null ? void 0 : obj[key];\n };\n };\n\n // Helper for collection methods to determine whether a collection\n // should be iterated as an array or as an object\n // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength\n // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094\n var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;\n var getLength = property('length');\n var isArrayLike = function(collection) {\n var length = getLength(collection);\n return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;\n };\n\n // Collection Functions\n // --------------------\n\n // The cornerstone, an `each` implementation, aka `forEach`.\n // Handles raw objects in addition to array-likes. Treats all\n // sparse array-likes as if they were dense.\n _.each = _.forEach = function(obj, iteratee, context) {\n iteratee = optimizeCb(iteratee, context);\n var i, length;\n if (isArrayLike(obj)) {\n for (i = 0, length = obj.length; i < length; i++) {\n iteratee(obj[i], i, obj);\n }\n } else {\n var keys = _.keys(obj);\n for (i = 0, length = keys.length; i < length; i++) {\n iteratee(obj[keys[i]], keys[i], obj);\n }\n }\n return obj;\n };\n\n // Return the results of applying the iteratee to each element.\n _.map = _.collect = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n results = Array(length);\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n results[index] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Create a reducing function iterating left or right.\n function createReduce(dir) {\n // Optimized iterator function as using arguments.length\n // in the main function will deoptimize the, see #1991.\n function iterator(obj, iteratee, memo, keys, index, length) {\n for (; index >= 0 && index < length; index += dir) {\n var currentKey = keys ? keys[index] : index;\n memo = iteratee(memo, obj[currentKey], currentKey, obj);\n }\n return memo;\n }\n\n return function(obj, iteratee, memo, context) {\n iteratee = optimizeCb(iteratee, context, 4);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n index = dir > 0 ? 0 : length - 1;\n // Determine the initial value if none is provided.\n if (arguments.length < 3) {\n memo = obj[keys ? keys[index] : index];\n index += dir;\n }\n return iterator(obj, iteratee, memo, keys, index, length);\n };\n }\n\n // **Reduce** builds up a single result from a list of values, aka `inject`,\n // or `foldl`.\n _.reduce = _.foldl = _.inject = createReduce(1);\n\n // The right-associative version of reduce, also known as `foldr`.\n _.reduceRight = _.foldr = createReduce(-1);\n\n // Return the first value which passes a truth test. Aliased as `detect`.\n _.find = _.detect = function(obj, predicate, context) {\n var key;\n if (isArrayLike(obj)) {\n key = _.findIndex(obj, predicate, context);\n } else {\n key = _.findKey(obj, predicate, context);\n }\n if (key !== void 0 && key !== -1) return obj[key];\n };\n\n // Return all the elements that pass a truth test.\n // Aliased as `select`.\n _.filter = _.select = function(obj, predicate, context) {\n var results = [];\n predicate = cb(predicate, context);\n _.each(obj, function(value, index, list) {\n if (predicate(value, index, list)) results.push(value);\n });\n return results;\n };\n\n // Return all the elements for which a truth test fails.\n _.reject = function(obj, predicate, context) {\n return _.filter(obj, _.negate(cb(predicate)), context);\n };\n\n // Determine whether all of the elements match a truth test.\n // Aliased as `all`.\n _.every = _.all = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (!predicate(obj[currentKey], currentKey, obj)) return false;\n }\n return true;\n };\n\n // Determine if at least one element in the object matches a truth test.\n // Aliased as `any`.\n _.some = _.any = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (predicate(obj[currentKey], currentKey, obj)) return true;\n }\n return false;\n };\n\n // Determine if the array or object contains a given item (using `===`).\n // Aliased as `includes` and `include`.\n _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n if (typeof fromIndex != 'number' || guard) fromIndex = 0;\n return _.indexOf(obj, item, fromIndex) >= 0;\n };\n\n // Invoke a method (with arguments) on every item in a collection.\n _.invoke = function(obj, method) {\n var args = slice.call(arguments, 2);\n var isFunc = _.isFunction(method);\n return _.map(obj, function(value) {\n var func = isFunc ? method : value[method];\n return func == null ? func : func.apply(value, args);\n });\n };\n\n // Convenience version of a common use case of `map`: fetching a property.\n _.pluck = function(obj, key) {\n return _.map(obj, _.property(key));\n };\n\n // Convenience version of a common use case of `filter`: selecting only objects\n // containing specific `key:value` pairs.\n _.where = function(obj, attrs) {\n return _.filter(obj, _.matcher(attrs));\n };\n\n // Convenience version of a common use case of `find`: getting the first object\n // containing specific `key:value` pairs.\n _.findWhere = function(obj, attrs) {\n return _.find(obj, _.matcher(attrs));\n };\n\n // Return the maximum element (or element-based computation).\n _.max = function(obj, iteratee, context) {\n var result = -Infinity, lastComputed = -Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value > result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed > lastComputed || computed === -Infinity && result === -Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Return the minimum element (or element-based computation).\n _.min = function(obj, iteratee, context) {\n var result = Infinity, lastComputed = Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value < result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed < lastComputed || computed === Infinity && result === Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Shuffle a collection, using the modern version of the\n // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).\n _.shuffle = function(obj) {\n var set = isArrayLike(obj) ? obj : _.values(obj);\n var length = set.length;\n var shuffled = Array(length);\n for (var index = 0, rand; index < length; index++) {\n rand = _.random(0, index);\n if (rand !== index) shuffled[index] = shuffled[rand];\n shuffled[rand] = set[index];\n }\n return shuffled;\n };\n\n // Sample **n** random values from a collection.\n // If **n** is not specified, returns a single random element.\n // The internal `guard` argument allows it to work with `map`.\n _.sample = function(obj, n, guard) {\n if (n == null || guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n return obj[_.random(obj.length - 1)];\n }\n return _.shuffle(obj).slice(0, Math.max(0, n));\n };\n\n // Sort the object's values by a criterion produced by an iteratee.\n _.sortBy = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n return _.pluck(_.map(obj, function(value, index, list) {\n return {\n value: value,\n index: index,\n criteria: iteratee(value, index, list)\n };\n }).sort(function(left, right) {\n var a = left.criteria;\n var b = right.criteria;\n if (a !== b) {\n if (a > b || a === void 0) return 1;\n if (a < b || b === void 0) return -1;\n }\n return left.index - right.index;\n }), 'value');\n };\n\n // An internal function used for aggregate \"group by\" operations.\n var group = function(behavior) {\n return function(obj, iteratee, context) {\n var result = {};\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index) {\n var key = iteratee(value, index, obj);\n behavior(result, value, key);\n });\n return result;\n };\n };\n\n // Groups the object's values by a criterion. Pass either a string attribute\n // to group by, or a function that returns the criterion.\n _.groupBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key].push(value); else result[key] = [value];\n });\n\n // Indexes the object's values by a criterion, similar to `groupBy`, but for\n // when you know that your index values will be unique.\n _.indexBy = group(function(result, value, key) {\n result[key] = value;\n });\n\n // Counts instances of an object that group by a certain criterion. Pass\n // either a string attribute to count by, or a function that returns the\n // criterion.\n _.countBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key]++; else result[key] = 1;\n });\n\n // Safely create a real, live array from anything iterable.\n _.toArray = function(obj) {\n if (!obj) return [];\n if (_.isArray(obj)) return slice.call(obj);\n if (isArrayLike(obj)) return _.map(obj, _.identity);\n return _.values(obj);\n };\n\n // Return the number of elements in an object.\n _.size = function(obj) {\n if (obj == null) return 0;\n return isArrayLike(obj) ? obj.length : _.keys(obj).length;\n };\n\n // Split a collection into two arrays: one whose elements all satisfy the given\n // predicate, and one whose elements all do not satisfy the predicate.\n _.partition = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var pass = [], fail = [];\n _.each(obj, function(value, key, obj) {\n (predicate(value, key, obj) ? pass : fail).push(value);\n });\n return [pass, fail];\n };\n\n // Array Functions\n // ---------------\n\n // Get the first element of an array. Passing **n** will return the first N\n // values in the array. Aliased as `head` and `take`. The **guard** check\n // allows it to work with `_.map`.\n _.first = _.head = _.take = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[0];\n return _.initial(array, array.length - n);\n };\n\n // Returns everything but the last entry of the array. Especially useful on\n // the arguments object. Passing **n** will return all the values in\n // the array, excluding the last N.\n _.initial = function(array, n, guard) {\n return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));\n };\n\n // Get the last element of an array. Passing **n** will return the last N\n // values in the array.\n _.last = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[array.length - 1];\n return _.rest(array, Math.max(0, array.length - n));\n };\n\n // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.\n // Especially useful on the arguments object. Passing an **n** will return\n // the rest N values in the array.\n _.rest = _.tail = _.drop = function(array, n, guard) {\n return slice.call(array, n == null || guard ? 1 : n);\n };\n\n // Trim out all falsy values from an array.\n _.compact = function(array) {\n return _.filter(array, _.identity);\n };\n\n // Internal implementation of a recursive `flatten` function.\n var flatten = function(input, shallow, strict, startIndex) {\n var output = [], idx = 0;\n for (var i = startIndex || 0, length = getLength(input); i < length; i++) {\n var value = input[i];\n if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {\n //flatten current level of array or arguments object\n if (!shallow) value = flatten(value, shallow, strict);\n var j = 0, len = value.length;\n output.length += len;\n while (j < len) {\n output[idx++] = value[j++];\n }\n } else if (!strict) {\n output[idx++] = value;\n }\n }\n return output;\n };\n\n // Flatten out an array, either recursively (by default), or just one level.\n _.flatten = function(array, shallow) {\n return flatten(array, shallow, false);\n };\n\n // Return a version of the array that does not contain the specified value(s).\n _.without = function(array) {\n return _.difference(array, slice.call(arguments, 1));\n };\n\n // Produce a duplicate-free version of the array. If the array has already\n // been sorted, you have the option of using a faster algorithm.\n // Aliased as `unique`.\n _.uniq = _.unique = function(array, isSorted, iteratee, context) {\n if (!_.isBoolean(isSorted)) {\n context = iteratee;\n iteratee = isSorted;\n isSorted = false;\n }\n if (iteratee != null) iteratee = cb(iteratee, context);\n var result = [];\n var seen = [];\n for (var i = 0, length = getLength(array); i < length; i++) {\n var value = array[i],\n computed = iteratee ? iteratee(value, i, array) : value;\n if (isSorted) {\n if (!i || seen !== computed) result.push(value);\n seen = computed;\n } else if (iteratee) {\n if (!_.contains(seen, computed)) {\n seen.push(computed);\n result.push(value);\n }\n } else if (!_.contains(result, value)) {\n result.push(value);\n }\n }\n return result;\n };\n\n // Produce an array that contains the union: each distinct element from all of\n // the passed-in arrays.\n _.union = function() {\n return _.uniq(flatten(arguments, true, true));\n };\n\n // Produce an array that contains every item shared between all the\n // passed-in arrays.\n _.intersection = function(array) {\n var result = [];\n var argsLength = arguments.length;\n for (var i = 0, length = getLength(array); i < length; i++) {\n var item = array[i];\n if (_.contains(result, item)) continue;\n for (var j = 1; j < argsLength; j++) {\n if (!_.contains(arguments[j], item)) break;\n }\n if (j === argsLength) result.push(item);\n }\n return result;\n };\n\n // Take the difference between one array and a number of other arrays.\n // Only the elements present in just the first array will remain.\n _.difference = function(array) {\n var rest = flatten(arguments, true, true, 1);\n return _.filter(array, function(value){\n return !_.contains(rest, value);\n });\n };\n\n // Zip together multiple lists into a single array -- elements that share\n // an index go together.\n _.zip = function() {\n return _.unzip(arguments);\n };\n\n // Complement of _.zip. Unzip accepts an array of arrays and groups\n // each array's elements on shared indices\n _.unzip = function(array) {\n var length = array && _.max(array, getLength).length || 0;\n var result = Array(length);\n\n for (var index = 0; index < length; index++) {\n result[index] = _.pluck(array, index);\n }\n return result;\n };\n\n // Converts lists into objects. Pass either a single array of `[key, value]`\n // pairs, or two parallel arrays of the same length -- one of keys, and one of\n // the corresponding values.\n _.object = function(list, values) {\n var result = {};\n for (var i = 0, length = getLength(list); i < length; i++) {\n if (values) {\n result[list[i]] = values[i];\n } else {\n result[list[i][0]] = list[i][1];\n }\n }\n return result;\n };\n\n // Generator function to create the findIndex and findLastIndex functions\n function createPredicateIndexFinder(dir) {\n return function(array, predicate, context) {\n predicate = cb(predicate, context);\n var length = getLength(array);\n var index = dir > 0 ? 0 : length - 1;\n for (; index >= 0 && index < length; index += dir) {\n if (predicate(array[index], index, array)) return index;\n }\n return -1;\n };\n }\n\n // Returns the first index on an array-like that passes a predicate test\n _.findIndex = createPredicateIndexFinder(1);\n _.findLastIndex = createPredicateIndexFinder(-1);\n\n // Use a comparator function to figure out the smallest index at which\n // an object should be inserted so as to maintain order. Uses binary search.\n _.sortedIndex = function(array, obj, iteratee, context) {\n iteratee = cb(iteratee, context, 1);\n var value = iteratee(obj);\n var low = 0, high = getLength(array);\n while (low < high) {\n var mid = Math.floor((low + high) / 2);\n if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;\n }\n return low;\n };\n\n // Generator function to create the indexOf and lastIndexOf functions\n function createIndexFinder(dir, predicateFind, sortedIndex) {\n return function(array, item, idx) {\n var i = 0, length = getLength(array);\n if (typeof idx == 'number') {\n if (dir > 0) {\n i = idx >= 0 ? idx : Math.max(idx + length, i);\n } else {\n length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;\n }\n } else if (sortedIndex && idx && length) {\n idx = sortedIndex(array, item);\n return array[idx] === item ? idx : -1;\n }\n if (item !== item) {\n idx = predicateFind(slice.call(array, i, length), _.isNaN);\n return idx >= 0 ? idx + i : -1;\n }\n for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {\n if (array[idx] === item) return idx;\n }\n return -1;\n };\n }\n\n // Return the position of the first occurrence of an item in an array,\n // or -1 if the item is not included in the array.\n // If the array is large and already in sort order, pass `true`\n // for **isSorted** to use binary search.\n _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);\n _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);\n\n // Generate an integer Array containing an arithmetic progression. A port of\n // the native Python `range()` function. See\n // [the Python documentation](http://docs.python.org/library/functions.html#range).\n _.range = function(start, stop, step) {\n if (stop == null) {\n stop = start || 0;\n start = 0;\n }\n step = step || 1;\n\n var length = Math.max(Math.ceil((stop - start) / step), 0);\n var range = Array(length);\n\n for (var idx = 0; idx < length; idx++, start += step) {\n range[idx] = start;\n }\n\n return range;\n };\n\n // Function (ahem) Functions\n // ------------------\n\n // Determines whether to execute a function as a constructor\n // or a normal function with the provided arguments\n var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {\n if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);\n var self = baseCreate(sourceFunc.prototype);\n var result = sourceFunc.apply(self, args);\n if (_.isObject(result)) return result;\n return self;\n };\n\n // Create a function bound to a given object (assigning `this`, and arguments,\n // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if\n // available.\n _.bind = function(func, context) {\n if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');\n var args = slice.call(arguments, 2);\n var bound = function() {\n return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));\n };\n return bound;\n };\n\n // Partially apply a function by creating a version that has had some of its\n // arguments pre-filled, without changing its dynamic `this` context. _ acts\n // as a placeholder, allowing any combination of arguments to be pre-filled.\n _.partial = function(func) {\n var boundArgs = slice.call(arguments, 1);\n var bound = function() {\n var position = 0, length = boundArgs.length;\n var args = Array(length);\n for (var i = 0; i < length; i++) {\n args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];\n }\n while (position < arguments.length) args.push(arguments[position++]);\n return executeBound(func, bound, this, this, args);\n };\n return bound;\n };\n\n // Bind a number of an object's methods to that object. Remaining arguments\n // are the method names to be bound. Useful for ensuring that all callbacks\n // defined on an object belong to it.\n _.bindAll = function(obj) {\n var i, length = arguments.length, key;\n if (length <= 1) throw new Error('bindAll must be passed function names');\n for (i = 1; i < length; i++) {\n key = arguments[i];\n obj[key] = _.bind(obj[key], obj);\n }\n return obj;\n };\n\n // Memoize an expensive function by storing its results.\n _.memoize = function(func, hasher) {\n var memoize = function(key) {\n var cache = memoize.cache;\n var address = '' + (hasher ? hasher.apply(this, arguments) : key);\n if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);\n return cache[address];\n };\n memoize.cache = {};\n return memoize;\n };\n\n // Delays a function for the given number of milliseconds, and then calls\n // it with the arguments supplied.\n _.delay = function(func, wait) {\n var args = slice.call(arguments, 2);\n return setTimeout(function(){\n return func.apply(null, args);\n }, wait);\n };\n\n // Defers a function, scheduling it to run after the current call stack has\n // cleared.\n _.defer = _.partial(_.delay, _, 1);\n\n // Returns a function, that, when invoked, will only be triggered at most once\n // during a given window of time. Normally, the throttled function will run\n // as much as it can, without ever going more than once per `wait` duration;\n // but if you'd like to disable the execution on the leading edge, pass\n // `{leading: false}`. To disable execution on the trailing edge, ditto.\n _.throttle = function(func, wait, options) {\n var context, args, result;\n var timeout = null;\n var previous = 0;\n if (!options) options = {};\n var later = function() {\n previous = options.leading === false ? 0 : _.now();\n timeout = null;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n };\n return function() {\n var now = _.now();\n if (!previous && options.leading === false) previous = now;\n var remaining = wait - (now - previous);\n context = this;\n args = arguments;\n if (remaining <= 0 || remaining > wait) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n previous = now;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n } else if (!timeout && options.trailing !== false) {\n timeout = setTimeout(later, remaining);\n }\n return result;\n };\n };\n\n // Returns a function, that, as long as it continues to be invoked, will not\n // be triggered. The function will be called after it stops being called for\n // N milliseconds. If `immediate` is passed, trigger the function on the\n // leading edge, instead of the trailing.\n _.debounce = function(func, wait, immediate) {\n var timeout, args, context, timestamp, result;\n\n var later = function() {\n var last = _.now() - timestamp;\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last);\n } else {\n timeout = null;\n if (!immediate) {\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n }\n }\n };\n\n return function() {\n context = this;\n args = arguments;\n timestamp = _.now();\n var callNow = immediate && !timeout;\n if (!timeout) timeout = setTimeout(later, wait);\n if (callNow) {\n result = func.apply(context, args);\n context = args = null;\n }\n\n return result;\n };\n };\n\n // Returns the first function passed as an argument to the second,\n // allowing you to adjust arguments, run code before and after, and\n // conditionally execute the original function.\n _.wrap = function(func, wrapper) {\n return _.partial(wrapper, func);\n };\n\n // Returns a negated version of the passed-in predicate.\n _.negate = function(predicate) {\n return function() {\n return !predicate.apply(this, arguments);\n };\n };\n\n // Returns a function that is the composition of a list of functions, each\n // consuming the return value of the function that follows.\n _.compose = function() {\n var args = arguments;\n var start = args.length - 1;\n return function() {\n var i = start;\n var result = args[start].apply(this, arguments);\n while (i--) result = args[i].call(this, result);\n return result;\n };\n };\n\n // Returns a function that will only be executed on and after the Nth call.\n _.after = function(times, func) {\n return function() {\n if (--times < 1) {\n return func.apply(this, arguments);\n }\n };\n };\n\n // Returns a function that will only be executed up to (but not including) the Nth call.\n _.before = function(times, func) {\n var memo;\n return function() {\n if (--times > 0) {\n memo = func.apply(this, arguments);\n }\n if (times <= 1) func = null;\n return memo;\n };\n };\n\n // Returns a function that will be executed at most one time, no matter how\n // often you call it. Useful for lazy initialization.\n _.once = _.partial(_.before, 2);\n\n // Object Functions\n // ----------------\n\n // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.\n var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');\n var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',\n 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];\n\n function collectNonEnumProps(obj, keys) {\n var nonEnumIdx = nonEnumerableProps.length;\n var constructor = obj.constructor;\n var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;\n\n // Constructor is a special case.\n var prop = 'constructor';\n if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);\n\n while (nonEnumIdx--) {\n prop = nonEnumerableProps[nonEnumIdx];\n if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {\n keys.push(prop);\n }\n }\n }\n\n // Retrieve the names of an object's own properties.\n // Delegates to **ECMAScript 5**'s native `Object.keys`\n _.keys = function(obj) {\n if (!_.isObject(obj)) return [];\n if (nativeKeys) return nativeKeys(obj);\n var keys = [];\n for (var key in obj) if (_.has(obj, key)) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve all the property names of an object.\n _.allKeys = function(obj) {\n if (!_.isObject(obj)) return [];\n var keys = [];\n for (var key in obj) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve the values of an object's properties.\n _.values = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var values = Array(length);\n for (var i = 0; i < length; i++) {\n values[i] = obj[keys[i]];\n }\n return values;\n };\n\n // Returns the results of applying the iteratee to each element of the object\n // In contrast to _.map it returns an object\n _.mapObject = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = _.keys(obj),\n length = keys.length,\n results = {},\n currentKey;\n for (var index = 0; index < length; index++) {\n currentKey = keys[index];\n results[currentKey] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Convert an object into a list of `[key, value]` pairs.\n _.pairs = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var pairs = Array(length);\n for (var i = 0; i < length; i++) {\n pairs[i] = [keys[i], obj[keys[i]]];\n }\n return pairs;\n };\n\n // Invert the keys and values of an object. The values must be serializable.\n _.invert = function(obj) {\n var result = {};\n var keys = _.keys(obj);\n for (var i = 0, length = keys.length; i < length; i++) {\n result[obj[keys[i]]] = keys[i];\n }\n return result;\n };\n\n // Return a sorted list of the function names available on the object.\n // Aliased as `methods`\n _.functions = _.methods = function(obj) {\n var names = [];\n for (var key in obj) {\n if (_.isFunction(obj[key])) names.push(key);\n }\n return names.sort();\n };\n\n // Extend a given object with all the properties in passed-in object(s).\n _.extend = createAssigner(_.allKeys);\n\n // Assigns a given object with all the own properties in the passed-in object(s)\n // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)\n _.extendOwn = _.assign = createAssigner(_.keys);\n\n // Returns the first key on an object that passes a predicate test\n _.findKey = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = _.keys(obj), key;\n for (var i = 0, length = keys.length; i < length; i++) {\n key = keys[i];\n if (predicate(obj[key], key, obj)) return key;\n }\n };\n\n // Return a copy of the object only containing the whitelisted properties.\n _.pick = function(object, oiteratee, context) {\n var result = {}, obj = object, iteratee, keys;\n if (obj == null) return result;\n if (_.isFunction(oiteratee)) {\n keys = _.allKeys(obj);\n iteratee = optimizeCb(oiteratee, context);\n } else {\n keys = flatten(arguments, false, false, 1);\n iteratee = function(value, key, obj) { return key in obj; };\n obj = Object(obj);\n }\n for (var i = 0, length = keys.length; i < length; i++) {\n var key = keys[i];\n var value = obj[key];\n if (iteratee(value, key, obj)) result[key] = value;\n }\n return result;\n };\n\n // Return a copy of the object without the blacklisted properties.\n _.omit = function(obj, iteratee, context) {\n if (_.isFunction(iteratee)) {\n iteratee = _.negate(iteratee);\n } else {\n var keys = _.map(flatten(arguments, false, false, 1), String);\n iteratee = function(value, key) {\n return !_.contains(keys, key);\n };\n }\n return _.pick(obj, iteratee, context);\n };\n\n // Fill in a given object with default properties.\n _.defaults = createAssigner(_.allKeys, true);\n\n // Creates an object that inherits from the given prototype object.\n // If additional properties are provided then they will be added to the\n // created object.\n _.create = function(prototype, props) {\n var result = baseCreate(prototype);\n if (props) _.extendOwn(result, props);\n return result;\n };\n\n // Create a (shallow-cloned) duplicate of an object.\n _.clone = function(obj) {\n if (!_.isObject(obj)) return obj;\n return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n };\n\n // Invokes interceptor with the obj, and then returns obj.\n // The primary purpose of this method is to \"tap into\" a method chain, in\n // order to perform operations on intermediate results within the chain.\n _.tap = function(obj, interceptor) {\n interceptor(obj);\n return obj;\n };\n\n // Returns whether an object has a given set of `key:value` pairs.\n _.isMatch = function(object, attrs) {\n var keys = _.keys(attrs), length = keys.length;\n if (object == null) return !length;\n var obj = Object(object);\n for (var i = 0; i < length; i++) {\n var key = keys[i];\n if (attrs[key] !== obj[key] || !(key in obj)) return false;\n }\n return true;\n };\n\n\n // Internal recursive comparison function for `isEqual`.\n var eq = function(a, b, aStack, bStack) {\n // Identical objects are equal. `0 === -0`, but they aren't identical.\n // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).\n if (a === b) return a !== 0 || 1 / a === 1 / b;\n // A strict comparison is necessary because `null == undefined`.\n if (a == null || b == null) return a === b;\n // Unwrap any wrapped objects.\n if (a instanceof _) a = a._wrapped;\n if (b instanceof _) b = b._wrapped;\n // Compare `[[Class]]` names.\n var className = toString.call(a);\n if (className !== toString.call(b)) return false;\n switch (className) {\n // Strings, numbers, regular expressions, dates, and booleans are compared by value.\n case '[object RegExp]':\n // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')\n case '[object String]':\n // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n // equivalent to `new String(\"5\")`.\n return '' + a === '' + b;\n case '[object Number]':\n // `NaN`s are equivalent, but non-reflexive.\n // Object(NaN) is equivalent to NaN\n if (+a !== +a) return +b !== +b;\n // An `egal` comparison is performed for other numeric values.\n return +a === 0 ? 1 / +a === 1 / b : +a === +b;\n case '[object Date]':\n case '[object Boolean]':\n // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n // millisecond representations. Note that invalid dates with millisecond representations\n // of `NaN` are not equivalent.\n return +a === +b;\n }\n\n var areArrays = className === '[object Array]';\n if (!areArrays) {\n if (typeof a != 'object' || typeof b != 'object') return false;\n\n // Objects with different constructors are not equivalent, but `Object`s or `Array`s\n // from different frames are.\n var aCtor = a.constructor, bCtor = b.constructor;\n if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&\n _.isFunction(bCtor) && bCtor instanceof bCtor)\n && ('constructor' in a && 'constructor' in b)) {\n return false;\n }\n }\n // Assume equality for cyclic structures. The algorithm for detecting cyclic\n // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n\n // Initializing stack of traversed objects.\n // It's done here since we only need them for objects and arrays comparison.\n aStack = aStack || [];\n bStack = bStack || [];\n var length = aStack.length;\n while (length--) {\n // Linear search. Performance is inversely proportional to the number of\n // unique nested structures.\n if (aStack[length] === a) return bStack[length] === b;\n }\n\n // Add the first object to the stack of traversed objects.\n aStack.push(a);\n bStack.push(b);\n\n // Recursively compare objects and arrays.\n if (areArrays) {\n // Compare array lengths to determine if a deep comparison is necessary.\n length = a.length;\n if (length !== b.length) return false;\n // Deep compare the contents, ignoring non-numeric properties.\n while (length--) {\n if (!eq(a[length], b[length], aStack, bStack)) return false;\n }\n } else {\n // Deep compare objects.\n var keys = _.keys(a), key;\n length = keys.length;\n // Ensure that both objects contain the same number of properties before comparing deep equality.\n if (_.keys(b).length !== length) return false;\n while (length--) {\n // Deep compare each member\n key = keys[length];\n if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;\n }\n }\n // Remove the first object from the stack of traversed objects.\n aStack.pop();\n bStack.pop();\n return true;\n };\n\n // Perform a deep comparison to check if two objects are equal.\n _.isEqual = function(a, b) {\n return eq(a, b);\n };\n\n // Is a given array, string, or object empty?\n // An \"empty\" object has no enumerable own-properties.\n _.isEmpty = function(obj) {\n if (obj == null) return true;\n if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;\n return _.keys(obj).length === 0;\n };\n\n // Is a given value a DOM element?\n _.isElement = function(obj) {\n return !!(obj && obj.nodeType === 1);\n };\n\n // Is a given value an array?\n // Delegates to ECMA5's native Array.isArray\n _.isArray = nativeIsArray || function(obj) {\n return toString.call(obj) === '[object Array]';\n };\n\n // Is a given variable an object?\n _.isObject = function(obj) {\n var type = typeof obj;\n return type === 'function' || type === 'object' && !!obj;\n };\n\n // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.\n _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {\n _['is' + name] = function(obj) {\n return toString.call(obj) === '[object ' + name + ']';\n };\n });\n\n // Define a fallback version of the method in browsers (ahem, IE < 9), where\n // there isn't any inspectable \"Arguments\" type.\n if (!_.isArguments(arguments)) {\n _.isArguments = function(obj) {\n return _.has(obj, 'callee');\n };\n }\n\n // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,\n // IE 11 (#1621), and in Safari 8 (#1929).\n if (typeof /./ != 'function' && typeof Int8Array != 'object') {\n _.isFunction = function(obj) {\n return typeof obj == 'function' || false;\n };\n }\n\n // Is a given object a finite number?\n _.isFinite = function(obj) {\n return isFinite(obj) && !isNaN(parseFloat(obj));\n };\n\n // Is the given value `NaN`? (NaN is the only number which does not equal itself).\n _.isNaN = function(obj) {\n return _.isNumber(obj) && obj !== +obj;\n };\n\n // Is a given value a boolean?\n _.isBoolean = function(obj) {\n return obj === true || obj === false || toString.call(obj) === '[object Boolean]';\n };\n\n // Is a given value equal to null?\n _.isNull = function(obj) {\n return obj === null;\n };\n\n // Is a given variable undefined?\n _.isUndefined = function(obj) {\n return obj === void 0;\n };\n\n // Shortcut function for checking if an object has a given property directly\n // on itself (in other words, not on a prototype).\n _.has = function(obj, key) {\n return obj != null && hasOwnProperty.call(obj, key);\n };\n\n // Utility Functions\n // -----------------\n\n // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n // previous owner. Returns a reference to the Underscore object.\n _.noConflict = function() {\n root._ = previousUnderscore;\n return this;\n };\n\n // Keep the identity function around for default iteratees.\n _.identity = function(value) {\n return value;\n };\n\n // Predicate-generating functions. Often useful outside of Underscore.\n _.constant = function(value) {\n return function() {\n return value;\n };\n };\n\n _.noop = function(){};\n\n _.property = property;\n\n // Generates a function for a given object that returns a given property.\n _.propertyOf = function(obj) {\n return obj == null ? function(){} : function(key) {\n return obj[key];\n };\n };\n\n // Returns a predicate for checking whether an object has a given set of\n // `key:value` pairs.\n _.matcher = _.matches = function(attrs) {\n attrs = _.extendOwn({}, attrs);\n return function(obj) {\n return _.isMatch(obj, attrs);\n };\n };\n\n // Run a function **n** times.\n _.times = function(n, iteratee, context) {\n var accum = Array(Math.max(0, n));\n iteratee = optimizeCb(iteratee, context, 1);\n for (var i = 0; i < n; i++) accum[i] = iteratee(i);\n return accum;\n };\n\n // Return a random integer between min and max (inclusive).\n _.random = function(min, max) {\n if (max == null) {\n max = min;\n min = 0;\n }\n return min + Math.floor(Math.random() * (max - min + 1));\n };\n\n // A (possibly faster) way to get the current timestamp as an integer.\n _.now = Date.now || function() {\n return new Date().getTime();\n };\n\n // List of HTML entities for escaping.\n var escapeMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '`': '`'\n };\n var unescapeMap = _.invert(escapeMap);\n\n // Functions for escaping and unescaping strings to/from HTML interpolation.\n var createEscaper = function(map) {\n var escaper = function(match) {\n return map[match];\n };\n // Regexes for identifying a key that needs to be escaped\n var source = '(?:' + _.keys(map).join('|') + ')';\n var testRegexp = RegExp(source);\n var replaceRegexp = RegExp(source, 'g');\n return function(string) {\n string = string == null ? '' : '' + string;\n return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;\n };\n };\n _.escape = createEscaper(escapeMap);\n _.unescape = createEscaper(unescapeMap);\n\n // If the value of the named `property` is a function then invoke it with the\n // `object` as context; otherwise, return it.\n _.result = function(object, property, fallback) {\n var value = object == null ? void 0 : object[property];\n if (value === void 0) {\n value = fallback;\n }\n return _.isFunction(value) ? value.call(object) : value;\n };\n\n // Generate a unique integer id (unique within the entire client session).\n // Useful for temporary DOM ids.\n var idCounter = 0;\n _.uniqueId = function(prefix) {\n var id = ++idCounter + '';\n return prefix ? prefix + id : id;\n };\n\n // By default, Underscore uses ERB-style template delimiters, change the\n // following template settings to use alternative delimiters.\n _.templateSettings = {\n evaluate : /<%([\\s\\S]+?)%>/g,\n interpolate : /<%=([\\s\\S]+?)%>/g,\n escape : /<%-([\\s\\S]+?)%>/g\n };\n\n // When customizing `templateSettings`, if you don't want to define an\n // interpolation, evaluation or escaping regex, we need one that is\n // guaranteed not to match.\n var noMatch = /(.)^/;\n\n // Certain characters need to be escaped so that they can be put into a\n // string literal.\n var escapes = {\n \"'\": \"'\",\n '\\\\': '\\\\',\n '\\r': 'r',\n '\\n': 'n',\n '\\u2028': 'u2028',\n '\\u2029': 'u2029'\n };\n\n var escaper = /\\\\|'|\\r|\\n|\\u2028|\\u2029/g;\n\n var escapeChar = function(match) {\n return '\\\\' + escapes[match];\n };\n\n // JavaScript micro-templating, similar to John Resig's implementation.\n // Underscore templating handles arbitrary delimiters, preserves whitespace,\n // and correctly escapes quotes within interpolated code.\n // NB: `oldSettings` only exists for backwards compatibility.\n _.template = function(text, settings, oldSettings) {\n if (!settings && oldSettings) settings = oldSettings;\n settings = _.defaults({}, settings, _.templateSettings);\n\n // Combine delimiters into one regular expression via alternation.\n var matcher = RegExp([\n (settings.escape || noMatch).source,\n (settings.interpolate || noMatch).source,\n (settings.evaluate || noMatch).source\n ].join('|') + '|$', 'g');\n\n // Compile the template source, escaping string literals appropriately.\n var index = 0;\n var source = \"__p+='\";\n text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {\n source += text.slice(index, offset).replace(escaper, escapeChar);\n index = offset + match.length;\n\n if (escape) {\n source += \"'+\\n((__t=(\" + escape + \"))==null?'':_.escape(__t))+\\n'\";\n } else if (interpolate) {\n source += \"'+\\n((__t=(\" + interpolate + \"))==null?'':__t)+\\n'\";\n } else if (evaluate) {\n source += \"';\\n\" + evaluate + \"\\n__p+='\";\n }\n\n // Adobe VMs need the match returned to produce the correct offest.\n return match;\n });\n source += \"';\\n\";\n\n // If a variable is not specified, place data values in local scope.\n if (!settings.variable) source = 'with(obj||{}){\\n' + source + '}\\n';\n\n source = \"var __t,__p='',__j=Array.prototype.join,\" +\n \"print=function(){__p+=__j.call(arguments,'');};\\n\" +\n source + 'return __p;\\n';\n\n try {\n var render = new Function(settings.variable || 'obj', '_', source);\n } catch (e) {\n e.source = source;\n throw e;\n }\n\n var template = function(data) {\n return render.call(this, data, _);\n };\n\n // Provide the compiled source as a convenience for precompilation.\n var argument = settings.variable || 'obj';\n template.source = 'function(' + argument + '){\\n' + source + '}';\n\n return template;\n };\n\n // Add a \"chain\" function. Start chaining a wrapped Underscore object.\n _.chain = function(obj) {\n var instance = _(obj);\n instance._chain = true;\n return instance;\n };\n\n // OOP\n // ---------------\n // If Underscore is called as a function, it returns a wrapped object that\n // can be used OO-style. This wrapper holds altered versions of all the\n // underscore functions. Wrapped objects may be chained.\n\n // Helper function to continue chaining intermediate results.\n var result = function(instance, obj) {\n return instance._chain ? _(obj).chain() : obj;\n };\n\n // Add your own custom functions to the Underscore object.\n _.mixin = function(obj) {\n _.each(_.functions(obj), function(name) {\n var func = _[name] = obj[name];\n _.prototype[name] = function() {\n var args = [this._wrapped];\n push.apply(args, arguments);\n return result(this, func.apply(_, args));\n };\n });\n };\n\n // Add all of the Underscore functions to the wrapper object.\n _.mixin(_);\n\n // Add all mutator Array functions to the wrapper.\n _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n var obj = this._wrapped;\n method.apply(obj, arguments);\n if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];\n return result(this, obj);\n };\n });\n\n // Add all accessor Array functions to the wrapper.\n _.each(['concat', 'join', 'slice'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n return result(this, method.apply(this._wrapped, arguments));\n };\n });\n\n // Extracts the result from a wrapped and chained object.\n _.prototype.value = function() {\n return this._wrapped;\n };\n\n // Provide unwrapping proxy for some methods used in engine operations\n // such as arithmetic and JSON stringification.\n _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;\n\n _.prototype.toString = function() {\n return '' + this._wrapped;\n };\n\n // AMD registration happens at the end for compatibility with AMD loaders\n // that may not enforce next-turn semantics on modules. Even though general\n // practice for AMD registration is to be anonymous, underscore registers\n // as a named module because, like jQuery, it is a base library that is\n // popular enough to be bundled in a third party lib, but not be part of\n // an AMD load request. Those cases could generate an error when an\n // anonymous define() is called outside of a loader request.\n if (typeof define === 'function' && define.amd) {\n define('underscore', [], function() {\n return _;\n });\n }\n}.call(this));\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/underscore/underscore.js\n// module id = 3\n// module chunks = 0","module.exports = {\n\t\"name\": \"bonobo-jupyter\",\n\t\"version\": \"0.0.1\",\n\t\"description\": \"Jupyter integration for Bonobo\",\n\t\"author\": \"\",\n\t\"main\": \"src/index.js\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"\"\n\t},\n\t\"keywords\": [\n\t\t\"jupyter\",\n\t\t\"widgets\",\n\t\t\"ipython\",\n\t\t\"ipywidgets\"\n\t],\n\t\"scripts\": {\n\t\t\"prepublish\": \"webpack\",\n\t\t\"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"json-loader\": \"^0.5.4\",\n\t\t\"webpack\": \"^1.12.14\"\n\t},\n\t\"dependencies\": {\n\t\t\"jupyter-js-widgets\": \"^2.0.9\",\n\t\t\"underscore\": \"^1.8.3\"\n\t}\n};\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./package.json\n// module id = 4\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file diff --git a/bonobo/ext/jupyter/plugin.py b/bonobo/ext/jupyter/plugin.py deleted file mode 100644 index 715b057..0000000 --- a/bonobo/ext/jupyter/plugin.py +++ /dev/null @@ -1,26 +0,0 @@ -import logging - -from bonobo.ext.jupyter.widget import BonoboWidget -from bonobo.plugins import Plugin - -try: - import IPython.core.display -except ImportError as e: - logging.exception( - 'You must install Jupyter to use the bonobo Jupyter extension. Easiest way is to install the ' - 'optional "jupyter" dependencies with «pip install bonobo[jupyter]», but you can also install a ' - 'specific version by yourself.' - ) - - -class JupyterOutputPlugin(Plugin): - def initialize(self): - self.widget = BonoboWidget() - IPython.core.display.display(self.widget) - - def run(self): - self.widget.value = [ - str(self.context.parent[i]) for i in self.context.parent.graph.topologically_sorted_indexes - ] - - finalize = run diff --git a/bonobo/ext/jupyter/static/index.js.map b/bonobo/ext/jupyter/static/index.js.map deleted file mode 100644 index eec5f06..0000000 --- a/bonobo/ext/jupyter/static/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack:///webpack/bootstrap f7d5605306ad4cac6219","webpack:///./src/index.js","webpack:///./src/bonobo.js","webpack:///external \"jupyter-js-widgets\"","webpack:///./~/underscore/underscore.js","webpack:///./package.json"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;;;;;;ACXA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;;;;;;;ACvCA,gD;;;;;;ACAA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;AACH;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA,wBAAuB,OAAO;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uCAAsC,YAAY;AAClD;AACA;AACA,MAAK;AACL;AACA,wCAAuC,YAAY;AACnD;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA6B,gBAAgB;AAC7C;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA,qDAAoD;AACpD,IAAG;;AAEH;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA,2CAA0C;AAC1C,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,6DAA4D,YAAY;AACxE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA,sBAAqB,gBAAgB;AACrC;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,8CAA6C,YAAY;AACzD;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAsD;AACtD;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAS;AACT;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,0BAA0B;AACpE;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA,sBAAqB,cAAc;AACnC;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,sBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAe,YAAY;AAC3B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,QAAO,eAAe;AACtB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,sBAAqB,eAAe;AACpC;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAsB;AACtB;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA,oBAAmB;AACnB;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,6CAA4C,mBAAmB;AAC/D;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,sDAAqD;AACrD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;AACA;;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8EAA6E;AAC7E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA,sCAAqC;AACrC;AACA;AACA;;AAEA;AACA;AACA;AACA,2BAA0B;AAC1B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,oBAAmB,OAAO;AAC1B;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,gBAAe;AACf,eAAc;AACd,eAAc;AACd,iBAAgB;AAChB,iBAAgB;AAChB,iBAAgB;AAChB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA4B;;AAE5B;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA,QAAO;AACP,sBAAqB;AACrB;;AAEA;AACA;AACA,MAAK;AACL,kBAAiB;;AAEjB;AACA,mDAAkD,EAAE,iBAAiB;;AAErE;AACA,yBAAwB,8BAA8B;AACtD,4BAA2B;;AAE3B;AACA;AACA,MAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,mDAAkD,iBAAiB;;AAEnE;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,EAAC;;;;;;;AC3gDD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA,G","file":"index.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap f7d5605306ad4cac6219","// Entry point for the notebook bundle containing custom model definitions.\n//\n// Setup notebook base URL\n//\n// Some static assets may be required by the custom widget javascript. The base\n// url for the notebook is not known at build time and is therefore computed\n// dynamically.\n__webpack_public_path__ = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/bonobo/';\n\n// Export widget models and views, and the npm package version number.\nmodule.exports = require('./bonobo.js');\nmodule.exports['version'] = require('../package.json').version;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = 0\n// module chunks = 0","var widgets = require('jupyter-js-widgets');\nvar _ = require('underscore');\n\n// Custom Model. Custom widgets models must at least provide default values\n// for model attributes, including `_model_name`, `_view_name`, `_model_module`\n// and `_view_module` when different from the base class.\n//\n// When serialiazing entire widget state for embedding, only values different from the\n// defaults will be specified.\n\nvar BonoboModel = widgets.DOMWidgetModel.extend({\n defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, {\n _model_name: 'BonoboModel',\n _view_name: 'BonoboView',\n _model_module: 'bonobo',\n _view_module: 'bonobo',\n value: []\n })\n});\n\n\n// Custom View. Renders the widget model.\nvar BonoboView = widgets.DOMWidgetView.extend({\n render: function () {\n this.value_changed();\n this.model.on('change:value', this.value_changed, this);\n },\n\n value_changed: function () {\n this.$el.html(\n this.model.get('value').join('
')\n );\n },\n});\n\n\nmodule.exports = {\n BonoboModel: BonoboModel,\n BonoboView: BonoboView\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/bonobo.js\n// module id = 1\n// module chunks = 0","module.exports = __WEBPACK_EXTERNAL_MODULE_2__;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"jupyter-js-widgets\"\n// module id = 2\n// module chunks = 0","// Underscore.js 1.8.3\n// http://underscorejs.org\n// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n// Underscore may be freely distributed under the MIT license.\n\n(function() {\n\n // Baseline setup\n // --------------\n\n // Establish the root object, `window` in the browser, or `exports` on the server.\n var root = this;\n\n // Save the previous value of the `_` variable.\n var previousUnderscore = root._;\n\n // Save bytes in the minified (but not gzipped) version:\n var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n // Create quick reference variables for speed access to core prototypes.\n var\n push = ArrayProto.push,\n slice = ArrayProto.slice,\n toString = ObjProto.toString,\n hasOwnProperty = ObjProto.hasOwnProperty;\n\n // All **ECMAScript 5** native function implementations that we hope to use\n // are declared here.\n var\n nativeIsArray = Array.isArray,\n nativeKeys = Object.keys,\n nativeBind = FuncProto.bind,\n nativeCreate = Object.create;\n\n // Naked function reference for surrogate-prototype-swapping.\n var Ctor = function(){};\n\n // Create a safe reference to the Underscore object for use below.\n var _ = function(obj) {\n if (obj instanceof _) return obj;\n if (!(this instanceof _)) return new _(obj);\n this._wrapped = obj;\n };\n\n // Export the Underscore object for **Node.js**, with\n // backwards-compatibility for the old `require()` API. If we're in\n // the browser, add `_` as a global object.\n if (typeof exports !== 'undefined') {\n if (typeof module !== 'undefined' && module.exports) {\n exports = module.exports = _;\n }\n exports._ = _;\n } else {\n root._ = _;\n }\n\n // Current version.\n _.VERSION = '1.8.3';\n\n // Internal function that returns an efficient (for current engines) version\n // of the passed-in callback, to be repeatedly applied in other Underscore\n // functions.\n var optimizeCb = function(func, context, argCount) {\n if (context === void 0) return func;\n switch (argCount == null ? 3 : argCount) {\n case 1: return function(value) {\n return func.call(context, value);\n };\n case 2: return function(value, other) {\n return func.call(context, value, other);\n };\n case 3: return function(value, index, collection) {\n return func.call(context, value, index, collection);\n };\n case 4: return function(accumulator, value, index, collection) {\n return func.call(context, accumulator, value, index, collection);\n };\n }\n return function() {\n return func.apply(context, arguments);\n };\n };\n\n // A mostly-internal function to generate callbacks that can be applied\n // to each element in a collection, returning the desired result — either\n // identity, an arbitrary callback, a property matcher, or a property accessor.\n var cb = function(value, context, argCount) {\n if (value == null) return _.identity;\n if (_.isFunction(value)) return optimizeCb(value, context, argCount);\n if (_.isObject(value)) return _.matcher(value);\n return _.property(value);\n };\n _.iteratee = function(value, context) {\n return cb(value, context, Infinity);\n };\n\n // An internal function for creating assigner functions.\n var createAssigner = function(keysFunc, undefinedOnly) {\n return function(obj) {\n var length = arguments.length;\n if (length < 2 || obj == null) return obj;\n for (var index = 1; index < length; index++) {\n var source = arguments[index],\n keys = keysFunc(source),\n l = keys.length;\n for (var i = 0; i < l; i++) {\n var key = keys[i];\n if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];\n }\n }\n return obj;\n };\n };\n\n // An internal function for creating a new object that inherits from another.\n var baseCreate = function(prototype) {\n if (!_.isObject(prototype)) return {};\n if (nativeCreate) return nativeCreate(prototype);\n Ctor.prototype = prototype;\n var result = new Ctor;\n Ctor.prototype = null;\n return result;\n };\n\n var property = function(key) {\n return function(obj) {\n return obj == null ? void 0 : obj[key];\n };\n };\n\n // Helper for collection methods to determine whether a collection\n // should be iterated as an array or as an object\n // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength\n // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094\n var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;\n var getLength = property('length');\n var isArrayLike = function(collection) {\n var length = getLength(collection);\n return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;\n };\n\n // Collection Functions\n // --------------------\n\n // The cornerstone, an `each` implementation, aka `forEach`.\n // Handles raw objects in addition to array-likes. Treats all\n // sparse array-likes as if they were dense.\n _.each = _.forEach = function(obj, iteratee, context) {\n iteratee = optimizeCb(iteratee, context);\n var i, length;\n if (isArrayLike(obj)) {\n for (i = 0, length = obj.length; i < length; i++) {\n iteratee(obj[i], i, obj);\n }\n } else {\n var keys = _.keys(obj);\n for (i = 0, length = keys.length; i < length; i++) {\n iteratee(obj[keys[i]], keys[i], obj);\n }\n }\n return obj;\n };\n\n // Return the results of applying the iteratee to each element.\n _.map = _.collect = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n results = Array(length);\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n results[index] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Create a reducing function iterating left or right.\n function createReduce(dir) {\n // Optimized iterator function as using arguments.length\n // in the main function will deoptimize the, see #1991.\n function iterator(obj, iteratee, memo, keys, index, length) {\n for (; index >= 0 && index < length; index += dir) {\n var currentKey = keys ? keys[index] : index;\n memo = iteratee(memo, obj[currentKey], currentKey, obj);\n }\n return memo;\n }\n\n return function(obj, iteratee, memo, context) {\n iteratee = optimizeCb(iteratee, context, 4);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n index = dir > 0 ? 0 : length - 1;\n // Determine the initial value if none is provided.\n if (arguments.length < 3) {\n memo = obj[keys ? keys[index] : index];\n index += dir;\n }\n return iterator(obj, iteratee, memo, keys, index, length);\n };\n }\n\n // **Reduce** builds up a single result from a list of values, aka `inject`,\n // or `foldl`.\n _.reduce = _.foldl = _.inject = createReduce(1);\n\n // The right-associative version of reduce, also known as `foldr`.\n _.reduceRight = _.foldr = createReduce(-1);\n\n // Return the first value which passes a truth test. Aliased as `detect`.\n _.find = _.detect = function(obj, predicate, context) {\n var key;\n if (isArrayLike(obj)) {\n key = _.findIndex(obj, predicate, context);\n } else {\n key = _.findKey(obj, predicate, context);\n }\n if (key !== void 0 && key !== -1) return obj[key];\n };\n\n // Return all the elements that pass a truth test.\n // Aliased as `select`.\n _.filter = _.select = function(obj, predicate, context) {\n var results = [];\n predicate = cb(predicate, context);\n _.each(obj, function(value, index, list) {\n if (predicate(value, index, list)) results.push(value);\n });\n return results;\n };\n\n // Return all the elements for which a truth test fails.\n _.reject = function(obj, predicate, context) {\n return _.filter(obj, _.negate(cb(predicate)), context);\n };\n\n // Determine whether all of the elements match a truth test.\n // Aliased as `all`.\n _.every = _.all = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (!predicate(obj[currentKey], currentKey, obj)) return false;\n }\n return true;\n };\n\n // Determine if at least one element in the object matches a truth test.\n // Aliased as `any`.\n _.some = _.any = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (predicate(obj[currentKey], currentKey, obj)) return true;\n }\n return false;\n };\n\n // Determine if the array or object contains a given item (using `===`).\n // Aliased as `includes` and `include`.\n _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n if (typeof fromIndex != 'number' || guard) fromIndex = 0;\n return _.indexOf(obj, item, fromIndex) >= 0;\n };\n\n // Invoke a method (with arguments) on every item in a collection.\n _.invoke = function(obj, method) {\n var args = slice.call(arguments, 2);\n var isFunc = _.isFunction(method);\n return _.map(obj, function(value) {\n var func = isFunc ? method : value[method];\n return func == null ? func : func.apply(value, args);\n });\n };\n\n // Convenience version of a common use case of `map`: fetching a property.\n _.pluck = function(obj, key) {\n return _.map(obj, _.property(key));\n };\n\n // Convenience version of a common use case of `filter`: selecting only objects\n // containing specific `key:value` pairs.\n _.where = function(obj, attrs) {\n return _.filter(obj, _.matcher(attrs));\n };\n\n // Convenience version of a common use case of `find`: getting the first object\n // containing specific `key:value` pairs.\n _.findWhere = function(obj, attrs) {\n return _.find(obj, _.matcher(attrs));\n };\n\n // Return the maximum element (or element-based computation).\n _.max = function(obj, iteratee, context) {\n var result = -Infinity, lastComputed = -Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value > result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed > lastComputed || computed === -Infinity && result === -Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Return the minimum element (or element-based computation).\n _.min = function(obj, iteratee, context) {\n var result = Infinity, lastComputed = Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value < result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed < lastComputed || computed === Infinity && result === Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Shuffle a collection, using the modern version of the\n // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).\n _.shuffle = function(obj) {\n var set = isArrayLike(obj) ? obj : _.values(obj);\n var length = set.length;\n var shuffled = Array(length);\n for (var index = 0, rand; index < length; index++) {\n rand = _.random(0, index);\n if (rand !== index) shuffled[index] = shuffled[rand];\n shuffled[rand] = set[index];\n }\n return shuffled;\n };\n\n // Sample **n** random values from a collection.\n // If **n** is not specified, returns a single random element.\n // The internal `guard` argument allows it to work with `map`.\n _.sample = function(obj, n, guard) {\n if (n == null || guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n return obj[_.random(obj.length - 1)];\n }\n return _.shuffle(obj).slice(0, Math.max(0, n));\n };\n\n // Sort the object's values by a criterion produced by an iteratee.\n _.sortBy = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n return _.pluck(_.map(obj, function(value, index, list) {\n return {\n value: value,\n index: index,\n criteria: iteratee(value, index, list)\n };\n }).sort(function(left, right) {\n var a = left.criteria;\n var b = right.criteria;\n if (a !== b) {\n if (a > b || a === void 0) return 1;\n if (a < b || b === void 0) return -1;\n }\n return left.index - right.index;\n }), 'value');\n };\n\n // An internal function used for aggregate \"group by\" operations.\n var group = function(behavior) {\n return function(obj, iteratee, context) {\n var result = {};\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index) {\n var key = iteratee(value, index, obj);\n behavior(result, value, key);\n });\n return result;\n };\n };\n\n // Groups the object's values by a criterion. Pass either a string attribute\n // to group by, or a function that returns the criterion.\n _.groupBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key].push(value); else result[key] = [value];\n });\n\n // Indexes the object's values by a criterion, similar to `groupBy`, but for\n // when you know that your index values will be unique.\n _.indexBy = group(function(result, value, key) {\n result[key] = value;\n });\n\n // Counts instances of an object that group by a certain criterion. Pass\n // either a string attribute to count by, or a function that returns the\n // criterion.\n _.countBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key]++; else result[key] = 1;\n });\n\n // Safely create a real, live array from anything iterable.\n _.toArray = function(obj) {\n if (!obj) return [];\n if (_.isArray(obj)) return slice.call(obj);\n if (isArrayLike(obj)) return _.map(obj, _.identity);\n return _.values(obj);\n };\n\n // Return the number of elements in an object.\n _.size = function(obj) {\n if (obj == null) return 0;\n return isArrayLike(obj) ? obj.length : _.keys(obj).length;\n };\n\n // Split a collection into two arrays: one whose elements all satisfy the given\n // predicate, and one whose elements all do not satisfy the predicate.\n _.partition = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var pass = [], fail = [];\n _.each(obj, function(value, key, obj) {\n (predicate(value, key, obj) ? pass : fail).push(value);\n });\n return [pass, fail];\n };\n\n // Array Functions\n // ---------------\n\n // Get the first element of an array. Passing **n** will return the first N\n // values in the array. Aliased as `head` and `take`. The **guard** check\n // allows it to work with `_.map`.\n _.first = _.head = _.take = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[0];\n return _.initial(array, array.length - n);\n };\n\n // Returns everything but the last entry of the array. Especially useful on\n // the arguments object. Passing **n** will return all the values in\n // the array, excluding the last N.\n _.initial = function(array, n, guard) {\n return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));\n };\n\n // Get the last element of an array. Passing **n** will return the last N\n // values in the array.\n _.last = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[array.length - 1];\n return _.rest(array, Math.max(0, array.length - n));\n };\n\n // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.\n // Especially useful on the arguments object. Passing an **n** will return\n // the rest N values in the array.\n _.rest = _.tail = _.drop = function(array, n, guard) {\n return slice.call(array, n == null || guard ? 1 : n);\n };\n\n // Trim out all falsy values from an array.\n _.compact = function(array) {\n return _.filter(array, _.identity);\n };\n\n // Internal implementation of a recursive `flatten` function.\n var flatten = function(input, shallow, strict, startIndex) {\n var output = [], idx = 0;\n for (var i = startIndex || 0, length = getLength(input); i < length; i++) {\n var value = input[i];\n if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {\n //flatten current level of array or arguments object\n if (!shallow) value = flatten(value, shallow, strict);\n var j = 0, len = value.length;\n output.length += len;\n while (j < len) {\n output[idx++] = value[j++];\n }\n } else if (!strict) {\n output[idx++] = value;\n }\n }\n return output;\n };\n\n // Flatten out an array, either recursively (by default), or just one level.\n _.flatten = function(array, shallow) {\n return flatten(array, shallow, false);\n };\n\n // Return a version of the array that does not contain the specified value(s).\n _.without = function(array) {\n return _.difference(array, slice.call(arguments, 1));\n };\n\n // Produce a duplicate-free version of the array. If the array has already\n // been sorted, you have the option of using a faster algorithm.\n // Aliased as `unique`.\n _.uniq = _.unique = function(array, isSorted, iteratee, context) {\n if (!_.isBoolean(isSorted)) {\n context = iteratee;\n iteratee = isSorted;\n isSorted = false;\n }\n if (iteratee != null) iteratee = cb(iteratee, context);\n var result = [];\n var seen = [];\n for (var i = 0, length = getLength(array); i < length; i++) {\n var value = array[i],\n computed = iteratee ? iteratee(value, i, array) : value;\n if (isSorted) {\n if (!i || seen !== computed) result.push(value);\n seen = computed;\n } else if (iteratee) {\n if (!_.contains(seen, computed)) {\n seen.push(computed);\n result.push(value);\n }\n } else if (!_.contains(result, value)) {\n result.push(value);\n }\n }\n return result;\n };\n\n // Produce an array that contains the union: each distinct element from all of\n // the passed-in arrays.\n _.union = function() {\n return _.uniq(flatten(arguments, true, true));\n };\n\n // Produce an array that contains every item shared between all the\n // passed-in arrays.\n _.intersection = function(array) {\n var result = [];\n var argsLength = arguments.length;\n for (var i = 0, length = getLength(array); i < length; i++) {\n var item = array[i];\n if (_.contains(result, item)) continue;\n for (var j = 1; j < argsLength; j++) {\n if (!_.contains(arguments[j], item)) break;\n }\n if (j === argsLength) result.push(item);\n }\n return result;\n };\n\n // Take the difference between one array and a number of other arrays.\n // Only the elements present in just the first array will remain.\n _.difference = function(array) {\n var rest = flatten(arguments, true, true, 1);\n return _.filter(array, function(value){\n return !_.contains(rest, value);\n });\n };\n\n // Zip together multiple lists into a single array -- elements that share\n // an index go together.\n _.zip = function() {\n return _.unzip(arguments);\n };\n\n // Complement of _.zip. Unzip accepts an array of arrays and groups\n // each array's elements on shared indices\n _.unzip = function(array) {\n var length = array && _.max(array, getLength).length || 0;\n var result = Array(length);\n\n for (var index = 0; index < length; index++) {\n result[index] = _.pluck(array, index);\n }\n return result;\n };\n\n // Converts lists into objects. Pass either a single array of `[key, value]`\n // pairs, or two parallel arrays of the same length -- one of keys, and one of\n // the corresponding values.\n _.object = function(list, values) {\n var result = {};\n for (var i = 0, length = getLength(list); i < length; i++) {\n if (values) {\n result[list[i]] = values[i];\n } else {\n result[list[i][0]] = list[i][1];\n }\n }\n return result;\n };\n\n // Generator function to create the findIndex and findLastIndex functions\n function createPredicateIndexFinder(dir) {\n return function(array, predicate, context) {\n predicate = cb(predicate, context);\n var length = getLength(array);\n var index = dir > 0 ? 0 : length - 1;\n for (; index >= 0 && index < length; index += dir) {\n if (predicate(array[index], index, array)) return index;\n }\n return -1;\n };\n }\n\n // Returns the first index on an array-like that passes a predicate test\n _.findIndex = createPredicateIndexFinder(1);\n _.findLastIndex = createPredicateIndexFinder(-1);\n\n // Use a comparator function to figure out the smallest index at which\n // an object should be inserted so as to maintain order. Uses binary search.\n _.sortedIndex = function(array, obj, iteratee, context) {\n iteratee = cb(iteratee, context, 1);\n var value = iteratee(obj);\n var low = 0, high = getLength(array);\n while (low < high) {\n var mid = Math.floor((low + high) / 2);\n if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;\n }\n return low;\n };\n\n // Generator function to create the indexOf and lastIndexOf functions\n function createIndexFinder(dir, predicateFind, sortedIndex) {\n return function(array, item, idx) {\n var i = 0, length = getLength(array);\n if (typeof idx == 'number') {\n if (dir > 0) {\n i = idx >= 0 ? idx : Math.max(idx + length, i);\n } else {\n length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;\n }\n } else if (sortedIndex && idx && length) {\n idx = sortedIndex(array, item);\n return array[idx] === item ? idx : -1;\n }\n if (item !== item) {\n idx = predicateFind(slice.call(array, i, length), _.isNaN);\n return idx >= 0 ? idx + i : -1;\n }\n for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {\n if (array[idx] === item) return idx;\n }\n return -1;\n };\n }\n\n // Return the position of the first occurrence of an item in an array,\n // or -1 if the item is not included in the array.\n // If the array is large and already in sort order, pass `true`\n // for **isSorted** to use binary search.\n _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);\n _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);\n\n // Generate an integer Array containing an arithmetic progression. A port of\n // the native Python `range()` function. See\n // [the Python documentation](http://docs.python.org/library/functions.html#range).\n _.range = function(start, stop, step) {\n if (stop == null) {\n stop = start || 0;\n start = 0;\n }\n step = step || 1;\n\n var length = Math.max(Math.ceil((stop - start) / step), 0);\n var range = Array(length);\n\n for (var idx = 0; idx < length; idx++, start += step) {\n range[idx] = start;\n }\n\n return range;\n };\n\n // Function (ahem) Functions\n // ------------------\n\n // Determines whether to execute a function as a constructor\n // or a normal function with the provided arguments\n var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {\n if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);\n var self = baseCreate(sourceFunc.prototype);\n var result = sourceFunc.apply(self, args);\n if (_.isObject(result)) return result;\n return self;\n };\n\n // Create a function bound to a given object (assigning `this`, and arguments,\n // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if\n // available.\n _.bind = function(func, context) {\n if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');\n var args = slice.call(arguments, 2);\n var bound = function() {\n return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));\n };\n return bound;\n };\n\n // Partially apply a function by creating a version that has had some of its\n // arguments pre-filled, without changing its dynamic `this` context. _ acts\n // as a placeholder, allowing any combination of arguments to be pre-filled.\n _.partial = function(func) {\n var boundArgs = slice.call(arguments, 1);\n var bound = function() {\n var position = 0, length = boundArgs.length;\n var args = Array(length);\n for (var i = 0; i < length; i++) {\n args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];\n }\n while (position < arguments.length) args.push(arguments[position++]);\n return executeBound(func, bound, this, this, args);\n };\n return bound;\n };\n\n // Bind a number of an object's methods to that object. Remaining arguments\n // are the method names to be bound. Useful for ensuring that all callbacks\n // defined on an object belong to it.\n _.bindAll = function(obj) {\n var i, length = arguments.length, key;\n if (length <= 1) throw new Error('bindAll must be passed function names');\n for (i = 1; i < length; i++) {\n key = arguments[i];\n obj[key] = _.bind(obj[key], obj);\n }\n return obj;\n };\n\n // Memoize an expensive function by storing its results.\n _.memoize = function(func, hasher) {\n var memoize = function(key) {\n var cache = memoize.cache;\n var address = '' + (hasher ? hasher.apply(this, arguments) : key);\n if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);\n return cache[address];\n };\n memoize.cache = {};\n return memoize;\n };\n\n // Delays a function for the given number of milliseconds, and then calls\n // it with the arguments supplied.\n _.delay = function(func, wait) {\n var args = slice.call(arguments, 2);\n return setTimeout(function(){\n return func.apply(null, args);\n }, wait);\n };\n\n // Defers a function, scheduling it to run after the current call stack has\n // cleared.\n _.defer = _.partial(_.delay, _, 1);\n\n // Returns a function, that, when invoked, will only be triggered at most once\n // during a given window of time. Normally, the throttled function will run\n // as much as it can, without ever going more than once per `wait` duration;\n // but if you'd like to disable the execution on the leading edge, pass\n // `{leading: false}`. To disable execution on the trailing edge, ditto.\n _.throttle = function(func, wait, options) {\n var context, args, result;\n var timeout = null;\n var previous = 0;\n if (!options) options = {};\n var later = function() {\n previous = options.leading === false ? 0 : _.now();\n timeout = null;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n };\n return function() {\n var now = _.now();\n if (!previous && options.leading === false) previous = now;\n var remaining = wait - (now - previous);\n context = this;\n args = arguments;\n if (remaining <= 0 || remaining > wait) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n previous = now;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n } else if (!timeout && options.trailing !== false) {\n timeout = setTimeout(later, remaining);\n }\n return result;\n };\n };\n\n // Returns a function, that, as long as it continues to be invoked, will not\n // be triggered. The function will be called after it stops being called for\n // N milliseconds. If `immediate` is passed, trigger the function on the\n // leading edge, instead of the trailing.\n _.debounce = function(func, wait, immediate) {\n var timeout, args, context, timestamp, result;\n\n var later = function() {\n var last = _.now() - timestamp;\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last);\n } else {\n timeout = null;\n if (!immediate) {\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n }\n }\n };\n\n return function() {\n context = this;\n args = arguments;\n timestamp = _.now();\n var callNow = immediate && !timeout;\n if (!timeout) timeout = setTimeout(later, wait);\n if (callNow) {\n result = func.apply(context, args);\n context = args = null;\n }\n\n return result;\n };\n };\n\n // Returns the first function passed as an argument to the second,\n // allowing you to adjust arguments, run code before and after, and\n // conditionally execute the original function.\n _.wrap = function(func, wrapper) {\n return _.partial(wrapper, func);\n };\n\n // Returns a negated version of the passed-in predicate.\n _.negate = function(predicate) {\n return function() {\n return !predicate.apply(this, arguments);\n };\n };\n\n // Returns a function that is the composition of a list of functions, each\n // consuming the return value of the function that follows.\n _.compose = function() {\n var args = arguments;\n var start = args.length - 1;\n return function() {\n var i = start;\n var result = args[start].apply(this, arguments);\n while (i--) result = args[i].call(this, result);\n return result;\n };\n };\n\n // Returns a function that will only be executed on and after the Nth call.\n _.after = function(times, func) {\n return function() {\n if (--times < 1) {\n return func.apply(this, arguments);\n }\n };\n };\n\n // Returns a function that will only be executed up to (but not including) the Nth call.\n _.before = function(times, func) {\n var memo;\n return function() {\n if (--times > 0) {\n memo = func.apply(this, arguments);\n }\n if (times <= 1) func = null;\n return memo;\n };\n };\n\n // Returns a function that will be executed at most one time, no matter how\n // often you call it. Useful for lazy initialization.\n _.once = _.partial(_.before, 2);\n\n // Object Functions\n // ----------------\n\n // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.\n var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');\n var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',\n 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];\n\n function collectNonEnumProps(obj, keys) {\n var nonEnumIdx = nonEnumerableProps.length;\n var constructor = obj.constructor;\n var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;\n\n // Constructor is a special case.\n var prop = 'constructor';\n if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);\n\n while (nonEnumIdx--) {\n prop = nonEnumerableProps[nonEnumIdx];\n if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {\n keys.push(prop);\n }\n }\n }\n\n // Retrieve the names of an object's own properties.\n // Delegates to **ECMAScript 5**'s native `Object.keys`\n _.keys = function(obj) {\n if (!_.isObject(obj)) return [];\n if (nativeKeys) return nativeKeys(obj);\n var keys = [];\n for (var key in obj) if (_.has(obj, key)) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve all the property names of an object.\n _.allKeys = function(obj) {\n if (!_.isObject(obj)) return [];\n var keys = [];\n for (var key in obj) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve the values of an object's properties.\n _.values = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var values = Array(length);\n for (var i = 0; i < length; i++) {\n values[i] = obj[keys[i]];\n }\n return values;\n };\n\n // Returns the results of applying the iteratee to each element of the object\n // In contrast to _.map it returns an object\n _.mapObject = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = _.keys(obj),\n length = keys.length,\n results = {},\n currentKey;\n for (var index = 0; index < length; index++) {\n currentKey = keys[index];\n results[currentKey] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Convert an object into a list of `[key, value]` pairs.\n _.pairs = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var pairs = Array(length);\n for (var i = 0; i < length; i++) {\n pairs[i] = [keys[i], obj[keys[i]]];\n }\n return pairs;\n };\n\n // Invert the keys and values of an object. The values must be serializable.\n _.invert = function(obj) {\n var result = {};\n var keys = _.keys(obj);\n for (var i = 0, length = keys.length; i < length; i++) {\n result[obj[keys[i]]] = keys[i];\n }\n return result;\n };\n\n // Return a sorted list of the function names available on the object.\n // Aliased as `methods`\n _.functions = _.methods = function(obj) {\n var names = [];\n for (var key in obj) {\n if (_.isFunction(obj[key])) names.push(key);\n }\n return names.sort();\n };\n\n // Extend a given object with all the properties in passed-in object(s).\n _.extend = createAssigner(_.allKeys);\n\n // Assigns a given object with all the own properties in the passed-in object(s)\n // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)\n _.extendOwn = _.assign = createAssigner(_.keys);\n\n // Returns the first key on an object that passes a predicate test\n _.findKey = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = _.keys(obj), key;\n for (var i = 0, length = keys.length; i < length; i++) {\n key = keys[i];\n if (predicate(obj[key], key, obj)) return key;\n }\n };\n\n // Return a copy of the object only containing the whitelisted properties.\n _.pick = function(object, oiteratee, context) {\n var result = {}, obj = object, iteratee, keys;\n if (obj == null) return result;\n if (_.isFunction(oiteratee)) {\n keys = _.allKeys(obj);\n iteratee = optimizeCb(oiteratee, context);\n } else {\n keys = flatten(arguments, false, false, 1);\n iteratee = function(value, key, obj) { return key in obj; };\n obj = Object(obj);\n }\n for (var i = 0, length = keys.length; i < length; i++) {\n var key = keys[i];\n var value = obj[key];\n if (iteratee(value, key, obj)) result[key] = value;\n }\n return result;\n };\n\n // Return a copy of the object without the blacklisted properties.\n _.omit = function(obj, iteratee, context) {\n if (_.isFunction(iteratee)) {\n iteratee = _.negate(iteratee);\n } else {\n var keys = _.map(flatten(arguments, false, false, 1), String);\n iteratee = function(value, key) {\n return !_.contains(keys, key);\n };\n }\n return _.pick(obj, iteratee, context);\n };\n\n // Fill in a given object with default properties.\n _.defaults = createAssigner(_.allKeys, true);\n\n // Creates an object that inherits from the given prototype object.\n // If additional properties are provided then they will be added to the\n // created object.\n _.create = function(prototype, props) {\n var result = baseCreate(prototype);\n if (props) _.extendOwn(result, props);\n return result;\n };\n\n // Create a (shallow-cloned) duplicate of an object.\n _.clone = function(obj) {\n if (!_.isObject(obj)) return obj;\n return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n };\n\n // Invokes interceptor with the obj, and then returns obj.\n // The primary purpose of this method is to \"tap into\" a method chain, in\n // order to perform operations on intermediate results within the chain.\n _.tap = function(obj, interceptor) {\n interceptor(obj);\n return obj;\n };\n\n // Returns whether an object has a given set of `key:value` pairs.\n _.isMatch = function(object, attrs) {\n var keys = _.keys(attrs), length = keys.length;\n if (object == null) return !length;\n var obj = Object(object);\n for (var i = 0; i < length; i++) {\n var key = keys[i];\n if (attrs[key] !== obj[key] || !(key in obj)) return false;\n }\n return true;\n };\n\n\n // Internal recursive comparison function for `isEqual`.\n var eq = function(a, b, aStack, bStack) {\n // Identical objects are equal. `0 === -0`, but they aren't identical.\n // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).\n if (a === b) return a !== 0 || 1 / a === 1 / b;\n // A strict comparison is necessary because `null == undefined`.\n if (a == null || b == null) return a === b;\n // Unwrap any wrapped objects.\n if (a instanceof _) a = a._wrapped;\n if (b instanceof _) b = b._wrapped;\n // Compare `[[Class]]` names.\n var className = toString.call(a);\n if (className !== toString.call(b)) return false;\n switch (className) {\n // Strings, numbers, regular expressions, dates, and booleans are compared by value.\n case '[object RegExp]':\n // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')\n case '[object String]':\n // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n // equivalent to `new String(\"5\")`.\n return '' + a === '' + b;\n case '[object Number]':\n // `NaN`s are equivalent, but non-reflexive.\n // Object(NaN) is equivalent to NaN\n if (+a !== +a) return +b !== +b;\n // An `egal` comparison is performed for other numeric values.\n return +a === 0 ? 1 / +a === 1 / b : +a === +b;\n case '[object Date]':\n case '[object Boolean]':\n // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n // millisecond representations. Note that invalid dates with millisecond representations\n // of `NaN` are not equivalent.\n return +a === +b;\n }\n\n var areArrays = className === '[object Array]';\n if (!areArrays) {\n if (typeof a != 'object' || typeof b != 'object') return false;\n\n // Objects with different constructors are not equivalent, but `Object`s or `Array`s\n // from different frames are.\n var aCtor = a.constructor, bCtor = b.constructor;\n if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&\n _.isFunction(bCtor) && bCtor instanceof bCtor)\n && ('constructor' in a && 'constructor' in b)) {\n return false;\n }\n }\n // Assume equality for cyclic structures. The algorithm for detecting cyclic\n // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n\n // Initializing stack of traversed objects.\n // It's done here since we only need them for objects and arrays comparison.\n aStack = aStack || [];\n bStack = bStack || [];\n var length = aStack.length;\n while (length--) {\n // Linear search. Performance is inversely proportional to the number of\n // unique nested structures.\n if (aStack[length] === a) return bStack[length] === b;\n }\n\n // Add the first object to the stack of traversed objects.\n aStack.push(a);\n bStack.push(b);\n\n // Recursively compare objects and arrays.\n if (areArrays) {\n // Compare array lengths to determine if a deep comparison is necessary.\n length = a.length;\n if (length !== b.length) return false;\n // Deep compare the contents, ignoring non-numeric properties.\n while (length--) {\n if (!eq(a[length], b[length], aStack, bStack)) return false;\n }\n } else {\n // Deep compare objects.\n var keys = _.keys(a), key;\n length = keys.length;\n // Ensure that both objects contain the same number of properties before comparing deep equality.\n if (_.keys(b).length !== length) return false;\n while (length--) {\n // Deep compare each member\n key = keys[length];\n if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;\n }\n }\n // Remove the first object from the stack of traversed objects.\n aStack.pop();\n bStack.pop();\n return true;\n };\n\n // Perform a deep comparison to check if two objects are equal.\n _.isEqual = function(a, b) {\n return eq(a, b);\n };\n\n // Is a given array, string, or object empty?\n // An \"empty\" object has no enumerable own-properties.\n _.isEmpty = function(obj) {\n if (obj == null) return true;\n if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;\n return _.keys(obj).length === 0;\n };\n\n // Is a given value a DOM element?\n _.isElement = function(obj) {\n return !!(obj && obj.nodeType === 1);\n };\n\n // Is a given value an array?\n // Delegates to ECMA5's native Array.isArray\n _.isArray = nativeIsArray || function(obj) {\n return toString.call(obj) === '[object Array]';\n };\n\n // Is a given variable an object?\n _.isObject = function(obj) {\n var type = typeof obj;\n return type === 'function' || type === 'object' && !!obj;\n };\n\n // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.\n _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {\n _['is' + name] = function(obj) {\n return toString.call(obj) === '[object ' + name + ']';\n };\n });\n\n // Define a fallback version of the method in browsers (ahem, IE < 9), where\n // there isn't any inspectable \"Arguments\" type.\n if (!_.isArguments(arguments)) {\n _.isArguments = function(obj) {\n return _.has(obj, 'callee');\n };\n }\n\n // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,\n // IE 11 (#1621), and in Safari 8 (#1929).\n if (typeof /./ != 'function' && typeof Int8Array != 'object') {\n _.isFunction = function(obj) {\n return typeof obj == 'function' || false;\n };\n }\n\n // Is a given object a finite number?\n _.isFinite = function(obj) {\n return isFinite(obj) && !isNaN(parseFloat(obj));\n };\n\n // Is the given value `NaN`? (NaN is the only number which does not equal itself).\n _.isNaN = function(obj) {\n return _.isNumber(obj) && obj !== +obj;\n };\n\n // Is a given value a boolean?\n _.isBoolean = function(obj) {\n return obj === true || obj === false || toString.call(obj) === '[object Boolean]';\n };\n\n // Is a given value equal to null?\n _.isNull = function(obj) {\n return obj === null;\n };\n\n // Is a given variable undefined?\n _.isUndefined = function(obj) {\n return obj === void 0;\n };\n\n // Shortcut function for checking if an object has a given property directly\n // on itself (in other words, not on a prototype).\n _.has = function(obj, key) {\n return obj != null && hasOwnProperty.call(obj, key);\n };\n\n // Utility Functions\n // -----------------\n\n // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n // previous owner. Returns a reference to the Underscore object.\n _.noConflict = function() {\n root._ = previousUnderscore;\n return this;\n };\n\n // Keep the identity function around for default iteratees.\n _.identity = function(value) {\n return value;\n };\n\n // Predicate-generating functions. Often useful outside of Underscore.\n _.constant = function(value) {\n return function() {\n return value;\n };\n };\n\n _.noop = function(){};\n\n _.property = property;\n\n // Generates a function for a given object that returns a given property.\n _.propertyOf = function(obj) {\n return obj == null ? function(){} : function(key) {\n return obj[key];\n };\n };\n\n // Returns a predicate for checking whether an object has a given set of\n // `key:value` pairs.\n _.matcher = _.matches = function(attrs) {\n attrs = _.extendOwn({}, attrs);\n return function(obj) {\n return _.isMatch(obj, attrs);\n };\n };\n\n // Run a function **n** times.\n _.times = function(n, iteratee, context) {\n var accum = Array(Math.max(0, n));\n iteratee = optimizeCb(iteratee, context, 1);\n for (var i = 0; i < n; i++) accum[i] = iteratee(i);\n return accum;\n };\n\n // Return a random integer between min and max (inclusive).\n _.random = function(min, max) {\n if (max == null) {\n max = min;\n min = 0;\n }\n return min + Math.floor(Math.random() * (max - min + 1));\n };\n\n // A (possibly faster) way to get the current timestamp as an integer.\n _.now = Date.now || function() {\n return new Date().getTime();\n };\n\n // List of HTML entities for escaping.\n var escapeMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '`': '`'\n };\n var unescapeMap = _.invert(escapeMap);\n\n // Functions for escaping and unescaping strings to/from HTML interpolation.\n var createEscaper = function(map) {\n var escaper = function(match) {\n return map[match];\n };\n // Regexes for identifying a key that needs to be escaped\n var source = '(?:' + _.keys(map).join('|') + ')';\n var testRegexp = RegExp(source);\n var replaceRegexp = RegExp(source, 'g');\n return function(string) {\n string = string == null ? '' : '' + string;\n return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;\n };\n };\n _.escape = createEscaper(escapeMap);\n _.unescape = createEscaper(unescapeMap);\n\n // If the value of the named `property` is a function then invoke it with the\n // `object` as context; otherwise, return it.\n _.result = function(object, property, fallback) {\n var value = object == null ? void 0 : object[property];\n if (value === void 0) {\n value = fallback;\n }\n return _.isFunction(value) ? value.call(object) : value;\n };\n\n // Generate a unique integer id (unique within the entire client session).\n // Useful for temporary DOM ids.\n var idCounter = 0;\n _.uniqueId = function(prefix) {\n var id = ++idCounter + '';\n return prefix ? prefix + id : id;\n };\n\n // By default, Underscore uses ERB-style template delimiters, change the\n // following template settings to use alternative delimiters.\n _.templateSettings = {\n evaluate : /<%([\\s\\S]+?)%>/g,\n interpolate : /<%=([\\s\\S]+?)%>/g,\n escape : /<%-([\\s\\S]+?)%>/g\n };\n\n // When customizing `templateSettings`, if you don't want to define an\n // interpolation, evaluation or escaping regex, we need one that is\n // guaranteed not to match.\n var noMatch = /(.)^/;\n\n // Certain characters need to be escaped so that they can be put into a\n // string literal.\n var escapes = {\n \"'\": \"'\",\n '\\\\': '\\\\',\n '\\r': 'r',\n '\\n': 'n',\n '\\u2028': 'u2028',\n '\\u2029': 'u2029'\n };\n\n var escaper = /\\\\|'|\\r|\\n|\\u2028|\\u2029/g;\n\n var escapeChar = function(match) {\n return '\\\\' + escapes[match];\n };\n\n // JavaScript micro-templating, similar to John Resig's implementation.\n // Underscore templating handles arbitrary delimiters, preserves whitespace,\n // and correctly escapes quotes within interpolated code.\n // NB: `oldSettings` only exists for backwards compatibility.\n _.template = function(text, settings, oldSettings) {\n if (!settings && oldSettings) settings = oldSettings;\n settings = _.defaults({}, settings, _.templateSettings);\n\n // Combine delimiters into one regular expression via alternation.\n var matcher = RegExp([\n (settings.escape || noMatch).source,\n (settings.interpolate || noMatch).source,\n (settings.evaluate || noMatch).source\n ].join('|') + '|$', 'g');\n\n // Compile the template source, escaping string literals appropriately.\n var index = 0;\n var source = \"__p+='\";\n text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {\n source += text.slice(index, offset).replace(escaper, escapeChar);\n index = offset + match.length;\n\n if (escape) {\n source += \"'+\\n((__t=(\" + escape + \"))==null?'':_.escape(__t))+\\n'\";\n } else if (interpolate) {\n source += \"'+\\n((__t=(\" + interpolate + \"))==null?'':__t)+\\n'\";\n } else if (evaluate) {\n source += \"';\\n\" + evaluate + \"\\n__p+='\";\n }\n\n // Adobe VMs need the match returned to produce the correct offest.\n return match;\n });\n source += \"';\\n\";\n\n // If a variable is not specified, place data values in local scope.\n if (!settings.variable) source = 'with(obj||{}){\\n' + source + '}\\n';\n\n source = \"var __t,__p='',__j=Array.prototype.join,\" +\n \"print=function(){__p+=__j.call(arguments,'');};\\n\" +\n source + 'return __p;\\n';\n\n try {\n var render = new Function(settings.variable || 'obj', '_', source);\n } catch (e) {\n e.source = source;\n throw e;\n }\n\n var template = function(data) {\n return render.call(this, data, _);\n };\n\n // Provide the compiled source as a convenience for precompilation.\n var argument = settings.variable || 'obj';\n template.source = 'function(' + argument + '){\\n' + source + '}';\n\n return template;\n };\n\n // Add a \"chain\" function. Start chaining a wrapped Underscore object.\n _.chain = function(obj) {\n var instance = _(obj);\n instance._chain = true;\n return instance;\n };\n\n // OOP\n // ---------------\n // If Underscore is called as a function, it returns a wrapped object that\n // can be used OO-style. This wrapper holds altered versions of all the\n // underscore functions. Wrapped objects may be chained.\n\n // Helper function to continue chaining intermediate results.\n var result = function(instance, obj) {\n return instance._chain ? _(obj).chain() : obj;\n };\n\n // Add your own custom functions to the Underscore object.\n _.mixin = function(obj) {\n _.each(_.functions(obj), function(name) {\n var func = _[name] = obj[name];\n _.prototype[name] = function() {\n var args = [this._wrapped];\n push.apply(args, arguments);\n return result(this, func.apply(_, args));\n };\n });\n };\n\n // Add all of the Underscore functions to the wrapper object.\n _.mixin(_);\n\n // Add all mutator Array functions to the wrapper.\n _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n var obj = this._wrapped;\n method.apply(obj, arguments);\n if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];\n return result(this, obj);\n };\n });\n\n // Add all accessor Array functions to the wrapper.\n _.each(['concat', 'join', 'slice'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n return result(this, method.apply(this._wrapped, arguments));\n };\n });\n\n // Extracts the result from a wrapped and chained object.\n _.prototype.value = function() {\n return this._wrapped;\n };\n\n // Provide unwrapping proxy for some methods used in engine operations\n // such as arithmetic and JSON stringification.\n _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;\n\n _.prototype.toString = function() {\n return '' + this._wrapped;\n };\n\n // AMD registration happens at the end for compatibility with AMD loaders\n // that may not enforce next-turn semantics on modules. Even though general\n // practice for AMD registration is to be anonymous, underscore registers\n // as a named module because, like jQuery, it is a base library that is\n // popular enough to be bundled in a third party lib, but not be part of\n // an AMD load request. Those cases could generate an error when an\n // anonymous define() is called outside of a loader request.\n if (typeof define === 'function' && define.amd) {\n define('underscore', [], function() {\n return _;\n });\n }\n}.call(this));\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/underscore/underscore.js\n// module id = 3\n// module chunks = 0","module.exports = {\n\t\"name\": \"bonobo-jupyter\",\n\t\"version\": \"0.0.1\",\n\t\"description\": \"Jupyter integration for Bonobo\",\n\t\"author\": \"\",\n\t\"main\": \"src/index.js\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"\"\n\t},\n\t\"keywords\": [\n\t\t\"jupyter\",\n\t\t\"widgets\",\n\t\t\"ipython\",\n\t\t\"ipywidgets\"\n\t],\n\t\"scripts\": {\n\t\t\"prepublish\": \"webpack\",\n\t\t\"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"json-loader\": \"^0.5.4\",\n\t\t\"webpack\": \"^1.12.14\"\n\t},\n\t\"dependencies\": {\n\t\t\"jupyter-js-widgets\": \"^2.0.9\",\n\t\t\"underscore\": \"^1.8.3\"\n\t}\n};\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./package.json\n// module id = 4\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file diff --git a/bonobo/plugins/__init__.py b/bonobo/plugins/__init__.py index 897b687..68f91b2 100644 --- a/bonobo/plugins/__init__.py +++ b/bonobo/plugins/__init__.py @@ -3,8 +3,11 @@ class Plugin: A plugin is an extension to the core behavior of bonobo. If you're writing transformations, you should not need to use this interface. - For examples, you can read bonobo.ext.console.ConsoleOutputPlugin, or bonobo.ext.jupyter.JupyterOutputPlugin that - respectively permits an interactive output on an ANSI console and a rich output in a jupyter notebook. + For examples, you can read bonobo.plugins.console.ConsoleOutputPlugin, or bonobo.plugins.jupyter.JupyterOutputPlugin + that respectively permits an interactive output on an ANSI console and a rich output in a jupyter notebook. Note + that you most probably won't instanciate them by yourself at runtime, as it's the default behaviour of bonobo to use + them if your in a compatible context (aka an interactive terminal for the console plugin, or a jupyter notebook for + the notebook plugin.) Warning: THE PLUGIN API IS PRE-ALPHA AND WILL EVOLVE BEFORE 1.0, DO NOT RELY ON IT BEING STABLE! diff --git a/bonobo/plugins/jupyter.py b/bonobo/plugins/jupyter.py new file mode 100644 index 0000000..9049964 --- /dev/null +++ b/bonobo/plugins/jupyter.py @@ -0,0 +1,35 @@ +import logging + +from bonobo.contrib.jupyter.widget import BonoboWidget +from bonobo.execution import events +from bonobo.plugins import Plugin + +try: + import IPython.core.display +except ImportError as e: + logging.exception( + 'You must install Jupyter to use the bonobo Jupyter extension. Easiest way is to install the ' + 'optional "jupyter" dependencies with «pip install bonobo[jupyter]», but you can also install a ' + 'specific version by yourself.' + ) + + +class JupyterOutputPlugin(Plugin): + def register(self, dispatcher): + dispatcher.add_listener(events.START, self.setup) + dispatcher.add_listener(events.TICK, self.tick) + dispatcher.add_listener(events.STOPPED, self.tick) + + def unregister(self, dispatcher): + dispatcher.remove_listener(events.STOPPED, self.tick) + dispatcher.remove_listener(events.TICK, self.tick) + dispatcher.remove_listener(events.START, self.setup) + + def setup(self, event): + self.widget = BonoboWidget() + IPython.core.display.display(self.widget) + + def tick(self, event): + self.widget.value = [ + event.context[i].as_dict() for i in event.context.graph.topologically_sorted_indexes + ] diff --git a/bonobo/structs/graphs.py b/bonobo/structs/graphs.py index e89b7e7..f11cd31 100644 --- a/bonobo/structs/graphs.py +++ b/bonobo/structs/graphs.py @@ -1,6 +1,9 @@ +import html import json from copy import copy +from graphviz.dot import Digraph + from bonobo.constants import BEGIN from bonobo.util import get_name @@ -112,23 +115,31 @@ class Graph: self._topologcally_sorted_indexes_cache = tuple(filter(lambda i: type(i) is int, reversed(order))) return self._topologcally_sorted_indexes_cache + @property + def graphviz(self): + try: + return self._graphviz + except AttributeError: + g = Digraph() + g.attr(rankdir='LR') + g.node('BEGIN', shape='point') + for i in self.outputs_of(BEGIN): + g.edge('BEGIN', str(i)) + for ix in self.topologically_sorted_indexes: + g.node(str(ix), label=get_name(self[ix])) + for iy in self.outputs_of(ix): + g.edge(str(ix), str(iy)) + self._graphviz = g + return self._graphviz + def _repr_dot_(self): - src = [ - 'digraph {', - ' rankdir = LR;', - ' "BEGIN" [shape="point"];', - ] + return str(self.graphviz) - for i in self.outputs_of(BEGIN): - src.append(' "BEGIN" -> ' + _get_graphviz_node_id(self, i) + ';') + def _repr_svg_(self): + return self.graphviz._repr_svg_() - for ix in self.topologically_sorted_indexes: - for iy in self.outputs_of(ix): - src.append(' {} -> {};'.format(_get_graphviz_node_id(self, ix), _get_graphviz_node_id(self, iy))) - - src.append('}') - - return '\n'.join(src) + def _repr_html_(self): + return '
{}
{}
'.format(self.graphviz._repr_svg_(), html.escape(repr(self))) 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. diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html index 9210c4f..f46896c 100644 --- a/docs/_templates/sidebarlogo.html +++ b/docs/_templates/sidebarlogo.html @@ -1,7 +1,9 @@

- Bonobo + + Bonobo +

diff --git a/docs/conf.py b/docs/conf.py index 07d0424..38a6dd9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -193,5 +193,5 @@ rst_epilog = """ .. |longversion| replace:: v.{version} """.format( - version = version, + version=version, ) diff --git a/docs/tutorial/1-init.rst b/docs/tutorial/1-init.rst index 780d34d..c4f6b65 100644 --- a/docs/tutorial/1-init.rst +++ b/docs/tutorial/1-init.rst @@ -242,8 +242,8 @@ The console output contains two things. a call, but the execution will move to the next row. -Moving forward -:::::::::::::: +Wrap up +::::::: That's all for this first step. diff --git a/requirements-dev.txt b/requirements-dev.txt index 8000471..0d5467e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,18 +1,18 @@ -e .[dev] alabaster==0.7.10 babel==2.5.1 -certifi==2017.7.27.1 +certifi==2017.11.5 chardet==3.0.4 -coverage==4.4.1 +coverage==4.4.2 docutils==0.14 idna==2.6 imagesize==0.7.1 -jinja2==2.9.6 +jinja2==2.10 markupsafe==1.0 py==1.4.34 pygments==2.2.0 pytest-cov==2.5.1 -pytest-sugar==0.8.0 +pytest-sugar==0.9.0 pytest-timeout==1.2.0 pytest==3.2.3 pytz==2017.3 diff --git a/requirements-docker.txt b/requirements-docker.txt index 8eb4059..0e84452 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,16 +1,16 @@ -e .[docker] appdirs==1.4.3 bonobo-docker==0.5.0 -certifi==2017.7.27.1 +certifi==2017.11.5 chardet==3.0.4 colorama==0.3.9 docker-pycreds==0.2.1 docker==2.3.0 -fs==2.0.12 +fs==2.0.16 idna==2.6 packaging==16.8 pbr==3.1.1 -psutil==5.4.0 +psutil==5.4.1 pyparsing==2.2.0 pytz==2017.3 requests==2.18.4 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 2c499ad..ed9c8d1 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -3,32 +3,32 @@ appnope==0.1.0 bleach==2.1.1 decorator==4.1.2 entrypoints==0.2.3 -html5lib==0.999999999 +html5lib==1.0b10 ipykernel==4.6.1 ipython-genutils==0.2.0 ipython==6.2.1 ipywidgets==6.0.1 jedi==0.11.0 -jinja2==2.9.6 +jinja2==2.10 jsonschema==2.6.0 jupyter-client==5.1.0 jupyter-console==5.2.0 jupyter-core==4.4.0 jupyter==1.0.0 markupsafe==1.0 -mistune==0.8 +mistune==0.8.1 nbconvert==5.3.1 nbformat==4.4.0 notebook==5.2.1 pandocfilters==1.4.2 parso==0.1.0 -pexpect==4.2.1 +pexpect==4.3.0 pickleshare==0.7.4 prompt-toolkit==1.0.15 ptyprocess==0.5.2 pygments==2.2.0 python-dateutil==2.6.1 -pyzmq==16.0.3 +pyzmq==17.0.0b3 qtconsole==4.3.1 simplegeneric==0.8.1 six==1.11.0 diff --git a/requirements-sqlalchemy.txt b/requirements-sqlalchemy.txt index d33c754..07323d9 100644 --- a/requirements-sqlalchemy.txt +++ b/requirements-sqlalchemy.txt @@ -1,14 +1,14 @@ -e .[sqlalchemy] appdirs==1.4.3 bonobo-sqlalchemy==0.5.1 -certifi==2017.7.27.1 +certifi==2017.11.5 chardet==3.0.4 colorama==0.3.9 -fs==2.0.12 +fs==2.0.16 idna==2.6 packaging==16.8 pbr==3.1.1 -psutil==5.4.0 +psutil==5.4.1 pyparsing==2.2.0 pytz==2017.3 requests==2.18.4 diff --git a/requirements.txt b/requirements.txt index 3a5c70d..a81bda4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,17 @@ -e . appdirs==1.4.3 -certifi==2017.7.27.1 +certifi==2017.11.5 chardet==3.0.4 colorama==0.3.9 -fs==2.0.12 +fs==2.0.16 +graphviz==0.8.1 idna==2.6 -jinja2==2.9.6 +jinja2==2.10 markupsafe==1.0 mondrian==0.4.0 packaging==16.8 pbr==3.1.1 -psutil==5.4.0 +psutil==5.4.1 pyparsing==2.2.0 pytz==2017.3 requests==2.18.4 diff --git a/setup.py b/setup.py index d9eb59b..a04440d 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,14 @@ 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' + ] + ) + ], description=('Bonobo, a simple, modern and atomic extract-transform-load toolkit for ' 'python 3.5+.'), license='Apache License, Version 2.0', @@ -53,14 +61,14 @@ setup( packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, install_requires=[ - 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'jinja2 (>= 2.9, < 2.10)', 'mondrian (>= 0.4, < 0.5)', - 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6.0)', 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.27, < 1.28)', - 'whistle (>= 1.0, < 1.1)' + '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)' ], extras_require={ 'dev': [ 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', 'pytest-cov (>= 2.5, < 3.0)', - 'pytest-sugar (>= 0.8, < 0.9)', 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)', 'yapf' + 'pytest-sugar (>= 0.9, < 0.10)', 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)', 'yapf' ], 'docker': ['bonobo-docker (>= 0.5.0)'], 'jupyter': ['ipywidgets (>= 6.0.0, < 7)', 'jupyter (>= 1.0, < 1.1)'], diff --git a/tests/commands/test_convert.py b/tests/commands/test_convert.py index ed6f9e2..8f1aea8 100644 --- a/tests/commands/test_convert.py +++ b/tests/commands/test_convert.py @@ -6,8 +6,9 @@ from bonobo.util.environ import change_working_directory from bonobo.util.testing import all_runners -@pytest.mark.skipif(sys.version_info < (3, 6), - reason="python 3.5 does not preserve kwargs order and this cant pass for now") +@pytest.mark.skipif( + sys.version_info < (3, 6), reason="python 3.5 does not preserve kwargs order and this cant pass for now" +) @all_runners def test_convert(runner, tmpdir): csv_content = 'id;name\n1;Romain' diff --git a/tests/ext/test_ods.py b/tests/ext/test_ods.py index 6b6a242..222a5a3 100644 --- a/tests/ext/test_ods.py +++ b/tests/ext/test_ods.py @@ -1,6 +1,6 @@ from unittest.mock import patch -from bonobo.ext.opendatasoft import OpenDataSoftAPI +from bonobo.contrib.opendatasoft import OpenDataSoftAPI from bonobo.util.objects import ValueHolder From a97b1c5d2e1feda2870dddcbec4faeb058787c14 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 09:08:32 +0100 Subject: [PATCH 109/145] Adds a benchmarks directory with small scripts to test performances of things. --- benchmarks/parameters.py | 44 ++++++++++++++++++++++++++++++++++++++ benchmarks/person.json | 46 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 benchmarks/parameters.py create mode 100644 benchmarks/person.json diff --git a/benchmarks/parameters.py b/benchmarks/parameters.py new file mode 100644 index 0000000..9098fde --- /dev/null +++ b/benchmarks/parameters.py @@ -0,0 +1,44 @@ +""" +Compare passing a dict to passing a dict as kwargs to a stupid transformation + +Last results (1 mill calls): + +j1 1.5026444319955772 +k1 1.8377482700016117 +j2 1.1962292949901894 +k2 1.5545833489886718 +j3 1.0014333260041894 +k3 1.353256585993222 + +""" +import json +import timeit + +def j1(d): + return {'prepend': 'foo', **d, 'append': 'bar'} + +def k1(**d): + return {'prepend': 'foo', **d, 'append': 'bar'} + +def j2(d): + return {**d} + +def k2(**d): + return {**d} + +def j3(d): + return None + +def k3(**d): + return None + +if __name__ == '__main__': + import timeit + + with open('person.json') as f: + json_data = json.load(f) + + + for i in 1,2,3: + print('j{}'.format(i), timeit.timeit("j{}({!r})".format(i, json_data), setup="from __main__ import j{}".format(i))) + print('k{}'.format(i), timeit.timeit("k{}(**{!r})".format(i, json_data), setup="from __main__ import k{}".format(i))) diff --git a/benchmarks/person.json b/benchmarks/person.json new file mode 100644 index 0000000..2bd2886 --- /dev/null +++ b/benchmarks/person.json @@ -0,0 +1,46 @@ +{ + "@context": "http://schema.org", + "@type": "MusicEvent", + "location": { + "@type": "MusicVenue", + "name": "Chicago Symphony Center", + "address": "220 S. Michigan Ave, Chicago, Illinois, USA" + }, + "name": "Shostakovich Leningrad", + "offers": { + "@type": "Offer", + "url": "/examples/ticket/12341234", + "price": "40", + "priceCurrency": "USD", + "availability": "http://schema.org/InStock" + }, + "performer": [ + { + "@type": "MusicGroup", + "name": "Chicago Symphony Orchestra", + "sameAs": [ + "http://cso.org/", + "http://en.wikipedia.org/wiki/Chicago_Symphony_Orchestra" + ] + }, + { + "@type": "Person", + "image": "/examples/jvanzweden_s.jpg", + "name": "Jaap van Zweden", + "sameAs": "http://www.jaapvanzweden.com/" + } + ], + "startDate": "2014-05-23T20:00", + "workPerformed": [ + { + "@type": "CreativeWork", + "name": "Britten Four Sea Interludes and Passacaglia from Peter Grimes", + "sameAs": "http://en.wikipedia.org/wiki/Peter_Grimes" + }, + { + "@type": "CreativeWork", + "name": "Shostakovich Symphony No. 7 (Leningrad)", + "sameAs": "http://en.wikipedia.org/wiki/Symphony_No._7_(Shostakovich)" + } + ] +} From c2f17296f6a5601f1103e26ec8a80151b44646fc Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 10:06:15 +0100 Subject: [PATCH 110/145] [stdlib] Adds Update(...) and FixedWindow(...) the the standard nodes provided with bonobo. --- benchmarks/parameters.py | 20 ++++++++-- bonobo/_api.py | 32 ++++++++++++++-- bonobo/contrib/django/commands.py | 4 +- bonobo/contrib/jupyter/__init__.py | 7 +--- bonobo/execution/strategies/executor.py | 1 + bonobo/nodes/basics.py | 51 ++++++++++++++++++++++++- bonobo/plugins/jupyter.py | 4 +- bonobo/structs/bags.py | 23 +++++------ bonobo/util/objects.py | 4 +- tests/{ => nodes}/io/test_csv.py | 0 tests/{ => nodes}/io/test_file.py | 0 tests/{ => nodes}/io/test_json.py | 0 tests/{ => nodes}/io/test_pickle.py | 0 tests/{ => nodes}/test_basics.py | 37 ++++++++++++++++++ tests/structs/test_bags.py | 2 +- 15 files changed, 147 insertions(+), 38 deletions(-) rename tests/{ => nodes}/io/test_csv.py (100%) rename tests/{ => nodes}/io/test_file.py (100%) rename tests/{ => nodes}/io/test_json.py (100%) rename tests/{ => nodes}/io/test_pickle.py (100%) rename tests/{ => nodes}/test_basics.py (55%) diff --git a/benchmarks/parameters.py b/benchmarks/parameters.py index 9098fde..f51e389 100644 --- a/benchmarks/parameters.py +++ b/benchmarks/parameters.py @@ -14,31 +14,43 @@ k3 1.353256585993222 import json import timeit + def j1(d): return {'prepend': 'foo', **d, 'append': 'bar'} + def k1(**d): return {'prepend': 'foo', **d, 'append': 'bar'} + def j2(d): return {**d} + def k2(**d): return {**d} + def j3(d): return None + def k3(**d): return None + if __name__ == '__main__': import timeit with open('person.json') as f: json_data = json.load(f) - - for i in 1,2,3: - print('j{}'.format(i), timeit.timeit("j{}({!r})".format(i, json_data), setup="from __main__ import j{}".format(i))) - print('k{}'.format(i), timeit.timeit("k{}(**{!r})".format(i, json_data), setup="from __main__ import k{}".format(i))) + for i in 1, 2, 3: + print( + 'j{}'.format(i), + timeit.timeit("j{}({!r})".format(i, json_data), setup="from __main__ import j{}".format(i)) + ) + print( + 'k{}'.format(i), + timeit.timeit("k{}(**{!r})".format(i, json_data), setup="from __main__ import k{}".format(i)) + ) diff --git a/bonobo/_api.py b/bonobo/_api.py index 925e6ce..e7362dd 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,6 +1,26 @@ from bonobo.execution.strategies import create_strategy -from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ - PickleReader, PickleWriter, PrettyPrinter, RateLimited, Tee, arg0_to_kwargs, count, identity, kwargs_to_arg0, noop +from bonobo.nodes import ( + CsvReader, + CsvWriter, + FileReader, + FileWriter, + Filter, + FixedWindow, + JsonReader, + JsonWriter, + Limit, + PickleReader, + PickleWriter, + PrettyPrinter, + RateLimited, + 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.util import get_name @@ -25,8 +45,10 @@ def register_graph_api(x, __all__=__all__): required_parameters = {'plugins', 'services', 'strategy'} assert parameters[0] == 'graph', 'First parameter of a graph api function must be "graph".' assert required_parameters.intersection( - parameters) == required_parameters, 'Graph api functions must define the following parameters: ' + ', '.join( - sorted(required_parameters)) + parameters + ) == required_parameters, 'Graph api functions must define the following parameters: ' + ', '.join( + sorted(required_parameters) + ) return register_api(x, __all__=__all__) @@ -149,6 +171,7 @@ register_api_group( FileReader, FileWriter, Filter, + FixedWindow, JsonReader, JsonWriter, LdjsonReader, @@ -159,6 +182,7 @@ register_api_group( PrettyPrinter, RateLimited, Tee, + Update, arg0_to_kwargs, count, identity, diff --git a/bonobo/contrib/django/commands.py b/bonobo/contrib/django/commands.py index 11ec680..457273d 100644 --- a/bonobo/contrib/django/commands.py +++ b/bonobo/contrib/django/commands.py @@ -22,9 +22,7 @@ class ETLCommand(BaseCommand): create_or_update = staticmethod(create_or_update) def create_parser(self, prog_name, subcommand): - return bonobo.get_argument_parser( - super().create_parser(prog_name, subcommand) - ) + return bonobo.get_argument_parser(super().create_parser(prog_name, subcommand)) def get_graph(self, *args, **options): def not_implemented(): diff --git a/bonobo/contrib/jupyter/__init__.py b/bonobo/contrib/jupyter/__init__.py index 7e5f892..49242be 100644 --- a/bonobo/contrib/jupyter/__init__.py +++ b/bonobo/contrib/jupyter/__init__.py @@ -2,12 +2,7 @@ from bonobo.plugins.jupyter import JupyterOutputPlugin def _jupyter_nbextension_paths(): - return [{ - 'section': 'notebook', - 'src': 'static', - 'dest': 'bonobo-jupyter', - 'require': 'bonobo-jupyter/extension' - }] + return [{'section': 'notebook', 'src': 'static', 'dest': 'bonobo-jupyter', 'require': 'bonobo-jupyter/extension'}] __all__ = [ diff --git a/bonobo/execution/strategies/executor.py b/bonobo/execution/strategies/executor.py index ea6c08e..f99c4cc 100644 --- a/bonobo/execution/strategies/executor.py +++ b/bonobo/execution/strategies/executor.py @@ -10,6 +10,7 @@ from bonobo.util import get_name logger = logging.getLogger(__name__) + class ExecutorStrategy(Strategy): """ Strategy based on a concurrent.futures.Executor subclass (or similar interface). diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index fa74e40..4054d3d 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -4,16 +4,17 @@ import itertools from bonobo import settings from bonobo.config import Configurable, Option from bonobo.config.processors import ContextProcessor +from bonobo.constants import NOT_MODIFIED from bonobo.structs.bags import Bag from bonobo.util.objects import ValueHolder from bonobo.util.term import CLEAR_EOL -from bonobo.constants import NOT_MODIFIED - __all__ = [ + 'FixedWindow', 'Limit', 'PrettyPrinter', 'Tee', + 'Update', 'arg0_to_kwargs', 'count', 'identity', @@ -128,3 +129,49 @@ def kwargs_to_arg0(**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. + + For example, if the input is successively 1, 2, 3, 4, etc. and you pass it through a ``FixedWindow(2)``, you'll get + lists of elements 2 by 2: [1, 2], [3, 4], ... + + """ + + length = Option(int, positional=True) # type: int + + @ContextProcessor + def buffer(self, context): + buffer = yield ValueHolder([]) + if len(buffer): + context.send(Bag(buffer.get())) + + def call(self, buffer, x): + buffer.append(x) + if len(buffer) >= self.length: + yield buffer.get() + buffer.set([]) + + + diff --git a/bonobo/plugins/jupyter.py b/bonobo/plugins/jupyter.py index 9049964..245ac95 100644 --- a/bonobo/plugins/jupyter.py +++ b/bonobo/plugins/jupyter.py @@ -30,6 +30,4 @@ class JupyterOutputPlugin(Plugin): IPython.core.display.display(self.widget) def tick(self, event): - self.widget.value = [ - event.context[i].as_dict() for i in event.context.graph.topologically_sorted_indexes - ] + self.widget.value = [event.context[i].as_dict() for i in event.context.graph.topologically_sorted_indexes] diff --git a/bonobo/structs/bags.py b/bonobo/structs/bags.py index 8683175..f303f92 100644 --- a/bonobo/structs/bags.py +++ b/bonobo/structs/bags.py @@ -1,7 +1,7 @@ import itertools -from bonobo.structs.tokens import Token from bonobo.constants import INHERIT_INPUT, LOOPBACK +from bonobo.structs.tokens import Token __all__ = [ 'Bag', @@ -36,6 +36,10 @@ class Bag: 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: @@ -86,6 +90,9 @@ class Bag: 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: @@ -141,7 +148,7 @@ class Bag: @classmethod def inherit(cls, *args, **kwargs): - return cls(*args, _flags=(INHERIT_INPUT, ), **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. @@ -169,19 +176,9 @@ class Bag: return len(self.args) == 1 and not self.kwargs and self.args[0] == other - def __repr__(self): - return '<{} ({})>'.format( - type(self).__name__, ', '.join( - itertools.chain( - map(repr, self.args), - ('{}={}'.format(k, repr(v)) for k, v in self.kwargs.items()), - ) - ) - ) - class LoopbackBag(Bag): - default_flags = (LOOPBACK, ) + default_flags = (LOOPBACK,) class ErrorBag(Bag): diff --git a/bonobo/util/objects.py b/bonobo/util/objects.py index 209f4db..f3ffa5e 100644 --- a/bonobo/util/objects.py +++ b/bonobo/util/objects.py @@ -142,10 +142,10 @@ class ValueHolder: return divmod(other, self._value) def __pow__(self, other): - return self._value ** other + return self._value**other def __rpow__(self, other): - return other ** self._value + return other**self._value def __ipow__(self, other): self._value **= other diff --git a/tests/io/test_csv.py b/tests/nodes/io/test_csv.py similarity index 100% rename from tests/io/test_csv.py rename to tests/nodes/io/test_csv.py diff --git a/tests/io/test_file.py b/tests/nodes/io/test_file.py similarity index 100% rename from tests/io/test_file.py rename to tests/nodes/io/test_file.py diff --git a/tests/io/test_json.py b/tests/nodes/io/test_json.py similarity index 100% rename from tests/io/test_json.py rename to tests/nodes/io/test_json.py diff --git a/tests/io/test_pickle.py b/tests/nodes/io/test_pickle.py similarity index 100% rename from tests/io/test_pickle.py rename to tests/nodes/io/test_pickle.py diff --git a/tests/test_basics.py b/tests/nodes/test_basics.py similarity index 55% rename from tests/test_basics.py rename to tests/nodes/test_basics.py index 283e3d7..de72069 100644 --- a/tests/test_basics.py +++ b/tests/nodes/test_basics.py @@ -1,3 +1,4 @@ +from operator import methodcaller from unittest.mock import MagicMock import pytest @@ -5,6 +6,7 @@ import pytest import bonobo from bonobo.config.processors import ContextCurrifier from bonobo.constants import NOT_MODIFIED +from bonobo.util.testing import BufferingNodeExecutionContext def test_count(): @@ -72,3 +74,38 @@ def test_tee(): 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]] + + 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]] + + with BufferingNodeExecutionContext(bonobo.FixedWindow(1)) as context: + context.write_sync(*range(3)) + 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'] + + with BufferingNodeExecutionContext(methodcaller('zfill', 5)) as context: + context.write_sync('a', 'bb', 'ccc') + assert context.get_buffer() == ['0000a', '000bb', '00ccc'] diff --git a/tests/structs/test_bags.py b/tests/structs/test_bags.py index b5517e3..6a13bbf 100644 --- a/tests/structs/test_bags.py +++ b/tests/structs/test_bags.py @@ -159,7 +159,7 @@ def test_eq_operator_dict(): def test_repr(): bag = Bag('a', a=1) - assert repr(bag) == "" + assert repr(bag) == "Bag('a', a=1)" def test_iterator(): From 0b3694142bef95097f5d58ce3cc834d73ef3ad37 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 10:36:07 +0100 Subject: [PATCH 111/145] Less strict CSV processing, to allow dirty input. --- bonobo/nodes/io/csv.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index 220830f..6f24df6 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -1,4 +1,5 @@ import csv +import warnings from bonobo.config import Option from bonobo.config.options import RemovedOption @@ -62,7 +63,7 @@ class CsvReader(FileReader, CsvHandler): for row in reader: if len(row) != field_count: - raise ValueError('Got a line with %d fields, expecting %d.' % ( + warnings.warn('Got a line with %d fields, expecting %d.' % ( len(row), field_count, )) @@ -81,6 +82,6 @@ class CsvWriter(FileWriter, CsvHandler): if not lineno: headers.set(headers.value or row.keys()) writer.writerow(headers.get()) - writer.writerow(row[header] for header in headers.get()) + writer.writerow(row.get(header, '') for header in headers.get()) lineno += 1 return NOT_MODIFIED From 739a64d8f4cf83f94717058440c331e68df0b70f Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 11:16:52 +0100 Subject: [PATCH 112/145] Simplification of node execution context, handle_result is now in step() as it is the only logical place where this will actually be called. --- bonobo/execution/contexts/node.py | 31 +++++++++++++++---------------- bonobo/nodes/io/csv.py | 7 ++----- bonobo/util/statistics.py | 4 ++-- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/bonobo/execution/contexts/node.py b/bonobo/execution/contexts/node.py index 767b929..a18f4d7 100644 --- a/bonobo/execution/contexts/node.py +++ b/bonobo/execution/contexts/node.py @@ -1,5 +1,6 @@ import logging import sys +import warnings from queue import Empty from time import sleep from types import GeneratorType @@ -27,7 +28,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): def __init__(self, wrapped, parent=None, services=None, _input=None, _outputs=None): LoopingExecutionContext.__init__(self, wrapped, parent=parent, services=services) - WithStatistics.__init__(self, 'in', 'out', 'err') + WithStatistics.__init__(self, 'in', 'out', 'err', 'warn') self.input = _input or Input() self.outputs = _outputs or [] @@ -125,19 +126,8 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): input_bag = self.get() - # todo add timer - self.handle_results(input_bag, input_bag.apply(self._stack)) + results = input_bag.apply(self._stack) - def kill(self): - if not self.started: - raise RuntimeError('Cannot kill a node context that has not started yet.') - - if self.stopped: - raise RuntimeError('Cannot kill a node context that has already stopped.') - - self._killed = True - - def handle_results(self, input_bag, results): # self._exec_time += timer.duration # Put data onto output channels @@ -159,6 +149,15 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): # self._exec_count += 1 pass + def kill(self): + if not self.started: + raise RuntimeError('Cannot kill a node context that has not started yet.') + + if self.stopped: + raise RuntimeError('Cannot kill a node context that has already stopped.') + + self._killed = True + def as_dict(self): return { 'status': self.status, @@ -169,7 +168,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): def isflag(param): - return isinstance(param, Token) and param in (NOT_MODIFIED, ) + return isinstance(param, Token) and param in (NOT_MODIFIED,) def split_tokens(output): @@ -181,11 +180,11 @@ def split_tokens(output): """ if isinstance(output, Token): # just a flag - return (output, ), () + return (output,), () if not istuple(output): # no flag - return (), (output, ) + return (), (output,) i = 0 while isflag(output[i]): diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index 6f24df6..c846a5f 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -61,12 +61,9 @@ class CsvReader(FileReader, CsvHandler): for _ in range(0, self.skip): next(reader) - for row in reader: + for lineno, row in enumerate(reader): if len(row) != field_count: - warnings.warn('Got a line with %d fields, expecting %d.' % ( - len(row), - field_count, - )) + warnings.warn('Got %d fields on line #%d, expecting %d.' % (len(row), lineno, field_count,)) yield dict(zip(_headers, row)) diff --git a/bonobo/util/statistics.py b/bonobo/util/statistics.py index 2f9c5c2..31da8b9 100644 --- a/bonobo/util/statistics.py +++ b/bonobo/util/statistics.py @@ -28,8 +28,8 @@ class WithStatistics: stats = tuple('{0}={1}'.format(name, cnt) for name, cnt in self.get_statistics(*args, **kwargs) if cnt > 0) return (kwargs.get('prefix', '') + ' '.join(stats)) if len(stats) else '' - def increment(self, name): - self.statistics[name] += 1 + def increment(self, name, *, amount=1): + self.statistics[name] += amount class Timer: From c87775f090712b1f754a537863438790f4f597b9 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 12 Nov 2017 14:22:29 +0100 Subject: [PATCH 113/145] Core: refactoring contexts with more logical responsibilities, stopping to rely on kargs ordering for compat with python3.5 --- .gitignore | 1 + bonobo/config/options.py | 31 +++- bonobo/constants.py | 4 + bonobo/execution/contexts/base.py | 119 ++++++------- bonobo/execution/contexts/node.py | 227 +++++++++++++----------- bonobo/execution/contexts/plugin.py | 4 +- bonobo/execution/strategies/executor.py | 11 +- bonobo/nodes/basics.py | 28 +-- bonobo/nodes/io/__init__.py | 2 +- bonobo/nodes/io/base.py | 3 - bonobo/nodes/io/csv.py | 97 ++++++---- bonobo/nodes/io/pickle.py | 1 - bonobo/structs/bags.py | 16 +- bonobo/util/collections.py | 2 + bonobo/util/testing.py | 5 +- tests/config/test_methods.py | 2 +- tests/nodes/io/test_csv.py | 24 +-- 17 files changed, 325 insertions(+), 252 deletions(-) diff --git a/.gitignore b/.gitignore index db473d4..ae199da 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ *.so *.spec .*.sw? +.DS_Store .Python .cache .coverage diff --git a/bonobo/config/options.py b/bonobo/config/options.py index ec02543..51702c3 100644 --- a/bonobo/config/options.py +++ b/bonobo/config/options.py @@ -1,3 +1,5 @@ +import types + from bonobo.util.inspect import istype @@ -143,10 +145,23 @@ class Method(Option): >>> example3 = OtherChildMethodExample() + It's possible to pass a default implementation to a Method by calling it, making it suitable to use as a decorator. + + >>> class MethodExampleWithDefault(Configurable): + ... @Method() + ... def handler(self): + ... pass + """ - def __init__(self, *, required=True, positional=True): - super().__init__(None, required=required, positional=positional) + def __init__(self, *, required=True, positional=True, __doc__=None): + super().__init__(None, required=required, positional=positional, __doc__=__doc__) + + def __get__(self, inst, type_): + x = super(Method, self).__get__(inst, type_) + if inst: + x = types.MethodType(x, inst) + return x def __set__(self, inst, value): if not hasattr(value, '__call__'): @@ -157,6 +172,12 @@ class Method(Option): ) inst._options_values[self.name] = self.type(value) if self.type else value - def __call__(self, *args, **kwargs): - # only here to trick IDEs into thinking this is callable. - raise NotImplementedError('You cannot call the descriptor') + def __call__(self, impl): + if self.default: + raise RuntimeError('Can only be used once as a decorator.') + self.default = impl + self.required = False + return self + + def get_default(self): + return self.default diff --git a/bonobo/constants.py b/bonobo/constants.py index 8c6eba5..7f20fcd 100644 --- a/bonobo/constants.py +++ b/bonobo/constants.py @@ -7,3 +7,7 @@ LOOPBACK = Token('Loopback') NOT_MODIFIED = Token('NotModified') DEFAULT_SERVICES_FILENAME = '_services.py' DEFAULT_SERVICES_ATTR = 'get_services' + +TICK_PERIOD = 0.2 + +ARGNAMES = '_argnames' diff --git a/bonobo/execution/contexts/base.py b/bonobo/execution/contexts/base.py index 3ca580a..847633b 100644 --- a/bonobo/execution/contexts/base.py +++ b/bonobo/execution/contexts/base.py @@ -1,14 +1,10 @@ import logging import sys from contextlib import contextmanager -from logging import WARNING, ERROR +from logging import ERROR -import mondrian -from bonobo.config import create_container -from bonobo.config.processors import ContextCurrifier -from bonobo.execution import logger -from bonobo.util import isconfigurabletype from bonobo.util.objects import Wrapper, get_name +from mondrian import term @contextmanager @@ -28,8 +24,12 @@ def unrecoverable(error_handler): raise # raise unrecoverableerror from x ? -class LoopingExecutionContext(Wrapper): - PERIOD = 0.5 +class Lifecycle: + def __init__(self): + self._started = False + self._stopped = False + self._killed = False + self._defunct = False @property def started(self): @@ -39,6 +39,10 @@ class LoopingExecutionContext(Wrapper): def stopped(self): return self._stopped + @property + def killed(self): + return self._killed + @property def defunct(self): return self._defunct @@ -47,6 +51,11 @@ class LoopingExecutionContext(Wrapper): def alive(self): return self._started and not self._stopped + @property + def should_loop(self): + # TODO XXX started/stopped? + return not any((self.defunct, self.killed)) + @property def status(self): """One character status for this node. """ @@ -58,23 +67,6 @@ class LoopingExecutionContext(Wrapper): return '+' return '-' - def __init__(self, wrapped, parent, services=None): - super().__init__(wrapped) - - self.parent = parent - - if services: - if parent: - raise RuntimeError( - 'Having services defined both in GraphExecutionContext and child NodeExecutionContext is not supported, for now.' - ) - self.services = create_container(services) - else: - self.services = None - - self._started, self._stopped, self._defunct = False, False, False - self._stack = None - def __enter__(self): self.start() return self @@ -82,57 +74,54 @@ class LoopingExecutionContext(Wrapper): def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): self.stop() + def get_flags_as_string(self): + if self._defunct: + return term.red('[defunct]') + if self.killed: + return term.lightred('[killed]') + if self.stopped: + return term.lightblack('[done]') + return '' + def start(self): if self.started: - raise RuntimeError('Cannot start a node twice ({}).'.format(get_name(self))) + raise RuntimeError('This context is already started ({}).'.format(get_name(self))) self._started = True - try: - self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context()) - 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...' - ) - self._stack.setup(self) - except Exception: - return self.fatal(sys.exc_info()) - - def loop(self): - """Generic loop. A bit boring. """ - while self.alive: - self.step() - - def step(self): - """Left as an exercise for the children.""" - raise NotImplementedError('Abstract.') - def stop(self): if not self.started: - raise RuntimeError('Cannot stop an unstarted node ({}).'.format(get_name(self))) + raise RuntimeError('This context cannot be stopped as it never started ({}).'.format(get_name(self))) - if self._stopped: + self._stopped = True + + if self._stopped: # Stopping twice has no effect return - try: - if self._stack: - self._stack.teardown() - finally: - self._stopped = True + def kill(self): + if not self.started: + raise RuntimeError('Cannot kill an unstarted context.') - def _get_initial_context(self): - if self.parent: - return self.parent.services.args_for(self.wrapped) - if self.services: - return self.services.args_for(self.wrapped) - return () + if self.stopped: + raise RuntimeError('Cannot kill a stopped context.') - def handle_error(self, exctype, exc, tb, *, level=logging.ERROR): - logging.getLogger(__name__).log(level, repr(self), exc_info=(exctype, exc, tb)) + self._killed = True - def fatal(self, exc_info): + def fatal(self, exc_info, *, level=logging.CRITICAL): + logging.getLogger(__name__).log(level, repr(self), exc_info=exc_info) self._defunct = True - self.input.shutdown() - self.handle_error(*exc_info, level=logging.CRITICAL) + + def as_dict(self): + return { + 'status': self.status, + 'name': self.name, + 'stats': self.get_statistics_as_string(), + 'flags': self.get_flags_as_string(), + } + + +class BaseContext(Lifecycle, Wrapper): + def __init__(self, wrapped, *, parent=None): + Lifecycle.__init__(self) + Wrapper.__init__(self, wrapped) + self.parent = parent diff --git a/bonobo/execution/contexts/node.py b/bonobo/execution/contexts/node.py index a18f4d7..3cb3521 100644 --- a/bonobo/execution/contexts/node.py +++ b/bonobo/execution/contexts/node.py @@ -1,38 +1,44 @@ import logging import sys -import warnings from queue import Empty from time import sleep from types import GeneratorType -from bonobo.constants import NOT_MODIFIED, BEGIN, END +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.execution.contexts.base import LoopingExecutionContext +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 -from bonobo.util.compat import deprecated_alias +from bonobo.util import get_name, iserrorbag, isloopbackbag, isbag, istuple, isconfigurabletype from bonobo.util.statistics import WithStatistics -from mondrian import term + +logger = logging.getLogger(__name__) -class NodeExecutionContext(WithStatistics, LoopingExecutionContext): - """ - todo: make the counter dependant of parent context? - """ - - @property - def killed(self): - return self._killed - - def __init__(self, wrapped, parent=None, services=None, _input=None, _outputs=None): - LoopingExecutionContext.__init__(self, wrapped, parent=parent, services=services) +class NodeExecutionContext(BaseContext, WithStatistics): + def __init__(self, wrapped, *, parent=None, services=None, _input=None, _outputs=None): + BaseContext.__init__(self, wrapped, parent=parent) WithStatistics.__init__(self, 'in', 'out', 'err', 'warn') + # Services: how we'll access external dependencies + if services: + if self.parent: + raise RuntimeError( + 'Having services defined both in GraphExecutionContext and child NodeExecutionContext is not supported, for now.' + ) + self.services = create_container(services) + else: + self.services = None + + # Input / Output: how the wrapped node will communicate self.input = _input or Input() self.outputs = _outputs or [] - self._killed = False + + # Stack: context decorators for the execution + self._stack = None def __str__(self): return self.__name__ + self.get_statistics_as_string(prefix=' ') @@ -41,14 +47,94 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): name, type_name = get_name(self), get_name(type(self)) return '<{}({}{}){}>'.format(type_name, self.status, name, self.get_statistics_as_string(prefix=' ')) - def get_flags_as_string(self): - if self._defunct: - return term.red('[defunct]') - if self.killed: - return term.lightred('[killed]') - if self.stopped: - return term.lightblack('[done]') - return '' + def start(self): + super().start() + + try: + self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context()) + 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...' + ) + self._stack.setup(self) + except Exception: + return self.fatal(sys.exc_info()) + + def loop(self): + logger.debug('Node loop starts for {!r}.'.format(self)) + while self.should_loop: + try: + self.step() + except InactiveReadableError: + break + 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 Exception: # pylint: disable=broad-except + self.handle_error(*sys.exc_info()) + except BaseException: + self.handle_error(*sys.exc_info()) + break + 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.""" + + # Pull 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()) + + # self._exec_time += timer.duration + # Put data onto output channels + + if isinstance(results, GeneratorType): + while True: + try: + # if kill flag was step, stop iterating. + if self._killed: + break + result = next(results) + 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)) + elif results: + self.send(_resolve(input_bag, results)) + else: + # case with no result, an execution went through anyway, use for stats. + # self._exec_count += 1 + pass + + def stop(self): + if self._stack: + self._stack.teardown() + + 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 fatal(self, exc_info, *, level=logging.CRITICAL): + super().fatal(exc_info, level=level) + self.input.shutdown() def write(self, *messages): """ @@ -64,9 +150,6 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): for _ in messages: self.step() - # XXX deprecated alias - recv = deprecated_alias('recv', write) - def send(self, value, _control=False): """ Sends a message to all of this context's outputs. @@ -86,89 +169,25 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): for output in self.outputs: output.put(value) - push = deprecated_alias('push', send) - - def get(self): # recv() ? input_data = self.receive() + 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(timeout=self.PERIOD) + row = self.input.get() # XXX TIMEOUT ??? self.increment('in') return row - def should_loop(self): - return not any((self.defunct, self.killed)) - - def loop(self): - while self.should_loop(): - try: - self.step() - except InactiveReadableError: - break - except Empty: - sleep(self.PERIOD) - continue - except UnrecoverableError: - self.handle_error(*sys.exc_info()) - self.input.shutdown() - break - except Exception: # pylint: disable=broad-except - self.handle_error(*sys.exc_info()) - except BaseException: - self.handle_error(*sys.exc_info()) - break - - def step(self): - # Pull data from the first available input channel. - """Runs a transformation callable with given args/kwargs and flush the result into the right - output channel.""" - - input_bag = self.get() - - results = input_bag.apply(self._stack) - - # self._exec_time += timer.duration - # Put data onto output channels - - if isinstance(results, GeneratorType): - while True: - try: - # if kill flag was step, stop iterating. - if self._killed: - break - result = next(results) - except StopIteration: - break - else: - self.send(_resolve(input_bag, result)) - elif results: - self.send(_resolve(input_bag, results)) - else: - # case with no result, an execution went through anyway, use for stats. - # self._exec_count += 1 - pass - - def kill(self): - if not self.started: - raise RuntimeError('Cannot kill a node context that has not started yet.') - - if self.stopped: - raise RuntimeError('Cannot kill a node context that has already stopped.') - - self._killed = True - - def as_dict(self): - return { - 'status': self.status, - 'name': self.name, - 'stats': self.get_statistics_as_string(), - 'flags': self.get_flags_as_string(), - } + def _get_initial_context(self): + if self.parent: + return self.parent.services.args_for(self.wrapped) + if self.services: + return self.services.args_for(self.wrapped) + return () def isflag(param): - return isinstance(param, Token) and param in (NOT_MODIFIED,) + return isinstance(param, Token) and param in (NOT_MODIFIED, ) def split_tokens(output): @@ -180,11 +199,11 @@ def split_tokens(output): """ if isinstance(output, Token): # just a flag - return (output,), () + return (output, ), () if not istuple(output): # no flag - return (), (output,) + return (), (output, ) i = 0 while isflag(output[i]): diff --git a/bonobo/execution/contexts/plugin.py b/bonobo/execution/contexts/plugin.py index 524c2e1..3551d0d 100644 --- a/bonobo/execution/contexts/plugin.py +++ b/bonobo/execution/contexts/plugin.py @@ -1,7 +1,7 @@ -from bonobo.execution.contexts.base import LoopingExecutionContext +from bonobo.execution.contexts.base import BaseContext -class PluginExecutionContext(LoopingExecutionContext): +class PluginExecutionContext(BaseContext): @property def dispatcher(self): return self.parent.dispatcher diff --git a/bonobo/execution/strategies/executor.py b/bonobo/execution/strategies/executor.py index f99c4cc..d7a0017 100644 --- a/bonobo/execution/strategies/executor.py +++ b/bonobo/execution/strategies/executor.py @@ -52,15 +52,8 @@ class ExecutorStrategy(Strategy): def starter(node): @functools.wraps(node) def _runner(): - try: - with node: - node.loop() - except: - logging.getLogger(__name__).critical( - 'Uncaught exception in node execution for {}.'.format(node), exc_info=True - ) - node.shutdown() - node.stop() + with node: + node.loop() try: futures.append(executor.submit(_runner)) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 4054d3d..3a53d2d 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -4,7 +4,7 @@ import itertools from bonobo import settings from bonobo.config import Configurable, Option from bonobo.config.processors import ContextProcessor -from bonobo.constants import NOT_MODIFIED +from bonobo.constants import NOT_MODIFIED, ARGNAMES from bonobo.structs.bags import Bag from bonobo.util.objects import ValueHolder from bonobo.util.term import CLEAR_EOL @@ -88,18 +88,29 @@ class PrettyPrinter(Configurable): def call(self, *args, **kwargs): formater = self._format_quiet if settings.QUIET.get() else self._format_console + argnames = kwargs.get(ARGNAMES, None) - for i, (item, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())): - print(formater(i, item, value)) + 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)) - def _format_quiet(self, i, item, value): + def _format_quiet(self, i, item, value, *, argnames=None): + # XXX should we implement argnames here ? return ' '.join(((' ' if i else '-'), str(item), ':', str(value).strip())) - def _format_console(self, i, item, value): + def _format_console(self, i, item, value, *, argnames=None): + argnames = argnames or [] + if not isinstance(item, str): + if len(argnames) >= item: + item = '{} / {}'.format(item, argnames[item]) + else: + item = str(i) + return ' '.join( ( - (' ' if i else '•'), str(item), '=', _shorten(str(value).strip(), - self.max_width).replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL + (' ' if i else '•'), item, '=', _shorten(str(value).strip(), + self.max_width).replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL ) ) @@ -172,6 +183,3 @@ class FixedWindow(Configurable): if len(buffer) >= self.length: yield buffer.get() buffer.set([]) - - - diff --git a/bonobo/nodes/io/__init__.py b/bonobo/nodes/io/__init__.py index 4e7fbe6..1369ed2 100644 --- a/bonobo/nodes/io/__init__.py +++ b/bonobo/nodes/io/__init__.py @@ -1,8 +1,8 @@ """ Readers and writers for common file formats. """ +from .csv import CsvReader, CsvWriter from .file import FileReader, FileWriter from .json import JsonReader, JsonWriter, LdjsonReader, LdjsonWriter -from .csv import CsvReader, CsvWriter from .pickle import PickleReader, PickleWriter __all__ = [ diff --git a/bonobo/nodes/io/base.py b/bonobo/nodes/io/base.py index db0bc80..af9e609 100644 --- a/bonobo/nodes/io/base.py +++ b/bonobo/nodes/io/base.py @@ -1,7 +1,4 @@ -from fs.errors import ResourceNotFound - from bonobo.config import Configurable, ContextProcessor, Option, Service -from bonobo.errors import UnrecoverableError class FileHandler(Configurable): diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index c846a5f..188fd80 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -1,13 +1,13 @@ import csv import warnings -from bonobo.config import Option -from bonobo.config.options import RemovedOption -from bonobo.config.processors import ContextProcessor -from bonobo.constants import NOT_MODIFIED +from bonobo.config import Option, ContextProcessor +from bonobo.config.options import RemovedOption, Method +from bonobo.constants import NOT_MODIFIED, ARGNAMES from bonobo.nodes.io.base import FileHandler from bonobo.nodes.io.file import FileReader, FileWriter -from bonobo.util.objects import ValueHolder +from bonobo.structs.bags import Bag +from bonobo.util import ensure_tuple class CsvHandler(FileHandler): @@ -28,7 +28,7 @@ class CsvHandler(FileHandler): """ delimiter = Option(str, default=';') quotechar = Option(str, default='"') - headers = Option(tuple, required=False) + headers = Option(ensure_tuple, required=False) ioformat = RemovedOption(positional=False, value='kwargs') @@ -44,41 +44,66 @@ class CsvReader(FileReader, CsvHandler): skip = Option(int, default=0) - @ContextProcessor - def csv_headers(self, context, fs, file): - yield ValueHolder(self.headers) + @Method( + __doc__=''' + Builds the CSV reader, a.k.a an object we can iterate, each iteration giving one line of fields, as an + iterable. + + Defaults to builtin csv.reader(...), but can be overriden to fit your special needs. + ''' + ) + def reader_factory(self, file): + return csv.reader(file, delimiter=self.delimiter, quotechar=self.quotechar) - def read(self, fs, file, headers): - reader = csv.reader(file, delimiter=self.delimiter, quotechar=self.quotechar) - - if not headers.get(): - headers.set(next(reader)) - _headers = headers.get() - - field_count = len(headers) - - if self.skip and self.skip > 0: - for _ in range(0, self.skip): - next(reader) - - for lineno, row in enumerate(reader): - if len(row) != field_count: - warnings.warn('Got %d fields on line #%d, expecting %d.' % (len(row), lineno, field_count,)) - - yield dict(zip(_headers, row)) + def read(self, fs, file): + reader = self.reader_factory(file) + headers = self.headers or next(reader) + for row in reader: + yield Bag(*row, **{ARGNAMES: headers}) class CsvWriter(FileWriter, CsvHandler): @ContextProcessor - def writer(self, context, fs, file, lineno): - writer = csv.writer(file, delimiter=self.delimiter, quotechar=self.quotechar, lineterminator=self.eol) - headers = ValueHolder(list(self.headers) if self.headers else None) - yield writer, headers + 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 + target file. + + Defaults to builtin csv.writer(...).writerow, but can be overriden to fit your special needs. + ''' + ) + def writer_factory(self, file): + return csv.writer(file, delimiter=self.delimiter, quotechar=self.quotechar, lineterminator=self.eol).writerow + + def write(self, fs, file, lineno, context, *args, _argnames=None): + try: + writer = context.writer + except AttributeError: + context.writer = self.writer_factory(file) + writer = context.writer + context.headers = self.headers or _argnames + + if context.headers and not lineno: + writer(context.headers) - def write(self, fs, file, lineno, writer, headers, **row): - if not lineno: - headers.set(headers.value or row.keys()) - writer.writerow(headers.get()) - writer.writerow(row.get(header, '') for header in headers.get()) 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) + ) + ) + row = [(args[i] if i < len(args) else '') for i, header in enumerate(context.headers) if header] + else: + row = args + + writer(row) + return NOT_MODIFIED diff --git a/bonobo/nodes/io/pickle.py b/bonobo/nodes/io/pickle.py index 3bb95d6..bc02ce8 100644 --- a/bonobo/nodes/io/pickle.py +++ b/bonobo/nodes/io/pickle.py @@ -1,7 +1,6 @@ import pickle from bonobo.config import Option -from bonobo.config.options import RemovedOption from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED from bonobo.nodes.io.base import FileHandler diff --git a/bonobo/structs/bags.py b/bonobo/structs/bags.py index f303f92..0738790 100644 --- a/bonobo/structs/bags.py +++ b/bonobo/structs/bags.py @@ -52,8 +52,9 @@ class Bag: # Otherwise, type will handle that for us. return super().__new__(cls) - def __init__(self, *args, _flags=None, _parent=None, **kwargs): + 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: @@ -115,9 +116,13 @@ class Bag: 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) + return func_or_iter(*args, *self.args, **kwargs, **self.kwargs, **self.specials) if len(args) == 0 and len(kwargs) == 0: try: @@ -148,7 +153,7 @@ class Bag: @classmethod def inherit(cls, *args, **kwargs): - return cls(*args, _flags=(INHERIT_INPUT,), **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. @@ -176,9 +181,12 @@ class Bag: 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,) + default_flags = (LOOPBACK, ) class ErrorBag(Bag): diff --git a/bonobo/util/collections.py b/bonobo/util/collections.py index 31765c4..d5a4624 100644 --- a/bonobo/util/collections.py +++ b/bonobo/util/collections.py @@ -16,6 +16,8 @@ def ensure_tuple(tuple_or_mixed): :return: tuple """ + if tuple_or_mixed is None: + return () if isinstance(tuple_or_mixed, tuple): return tuple_or_mixed return (tuple_or_mixed, ) diff --git a/bonobo/util/testing.py b/bonobo/util/testing.py index 9044715..66af870 100644 --- a/bonobo/util/testing.py +++ b/bonobo/util/testing.py @@ -8,7 +8,7 @@ from unittest.mock import patch import pytest -from bonobo import open_fs, Token, __main__, get_examples_path +from bonobo import open_fs, Token, __main__, get_examples_path, Bag from bonobo.commands import entrypoint from bonobo.execution.contexts.graph import GraphExecutionContext from bonobo.execution.contexts.node import NodeExecutionContext @@ -57,6 +57,9 @@ class BufferingContext: def get_buffer(self): 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)) + class BufferingNodeExecutionContext(BufferingContext, NodeExecutionContext): def __init__(self, *args, buffer=None, **kwargs): diff --git a/tests/config/test_methods.py b/tests/config/test_methods.py index b0154fb..0a3b423 100644 --- a/tests/config/test_methods.py +++ b/tests/config/test_methods.py @@ -58,7 +58,7 @@ def test_define_with_decorator(): Concrete = MethodBasedConfigurable(my_handler) assert callable(Concrete.handler) - assert Concrete.handler == my_handler + assert Concrete.handler.__func__ == my_handler with inspect_node(Concrete) as ci: assert ci.type == MethodBasedConfigurable diff --git a/tests/nodes/io/test_csv.py b/tests/nodes/io/test_csv.py index b0b91c5..0d713bd 100644 --- a/tests/nodes/io/test_csv.py +++ b/tests/nodes/io/test_csv.py @@ -17,17 +17,21 @@ def test_write_csv_ioformat_arg0(tmpdir): CsvReader(path=filename, delimiter=',', ioformat=settings.IOFORMAT_ARG0), -@pytest.mark.parametrize('add_kwargs', ( - {}, - { - 'ioformat': settings.IOFORMAT_KWARGS, - }, -)) -def test_write_csv_to_file_kwargs(tmpdir, add_kwargs): +def test_write_csv_to_file_no_headers(tmpdir): fs, filename, services = csv_tester.get_services_for_writer(tmpdir) - with NodeExecutionContext(CsvWriter(filename, **add_kwargs), services=services) as context: - context.write_sync({'foo': 'bar'}, {'foo': 'baz', 'ignore': 'this'}) + 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' @@ -45,7 +49,7 @@ def test_read_csv_from_file_kwargs(tmpdir): ) as context: context.write_sync(()) - assert context.get_buffer() == [ + assert context.get_buffer_args_as_dicts() == [ { 'a': 'a foo', 'b': 'b foo', From 72ae354810610ab32872e02dd537cbf7c4e00425 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 13 Nov 2017 16:31:00 +0100 Subject: [PATCH 114/145] Fix two typos in the ContextProcessor documentation --- bonobo/config/processors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index 73c9949..3740192 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -15,10 +15,10 @@ class ContextProcessor(Option): It works like a yielding context manager, and is the recommended way to setup and teardown objects you'll need in the context of one execution. It's the way to overcome the stateless nature of transformations. - The yielded values will be passed as positional arguments to the next context processors (order do matter), and + The yielded values will be passed as positional arguments to the next context processors (order does matter), and finally to the __call__ method of the transformation. - Warning: this may change for a similar but simpler implementation, don't relly too much on it (yet). + Warning: this may change for a similar but simpler implementation, don't rely too much on it (yet). Example: From 9b0a51e647c55cf58619bcb463fadb922a6e1f5f Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 13 Nov 2017 16:38:53 +0100 Subject: [PATCH 115/145] Fix another typo in `run` --- bonobo/_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index e7362dd..7b8edc9 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -61,7 +61,7 @@ def register_api_group(*args): @register_graph_api def run(graph, *, plugins=None, services=None, strategy=None): """ - Main entry point of bonobo. It takes a graph and creates all the necessary plumbery around to execute it. + Main entry point of bonobo. It takes a graph and creates all the necessary plumbing around to execute it. The only necessary argument is a :class:`Graph` instance, containing the logic you actually want to execute. From cf120f6d650724a9d76c3efbdd71fbc5cb282259 Mon Sep 17 00:00:00 2001 From: Kenneth Koski Date: Mon, 13 Nov 2017 16:30:56 -0600 Subject: [PATCH 116/145] Adding services to naive execution --- bonobo/strategies/naive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bonobo/strategies/naive.py b/bonobo/strategies/naive.py index cab9c57..49f80cb 100644 --- a/bonobo/strategies/naive.py +++ b/bonobo/strategies/naive.py @@ -4,8 +4,8 @@ from bonobo.structs.bags import Bag class NaiveStrategy(Strategy): - def execute(self, graph, *args, plugins=None, **kwargs): - context = self.create_graph_execution_context(graph, plugins=plugins) + def execute(self, graph, *args, plugins=None, services=None, **kwargs): + context = self.create_graph_execution_context(graph, plugins=plugins, services=services) context.write(BEGIN, Bag(), END) # TODO: how to run plugins in "naive" mode ? From 5e0b6567cd4cd3840739cac456cc396f7a4c9543 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 27 Nov 2017 00:04:51 +0100 Subject: [PATCH 117/145] 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
. 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. --- .style.yapf | 7 + Makefile | 2 +- Projectfile | 3 +- RELEASE-0.6.rst | 66 ++++ bonobo/__init__.py | 17 + bonobo/_api.py | 12 +- bonobo/_version.py | 2 +- bonobo/commands/convert.py | 18 +- bonobo/commands/version.py | 36 +- bonobo/config/__init__.py | 13 +- bonobo/config/configurables.py | 28 +- bonobo/config/functools.py | 15 + bonobo/config/options.py | 34 +- bonobo/config/processors.py | 113 ++++-- bonobo/config/services.py | 8 +- bonobo/constants.py | 17 +- bonobo/contrib/google/__init__.py | 24 +- bonobo/errors.py | 4 + bonobo/examples/__init__.py | 32 ++ .../{nodes/__init__.py => coffeeshops.csv} | 0 bonobo/examples/datasets/__main__.py | 84 ++++ bonobo/examples/datasets/_services.py | 7 - bonobo/examples/datasets/coffeeshops.csv | 1 + bonobo/examples/datasets/coffeeshops.json | 363 +++++++++-------- bonobo/examples/datasets/coffeeshops.ldjson | 181 +++++++++ bonobo/examples/datasets/coffeeshops.py | 29 -- bonobo/examples/datasets/coffeeshops.txt | 365 +++++++++--------- bonobo/examples/files/_services.py | 5 +- bonobo/examples/files/csv_handlers.py | 38 +- bonobo/examples/files/json_handlers.py | 53 ++- bonobo/examples/files/pickle_handlers.py | 50 ++- bonobo/examples/files/text_handlers.py | 28 +- bonobo/examples/nodes/_services.py | 5 - bonobo/examples/nodes/bags.py | 41 -- bonobo/examples/nodes/count.py | 21 - bonobo/examples/nodes/dicts.py | 43 --- bonobo/examples/nodes/filter.py | 24 -- bonobo/examples/nodes/slow.py | 19 - bonobo/examples/nodes/strings.py | 39 -- .../examples/tutorials/tut02e03_writeasmap.py | 3 +- bonobo/examples/types/__init__.py | 7 - bonobo/examples/types/bags.py | 41 -- bonobo/examples/types/dicts.py | 43 --- bonobo/examples/types/strings.py | 4 +- bonobo/execution/contexts/base.py | 11 +- bonobo/execution/contexts/graph.py | 4 +- bonobo/execution/contexts/node.py | 277 +++++++++---- bonobo/execution/strategies/executor.py | 4 +- bonobo/execution/strategies/naive.py | 3 +- bonobo/nodes/basics.py | 257 +++++++----- bonobo/nodes/filter.py | 2 +- bonobo/nodes/io/base.py | 23 +- bonobo/nodes/io/csv.py | 115 ++++-- bonobo/nodes/io/file.py | 62 ++- bonobo/nodes/io/json.py | 98 ++--- bonobo/nodes/io/pickle.py | 40 +- bonobo/nodes/io/xml.py | 0 bonobo/nodes/throttle.py | 2 +- bonobo/plugins/console.py | 41 +- bonobo/plugins/sentry.py | 6 + bonobo/structs/__init__.py | 5 - bonobo/structs/bags.py | 193 --------- bonobo/structs/graphs.py | 27 +- bonobo/structs/tokens.py | 8 - bonobo/util/__init__.py | 6 - bonobo/util/bags.py | 187 +++++++++ bonobo/util/collections.py | 16 +- bonobo/util/inspect.py | 45 +-- bonobo/util/testing.py | 128 +++++- docs/guide/services.rst | 2 +- docs/guide/transformations.rst | 2 +- docs/tutorial/4-services.rst | 8 +- docs/tutorial/tut03.rst | 8 +- requirements-dev.txt | 6 +- requirements-docker.txt | 2 +- requirements-jupyter.txt | 2 +- requirements-sqlalchemy.txt | 2 +- requirements.txt | 6 +- setup.py | 18 +- tests/config/test_configurables.py | 2 + tests/config/test_methods.py | 2 +- tests/config/test_methods_partial.py | 2 +- tests/config/test_processors.py | 15 +- tests/config/test_services.py | 20 +- tests/execution/contexts/test_node.py | 55 ++- tests/ext/test_ods.py | 18 +- tests/features/test_not_modified.py | 8 +- tests/nodes/io/test_csv.py | 188 ++++++--- tests/nodes/io/test_file.py | 16 +- tests/nodes/io/test_json.py | 326 +++++++++++++--- tests/nodes/io/test_pickle.py | 25 +- tests/nodes/test_basics.py | 119 +++--- tests/structs/test_bags.py | 170 -------- tests/structs/test_tokens.py | 2 +- tests/test_execution.py | 21 +- tests/util/test_bags.py | 278 +++++++++++++ 96 files changed, 2958 insertions(+), 1870 deletions(-) create mode 100644 RELEASE-0.6.rst create mode 100644 bonobo/config/functools.py rename bonobo/examples/{nodes/__init__.py => coffeeshops.csv} (100%) create mode 100644 bonobo/examples/datasets/__main__.py delete mode 100644 bonobo/examples/datasets/_services.py create mode 100644 bonobo/examples/datasets/coffeeshops.csv create mode 100644 bonobo/examples/datasets/coffeeshops.ldjson delete mode 100644 bonobo/examples/datasets/coffeeshops.py delete mode 100644 bonobo/examples/nodes/_services.py delete mode 100644 bonobo/examples/nodes/bags.py delete mode 100644 bonobo/examples/nodes/count.py delete mode 100644 bonobo/examples/nodes/dicts.py delete mode 100644 bonobo/examples/nodes/filter.py delete mode 100644 bonobo/examples/nodes/slow.py delete mode 100644 bonobo/examples/nodes/strings.py delete mode 100644 bonobo/examples/types/bags.py delete mode 100644 bonobo/examples/types/dicts.py delete mode 100644 bonobo/nodes/io/xml.py create mode 100644 bonobo/plugins/sentry.py delete mode 100644 bonobo/structs/bags.py delete mode 100644 bonobo/structs/tokens.py create mode 100644 bonobo/util/bags.py delete mode 100644 tests/structs/test_bags.py create mode 100644 tests/util/test_bags.py diff --git a/.style.yapf b/.style.yapf index eaa5a7a..1da51e8 100644 --- a/.style.yapf +++ b/.style.yapf @@ -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 diff --git a/Makefile b/Makefile index 445b863..d4c3556 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4.1 on 2017-11-12. +# Generated by Medikit 0.4.2 on 2017-11-26. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index be2e0c8..9b879c9 100644 --- a/Projectfile +++ b/Projectfile @@ -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', diff --git a/RELEASE-0.6.rst b/RELEASE-0.6.rst new file mode 100644 index 0000000..805a4e3 --- /dev/null +++ b/RELEASE-0.6.rst @@ -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. + + diff --git a/bonobo/__init__.py b/bonobo/__init__.py index 0ac9bc3..32baf98 100644 --- a/bonobo/__init__.py +++ b/bonobo/__init__.py @@ -15,6 +15,23 @@ from bonobo._api import __all__ from bonobo._version import __version__ __all__ = ['__version__'] + __all__ +__logo__ = '' __version__ = __version__ + +def _repr_html_(): + """This allows to easily display a version snippet in Jupyter.""" + from bonobo.util.pkgs import bonobo_packages + from bonobo.commands.version import get_versions + + return ( + '
' + '
{}
' + '
{}
' + '
' + ).format( + __logo__, '
'.join(get_versions(all=True)) + ) + + del sys diff --git a/bonobo/_api.py b/bonobo/_api.py index 7b8edc9..2f5522c 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -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, ) diff --git a/bonobo/_version.py b/bonobo/_version.py index 2724bac..1986ae4 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.6.dev0' +__version__ = '0.6.0a0' diff --git a/bonobo/commands/convert.py b/bonobo/commands/convert.py index 198dce0..f490213 100644 --- a/bonobo/commands/convert.py +++ b/bonobo/commands/convert.py @@ -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 [])) diff --git a/bonobo/commands/version.py b/bonobo/commands/version.py index 3e3239a..5ca2311 100644 --- a/bonobo/commands/version.py +++ b/bonobo/commands/version.py @@ -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') diff --git a/bonobo/config/__init__.py b/bonobo/config/__init__.py index a86e8ba..6ba99e8 100644 --- a/bonobo/config/__init__.py +++ b/bonobo/config/__init__.py @@ -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', ] diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index 85ecdde..3e6f154 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -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.') diff --git a/bonobo/config/functools.py b/bonobo/config/functools.py new file mode 100644 index 0000000..506306b --- /dev/null +++ b/bonobo/config/functools.py @@ -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 diff --git a/bonobo/config/options.py b/bonobo/config/options.py index 51702c3..85b50e7 100644 --- a/bonobo/config/options.py +++ b/bonobo/config/options.py @@ -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 diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index 3740192..c8a2ddf 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -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) diff --git a/bonobo/config/services.py b/bonobo/config/services.py index acf4dfd..282d88f 100644 --- a/bonobo/config/services.py +++ b/bonobo/config/services.py @@ -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__ diff --git a/bonobo/constants.py b/bonobo/constants.py index 7f20fcd..fceb8f9 100644 --- a/bonobo/constants.py +++ b/bonobo/constants.py @@ -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' diff --git a/bonobo/contrib/google/__init__.py b/bonobo/contrib/google/__init__.py index e8e66b5..74d4612 100644 --- a/bonobo/contrib/google/__init__.py +++ b/bonobo/contrib/google/__init__.py @@ -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) diff --git a/bonobo/errors.py b/bonobo/errors.py index 08b97d4..53d0a5d 100644 --- a/bonobo/errors.py +++ b/bonobo/errors.py @@ -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 diff --git a/bonobo/examples/__init__.py b/bonobo/examples/__init__.py index e69de29..ec68fc5 100644 --- a/bonobo/examples/__init__.py +++ b/bonobo/examples/__init__.py @@ -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 (), + } diff --git a/bonobo/examples/nodes/__init__.py b/bonobo/examples/coffeeshops.csv similarity index 100% rename from bonobo/examples/nodes/__init__.py rename to bonobo/examples/coffeeshops.csv diff --git a/bonobo/examples/datasets/__main__.py b/bonobo/examples/datasets/__main__.py new file mode 100644 index 0000000..37444ac --- /dev/null +++ b/bonobo/examples/datasets/__main__.py @@ -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()) diff --git a/bonobo/examples/datasets/_services.py b/bonobo/examples/datasets/_services.py deleted file mode 100644 index 36d3b18..0000000 --- a/bonobo/examples/datasets/_services.py +++ /dev/null @@ -1,7 +0,0 @@ -from os.path import dirname - -import bonobo - - -def get_services(): - return {'fs': bonobo.open_fs(dirname(__file__))} diff --git a/bonobo/examples/datasets/coffeeshops.csv b/bonobo/examples/datasets/coffeeshops.csv new file mode 100644 index 0000000..c4c10e3 --- /dev/null +++ b/bonobo/examples/datasets/coffeeshops.csv @@ -0,0 +1 @@ +"['name', 'address', 'zipcode', 'city']" diff --git a/bonobo/examples/datasets/coffeeshops.json b/bonobo/examples/datasets/coffeeshops.json index 391b5e8..8544018 100644 --- a/bonobo/examples/datasets/coffeeshops.json +++ b/bonobo/examples/datasets/coffeeshops.json @@ -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"} \ No newline at end of file +[{"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"}] \ No newline at end of file diff --git a/bonobo/examples/datasets/coffeeshops.ldjson b/bonobo/examples/datasets/coffeeshops.ldjson new file mode 100644 index 0000000..30a5316 --- /dev/null +++ b/bonobo/examples/datasets/coffeeshops.ldjson @@ -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"} \ No newline at end of file diff --git a/bonobo/examples/datasets/coffeeshops.py b/bonobo/examples/datasets/coffeeshops.py deleted file mode 100644 index 80f2d8d..0000000 --- a/bonobo/examples/datasets/coffeeshops.py +++ /dev/null @@ -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__)) diff --git a/bonobo/examples/datasets/coffeeshops.txt b/bonobo/examples/datasets/coffeeshops.txt index 9e3c181..8382078 100644 --- a/bonobo/examples/datasets/coffeeshops.txt +++ b/bonobo/examples/datasets/coffeeshops.txt @@ -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 d’Acclimatation, 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 \ No newline at end of file +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 d’Acclimatation",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 diff --git a/bonobo/examples/files/_services.py b/bonobo/examples/files/_services.py index 337bf6b..825e39d 100644 --- a/bonobo/examples/files/_services.py +++ b/bonobo/examples/files/_services.py @@ -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(), + } diff --git a/bonobo/examples/files/csv_handlers.py b/bonobo/examples/files/csv_handlers.py index 555bc67..acc6189 100644 --- a/bonobo/examples/files/csv_handlers.py +++ b/bonobo/examples/files/csv_handlers.py @@ -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() + ) diff --git a/bonobo/examples/files/json_handlers.py b/bonobo/examples/files/json_handlers.py index f1818cd..819a8fd 100644 --- a/bonobo/examples/files/json_handlers.py +++ b/bonobo/examples/files/json_handlers.py @@ -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() + ) diff --git a/bonobo/examples/files/pickle_handlers.py b/bonobo/examples/files/pickle_handlers.py index ed2ecd4..a04c4ea 100644 --- a/bonobo/examples/files/pickle_handlers.py +++ b/bonobo/examples/files/pickle_handlers.py @@ -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() + ) diff --git a/bonobo/examples/files/text_handlers.py b/bonobo/examples/files/text_handlers.py index abbae1a..2e91227 100644 --- a/bonobo/examples/files/text_handlers.py +++ b/bonobo/examples/files/text_handlers.py @@ -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() + ) diff --git a/bonobo/examples/nodes/_services.py b/bonobo/examples/nodes/_services.py deleted file mode 100644 index 337bf6b..0000000 --- a/bonobo/examples/nodes/_services.py +++ /dev/null @@ -1,5 +0,0 @@ -from bonobo import get_examples_path, open_fs - - -def get_services(): - return {'fs': open_fs(get_examples_path())} diff --git a/bonobo/examples/nodes/bags.py b/bonobo/examples/nodes/bags.py deleted file mode 100644 index 2bfe5de..0000000 --- a/bonobo/examples/nodes/bags.py +++ /dev/null @@ -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) diff --git a/bonobo/examples/nodes/count.py b/bonobo/examples/nodes/count.py deleted file mode 100644 index ea440a0..0000000 --- a/bonobo/examples/nodes/count.py +++ /dev/null @@ -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) diff --git a/bonobo/examples/nodes/dicts.py b/bonobo/examples/nodes/dicts.py deleted file mode 100644 index fde4b08..0000000 --- a/bonobo/examples/nodes/dicts.py +++ /dev/null @@ -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) diff --git a/bonobo/examples/nodes/filter.py b/bonobo/examples/nodes/filter.py deleted file mode 100644 index 4f7219a..0000000 --- a/bonobo/examples/nodes/filter.py +++ /dev/null @@ -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) diff --git a/bonobo/examples/nodes/slow.py b/bonobo/examples/nodes/slow.py deleted file mode 100644 index ecaaf44..0000000 --- a/bonobo/examples/nodes/slow.py +++ /dev/null @@ -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) diff --git a/bonobo/examples/nodes/strings.py b/bonobo/examples/nodes/strings.py deleted file mode 100644 index 1903151..0000000 --- a/bonobo/examples/nodes/strings.py +++ /dev/null @@ -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) diff --git a/bonobo/examples/tutorials/tut02e03_writeasmap.py b/bonobo/examples/tutorials/tut02e03_writeasmap.py index c7c7711..afc251e 100644 --- a/bonobo/examples/tutorials/tut02e03_writeasmap.py +++ b/bonobo/examples/tutorials/tut02e03_writeasmap.py @@ -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] ) diff --git a/bonobo/examples/types/__init__.py b/bonobo/examples/types/__init__.py index a2c0ceb..e69de29 100644 --- a/bonobo/examples/types/__init__.py +++ b/bonobo/examples/types/__init__.py @@ -1,7 +0,0 @@ -from . import bags, dicts, strings - -__all__ = [ - 'bags', - 'dicts', - 'strings', -] \ No newline at end of file diff --git a/bonobo/examples/types/bags.py b/bonobo/examples/types/bags.py deleted file mode 100644 index 2bfe5de..0000000 --- a/bonobo/examples/types/bags.py +++ /dev/null @@ -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) diff --git a/bonobo/examples/types/dicts.py b/bonobo/examples/types/dicts.py deleted file mode 100644 index fde4b08..0000000 --- a/bonobo/examples/types/dicts.py +++ /dev/null @@ -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) diff --git a/bonobo/examples/types/strings.py b/bonobo/examples/types/strings.py index 2fa765f..6675a77 100644 --- a/bonobo/examples/types/strings.py +++ b/bonobo/examples/types/strings.py @@ -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) diff --git a/bonobo/execution/contexts/base.py b/bonobo/execution/contexts/base.py index 847633b..953f13c 100644 --- a/bonobo/execution/contexts/base.py +++ b/bonobo/execution/contexts/base.py @@ -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 diff --git a/bonobo/execution/contexts/graph.py b/bonobo/execution/contexts/graph.py index 55dbf7e..a6559a3 100644 --- a/bonobo/execution/contexts/graph.py +++ b/bonobo/execution/contexts/graph.py @@ -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): diff --git a/bonobo/execution/contexts/node.py b/bonobo/execution/contexts/node.py index 3cb3521..194cf36 100644 --- a/bonobo/execution/contexts/node.py +++ b/bonobo/execution/contexts/node.py @@ -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) diff --git a/bonobo/execution/strategies/executor.py b/bonobo/execution/strategies/executor.py index d7a0017..fc72226 100644 --- a/bonobo/execution/strategies/executor.py +++ b/bonobo/execution/strategies/executor.py @@ -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 = [] diff --git a/bonobo/execution/strategies/naive.py b/bonobo/execution/strategies/naive.py index bd581ff..01b0416 100644 --- a/bonobo/execution/strategies/naive.py +++ b/bonobo/execution/strategies/naive.py @@ -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() diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 3a53d2d..9710ef7 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -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 diff --git a/bonobo/nodes/filter.py b/bonobo/nodes/filter.py index 2ec0130..0e0026f 100644 --- a/bonobo/nodes/filter.py +++ b/bonobo/nodes/filter.py @@ -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 diff --git a/bonobo/nodes/io/base.py b/bonobo/nodes/io/base.py index af9e609..325d081 100644 --- a/bonobo/nodes/io/base.py +++ b/bonobo/nodes/io/base.py @@ -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 diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index 188fd80..ed0a738 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -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 diff --git a/bonobo/nodes/io/file.py b/bonobo/nodes/io/file.py index e49d6de..a8b2c2e 100644 --- a/bonobo/nodes/io/file.py +++ b/bonobo/nodes/io/file.py @@ -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 diff --git a/bonobo/nodes/io/json.py b/bonobo/nodes/io/json.py index 6c2b4b2..86d3262 100644 --- a/bonobo/nodes/io/json.py +++ b/bonobo/nodes/io/json.py @@ -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 + """ diff --git a/bonobo/nodes/io/pickle.py b/bonobo/nodes/io/pickle.py index bc02ce8..da96a6d 100644 --- a/bonobo/nodes/io/pickle.py +++ b/bonobo/nodes/io/pickle.py @@ -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 diff --git a/bonobo/nodes/io/xml.py b/bonobo/nodes/io/xml.py deleted file mode 100644 index e69de29..0000000 diff --git a/bonobo/nodes/throttle.py b/bonobo/nodes/throttle.py index 58f5c09..04e5cf3 100644 --- a/bonobo/nodes/throttle.py +++ b/bonobo/nodes/throttle.py @@ -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) diff --git a/bonobo/plugins/console.py b/bonobo/plugins/console.py index 584244c..69a044c 100644 --- a/bonobo/plugins/console.py +++ b/bonobo/plugins/console.py @@ -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) diff --git a/bonobo/plugins/sentry.py b/bonobo/plugins/sentry.py new file mode 100644 index 0000000..44799da --- /dev/null +++ b/bonobo/plugins/sentry.py @@ -0,0 +1,6 @@ +from bonobo.plugins import Plugin +from raven import Client + + +class SentryPlugin(Plugin): + pass diff --git a/bonobo/structs/__init__.py b/bonobo/structs/__init__.py index 6c0d9ab..ba640c9 100644 --- a/bonobo/structs/__init__.py +++ b/bonobo/structs/__init__.py @@ -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', ] diff --git a/bonobo/structs/bags.py b/bonobo/structs/bags.py deleted file mode 100644 index 0738790..0000000 --- a/bonobo/structs/bags.py +++ /dev/null @@ -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 diff --git a/bonobo/structs/graphs.py b/bonobo/structs/graphs.py index f11cd31..39de1fe 100644 --- a/bonobo/structs/graphs.py +++ b/bonobo/structs/graphs.py @@ -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 '
{}
{}
'.format(self.graphviz._repr_svg_(), html.escape(repr(self))) + try: + return '
{}
{}
'.format(self.graphviz._repr_svg_(), html.escape(repr(self))) + except (ExecutableNotFound, FileNotFoundError) as exc: + return '{}: {}'.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. diff --git a/bonobo/structs/tokens.py b/bonobo/structs/tokens.py deleted file mode 100644 index 325a3b8..0000000 --- a/bonobo/structs/tokens.py +++ /dev/null @@ -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__) diff --git a/bonobo/util/__init__.py b/bonobo/util/__init__.py index 586fe3b..d07b0b4 100644 --- a/bonobo/util/__init__.py +++ b/bonobo/util/__init__.py @@ -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', diff --git a/bonobo/util/bags.py b/bonobo/util/bags.py new file mode 100644 index 0000000..fd31d06 --- /dev/null +++ b/bonobo/util/bags.py @@ -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 diff --git a/bonobo/util/collections.py b/bonobo/util/collections.py index d5a4624..745c3b9 100644 --- a/bonobo/util/collections.py +++ b/bonobo/util/collections.py @@ -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): diff --git a/bonobo/util/inspect.py b/bonobo/util/inspect.py index a3c71d7..f1463a4 100644 --- a/bonobo/util/inspect.py +++ b/bonobo/util/inspect.py @@ -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) diff --git a/bonobo/util/testing.py b/bonobo/util/testing.py index 66af870..8000e89 100644 --- a/bonobo/util/testing.py +++ b/bonobo/util/testing.py @@ -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())) diff --git a/docs/guide/services.rst b/docs/guide/services.rst index 8c4ec69..4c1ff30 100644 --- a/docs/guide/services.rst +++ b/docs/guide/services.rst @@ -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']) diff --git a/docs/guide/transformations.rst b/docs/guide/transformations.rst index 5b6b954..0005da2 100644 --- a/docs/guide/transformations.rst +++ b/docs/guide/transformations.rst @@ -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 diff --git a/docs/tutorial/4-services.rst b/docs/tutorial/4-services.rst index e39f15b..f2ada53 100644 --- a/docs/tutorial/4-services.rst +++ b/docs/tutorial/4-services.rst @@ -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(): diff --git a/docs/tutorial/tut03.rst b/docs/tutorial/tut03.rst index 4875bd8..16fa764 100644 --- a/docs/tutorial/tut03.rst +++ b/docs/tutorial/tut03.rst @@ -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(): diff --git a/requirements-dev.txt b/requirements-dev.txt index 0d5467e..cfd8c44 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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 diff --git a/requirements-docker.txt b/requirements-docker.txt index 0e84452..880bb57 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -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 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index ed9c8d1..c4a9157 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -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 diff --git a/requirements-sqlalchemy.txt b/requirements-sqlalchemy.txt index 07323d9..7d97881 100644 --- a/requirements-sqlalchemy.txt +++ b/requirements-sqlalchemy.txt @@ -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 diff --git a/requirements.txt b/requirements.txt index a81bda4..f0ca1e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.py b/setup.py index a04440d..c29e9ad 100644 --- a/setup.py +++ b/setup.py @@ -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': [ diff --git a/tests/config/test_configurables.py b/tests/config/test_configurables.py index bee501e..29ff2b8 100644 --- a/tests/config/test_configurables.py +++ b/tests/config/test_configurables.py @@ -1,3 +1,5 @@ +import pprint + import pytest from bonobo.config.configurables import Configurable diff --git a/tests/config/test_methods.py b/tests/config/test_methods.py index 0a3b423..b5c3a45 100644 --- a/tests/config/test_methods.py +++ b/tests/config/test_methods.py @@ -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) diff --git a/tests/config/test_methods_partial.py b/tests/config/test_methods_partial.py index e2a1a65..e45f11d 100644 --- a/tests/config/test_methods_partial.py +++ b/tests/config/test_methods_partial.py @@ -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) diff --git a/tests/config/test_processors.py b/tests/config/test_processors.py index 3377925..c6b3c9a 100644 --- a/tests/config/test_processors.py +++ b/tests/config/test_processors.py @@ -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'] diff --git a/tests/config/test_services.py b/tests/config/test_services.py index 469b0c1..078832c 100644 --- a/tests/config/test_services.py +++ b/tests/config/test_services.py @@ -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, {}]) diff --git a/tests/execution/contexts/test_node.py b/tests/execution/contexts/test_node.py index ef29c6e..0ebae6d 100644 --- a/tests/execution/contexts/test_node.py +++ b/tests/execution/contexts/test_node.py @@ -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 diff --git a/tests/ext/test_ods.py b/tests/ext/test_ods.py index 222a5a3..a07ae81 100644 --- a/tests/ext/test_ods.py +++ b/tests/ext/test_ods.py @@ -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 diff --git a/tests/features/test_not_modified.py b/tests/features/test_not_modified.py index 5b1b673..20f6f96 100644 --- a/tests/features/test_not_modified.py +++ b/tests/features/test_not_modified.py @@ -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: diff --git a/tests/nodes/io/test_csv.py b/tests/nodes/io/test_csv.py index 0d713bd..491c6b9 100644 --- a/tests/nodes/io/test_csv.py +++ b/tests/nodes/io/test_csv.py @@ -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() == () diff --git a/tests/nodes/io/test_file.py b/tests/nodes/io/test_file.py index 5fc2823..ee8548b 100644 --- a/tests/nodes/io/test_file.py +++ b/tests/nodes/io/test_file.py @@ -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', ) diff --git a/tests/nodes/io/test_json.py b/tests/nodes/io/test_json.py index b5b0781..c2caedd 100644 --- a/tests/nodes/io/test_json.py +++ b/tests/nodes/io/test_json.py @@ -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() == () diff --git a/tests/nodes/io/test_pickle.py b/tests/nodes/io/test_pickle.py index 8416a9f..0662848 100644 --- a/tests/nodes/io/test_pickle.py +++ b/tests/nodes/io/test_pickle.py @@ -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'), + ] diff --git a/tests/nodes/test_basics.py b/tests/nodes/test_basics.py index de72069..4b6a8be 100644 --- a/tests/nodes/test_basics.py +++ b/tests/nodes/test_basics.py @@ -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'])) diff --git a/tests/structs/test_bags.py b/tests/structs/test_bags.py deleted file mode 100644 index 6a13bbf..0000000 --- a/tests/structs/test_bags.py +++ /dev/null @@ -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'] diff --git a/tests/structs/test_tokens.py b/tests/structs/test_tokens.py index 1ca2166..e66b796 100644 --- a/tests/structs/test_tokens.py +++ b/tests/structs/test_tokens.py @@ -1,4 +1,4 @@ -from bonobo.structs import Token +from bonobo.constants import Token def test_token_repr(): diff --git a/tests/test_execution.py b/tests/test_execution.py index 84f40c5..92cd30c 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -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 diff --git a/tests/util/test_bags.py b/tests/util/test_bags.py new file mode 100644 index 0000000..469fbb6 --- /dev/null +++ b/tests/util/test_bags.py @@ -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' + ) From 686d1c81b3ffc6f01a0b96bd680be595c5afc233 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Tue, 28 Nov 2017 21:57:08 +0100 Subject: [PATCH 118/145] Simple "examples" command that just show examples for now. --- Makefile | 2 +- Projectfile | 5 +- bonobo/commands/examples.py | 24 ++ bonobo/examples/__init__.py | 4 +- bonobo/examples/__main__.py | 5 + bonobo/examples/datasets/__main__.py | 9 +- bonobo/examples/datasets/coffeeshops.csv | 184 +++++++++- bonobo/examples/datasets/coffeeshops.json | 362 ++++++++++---------- bonobo/examples/datasets/coffeeshops.ldjson | 362 ++++++++++---------- requirements-jupyter.txt | 6 +- setup.py | 5 +- 11 files changed, 589 insertions(+), 379 deletions(-) create mode 100644 bonobo/commands/examples.py create mode 100644 bonobo/examples/__main__.py diff --git a/Makefile b/Makefile index d4c3556..50338ee 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4.2 on 2017-11-26. +# Generated by Medikit 0.4.2 on 2017-11-27. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index 9b879c9..5d36b74 100644 --- a/Projectfile +++ b/Projectfile @@ -7,6 +7,8 @@ python = require('python') sphinx = require('sphinx') yapf = require('yapf') +# python.set_versions('3.5', '3.6', '3.7') --> not yet implemented + python.setup( name='bonobo', description='Bonobo, a simple, modern and atomic extract-transform-load toolkit for python 3.5+.', @@ -30,11 +32,12 @@ python.setup( ], 'bonobo.commands': [ 'convert = bonobo.commands.convert:ConvertCommand', + 'download = bonobo.commands.download:DownloadCommand', + 'examples = bonobo.commands.examples:ExamplesCommand', 'init = bonobo.commands.init:InitCommand', 'inspect = bonobo.commands.inspect:InspectCommand', 'run = bonobo.commands.run:RunCommand', 'version = bonobo.commands.version:VersionCommand', - 'download = bonobo.commands.download:DownloadCommand', ], } ) diff --git a/bonobo/commands/examples.py b/bonobo/commands/examples.py new file mode 100644 index 0000000..e6a2764 --- /dev/null +++ b/bonobo/commands/examples.py @@ -0,0 +1,24 @@ +from bonobo.commands import BaseCommand + +all_examples = ( + 'clock', + 'datasets', + 'environ', + 'files.csv_handlers', + 'files.json_handlers', + 'files.pickle_handlers', + 'files.text_handlers', + 'types', +) + + +class ExamplesCommand(BaseCommand): + def handle(self): + print('You can run the following examples:') + print() + for example in all_examples: + print(' $ python -m bonobo.examples.{}'.format(example)) + print() + + def add_arguments(self, parser): + pass diff --git a/bonobo/examples/__init__.py b/bonobo/examples/__init__.py index ec68fc5..e1815f8 100644 --- a/bonobo/examples/__init__.py +++ b/bonobo/examples/__init__.py @@ -27,6 +27,6 @@ def get_graph_options(options): _print = options.pop('print', False) return { - '_limit': (bonobo.Limit(_limit), ) if _limit else (), - '_print': (bonobo.PrettyPrinter(), ) if _print else (), + '_limit': (bonobo.Limit(_limit),) if _limit else (), + '_print': (bonobo.PrettyPrinter(),) if _print else (), } diff --git a/bonobo/examples/__main__.py b/bonobo/examples/__main__.py new file mode 100644 index 0000000..92cc165 --- /dev/null +++ b/bonobo/examples/__main__.py @@ -0,0 +1,5 @@ +if __name__ == '__main__': + from bonobo.commands import entrypoint + import sys + + entrypoint(['examples'] + sys.argv[1:]) diff --git a/bonobo/examples/datasets/__main__.py b/bonobo/examples/datasets/__main__.py index 37444ac..768ac5c 100644 --- a/bonobo/examples/datasets/__main__.py +++ b/bonobo/examples/datasets/__main__.py @@ -66,16 +66,11 @@ def get_services(): 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='+') + parser.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_names = list(options['target'] if options['target'] else sorted(graphs.keys())) graph = bonobo.Graph() for name in graph_names: diff --git a/bonobo/examples/datasets/coffeeshops.csv b/bonobo/examples/datasets/coffeeshops.csv index c4c10e3..b17f71a 100644 --- a/bonobo/examples/datasets/coffeeshops.csv +++ b/bonobo/examples/datasets/coffeeshops.csv @@ -1 +1,183 @@ -"['name', 'address', 'zipcode', 'city']" +prix_terasse,geoloc,address,name,date,zipcode,geometry,recordid,prix_salle,prix_comptoir,city,country +-,"[48.839512, 2.303007]",344Vrue Vaugirard,Coffee Chope,2014-02-01,75015,"{'coordinates': [2.303007, 48.839512], 'type': 'Point'}",3c276428d45ad68ccdf6875e4ddcfe95d0c0d4cf,-,1,Paris,France +-,"[48.876737, 2.357601]","5, rue d'Alsace",Extérieur Quai,2014-02-01,75010,"{'coordinates': [2.357601, 48.876737], 'type': 'Point'}",97ad81cd1127a8566085ad796eeb44a06bec7514,-,1,Paris,France +-,"[48.850852, 2.362029]",6 Bd henri IV,Le Sully,2014-02-01,75004,"{'coordinates': [2.362029, 48.850852], 'type': 'Point'}",aa4294c1b8d660a23db0dc81321e509bae1dae68,-,1,Paris,France +1,"[48.893517, 2.340271]",53 rue du ruisseau,O q de poule,2013-08-22,75018,"{'coordinates': [2.340271, 48.893517], 'type': 'Point'}",a81362dbed35247555fb105bd83ff2906904a66e,-,1,Paris,France +-,"[48.864655, 2.350089]",1 Passage du Grand Cerf,Le Pas Sage,2013-08-22,75002,"{'coordinates': [2.350089, 48.864655], 'type': 'Point'}",7ced86acbd5ccfba229bcc07d70d0d117aee16a5,-,1,Paris,France +-,"[48.895825, 2.339712]",112 Rue Championnet,La Renaissance,2013-08-22,75018,"{'coordinates': [2.339712, 48.895825], 'type': 'Point'}",5582c8572bd7637bf305b74c1c0bdb74a8e4247f,-,1,Paris,France +-,"[48.868581, 2.373015]",Rue de la Fontaine au Roi,La Caravane,2012-05-11,75011,"{'coordinates': [2.373015, 48.868581], 'type': 'Point'}",50bb0fa06e562a242f115ddbdae2ed9c7df93d57,-,0,Paris,France +-,"[48.875155, 2.335536]",51 Rue Victoire,Le chantereine,2012-10-22,75009,"{'coordinates': [2.335536, 48.875155], 'type': 'Point'}",eb8a62feeedaf7ed8b8c912305270ee857068689,1,1,Paris,France +-,"[48.886536, 2.346525]",11 rue Feutrier,Le Müller,2012-10-22,75018,"{'coordinates': [2.346525, 48.886536], 'type': 'Point'}",62c552f167f671f88569c1f2d6a44098fb514c51,1,1,Paris,France +-,"[48.841494, 2.307117]",21 rue Copreaux,Le drapeau de la fidelité,2012-10-22,75015,"{'coordinates': [2.307117, 48.841494], 'type': 'Point'}",5120ea0b9d7387766072b90655166486928e25c8,1,1,Paris,France +-,"[48.839743, 2.296898]",125 rue Blomet,Le café des amis,2012-10-22,75015,"{'coordinates': [2.296898, 48.839743], 'type': 'Point'}",865f62415adc5c34e3ca38a1748b7a324dfba209,1,1,Paris,France +-,"[48.857728, 2.349641]",10 rue Saint Martin,Le Café Livres,2012-10-09,75004,"{'coordinates': [2.349641, 48.857728], 'type': 'Point'}",7ef54a78802d49cafd2701458df2b0d0530d123b,-,1,Paris,France +-,"[48.856003, 2.30457]",46 avenue Bosquet,Le Bosquet,2012-10-09,75007,"{'coordinates': [2.30457, 48.856003], 'type': 'Point'}",d701a759e08a71f4bbb01f29473274b0152135d0,-,1,Paris,France +-,"[48.889426, 2.332954]",12 rue Armand Carrel,Le Chaumontois,2012-03-07,75018,"{'coordinates': [2.332954, 48.889426], 'type': 'Point'}",e12ff00a644c91ad910ddc63a770c190be93a393,-,1,Paris,France +-,"[48.838521, 2.370478]",34 avenue Pierre Mendès-France,Le Kleemend's,2012-03-07,75013,"{'coordinates': [2.370478, 48.838521], 'type': 'Point'}",0f6cd1ee7751b00c9574efcfdcf66fa0e857d251,-,1,Paris,France +-,"[48.849861, 2.385342]",202 rue du faubourg st antoine,Café Pierre,2012-03-07,75012,"{'coordinates': [2.385342, 48.849861], 'type': 'Point'}",f9de9d0fb5e92f047a6f1986a31f9dd4d38bcb36,-,1,Paris,France +-,"[48.872202, 2.304624]",61 rue de Ponthieu,Les Arcades,2012-03-07,75008,"{'coordinates': [2.304624, 48.872202], 'type': 'Point'}",67eaf58afc856077c0680601e453e75c0922c9c0,-,1,Paris,France +-,"[48.859031, 2.320315]",31 rue Saint-Dominique,Le Square,2012-03-07,75007,"{'coordinates': [2.320315, 48.859031], 'type': 'Point'}",678558317bc9ad46652e5b1643e70c2142a76e7e,-,1,Paris,France +-,"[48.850092, 2.37463]","75, avenue Ledru-Rollin",Assaporare Dix sur Dix,2012-06-27,75012,"{'coordinates': [2.37463, 48.850092], 'type': 'Point'}",667474321887d08a3cc636adf043ad354b65fa61,-,1,Paris,France +-,"[48.86805, 2.353313]",129 boulevard sebastopol,Au cerceau d'or,2012-06-27,75002,"{'coordinates': [2.353313, 48.86805], 'type': 'Point'}",c9ef52ba2fabe0286700329f18bbbbea9a10b474,-,1,Paris,France +-,"[48.845927, 2.373051]",21 ter boulevard Diderot,Aux cadrans,2012-06-27,75012,"{'coordinates': [2.373051, 48.845927], 'type': 'Point'}",ed5f98686856bf4ddd2b381b43ad229246741a90,-,1,Paris,France +-,"[48.851662, 2.273883]",17 rue Jean de la Fontaine,Café antoine,2012-06-27,75016,"{'coordinates': [2.273883, 48.851662], 'type': 'Point'}",ab6d1e054e2e6ae7d6150013173f55e83c05ca23,-,1,Paris,France +-,"[48.877642, 2.312823]",rue de Lisbonne,Café de la Mairie (du VIII),2012-06-27,75008,"{'coordinates': [2.312823, 48.877642], 'type': 'Point'}",7de8a79b026ac63f453556612505b5bcd9229036,-,1,Paris,France +-,"[48.838633, 2.349916]",5 rue Claude Bernard,Café Lea,2012-06-27,75005,"{'coordinates': [2.349916, 48.838633], 'type': 'Point'}",fecd8900cf83027f74ceced9fc4ad80ac73b63a7,-,1,Paris,France +-,"[48.849293, 2.354486]",11 boulevard Saint-Germain,Cardinal Saint-Germain,2012-06-27,75005,"{'coordinates': [2.354486, 48.849293], 'type': 'Point'}",e4a078c30c98082896787f4e4b41a07554392529,-,1,Paris,France +-,"[48.869771, 2.342501]",52 rue Notre-Dame des Victoires,Dédé la frite,2012-06-27,75002,"{'coordinates': [2.342501, 48.869771], 'type': 'Point'}",ccb2ba2f98043e8eefd5fda829dee1ea7f1d2c7a,-,1,Paris,France +-,"[48.834051, 2.287345]",36 rue du hameau,La Bauloise,2012-06-27,75015,"{'coordinates': [2.287345, 48.834051], 'type': 'Point'}",c9fe10abd15ede7ccaeb55c309898d30d7b19d0e,-,1,Paris,France +-,"[48.888165, 2.377387]",71 quai de Seine,Le Bellerive,2012-06-27,75019,"{'coordinates': [2.377387, 48.888165], 'type': 'Point'}",4e0b5c2d33d7c25fc54c51171f3d37e509959fc0,-,1,Paris,France +-,"[48.864543, 2.340997]",42 rue coquillère,Le bistrot de Maëlle et Augustin,2012-06-27,75001,"{'coordinates': [2.340997, 48.864543], 'type': 'Point'}",52acab12469af291984e9a70962e08c72b058e10,-,1,Paris,France +-,"[48.872103, 2.346161]",14 rue Rougemont,Le Dellac,2012-06-27,75009,"{'coordinates': [2.346161, 48.872103], 'type': 'Point'}",4d1d627ecea2ffa279bb862f8ba495d95ca75350,-,1,Paris,France +-,"[48.859645, 2.355598]",1 rue Pecquay,Le Felteu,2012-06-27,75004,"{'coordinates': [2.355598, 48.859645], 'type': 'Point'}",2c1fa55460af282266d86fd003af4f929fdf4e7d,-,1,Paris,France +-,"[48.85763, 2.346101]",2 bis quai de la mégisserie,Le Reynou,2012-06-27,75001,"{'coordinates': [2.346101, 48.85763], 'type': 'Point'}",d4ddd30ab3e721a317fc7ea89d5b9001255ce9f4,-,1,Paris,France +-,"[48.884646, 2.337734]",23 rue des abbesses,Le Saint Jean,2012-06-27,75018,"{'coordinates': [2.337734, 48.884646], 'type': 'Point'}",51b47cf167b7f32eeebb108330956694d75d4268,-,1,Paris,France +-,"[48.841007, 2.31466]",65 boulevard Pasteur,les montparnos,2012-06-27,75015,"{'coordinates': [2.31466, 48.841007], 'type': 'Point'}",2aaca891ffd0694c657a43889516ab72afdfba07,-,1,Paris,France +-,"[48.850323, 2.33039]",16 rue DE MEZIERES,L'antre d'eux,2014-02-01,75006,"{'coordinates': [2.33039, 48.850323], 'type': 'Point'}",4ff4337934c66f61e00d1d9551f7cdddba03e544,-,1,Paris,France +-,"[48.864957, 2.346938]",58 rue de Montorgueil,Drole d'endroit pour une rencontre,2014-02-01,75002,"{'coordinates': [2.346938, 48.864957], 'type': 'Point'}",3451657f880abe75d0c7e386fc698405556c53e8,-,1,Paris,France +-,"[48.889565, 2.339735]",104 rue caulaincourt,Le pari's café,2014-02-01,75018,"{'coordinates': [2.339735, 48.889565], 'type': 'Point'}",e8c34a537b673fcb26c76e02deca4f5a728929dc,-,1,Paris,France +-,"[48.859115, 2.368871]",60 rue saint-sabin,Le Poulailler,2014-02-01,75011,"{'coordinates': [2.368871, 48.859115], 'type': 'Point'}",325ea74ba83f716dde87c08cffd36f7df7722a49,-,1,Paris,France +-,"[48.833595, 2.38604]",33 Cour Saint Emilion,Chai 33,2014-02-01,75012,"{'coordinates': [2.38604, 48.833595], 'type': 'Point'}",528de8d5d8780bee83145637e315483d48f5ae3c,-,1,Paris,France +-,"[48.868741, 2.379969]",99 rue Jean-Pierre Timbaud,L'Assassin,2014-02-01,75011,"{'coordinates': [2.379969, 48.868741], 'type': 'Point'}",fac0483890ff8bdaeb3feddbdb032c5112f24678,-,1,Paris,France +-,"[48.851463, 2.398691]",1 rue d'Avron,l'Usine,2014-02-01,75020,"{'coordinates': [2.398691, 48.851463], 'type': 'Point'}",fee1e3eb103bbc98e19e45d34365da0f27166541,-,1,Paris,France +-,"[48.896305, 2.332898]",52 rue Liebniz,La Bricole,2014-02-01,75018,"{'coordinates': [2.332898, 48.896305], 'type': 'Point'}",4744e866c244c59eec43b3fe159542d2ef433065,-,1,Paris,France +-,"[48.850311, 2.34885]",place maubert,le ronsard,2014-02-01,75005,"{'coordinates': [2.34885, 48.850311], 'type': 'Point'}",49a390322b45246bc2c1e50fcd46815ad271bca0,-,1,Paris,France +-,"[48.863038, 2.3604]",82 rue des archives,Face Bar,2014-02-01,75003,"{'coordinates': [2.3604, 48.863038], 'type': 'Point'}",d96e16ebf2460bb2f6c34198918a071233725cbc,-,1,Paris,France +-,"[48.872746, 2.366392]",49 rue bichat,American Kitchen,2013-08-22,75010,"{'coordinates': [2.366392, 48.872746], 'type': 'Point'}",6b9395475cbbbbbacbaaeb070f71d31c2d183dc4,-,1,Paris,France +-,"[48.870598, 2.365413]",55 bis quai de valmy,La Marine,2013-08-22,75010,"{'coordinates': [2.365413, 48.870598], 'type': 'Point'}",d4d2f92d27f38de59e57744f434781e61283551c,-,1,Paris,France +-,"[48.889101, 2.318001]",21 avenue Brochant,Le Bloc,2013-08-22,75017,"{'coordinates': [2.318001, 48.889101], 'type': 'Point'}",e425882ee969d1e8bffe7234336ae40da88c8439,-,1,Paris,France +-,"[48.874697, 2.405421]",229 avenue Gambetta,La Recoleta au Manoir,2013-08-22,75020,"{'coordinates': [2.405421, 48.874697], 'type': 'Point'}",02de82cffb2918beafb740f4e924029d470b07a1,-,1,Paris,France +-,"[48.847344, 2.286078]",80 Rue Saint-Charles,Le Pareloup,2013-08-22,75015,"{'coordinates': [2.286078, 48.847344], 'type': 'Point'}",0227ca95f76bb6097ae0a0e6f455af2624d49ae3,-,0,Paris,France +1,"[48.840771, 2.324589]",3 rue de la Gaité,La Brasserie Gaité,2013-08-22,75014,"{'coordinates': [2.324589, 48.840771], 'type': 'Point'}",e7c4cba08749c892a73db2715d06623d9e0c2f67,-,,Paris,France +-,"[48.875232, 2.336036]",46 rue Victoire,Café Zen,2012-05-11,75009,"{'coordinates': [2.336036, 48.875232], 'type': 'Point'}",5e9a6172f8b64cd098a5f2cf9b1d42567fe0a894,-,1,Paris,France +-,"[48.872677, 2.315276]",27 rue de Penthièvre,O'Breizh,2012-10-22,75008,"{'coordinates': [2.315276, 48.872677], 'type': 'Point'}",ead3108add27ef41bb92517aca834f7d7f632816,1,1,Paris,France +-,"[48.868838, 2.33605]",23 rue saint augustin,Le Petit Choiseul,2012-10-22,75002,"{'coordinates': [2.33605, 48.868838], 'type': 'Point'}",de601277ca00567b62fa4f5277e4a17679faa753,1,1,Paris,France +-,"[48.841526, 2.351012]",7 rue Epée de Bois,Invitez vous chez nous,2012-10-22,75005,"{'coordinates': [2.351012, 48.841526], 'type': 'Point'}",1d2373bdec8a07306298e3ee54894ac295ee1d55,1,1,Paris,France +-,"[48.86525, 2.350507]",142 Rue Saint-Denis 75002 Paris,La Cordonnerie,2012-10-22,75002,"{'coordinates': [2.350507, 48.86525], 'type': 'Point'}",5c9bf60617a99ad75445b454f98e75e1a104021d,1,1,Paris,France +-,"[48.892244, 2.346973]","3, rue Baudelique",Le Supercoin,2012-10-22,75018,"{'coordinates': [2.346973, 48.892244], 'type': 'Point'}",68a4ee10f1fc4d2a659501e811d148420fa80e95,1,1,Paris,France +-,"[48.890043, 2.362241]",86 bis rue Riquet,Populettes,2012-10-22,75018,"{'coordinates': [2.362241, 48.890043], 'type': 'Point'}",8cc55d58d72621a7e91cf6b456731d2cb2863afc,1,1,Paris,France +1,"[48.893017, 2.337776]",49 rue des Cloys,Au bon coin,2012-10-18,75018,"{'coordinates': [2.337776, 48.893017], 'type': 'Point'}",0408f272c08c52e3cae035ffdeb8928698787ea9,1,1,Paris,France +-,"[48.836919, 2.347003]",69 rue Broca,Le Couvent,2012-10-18,75013,"{'coordinates': [2.347003, 48.836919], 'type': 'Point'}",729b2f228d3fd2db6f78bb624d451f59555e4a04,1,1,Paris,France +-,"[48.840624, 2.349766]",111 rue mouffetard,La Brûlerie des Ternes,2012-10-18,75005,"{'coordinates': [2.349766, 48.840624], 'type': 'Point'}",233dd2a17620cd5eae70fef11cc627748e3313d5,1,1,Paris,France +-,"[48.832825, 2.336116]",59 Boulevard Saint-Jacques,L'Écir,2012-10-09,75014,"{'coordinates': [2.336116, 48.832825], 'type': 'Point'}",4a44324a5a806801fd3e05af89cad7c0f1e69d1e,-,1,Paris,France +-,"[48.850696, 2.378417]","126, rue du Faubourg Saint Antoine",Le Chat bossu,2012-10-09,75012,"{'coordinates': [2.378417, 48.850696], 'type': 'Point'}",d1d02463f7c90d38cccffd898e15f51e65910baf,-,1,Paris,France +-,"[48.834157, 2.33381]",58 boulvevard Saint Jacques,Denfert café,2012-10-09,75014,"{'coordinates': [2.33381, 48.834157], 'type': 'Point'}",f78f406e5ccb95cb902ce618ed54eba1d4776a3c,-,1,Paris,France +-,"[48.867948, 2.343582]",95 rue Montmartre,Le Café frappé,2012-10-09,75002,"{'coordinates': [2.343582, 48.867948], 'type': 'Point'}",4dd2a924a7b2c5f061cecaba2f272548a8c83c6c,-,1,Paris,France +-,"[48.859772, 2.360558]",78 rue vieille du temple,La Perle,2012-10-09,75003,"{'coordinates': [2.360558, 48.859772], 'type': 'Point'}",476c54e643613ec36a9b1c533f32122fd873f3c3,-,1,Paris,France +-,"[48.845047, 2.349583]",1 rue Thouin,Le Descartes,2012-10-09,75005,"{'coordinates': [2.349583, 48.845047], 'type': 'Point'}",e9cb0b0d6b6b512e9ecab889267ba342a4f0ea93,-,1,Paris,France +-,"[48.830151, 2.334213]",55 rue de la tombe Issoire,Le petit club,2012-03-07,75014,"{'coordinates': [2.334213, 48.830151], 'type': 'Point'}",c6ca1166fa7fd7050f5d1c626a1e17050116c91e,-,1,Paris,France +-,"[48.865707, 2.374382]",90 avenue Parmentier,Le Plein soleil,2012-03-07,75011,"{'coordinates': [2.374382, 48.865707], 'type': 'Point'}",95cf5fb735bd19826db70a3af4fe72fce647d4e5,-,1,Paris,France +-,"[48.875322, 2.312329]","146, boulevard Haussmann",Le Relais Haussmann,2012-03-07,75008,"{'coordinates': [2.312329, 48.875322], 'type': 'Point'}",6c5f68f47c916638342b77bd5edfc30bd7051303,-,1,Paris,France +-,"[48.859559, 2.30643]",88 rue Saint-Dominique,Le Malar,2012-03-07,75007,"{'coordinates': [2.30643, 48.859559], 'type': 'Point'}",c633cb01686aa5f3171bf59976dc9b7f23cbca54,-,1,Paris,France +-,"[48.864628, 2.408038]",47 rue Belgrand,Au panini de la place,2012-03-07,75020,"{'coordinates': [2.408038, 48.864628], 'type': 'Point'}",66dedd35fcbbbb24f328882d49098de7aa5f26ba,-,1,Paris,France +-,"[48.88435, 2.297978]",182 rue de Courcelles,Le Village,2012-03-07,75017,"{'coordinates': [2.297978, 48.88435], 'type': 'Point'}",1e07eaf8a93875906d0b18eb6a897c651943589a,-,1,Paris,France +-,"[48.853381, 2.376706]",41 rue de Charonne,Pause Café,2012-03-07,75011,"{'coordinates': [2.376706, 48.853381], 'type': 'Point'}",60d98c3236a70824df50e9aca83e7d7f13a310c5,-,1,Paris,France +-,"[48.853253, 2.383415]",14 rue Jean Macé,Le Pure café,2012-03-07,75011,"{'coordinates': [2.383415, 48.853253], 'type': 'Point'}",66707fb2e707d2145fc2eb078a1b980a45921616,-,1,Paris,France +-,"[48.848873, 2.392859]",307 fg saint Antoine,Extra old café,2012-03-07,75011,"{'coordinates': [2.392859, 48.848873], 'type': 'Point'}",039ec7dcb219cfc434547b06938ba497afeb83b4,-,1,Paris,France +-,"[48.873227, 2.360787]",44 rue Vinaigriers,Chez Fafa,2012-03-07,75010,"{'coordinates': [2.360787, 48.873227], 'type': 'Point'}",1572d199f186bf86d7753fe71ac23477a7a8bd2c,-,1,Paris,France +-,"[48.850836, 2.384069]",3 rue Faidherbe,En attendant l'or,2012-06-27,75011,"{'coordinates': [2.384069, 48.850836], 'type': 'Point'}",de5789bb4a4ffbd244cded8dc555639dbe7d2279,-,1,Paris,France +-,"[48.866993, 2.336006]",30 rue des Petits-Champs,Brûlerie San José,2012-06-27,75002,"{'coordinates': [2.336006, 48.866993], 'type': 'Point'}",b736e5fa17396ee56a642212ccd0ab29c7f2bef1,-,1,Paris,France +-,"[48.856434, 2.342683]",2 place Martin Nadaud,Café Martin,2012-06-27,75001,"{'coordinates': [2.342683, 48.856434], 'type': 'Point'}",25fbf857029c54d57909c158e3039349b77344ed,-,1,Paris,France +-,"[48.863675, 2.348701]","14 rue Turbigo, Paris",Etienne,2012-06-27,75001,"{'coordinates': [2.348701, 48.863675], 'type': 'Point'}",0190edd7b0766c6d3e43093deb41abe9446c1b22,-,1,Paris,France +-,"[48.854584, 2.385193]",184 bd Voltaire,L'ingénu,2012-06-27,75011,"{'coordinates': [2.385193, 48.854584], 'type': 'Point'}",7fcef7475ec632d5c61c9c36c1d52a402ad2e9e8,-,1,Paris,France +-,"[48.890605, 2.361349]",8 rue L'Olive,L'Olive,2012-06-27,75018,"{'coordinates': [2.361349, 48.890605], 'type': 'Point'}",35286273f281c8c5082b3d3bd17f6bbf207426f9,-,1,Paris,France +-,"[48.871396, 2.338321]",18 rue Favart,Le Biz,2012-06-27,75002,"{'coordinates': [2.338321, 48.871396], 'type': 'Point'}",cf2f6b9e283aaeeca2214cc1fe57b45e45668e25,-,1,Paris,France +-,"[48.868109, 2.331785]",1 rue Louis le Grand,Le Cap Bourbon,2012-06-27,75002,"{'coordinates': [2.331785, 48.868109], 'type': 'Point'}",339030e95e0846b41a8e2b91045456c4e4a50043,-,1,Paris,France +-,"[48.84167, 2.303053]",9 Place du General Beuret,Le General Beuret,2012-06-27,75015,"{'coordinates': [2.303053, 48.84167], 'type': 'Point'}",12b37036d28d28b32ebe81756ad15eb68372947f,-,1,Paris,France +-,"[48.846814, 2.289311]",95 avenue Emile Zola,Le Germinal,2012-06-27,75015,"{'coordinates': [2.289311, 48.846814], 'type': 'Point'}",4e03831a64e886a28a7232e54f2812c1ced23c5a,-,1,Paris,France +-,"[48.862655, 2.337607]",202 rue Saint-Honoré,Le Ragueneau,2012-06-27,75001,"{'coordinates': [2.337607, 48.862655], 'type': 'Point'}",e303131b2600d4c2287749a36bf7193d2fa60bd7,-,1,Paris,France +-,"[48.889982, 2.338933]",72 rue lamarck,Le refuge,2012-06-27,75018,"{'coordinates': [2.338933, 48.889982], 'type': 'Point'}",f64310461736a769c6854fdefb99b9f2e7b230a9,-,1,Paris,France +-,"[48.870294, 2.352821]",13 rue du Faubourg Saint Denis,Le sully,2012-06-27,75010,"{'coordinates': [2.352821, 48.870294], 'type': 'Point'}",166be8588ff16e50838fa6a164d1e580497b795d,-,1,Paris,France +-,"[48.842128, 2.280374]",60 rue des bergers,Le bal du pirate,2014-02-01,75015,"{'coordinates': [2.280374, 48.842128], 'type': 'Point'}",93ff6e35406a074a6ba2667d2b286abf91132f6a,-,1,Paris,France +-,"[48.838769, 2.39609]",95 rue claude decaen,zic zinc,2014-02-01,75012,"{'coordinates': [2.39609, 48.838769], 'type': 'Point'}",8fdc739d64ff1f01973235301e3ec86791016759,-,1,Paris,France +-,"[48.870247, 2.376306]",35 rue de l'orillon,l'orillon bar,2014-02-01,75011,"{'coordinates': [2.376306, 48.870247], 'type': 'Point'}",6e6e9695ef04c3fdbd4cfa19f0cfb0c0f93f4933,-,1,Paris,France +-,"[48.869848, 2.394247]",116 Rue de Ménilmontant,Le Zazabar,2013-08-22,75020,"{'coordinates': [2.394247, 48.869848], 'type': 'Point'}",22edfe92a72ce1bb0eea972508b5caa7af2db2df,-,1,Paris,France +-,"[48.845458, 2.354796]",22 rue Linné,L'Inévitable,2013-08-22,75005,"{'coordinates': [2.354796, 48.845458], 'type': 'Point'}",6893cb08e99319091de9ba80305f22e0ce4cc08d,-,1,Paris,France +-,"[48.83336, 2.365782]",77 rue Dunois,Le Dunois,2013-08-22,75013,"{'coordinates': [2.365782, 48.83336], 'type': 'Point'}",c3be1246dbc4ca5734d5bbd569436ba655105248,-,1,Paris,France +-,"[48.862655, 2.337607]",202 rue Saint Honoré,Ragueneau,2012-05-11,75001,"{'coordinates': [2.337607, 48.862655], 'type': 'Point'}",e946d9e8f8c5a130f98eca945efadfd9eec40dcb,-,1,Paris,France +,"[48.826608, 2.374571]",48 rue du Dessous des Berges,Le Caminito,2012-10-22,75013,"{'coordinates': [2.374571, 48.826608], 'type': 'Point'}",5d9ad8bcfbbc20adaec2aa7dcdb71326868c7686,1,1,Paris,France +-,"[48.870598, 2.365413]",55bis quai de Valmy,Epicerie Musicale,2012-10-22,75010,"{'coordinates': [2.365413, 48.870598], 'type': 'Point'}",8ffea133d93c608d337bc0129f4f9f3d5cad8dae,1,1,Paris,France +-,,Le petit Bretonneau - à l'intérieur de l'Hôpital,Le petit Bretonneau,2012-10-22,75018,{},dd1ffdbd55c2dc651201302b8015258f7d35fd35,1,1,Paris,France +-,"[48.862575, 2.367427]",104 rue amelot,Le Centenaire,2012-10-22,75011,"{'coordinates': [2.367427, 48.862575], 'type': 'Point'}",e47d25752c2c8bcab5efb0e3a41920ec7a8a766a,1,1,Paris,France +-,"[48.842833, 2.348314]",13 Rue du Pot de Fer,La Montagne Sans Geneviève,2012-10-22,75005,"{'coordinates': [2.348314, 48.842833], 'type': 'Point'}",314d170601f81e3a3f26d8801f0fbee39981c788,1,,Paris,France +-,"[48.851325, 2.40171]",46 rue de Buzenval,Les Pères Populaires,2012-10-18,75020,"{'coordinates': [2.40171, 48.851325], 'type': 'Point'}",e75e8a3fe6212a0e576beec82f0128dd394e56fa,1,1,Paris,France +-,"[48.857658, 2.305613]",188 rue de Grenelle,Cafe de grenelle,2012-10-09,75007,"{'coordinates': [2.305613, 48.857658], 'type': 'Point'}",72e274046d671e68bc7754808feacfc95b91b6ed,-,1,Paris,France +-,"[48.875207, 2.332944]",73 rue de la Victoire,Le relais de la victoire,2012-10-09,75009,"{'coordinates': [2.332944, 48.875207], 'type': 'Point'}",01c21abdf35484f9ad184782979ecd078de84523,-,1,Paris,France +-,,"Route de la Muette à Neuilly +Club hippique du Jardin d’Acclimatation",La chaumière gourmande,2012-03-07,75016,{},438ec18d35793d12eb6a137373c3fe4f3aa38a69,1,,Paris,France +-,"[48.891882, 2.33365]","216, rue Marcadet",Le Brio,2012-03-07,75018,"{'coordinates': [2.33365, 48.891882], 'type': 'Point'}",49e3584ce3d6a6a236b4a0db688865bdd3483fec,-,1,Paris,France +-,"[48.884753, 2.324648]",22 rue des Dames,Caves populaires,2012-03-07,75017,"{'coordinates': [2.324648, 48.884753], 'type': 'Point'}",af81e5eca2b84ea706ac2d379edf65b0fb2f879a,-,1,Paris,France +-,"[48.827428, 2.325652]",12 avenue Jean Moulin,Caprice café,2012-03-07,75014,"{'coordinates': [2.325652, 48.827428], 'type': 'Point'}",88e07bdb723b49cd27c90403b5930bca1c93b458,-,1,Paris,France +-,"[48.832964, 2.369266]",7 rue Clisson,Tamm Bara,2012-03-07,75013,"{'coordinates': [2.369266, 48.832964], 'type': 'Point'}",baf59c98acafbe48ed9e91b64377da216a20cbcc,-,1,Paris,France +-,"[48.876577, 2.348414]",1 rue de Montholon,L'anjou,2012-03-07,75009,"{'coordinates': [2.348414, 48.876577], 'type': 'Point'}",e6f5949dca40548aad296208c61c498f639f648c,-,1,Paris,France +-,"[48.862599, 2.315086]",2 rue Robert Esnault Pelterie,Café dans l'aerogare Air France Invalides,2012-03-07,75007,"{'coordinates': [2.315086, 48.862599], 'type': 'Point'}",6d175eb48c577fafdfc99df0ab55da468cf17164,-,1,Paris,France +-,"[48.844854, 2.345413]","10 rue d""Ulm",Waikiki,2012-03-07,75005,"{'coordinates': [2.345413, 48.844854], 'type': 'Point'}",fb1e2bc2ae55d3d47da682c71093a12fa64fbd45,-,1,Paris,France +-,"[48.871576, 2.364499]",36 rue Beaurepaire,Chez Prune,2012-03-07,75010,"{'coordinates': [2.364499, 48.871576], 'type': 'Point'}",19399ede5b619761877822185bbb4c98b565974c,-,1,Paris,France +-,"[48.833863, 2.329046]",21 rue Boulard,Au Vin Des Rues,2012-06-27,75014,"{'coordinates': [2.329046, 48.833863], 'type': 'Point'}",87263873b6f8346b5777844be0122a307f29fcab,-,1,Paris,France +-,"[48.838137, 2.301166]",14 rue d'alleray,bistrot les timbrés,2012-06-27,75015,"{'coordinates': [2.301166, 48.838137], 'type': 'Point'}",74b6d7113e5b14eb8e890663f061208bf4ff6728,-,1,Paris,France +-,"[48.871799, 2.315985]",9 rue de Miromesnil,Café beauveau,2012-06-27,75008,"{'coordinates': [2.315985, 48.871799], 'type': 'Point'}",2759f5cdda4bcb88ca3ae2f7299b37b8e62596c8,-,1,Paris,France +-,"[48.866259, 2.338739]",9 rue des petits champs,Café Pistache,2012-06-27,75001,"{'coordinates': [2.338739, 48.866259], 'type': 'Point'}",6d2675cdc912118d0376229be8e436feca9c8af7,-,1,Paris,France +-,"[48.874605, 2.387738]",13 Rue Jean-Baptiste Dumay,La Cagnotte,2012-06-27,75020,"{'coordinates': [2.387738, 48.874605], 'type': 'Point'}",f7085c754c0c97e418d7e5213753f74bd396fc27,-,1,Paris,France +-,"[48.842462, 2.310919]",172 rue de vaugirard,le 1 cinq,2012-06-27,75015,"{'coordinates': [2.310919, 48.842462], 'type': 'Point'}",17e917723fc99d6e5bd77eb9633ac2e789a9a6d9,-,1,Paris,France +-,"[48.84591, 2.375543]",28 bis boulevard Diderot,Le Killy Jen,2012-06-27,75012,"{'coordinates': [2.375543, 48.84591], 'type': 'Point'}",93132bd8b3ae67dfcc8cf8c1166e312ac4acb9b9,-,1,Paris,France +-,"[48.842868, 2.303173]",106 rue Lecourbe,Les Artisans,2012-06-27,75015,"{'coordinates': [2.303173, 48.842868], 'type': 'Point'}",c37ef573b4cb2d1e61795d6a9ef11de433dc9a99,-,1,Paris,France +-,"[48.865684, 2.334416]",83 avenue de Wagram,Peperoni,2012-06-27,75001,"{'coordinates': [2.334416, 48.865684], 'type': 'Point'}",9461f859ca009ced25555fa6af1e6867dda9223e,-,1,Paris,France +-,"[48.833146, 2.288834]",380 rue de vaugirard,le lutece,2014-02-01,75015,"{'coordinates': [2.288834, 48.833146], 'type': 'Point'}",ddd13990c1408700085366bd4ba313acd69a44ea,-,1,Paris,France +-,"[48.886431, 2.327429]",16 rue Ganneron,Brasiloja,2014-02-01,75018,"{'coordinates': [2.327429, 48.886431], 'type': 'Point'}",d679bb1642534278f4c0203d67be0bafd5306d81,-,1,Paris,France +-,"[48.855711, 2.359491]",16 rue de Rivoli,Rivolux,2014-02-01,75004,"{'coordinates': [2.359491, 48.855711], 'type': 'Point'}",bdd2b008cc765c7fe195c037b830cd2628420a2f,-,1,Paris,France +-,"[48.845898, 2.372766]",21 Bis Boulevard Diderot,L'européen,2013-08-22,75012,"{'coordinates': [2.372766, 48.845898], 'type': 'Point'}",693c0da7d4db24781ed161c01a661c36074a94fa,-,1,Paris,France +-,"[48.867465, 2.357791]",39 rue Notre Dame de Nazareth,NoMa,2013-08-22,75003,"{'coordinates': [2.357791, 48.867465], 'type': 'Point'}",60d8b670810cc95eb0439dd0c238f8205ea8ef76,-,1,Paris,France +-,"[48.871595, 2.385858]",1 Rue des Envierges,O'Paris,2013-08-22,75020,"{'coordinates': [2.385858, 48.871595], 'type': 'Point'}",297c040284a05efe35c69bb621505e6acfdcdda4,-,1,Paris,France +-,"[48.872402, 2.366532]",16 avenue Richerand,Café Clochette,2013-08-22,75010,"{'coordinates': [2.366532, 48.872402], 'type': 'Point'}",a561a941f538e8a1d321bf8d98576d06be037962,-,1,Paris,France +-,"[48.856584, 2.368574]",40 Boulevard Beaumarchais,La cantoche de Paname,2013-08-22,75011,"{'coordinates': [2.368574, 48.856584], 'type': 'Point'}",5ba2aaec9f1de9d01e65be95215cab13c693cdf3,-,0,Paris,France +-,"[48.856496, 2.394874]",148 Boulevard de Charonne,Le Saint René,2013-08-22,75020,"{'coordinates': [2.394874, 48.856496], 'type': 'Point'}",4d60b350d04d4b1bf4bfd4dd6cc59687dc792c74,-,1,Paris,France +-,"[48.850055, 2.383908]",196 rue du faubourg saint-antoine,La Liberté,2012-10-22,75012,"{'coordinates': [2.383908, 48.850055], 'type': 'Point'}",ee94e76326f8dcbe3500afec69f1a21eb1215ad0,1,1,Paris,France +-,"[48.866737, 2.33716]",16 rue des Petits Champs,Chez Rutabaga,2012-10-22,75002,"{'coordinates': [2.33716, 48.866737], 'type': 'Point'}",a420ea4608440b8dc8e0267fe8cc513daa950551,1,1,Paris,France +-,"[48.885367, 2.325325]",2 rue Lemercier,Le BB (Bouchon des Batignolles),2012-10-22,75017,"{'coordinates': [2.325325, 48.885367], 'type': 'Point'}",20986cbfe11018bd0aba8150a49db1c435f7642d,1,1,Paris,France +-,"[48.873175, 2.339193]",10 rue Rossini,La Brocante,2012-10-22,75009,"{'coordinates': [2.339193, 48.873175], 'type': 'Point'}",2e10601c35669394d43936a771b18408be0338ba,1,1,Paris,France +-,"[48.840771, 2.324589]",3 rue Gaîté,Le Plomb du cantal,2012-10-22,75014,"{'coordinates': [2.324589, 48.840771], 'type': 'Point'}",6fb510614e00b065bf16a5af8e2c0eaf561a5654,1,1,Paris,France +-,"[48.884753, 2.324648]",22 rue des Dames,Les caves populaires,2012-10-22,75017,"{'coordinates': [2.324648, 48.884753], 'type': 'Point'}",d650c509a0aa8ed7b9c9b88861263f31463bbd0e,1,1,Paris,France +-,"[48.869519, 2.39339]",108 rue de Ménilmontant,Chez Luna,2012-10-09,75020,"{'coordinates': [2.39339, 48.869519], 'type': 'Point'}",736f4d996f1f8b7c3a0ce2abfeebfcce2a4bab13,-,1,Paris,France +-,"[48.877903, 2.385365]",1 rue du Plateau,Le bar Fleuri,2012-03-07,75019,"{'coordinates': [2.385365, 48.877903], 'type': 'Point'}",be55720646093788ec161c6cadc5ad8059f4b90b,-,1,Paris,France +-,"[48.882939, 2.31809]",101 rue des dames,Trois pièces cuisine,2012-03-07,75017,"{'coordinates': [2.31809, 48.882939], 'type': 'Point'}",7bbbfb755020a2c25cce0067601994ce5ee4193f,-,1,Paris,France +-,"[48.849497, 2.298855]",61 avenue de la Motte Picquet,Le Zinc,2012-03-07,75015,"{'coordinates': [2.298855, 48.849497], 'type': 'Point'}",e7c35a94454518de6de5bbecbc015fc37f7aea14,-,1,Paris,France +-,"[48.880669, 2.349964]",136 rue du Faubourg poissonnière,La cantine de Zoé,2012-03-07,75010,"{'coordinates': [2.349964, 48.880669], 'type': 'Point'}",0edc473b3432a869b8ed66b6c4c989766b699947,-,1,Paris,France +-,"[48.844057, 2.328402]",6/8 rue Stanislas,Les Vendangeurs,2012-03-07,75006,"{'coordinates': [2.328402, 48.844057], 'type': 'Point'}",e9766ea36f6293bf670ed938bff02b975d012973,-,1,Paris,France +-,"[48.852053, 2.338779]",3 carrefour de l'Odéon,L'avant comptoir,2012-03-07,75006,"{'coordinates': [2.338779, 48.852053], 'type': 'Point'}",fe843d2f43dcaac9129f5b36dc367558dfd3b3e4,-,1,Paris,France +1,"[48.886504, 2.34498]",1 rue Paul albert,Botak cafe,2012-03-07,75018,"{'coordinates': [2.34498, 48.886504], 'type': 'Point'}",9e19c375e612f5fb803ec6a27881858619207812,1,1,Paris,France +-,"[48.872722, 2.354594]",67 rue du Château d'eau,le chateau d'eau,2012-03-07,75010,"{'coordinates': [2.354594, 48.872722], 'type': 'Point'}",05bb6a26ec5bfbba25da2d19a5f0e83d69800f38,-,1,Paris,France +-,"[48.85192, 2.373229]",58 rue du Fbg Saint-Antoine,Bistrot Saint-Antoine,2012-06-27,75012,"{'coordinates': [2.373229, 48.85192], 'type': 'Point'}",daa3908ddf69d378fec5b4548494727e1121adc4,-,1,Paris,France +-,"[48.854685, 2.368487]",11/13 boulevard Beaumarchais,Chez Oscar,2012-06-27,75004,"{'coordinates': [2.368487, 48.854685], 'type': 'Point'}",c73be0483480c59e6ab6bc3a906c8d9dd474887f,-,1,Paris,France +-,"[48.87226, 2.304441]",63 rue de Ponthieu,Le Fronton,2012-06-27,75008,"{'coordinates': [2.304441, 48.87226], 'type': 'Point'}",85a8200d3e3aed7724d3207ed8b1ee5ec50c1f90,-,1,Paris,France +-,"[48.851, 2.300378]",48 avenue de la Motte Picquet,Le Piquet,2012-06-27,75015,"{'coordinates': [2.300378, 48.851], 'type': 'Point'}",460e2adc95fd172f753b1b6ed296c2711639d49d,-,1,Paris,France +-,"[48.841089, 2.349565]",104 rue Mouffetard,Le Tournebride,2012-06-27,75005,"{'coordinates': [2.349565, 48.841089], 'type': 'Point'}",8a7a23ed68366f70ab939c877cbdce46f19d75c7,-,1,Paris,France +-,"[48.828704, 2.322074]",52 rue des plantes,maison du vin,2012-06-27,75014,"{'coordinates': [2.322074, 48.828704], 'type': 'Point'}",482507a8f0fe4960f94372b6fa12b16e7d4f2a93,-,1,Paris,France +-,"[48.842146, 2.375986]",157 rue Bercy 75012 Paris,L'entrepôt,2014-02-01,75012,"{'coordinates': [2.375986, 48.842146], 'type': 'Point'}",d8746118429eb118f38ecbee904636d9b33fa8ba,-,1,Paris,France +-,"[48.867092, 2.363288]",Place de la République,Le café Monde et Médias,2013-08-22,75003,"{'coordinates': [2.363288, 48.867092], 'type': 'Point'}",af04c90f25e25daf7f5cbbab1bc740bac26541d4,-,1,Paris,France +-,"[48.849821, 2.355337]",11 Quai de la Tournelle,Café rallye tournelles,2013-08-22,75005,"{'coordinates': [2.355337, 48.849821], 'type': 'Point'}",91d88e321a75b6a8c4dea816c399fda77c41f9d1,-,1,Paris,France +-,"[48.872498, 2.355136]",61 rue du château d'eau,Brasserie le Morvan,2013-08-22,75010,"{'coordinates': [2.355136, 48.872498], 'type': 'Point'}",6ec62058995948fc18d331f01a2d03acc0d9e0fa,-,1,Paris,France +1,"[48.874879, 2.386064]",6 rue Mélingue,Chez Miamophile,2013-08-22,75019,"{'coordinates': [2.386064, 48.874879], 'type': 'Point'}",13924872737e4fc640a43da58937c3777c2ac753,1,1,Paris,France +-,"[48.864269, 2.36858]",18 rue de Crussol,Panem,2012-05-11,75011,"{'coordinates': [2.36858, 48.864269], 'type': 'Point'}",67bdf3a6989f80749a1ba33a17b1370de0a0e1cd,-,0,Paris,France +-,"[48.885662, 2.319591]",47 rue de Batignolles,Petits Freres des Pauvres,2012-05-11,75017,"{'coordinates': [2.319591, 48.885662], 'type': 'Point'}",e27fd00149514bbfad7dd7e8f9b0c677df2d3f25,-,0,Paris,France +-,"[48.837212, 2.296046]",198 rue de la Convention,Café Dupont,2012-05-11,75015,"{'coordinates': [2.296046, 48.837212], 'type': 'Point'}",4d40e6d864dae81c152a05cb98e30933bde96aa1,-,0,Paris,France +-,"[48.871002, 2.30879]",28 rue de Ponthieu,L'Angle,2012-10-22,75008,"{'coordinates': [2.30879, 48.871002], 'type': 'Point'}",c40bd2d1f98b415e539c27cf68518d060ebab51e,1,1,Paris,France +-,"[48.888023, 2.353467]",19-23 rue Léon,Institut des Cultures d'Islam,2012-10-22,75018,"{'coordinates': [2.353467, 48.888023], 'type': 'Point'}",68d6d37b846e39bd8554e1f8f75974b486b0f27b,1,1,Paris,France +-,"[48.886044, 2.360781]",19 rue Pajol,Canopy Café associatif,2012-10-22,75018,"{'coordinates': [2.360781, 48.886044], 'type': 'Point'}",ff73bafb514bb68eb925c81aee43c3a58ac3c70d,1,1,Paris,France +-,"[48.870287, 2.332491]",place de l'opera,L'Entracte,2012-10-09,75002,"{'coordinates': [2.332491, 48.870287], 'type': 'Point'}",0039cd8bceb5e281677a158f832a660789088071,-,1,Paris,France +-,"[48.858709, 2.362701]",15 rue du Parc Royal,Le Sévigné,2012-10-09,75003,"{'coordinates': [2.362701, 48.858709], 'type': 'Point'}",adcc8b4f78f05ba7b24b0593e1516dfb7b415f91,-,1,Paris,France +-,"[48.839687, 2.347254]",35 rue Claude Bernard,Le Café d'avant,2012-10-09,75005,"{'coordinates': [2.347254, 48.839687], 'type': 'Point'}",b904fc48763938eee2169ba25aad2ffcc0dd6a9f,-,1,Paris,France +-,"[48.844244, 2.330407]",53 rue Notre-Dame des Champs,Le Lucernaire,2012-10-09,75006,"{'coordinates': [2.330407, 48.844244], 'type': 'Point'}",cc72af04314fd40e16ff611c799d378515043508,-,1,Paris,France +-,"[48.877599, 2.332111]",12 rue Blanche,Le Brigadier,2012-10-09,75009,"{'coordinates': [2.332111, 48.877599], 'type': 'Point'}",978d4bc68c9ebf81029d3e77274d2107777b8a75,-,1,Paris,France +-,"[48.826494, 2.359987]",26 rue du Docteur Magnan,L'âge d'or,2012-03-07,75013,"{'coordinates': [2.359987, 48.826494], 'type': 'Point'}",40bffbdc0c9ed1cbce820fed875d7c21d8964640,-,1,Paris,France +-,"[48.883717, 2.326861]",Place de Clichy,Bagels & Coffee Corner,2012-03-07,75017,"{'coordinates': [2.326861, 48.883717], 'type': 'Point'}",262facde9b8c4568c9ba7fbce8f069ff8c76948d,-,1,Paris,France +-,"[48.835843, 2.278501]",10 boulevard Victor,Café Victor,2012-03-07,75015,"{'coordinates': [2.278501, 48.835843], 'type': 'Point'}",e5817ec44ac5a7ea2e4a34b6a2e13d535156642b,-,1,Paris,France +-,"[48.845337, 2.379024]","54, avenue Daumesnil",L'empreinte,2012-03-07,75012,"{'coordinates': [2.379024, 48.845337], 'type': 'Point'}",b96ddd35cbbf5d93aaff79487afdf083b5ff0817,-,1,Paris,France +-,"[48.857312, 2.379055]","93, rue de la Roquette",L'horizon,2012-03-07,75011,"{'coordinates': [2.379055, 48.857312], 'type': 'Point'}",84c6b7335e7f82ac942c4f398723ec99076f148d,-,1,Paris,France +-,"[48.835878, 2.395723]",34 bis rue de Wattignies,Au pays de Vannes,2012-06-27,75012,"{'coordinates': [2.395723, 48.835878], 'type': 'Point'}",a17869dbb9d0d5b1e5ed7bb288053900b04ee944,-,1,Paris,France +-,"[48.85413, 2.323539]",36 rue de Varenne,Café Varenne,2012-06-27,75007,"{'coordinates': [2.323539, 48.85413], 'type': 'Point'}",a26ec0d5fca47b8de77d862ad8a99b75bb520a09,-,1,Paris,France +-,"[48.855161, 2.360218]",125 Rue Saint-Antoine,l'Eléphant du nil,2012-06-27,75004,"{'coordinates': [2.360218, 48.855161], 'type': 'Point'}",7b7ceefd1f9ed85041265c9577e0dc8bee01d45a,-,1,Paris,France +-,"[48.8357, 2.292961]",354 bis rue Vaugirard,Le Comptoir,2012-06-27,75015,"{'coordinates': [2.292961, 48.8357], 'type': 'Point'}",59d8fa304e535f4eb41f9746028034c9b30cbde4,-,1,Paris,France +-,"[48.835451, 2.292515]",358 rue de Vaugirard,Le Parc Vaugirard,2012-06-27,75015,"{'coordinates': [2.292515, 48.835451], 'type': 'Point'}",19f655206a8446959c8e796c2b3cb9001890f985,-,1,Paris,France +-,"[48.834972, 2.327007]",58 rue Daguerre,le Zango,2012-06-27,75014,"{'coordinates': [2.327007, 48.834972], 'type': 'Point'}",e1b54109015316a822747f788128f997a3478050,-,1,Paris,France +-,"[48.848887, 2.399972]",3 rue de Lagny,Melting Pot,2012-06-27,75020,"{'coordinates': [2.399972, 48.848887], 'type': 'Point'}",fd0de2cbf73e0a728cd73e4e2a9a4a9c646f76f2,-,1,Paris,France +-,"[48.892366, 2.317359]",174 avenue de Clichy,Pari's Café,2012-06-27,75017,"{'coordinates': [2.317359, 48.892366], 'type': 'Point'}",831446ae203f89de26d3300e625c20717e82d40a,-,1,Paris,France diff --git a/bonobo/examples/datasets/coffeeshops.json b/bonobo/examples/datasets/coffeeshops.json index 8544018..30b20ee 100644 --- a/bonobo/examples/datasets/coffeeshops.json +++ b/bonobo/examples/datasets/coffeeshops.json @@ -1,181 +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"}] \ No newline at end of file +[{"prix_terasse": "-", "geoloc": [48.839512, 2.303007], "address": "344Vrue Vaugirard", "name": "Coffee Chope", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.303007, 48.839512], "type": "Point"}, "recordid": "3c276428d45ad68ccdf6875e4ddcfe95d0c0d4cf", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.876737, 2.357601], "address": "5, rue d'Alsace", "name": "Ext\u00e9rieur Quai", "date": "2014-02-01", "zipcode": 75010, "geometry": {"coordinates": [2.357601, 48.876737], "type": "Point"}, "recordid": "97ad81cd1127a8566085ad796eeb44a06bec7514", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.850852, 2.362029], "address": "6 Bd henri IV", "name": "Le Sully", "date": "2014-02-01", "zipcode": 75004, "geometry": {"coordinates": [2.362029, 48.850852], "type": "Point"}, "recordid": "aa4294c1b8d660a23db0dc81321e509bae1dae68", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "1", "geoloc": [48.893517, 2.340271], "address": "53 rue du ruisseau", "name": "O q de poule", "date": "2013-08-22", "zipcode": 75018, "geometry": {"coordinates": [2.340271, 48.893517], "type": "Point"}, "recordid": "a81362dbed35247555fb105bd83ff2906904a66e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.864655, 2.350089], "address": "1 Passage du Grand Cerf", "name": "Le Pas Sage", "date": "2013-08-22", "zipcode": 75002, "geometry": {"coordinates": [2.350089, 48.864655], "type": "Point"}, "recordid": "7ced86acbd5ccfba229bcc07d70d0d117aee16a5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.895825, 2.339712], "address": "112 Rue Championnet", "name": "La Renaissance", "date": "2013-08-22", "zipcode": 75018, "geometry": {"coordinates": [2.339712, 48.895825], "type": "Point"}, "recordid": "5582c8572bd7637bf305b74c1c0bdb74a8e4247f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.868581, 2.373015], "address": "Rue de la Fontaine au Roi", "name": "La Caravane", "date": "2012-05-11", "zipcode": 75011, "geometry": {"coordinates": [2.373015, 48.868581], "type": "Point"}, "recordid": "50bb0fa06e562a242f115ddbdae2ed9c7df93d57", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.875155, 2.335536], "address": "51 Rue Victoire", "name": "Le chantereine", "date": "2012-10-22", "zipcode": 75009, "geometry": {"coordinates": [2.335536, 48.875155], "type": "Point"}, "recordid": "eb8a62feeedaf7ed8b8c912305270ee857068689", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.886536, 2.346525], "address": "11 rue Feutrier", "name": "Le M\u00fcller", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.346525, 48.886536], "type": "Point"}, "recordid": "62c552f167f671f88569c1f2d6a44098fb514c51", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.841494, 2.307117], "address": "21 rue Copreaux", "name": "Le drapeau de la fidelit\u00e9", "date": "2012-10-22", "zipcode": 75015, "geometry": {"coordinates": [2.307117, 48.841494], "type": "Point"}, "recordid": "5120ea0b9d7387766072b90655166486928e25c8", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.839743, 2.296898], "address": "125 rue Blomet", "name": "Le caf\u00e9 des amis", "date": "2012-10-22", "zipcode": 75015, "geometry": {"coordinates": [2.296898, 48.839743], "type": "Point"}, "recordid": "865f62415adc5c34e3ca38a1748b7a324dfba209", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.857728, 2.349641], "address": "10 rue Saint Martin", "name": "Le Caf\u00e9 Livres", "date": "2012-10-09", "zipcode": 75004, "geometry": {"coordinates": [2.349641, 48.857728], "type": "Point"}, "recordid": "7ef54a78802d49cafd2701458df2b0d0530d123b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.856003, 2.30457], "address": "46 avenue Bosquet", "name": "Le Bosquet", "date": "2012-10-09", "zipcode": 75007, "geometry": {"coordinates": [2.30457, 48.856003], "type": "Point"}, "recordid": "d701a759e08a71f4bbb01f29473274b0152135d0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.889426, 2.332954], "address": "12 rue Armand Carrel", "name": "Le Chaumontois", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.332954, 48.889426], "type": "Point"}, "recordid": "e12ff00a644c91ad910ddc63a770c190be93a393", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.838521, 2.370478], "address": "34 avenue Pierre Mend\u00e8s-France", "name": "Le Kleemend's", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.370478, 48.838521], "type": "Point"}, "recordid": "0f6cd1ee7751b00c9574efcfdcf66fa0e857d251", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.849861, 2.385342], "address": "202 rue du faubourg st antoine", "name": "Caf\u00e9 Pierre", "date": "2012-03-07", "zipcode": 75012, "geometry": {"coordinates": [2.385342, 48.849861], "type": "Point"}, "recordid": "f9de9d0fb5e92f047a6f1986a31f9dd4d38bcb36", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.872202, 2.304624], "address": "61 rue de Ponthieu", "name": "Les Arcades", "date": "2012-03-07", "zipcode": 75008, "geometry": {"coordinates": [2.304624, 48.872202], "type": "Point"}, "recordid": "67eaf58afc856077c0680601e453e75c0922c9c0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.859031, 2.320315], "address": "31 rue Saint-Dominique", "name": "Le Square", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.320315, 48.859031], "type": "Point"}, "recordid": "678558317bc9ad46652e5b1643e70c2142a76e7e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.850092, 2.37463], "address": "75, avenue Ledru-Rollin", "name": "Assaporare Dix sur Dix", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.37463, 48.850092], "type": "Point"}, "recordid": "667474321887d08a3cc636adf043ad354b65fa61", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.86805, 2.353313], "address": "129 boulevard sebastopol", "name": "Au cerceau d'or", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.353313, 48.86805], "type": "Point"}, "recordid": "c9ef52ba2fabe0286700329f18bbbbea9a10b474", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.845927, 2.373051], "address": "21 ter boulevard Diderot", "name": "Aux cadrans", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.373051, 48.845927], "type": "Point"}, "recordid": "ed5f98686856bf4ddd2b381b43ad229246741a90", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.851662, 2.273883], "address": "17 rue Jean de la Fontaine", "name": "Caf\u00e9 antoine", "date": "2012-06-27", "zipcode": 75016, "geometry": {"coordinates": [2.273883, 48.851662], "type": "Point"}, "recordid": "ab6d1e054e2e6ae7d6150013173f55e83c05ca23", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.877642, 2.312823], "address": "rue de Lisbonne", "name": "Caf\u00e9 de la Mairie (du VIII)", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.312823, 48.877642], "type": "Point"}, "recordid": "7de8a79b026ac63f453556612505b5bcd9229036", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.838633, 2.349916], "address": "5 rue Claude Bernard", "name": "Caf\u00e9 Lea", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.349916, 48.838633], "type": "Point"}, "recordid": "fecd8900cf83027f74ceced9fc4ad80ac73b63a7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.849293, 2.354486], "address": "11 boulevard Saint-Germain", "name": "Cardinal Saint-Germain", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.354486, 48.849293], "type": "Point"}, "recordid": "e4a078c30c98082896787f4e4b41a07554392529", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.869771, 2.342501], "address": "52 rue Notre-Dame des Victoires", "name": "D\u00e9d\u00e9 la frite", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.342501, 48.869771], "type": "Point"}, "recordid": "ccb2ba2f98043e8eefd5fda829dee1ea7f1d2c7a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.834051, 2.287345], "address": "36 rue du hameau", "name": "La Bauloise", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.287345, 48.834051], "type": "Point"}, "recordid": "c9fe10abd15ede7ccaeb55c309898d30d7b19d0e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.888165, 2.377387], "address": "71 quai de Seine", "name": "Le Bellerive", "date": "2012-06-27", "zipcode": 75019, "geometry": {"coordinates": [2.377387, 48.888165], "type": "Point"}, "recordid": "4e0b5c2d33d7c25fc54c51171f3d37e509959fc0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.864543, 2.340997], "address": "42 rue coquill\u00e8re", "name": "Le bistrot de Ma\u00eblle et Augustin", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.340997, 48.864543], "type": "Point"}, "recordid": "52acab12469af291984e9a70962e08c72b058e10", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.872103, 2.346161], "address": "14 rue Rougemont", "name": "Le Dellac", "date": "2012-06-27", "zipcode": 75009, "geometry": {"coordinates": [2.346161, 48.872103], "type": "Point"}, "recordid": "4d1d627ecea2ffa279bb862f8ba495d95ca75350", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.859645, 2.355598], "address": "1 rue Pecquay", "name": "Le Felteu", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.355598, 48.859645], "type": "Point"}, "recordid": "2c1fa55460af282266d86fd003af4f929fdf4e7d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.85763, 2.346101], "address": "2 bis quai de la m\u00e9gisserie", "name": "Le Reynou", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.346101, 48.85763], "type": "Point"}, "recordid": "d4ddd30ab3e721a317fc7ea89d5b9001255ce9f4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.884646, 2.337734], "address": "23 rue des abbesses", "name": "Le Saint Jean", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.337734, 48.884646], "type": "Point"}, "recordid": "51b47cf167b7f32eeebb108330956694d75d4268", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.841007, 2.31466], "address": "65 boulevard Pasteur", "name": "les montparnos", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.31466, 48.841007], "type": "Point"}, "recordid": "2aaca891ffd0694c657a43889516ab72afdfba07", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.850323, 2.33039], "address": "16 rue DE MEZIERES", "name": "L'antre d'eux", "date": "2014-02-01", "zipcode": 75006, "geometry": {"coordinates": [2.33039, 48.850323], "type": "Point"}, "recordid": "4ff4337934c66f61e00d1d9551f7cdddba03e544", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.864957, 2.346938], "address": "58 rue de Montorgueil", "name": "Drole d'endroit pour une rencontre", "date": "2014-02-01", "zipcode": 75002, "geometry": {"coordinates": [2.346938, 48.864957], "type": "Point"}, "recordid": "3451657f880abe75d0c7e386fc698405556c53e8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.889565, 2.339735], "address": "104 rue caulaincourt", "name": "Le pari's caf\u00e9", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.339735, 48.889565], "type": "Point"}, "recordid": "e8c34a537b673fcb26c76e02deca4f5a728929dc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.859115, 2.368871], "address": "60 rue saint-sabin", "name": "Le Poulailler", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.368871, 48.859115], "type": "Point"}, "recordid": "325ea74ba83f716dde87c08cffd36f7df7722a49", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.833595, 2.38604], "address": "33 Cour Saint Emilion", "name": "Chai 33", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.38604, 48.833595], "type": "Point"}, "recordid": "528de8d5d8780bee83145637e315483d48f5ae3c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.868741, 2.379969], "address": "99 rue Jean-Pierre Timbaud", "name": "L'Assassin", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.379969, 48.868741], "type": "Point"}, "recordid": "fac0483890ff8bdaeb3feddbdb032c5112f24678", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.851463, 2.398691], "address": "1 rue d'Avron", "name": "l'Usine", "date": "2014-02-01", "zipcode": 75020, "geometry": {"coordinates": [2.398691, 48.851463], "type": "Point"}, "recordid": "fee1e3eb103bbc98e19e45d34365da0f27166541", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.896305, 2.332898], "address": "52 rue Liebniz", "name": "La Bricole", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.332898, 48.896305], "type": "Point"}, "recordid": "4744e866c244c59eec43b3fe159542d2ef433065", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.850311, 2.34885], "address": "place maubert", "name": "le ronsard", "date": "2014-02-01", "zipcode": 75005, "geometry": {"coordinates": [2.34885, 48.850311], "type": "Point"}, "recordid": "49a390322b45246bc2c1e50fcd46815ad271bca0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.863038, 2.3604], "address": "82 rue des archives", "name": "Face Bar", "date": "2014-02-01", "zipcode": 75003, "geometry": {"coordinates": [2.3604, 48.863038], "type": "Point"}, "recordid": "d96e16ebf2460bb2f6c34198918a071233725cbc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.872746, 2.366392], "address": "49 rue bichat", "name": "American Kitchen", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.366392, 48.872746], "type": "Point"}, "recordid": "6b9395475cbbbbbacbaaeb070f71d31c2d183dc4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.870598, 2.365413], "address": "55 bis quai de valmy", "name": "La Marine", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.365413, 48.870598], "type": "Point"}, "recordid": "d4d2f92d27f38de59e57744f434781e61283551c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.889101, 2.318001], "address": "21 avenue Brochant", "name": "Le Bloc", "date": "2013-08-22", "zipcode": 75017, "geometry": {"coordinates": [2.318001, 48.889101], "type": "Point"}, "recordid": "e425882ee969d1e8bffe7234336ae40da88c8439", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.874697, 2.405421], "address": "229 avenue Gambetta", "name": "La Recoleta au Manoir", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.405421, 48.874697], "type": "Point"}, "recordid": "02de82cffb2918beafb740f4e924029d470b07a1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.847344, 2.286078], "address": "80 Rue Saint-Charles", "name": "Le Pareloup", "date": "2013-08-22", "zipcode": 75015, "geometry": {"coordinates": [2.286078, 48.847344], "type": "Point"}, "recordid": "0227ca95f76bb6097ae0a0e6f455af2624d49ae3", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, +{"prix_terasse": "1", "geoloc": [48.840771, 2.324589], "address": "3 rue de la Gait\u00e9", "name": "La Brasserie Gait\u00e9", "date": "2013-08-22", "zipcode": 75014, "geometry": {"coordinates": [2.324589, 48.840771], "type": "Point"}, "recordid": "e7c4cba08749c892a73db2715d06623d9e0c2f67", "prix_salle": "-", "prix_comptoir": null, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.875232, 2.336036], "address": "46 rue Victoire", "name": "Caf\u00e9 Zen", "date": "2012-05-11", "zipcode": 75009, "geometry": {"coordinates": [2.336036, 48.875232], "type": "Point"}, "recordid": "5e9a6172f8b64cd098a5f2cf9b1d42567fe0a894", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.872677, 2.315276], "address": "27 rue de Penthi\u00e8vre", "name": "O'Breizh", "date": "2012-10-22", "zipcode": 75008, "geometry": {"coordinates": [2.315276, 48.872677], "type": "Point"}, "recordid": "ead3108add27ef41bb92517aca834f7d7f632816", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.868838, 2.33605], "address": "23 rue saint augustin", "name": "Le Petit Choiseul", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.33605, 48.868838], "type": "Point"}, "recordid": "de601277ca00567b62fa4f5277e4a17679faa753", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.841526, 2.351012], "address": "7 rue Ep\u00e9e de Bois", "name": "Invitez vous chez nous", "date": "2012-10-22", "zipcode": 75005, "geometry": {"coordinates": [2.351012, 48.841526], "type": "Point"}, "recordid": "1d2373bdec8a07306298e3ee54894ac295ee1d55", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.86525, 2.350507], "address": "142 Rue Saint-Denis 75002 Paris", "name": "La Cordonnerie", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.350507, 48.86525], "type": "Point"}, "recordid": "5c9bf60617a99ad75445b454f98e75e1a104021d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.892244, 2.346973], "address": "3, rue Baudelique", "name": "Le Supercoin", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.346973, 48.892244], "type": "Point"}, "recordid": "68a4ee10f1fc4d2a659501e811d148420fa80e95", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.890043, 2.362241], "address": "86 bis rue Riquet", "name": "Populettes", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.362241, 48.890043], "type": "Point"}, "recordid": "8cc55d58d72621a7e91cf6b456731d2cb2863afc", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "1", "geoloc": [48.893017, 2.337776], "address": "49 rue des Cloys", "name": "Au bon coin", "date": "2012-10-18", "zipcode": 75018, "geometry": {"coordinates": [2.337776, 48.893017], "type": "Point"}, "recordid": "0408f272c08c52e3cae035ffdeb8928698787ea9", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.836919, 2.347003], "address": "69 rue Broca", "name": "Le Couvent", "date": "2012-10-18", "zipcode": 75013, "geometry": {"coordinates": [2.347003, 48.836919], "type": "Point"}, "recordid": "729b2f228d3fd2db6f78bb624d451f59555e4a04", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.840624, 2.349766], "address": "111 rue mouffetard", "name": "La Br\u00fblerie des Ternes", "date": "2012-10-18", "zipcode": 75005, "geometry": {"coordinates": [2.349766, 48.840624], "type": "Point"}, "recordid": "233dd2a17620cd5eae70fef11cc627748e3313d5", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.832825, 2.336116], "address": "59 Boulevard Saint-Jacques", "name": "L'\u00c9cir", "date": "2012-10-09", "zipcode": 75014, "geometry": {"coordinates": [2.336116, 48.832825], "type": "Point"}, "recordid": "4a44324a5a806801fd3e05af89cad7c0f1e69d1e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.850696, 2.378417], "address": "126, rue du Faubourg Saint Antoine", "name": "Le Chat bossu", "date": "2012-10-09", "zipcode": 75012, "geometry": {"coordinates": [2.378417, 48.850696], "type": "Point"}, "recordid": "d1d02463f7c90d38cccffd898e15f51e65910baf", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.834157, 2.33381], "address": "58 boulvevard Saint Jacques", "name": "Denfert caf\u00e9", "date": "2012-10-09", "zipcode": 75014, "geometry": {"coordinates": [2.33381, 48.834157], "type": "Point"}, "recordid": "f78f406e5ccb95cb902ce618ed54eba1d4776a3c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.867948, 2.343582], "address": "95 rue Montmartre", "name": "Le Caf\u00e9 frapp\u00e9", "date": "2012-10-09", "zipcode": 75002, "geometry": {"coordinates": [2.343582, 48.867948], "type": "Point"}, "recordid": "4dd2a924a7b2c5f061cecaba2f272548a8c83c6c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.859772, 2.360558], "address": "78 rue vieille du temple", "name": "La Perle", "date": "2012-10-09", "zipcode": 75003, "geometry": {"coordinates": [2.360558, 48.859772], "type": "Point"}, "recordid": "476c54e643613ec36a9b1c533f32122fd873f3c3", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.845047, 2.349583], "address": "1 rue Thouin", "name": "Le Descartes", "date": "2012-10-09", "zipcode": 75005, "geometry": {"coordinates": [2.349583, 48.845047], "type": "Point"}, "recordid": "e9cb0b0d6b6b512e9ecab889267ba342a4f0ea93", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.830151, 2.334213], "address": "55 rue de la tombe Issoire", "name": "Le petit club", "date": "2012-03-07", "zipcode": 75014, "geometry": {"coordinates": [2.334213, 48.830151], "type": "Point"}, "recordid": "c6ca1166fa7fd7050f5d1c626a1e17050116c91e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.865707, 2.374382], "address": "90 avenue Parmentier", "name": "Le Plein soleil", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.374382, 48.865707], "type": "Point"}, "recordid": "95cf5fb735bd19826db70a3af4fe72fce647d4e5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.875322, 2.312329], "address": "146, boulevard Haussmann", "name": "Le Relais Haussmann", "date": "2012-03-07", "zipcode": 75008, "geometry": {"coordinates": [2.312329, 48.875322], "type": "Point"}, "recordid": "6c5f68f47c916638342b77bd5edfc30bd7051303", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.859559, 2.30643], "address": "88 rue Saint-Dominique", "name": "Le Malar", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.30643, 48.859559], "type": "Point"}, "recordid": "c633cb01686aa5f3171bf59976dc9b7f23cbca54", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.864628, 2.408038], "address": "47 rue Belgrand", "name": "Au panini de la place", "date": "2012-03-07", "zipcode": 75020, "geometry": {"coordinates": [2.408038, 48.864628], "type": "Point"}, "recordid": "66dedd35fcbbbb24f328882d49098de7aa5f26ba", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.88435, 2.297978], "address": "182 rue de Courcelles", "name": "Le Village", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.297978, 48.88435], "type": "Point"}, "recordid": "1e07eaf8a93875906d0b18eb6a897c651943589a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.853381, 2.376706], "address": "41 rue de Charonne", "name": "Pause Caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.376706, 48.853381], "type": "Point"}, "recordid": "60d98c3236a70824df50e9aca83e7d7f13a310c5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.853253, 2.383415], "address": "14 rue Jean Mac\u00e9", "name": "Le Pure caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.383415, 48.853253], "type": "Point"}, "recordid": "66707fb2e707d2145fc2eb078a1b980a45921616", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.848873, 2.392859], "address": "307 fg saint Antoine", "name": "Extra old caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.392859, 48.848873], "type": "Point"}, "recordid": "039ec7dcb219cfc434547b06938ba497afeb83b4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.873227, 2.360787], "address": "44 rue Vinaigriers", "name": "Chez Fafa", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.360787, 48.873227], "type": "Point"}, "recordid": "1572d199f186bf86d7753fe71ac23477a7a8bd2c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.850836, 2.384069], "address": "3 rue Faidherbe", "name": "En attendant l'or", "date": "2012-06-27", "zipcode": 75011, "geometry": {"coordinates": [2.384069, 48.850836], "type": "Point"}, "recordid": "de5789bb4a4ffbd244cded8dc555639dbe7d2279", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.866993, 2.336006], "address": "30 rue des Petits-Champs", "name": "Br\u00fblerie San Jos\u00e9", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.336006, 48.866993], "type": "Point"}, "recordid": "b736e5fa17396ee56a642212ccd0ab29c7f2bef1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.856434, 2.342683], "address": "2 place Martin Nadaud", "name": "Caf\u00e9 Martin", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.342683, 48.856434], "type": "Point"}, "recordid": "25fbf857029c54d57909c158e3039349b77344ed", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.863675, 2.348701], "address": "14 rue Turbigo, Paris", "name": "Etienne", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.348701, 48.863675], "type": "Point"}, "recordid": "0190edd7b0766c6d3e43093deb41abe9446c1b22", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.854584, 2.385193], "address": "184 bd Voltaire", "name": "L'ing\u00e9nu", "date": "2012-06-27", "zipcode": 75011, "geometry": {"coordinates": [2.385193, 48.854584], "type": "Point"}, "recordid": "7fcef7475ec632d5c61c9c36c1d52a402ad2e9e8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.890605, 2.361349], "address": "8 rue L'Olive", "name": "L'Olive", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.361349, 48.890605], "type": "Point"}, "recordid": "35286273f281c8c5082b3d3bd17f6bbf207426f9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.871396, 2.338321], "address": "18 rue Favart", "name": "Le Biz", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.338321, 48.871396], "type": "Point"}, "recordid": "cf2f6b9e283aaeeca2214cc1fe57b45e45668e25", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.868109, 2.331785], "address": "1 rue Louis le Grand", "name": "Le Cap Bourbon", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.331785, 48.868109], "type": "Point"}, "recordid": "339030e95e0846b41a8e2b91045456c4e4a50043", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.84167, 2.303053], "address": "9 Place du General Beuret", "name": "Le General Beuret", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.303053, 48.84167], "type": "Point"}, "recordid": "12b37036d28d28b32ebe81756ad15eb68372947f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.846814, 2.289311], "address": "95 avenue Emile Zola", "name": "Le Germinal", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.289311, 48.846814], "type": "Point"}, "recordid": "4e03831a64e886a28a7232e54f2812c1ced23c5a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.862655, 2.337607], "address": "202 rue Saint-Honor\u00e9", "name": "Le Ragueneau", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.337607, 48.862655], "type": "Point"}, "recordid": "e303131b2600d4c2287749a36bf7193d2fa60bd7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.889982, 2.338933], "address": "72 rue lamarck", "name": "Le refuge", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.338933, 48.889982], "type": "Point"}, "recordid": "f64310461736a769c6854fdefb99b9f2e7b230a9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.870294, 2.352821], "address": "13 rue du Faubourg Saint Denis", "name": "Le sully", "date": "2012-06-27", "zipcode": 75010, "geometry": {"coordinates": [2.352821, 48.870294], "type": "Point"}, "recordid": "166be8588ff16e50838fa6a164d1e580497b795d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.842128, 2.280374], "address": "60 rue des bergers", "name": "Le bal du pirate", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.280374, 48.842128], "type": "Point"}, "recordid": "93ff6e35406a074a6ba2667d2b286abf91132f6a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.838769, 2.39609], "address": "95 rue claude decaen", "name": "zic zinc", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.39609, 48.838769], "type": "Point"}, "recordid": "8fdc739d64ff1f01973235301e3ec86791016759", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.870247, 2.376306], "address": "35 rue de l'orillon", "name": "l'orillon bar", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.376306, 48.870247], "type": "Point"}, "recordid": "6e6e9695ef04c3fdbd4cfa19f0cfb0c0f93f4933", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.869848, 2.394247], "address": "116 Rue de M\u00e9nilmontant", "name": "Le Zazabar", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.394247, 48.869848], "type": "Point"}, "recordid": "22edfe92a72ce1bb0eea972508b5caa7af2db2df", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.845458, 2.354796], "address": "22 rue Linn\u00e9", "name": "L'In\u00e9vitable", "date": "2013-08-22", "zipcode": 75005, "geometry": {"coordinates": [2.354796, 48.845458], "type": "Point"}, "recordid": "6893cb08e99319091de9ba80305f22e0ce4cc08d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.83336, 2.365782], "address": "77 rue Dunois", "name": "Le Dunois", "date": "2013-08-22", "zipcode": 75013, "geometry": {"coordinates": [2.365782, 48.83336], "type": "Point"}, "recordid": "c3be1246dbc4ca5734d5bbd569436ba655105248", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.862655, 2.337607], "address": "202 rue Saint Honor\u00e9", "name": "Ragueneau", "date": "2012-05-11", "zipcode": 75001, "geometry": {"coordinates": [2.337607, 48.862655], "type": "Point"}, "recordid": "e946d9e8f8c5a130f98eca945efadfd9eec40dcb", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": null, "geoloc": [48.826608, 2.374571], "address": "48 rue du Dessous des Berges", "name": "Le Caminito", "date": "2012-10-22", "zipcode": 75013, "geometry": {"coordinates": [2.374571, 48.826608], "type": "Point"}, "recordid": "5d9ad8bcfbbc20adaec2aa7dcdb71326868c7686", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.870598, 2.365413], "address": "55bis quai de Valmy", "name": "Epicerie Musicale", "date": "2012-10-22", "zipcode": 75010, "geometry": {"coordinates": [2.365413, 48.870598], "type": "Point"}, "recordid": "8ffea133d93c608d337bc0129f4f9f3d5cad8dae", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": null, "address": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital", "name": "Le petit Bretonneau", "date": "2012-10-22", "zipcode": 75018, "geometry": {}, "recordid": "dd1ffdbd55c2dc651201302b8015258f7d35fd35", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.862575, 2.367427], "address": "104 rue amelot", "name": "Le Centenaire", "date": "2012-10-22", "zipcode": 75011, "geometry": {"coordinates": [2.367427, 48.862575], "type": "Point"}, "recordid": "e47d25752c2c8bcab5efb0e3a41920ec7a8a766a", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.842833, 2.348314], "address": "13 Rue du Pot de Fer", "name": "La Montagne Sans Genevi\u00e8ve", "date": "2012-10-22", "zipcode": 75005, "geometry": {"coordinates": [2.348314, 48.842833], "type": "Point"}, "recordid": "314d170601f81e3a3f26d8801f0fbee39981c788", "prix_salle": "1", "prix_comptoir": null, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.851325, 2.40171], "address": "46 rue de Buzenval", "name": "Les P\u00e8res Populaires", "date": "2012-10-18", "zipcode": 75020, "geometry": {"coordinates": [2.40171, 48.851325], "type": "Point"}, "recordid": "e75e8a3fe6212a0e576beec82f0128dd394e56fa", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.857658, 2.305613], "address": "188 rue de Grenelle", "name": "Cafe de grenelle", "date": "2012-10-09", "zipcode": 75007, "geometry": {"coordinates": [2.305613, 48.857658], "type": "Point"}, "recordid": "72e274046d671e68bc7754808feacfc95b91b6ed", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.875207, 2.332944], "address": "73 rue de la Victoire", "name": "Le relais de la victoire", "date": "2012-10-09", "zipcode": 75009, "geometry": {"coordinates": [2.332944, 48.875207], "type": "Point"}, "recordid": "01c21abdf35484f9ad184782979ecd078de84523", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": null, "address": "Route de la Muette \u00e0 Neuilly\nClub hippique du Jardin d\u2019Acclimatation", "name": "La chaumi\u00e8re gourmande", "date": "2012-03-07", "zipcode": 75016, "geometry": {}, "recordid": "438ec18d35793d12eb6a137373c3fe4f3aa38a69", "prix_salle": "1", "prix_comptoir": null, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.891882, 2.33365], "address": "216, rue Marcadet", "name": "Le Brio", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.33365, 48.891882], "type": "Point"}, "recordid": "49e3584ce3d6a6a236b4a0db688865bdd3483fec", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.884753, 2.324648], "address": "22 rue des Dames", "name": "Caves populaires", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.324648, 48.884753], "type": "Point"}, "recordid": "af81e5eca2b84ea706ac2d379edf65b0fb2f879a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.827428, 2.325652], "address": "12 avenue Jean Moulin", "name": "Caprice caf\u00e9", "date": "2012-03-07", "zipcode": 75014, "geometry": {"coordinates": [2.325652, 48.827428], "type": "Point"}, "recordid": "88e07bdb723b49cd27c90403b5930bca1c93b458", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.832964, 2.369266], "address": "7 rue Clisson", "name": "Tamm Bara", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.369266, 48.832964], "type": "Point"}, "recordid": "baf59c98acafbe48ed9e91b64377da216a20cbcc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.876577, 2.348414], "address": "1 rue de Montholon", "name": "L'anjou", "date": "2012-03-07", "zipcode": 75009, "geometry": {"coordinates": [2.348414, 48.876577], "type": "Point"}, "recordid": "e6f5949dca40548aad296208c61c498f639f648c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.862599, 2.315086], "address": "2 rue Robert Esnault Pelterie", "name": "Caf\u00e9 dans l'aerogare Air France Invalides", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.315086, 48.862599], "type": "Point"}, "recordid": "6d175eb48c577fafdfc99df0ab55da468cf17164", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.844854, 2.345413], "address": "10 rue d\"Ulm", "name": "Waikiki", "date": "2012-03-07", "zipcode": 75005, "geometry": {"coordinates": [2.345413, 48.844854], "type": "Point"}, "recordid": "fb1e2bc2ae55d3d47da682c71093a12fa64fbd45", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.871576, 2.364499], "address": "36 rue Beaurepaire", "name": "Chez Prune", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.364499, 48.871576], "type": "Point"}, "recordid": "19399ede5b619761877822185bbb4c98b565974c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.833863, 2.329046], "address": "21 rue Boulard", "name": "Au Vin Des Rues", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.329046, 48.833863], "type": "Point"}, "recordid": "87263873b6f8346b5777844be0122a307f29fcab", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.838137, 2.301166], "address": "14 rue d'alleray", "name": "bistrot les timbr\u00e9s", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.301166, 48.838137], "type": "Point"}, "recordid": "74b6d7113e5b14eb8e890663f061208bf4ff6728", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.871799, 2.315985], "address": "9 rue de Miromesnil", "name": "Caf\u00e9 beauveau", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.315985, 48.871799], "type": "Point"}, "recordid": "2759f5cdda4bcb88ca3ae2f7299b37b8e62596c8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.866259, 2.338739], "address": "9 rue des petits champs", "name": "Caf\u00e9 Pistache", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.338739, 48.866259], "type": "Point"}, "recordid": "6d2675cdc912118d0376229be8e436feca9c8af7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.874605, 2.387738], "address": "13 Rue Jean-Baptiste Dumay", "name": "La Cagnotte", "date": "2012-06-27", "zipcode": 75020, "geometry": {"coordinates": [2.387738, 48.874605], "type": "Point"}, "recordid": "f7085c754c0c97e418d7e5213753f74bd396fc27", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.842462, 2.310919], "address": "172 rue de vaugirard", "name": "le 1 cinq", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.310919, 48.842462], "type": "Point"}, "recordid": "17e917723fc99d6e5bd77eb9633ac2e789a9a6d9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.84591, 2.375543], "address": "28 bis boulevard Diderot", "name": "Le Killy Jen", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.375543, 48.84591], "type": "Point"}, "recordid": "93132bd8b3ae67dfcc8cf8c1166e312ac4acb9b9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.842868, 2.303173], "address": "106 rue Lecourbe", "name": "Les Artisans", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.303173, 48.842868], "type": "Point"}, "recordid": "c37ef573b4cb2d1e61795d6a9ef11de433dc9a99", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.865684, 2.334416], "address": "83 avenue de Wagram", "name": "Peperoni", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.334416, 48.865684], "type": "Point"}, "recordid": "9461f859ca009ced25555fa6af1e6867dda9223e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.833146, 2.288834], "address": "380 rue de vaugirard", "name": "le lutece", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.288834, 48.833146], "type": "Point"}, "recordid": "ddd13990c1408700085366bd4ba313acd69a44ea", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.886431, 2.327429], "address": "16 rue Ganneron", "name": "Brasiloja", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.327429, 48.886431], "type": "Point"}, "recordid": "d679bb1642534278f4c0203d67be0bafd5306d81", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.855711, 2.359491], "address": "16 rue de Rivoli", "name": "Rivolux", "date": "2014-02-01", "zipcode": 75004, "geometry": {"coordinates": [2.359491, 48.855711], "type": "Point"}, "recordid": "bdd2b008cc765c7fe195c037b830cd2628420a2f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.845898, 2.372766], "address": "21 Bis Boulevard Diderot", "name": "L'europ\u00e9en", "date": "2013-08-22", "zipcode": 75012, "geometry": {"coordinates": [2.372766, 48.845898], "type": "Point"}, "recordid": "693c0da7d4db24781ed161c01a661c36074a94fa", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.867465, 2.357791], "address": "39 rue Notre Dame de Nazareth", "name": "NoMa", "date": "2013-08-22", "zipcode": 75003, "geometry": {"coordinates": [2.357791, 48.867465], "type": "Point"}, "recordid": "60d8b670810cc95eb0439dd0c238f8205ea8ef76", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.871595, 2.385858], "address": "1 Rue des Envierges", "name": "O'Paris", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.385858, 48.871595], "type": "Point"}, "recordid": "297c040284a05efe35c69bb621505e6acfdcdda4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.872402, 2.366532], "address": "16 avenue Richerand", "name": "Caf\u00e9 Clochette", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.366532, 48.872402], "type": "Point"}, "recordid": "a561a941f538e8a1d321bf8d98576d06be037962", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.856584, 2.368574], "address": "40 Boulevard Beaumarchais", "name": "La cantoche de Paname", "date": "2013-08-22", "zipcode": 75011, "geometry": {"coordinates": [2.368574, 48.856584], "type": "Point"}, "recordid": "5ba2aaec9f1de9d01e65be95215cab13c693cdf3", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.856496, 2.394874], "address": "148 Boulevard de Charonne", "name": "Le Saint Ren\u00e9", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.394874, 48.856496], "type": "Point"}, "recordid": "4d60b350d04d4b1bf4bfd4dd6cc59687dc792c74", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.850055, 2.383908], "address": "196 rue du faubourg saint-antoine", "name": "La Libert\u00e9", "date": "2012-10-22", "zipcode": 75012, "geometry": {"coordinates": [2.383908, 48.850055], "type": "Point"}, "recordid": "ee94e76326f8dcbe3500afec69f1a21eb1215ad0", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.866737, 2.33716], "address": "16 rue des Petits Champs", "name": "Chez Rutabaga", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.33716, 48.866737], "type": "Point"}, "recordid": "a420ea4608440b8dc8e0267fe8cc513daa950551", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.885367, 2.325325], "address": "2 rue Lemercier", "name": "Le BB (Bouchon des Batignolles)", "date": "2012-10-22", "zipcode": 75017, "geometry": {"coordinates": [2.325325, 48.885367], "type": "Point"}, "recordid": "20986cbfe11018bd0aba8150a49db1c435f7642d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.873175, 2.339193], "address": "10 rue Rossini", "name": "La Brocante", "date": "2012-10-22", "zipcode": 75009, "geometry": {"coordinates": [2.339193, 48.873175], "type": "Point"}, "recordid": "2e10601c35669394d43936a771b18408be0338ba", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.840771, 2.324589], "address": "3 rue Ga\u00eet\u00e9", "name": "Le Plomb du cantal", "date": "2012-10-22", "zipcode": 75014, "geometry": {"coordinates": [2.324589, 48.840771], "type": "Point"}, "recordid": "6fb510614e00b065bf16a5af8e2c0eaf561a5654", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.884753, 2.324648], "address": "22 rue des Dames", "name": "Les caves populaires", "date": "2012-10-22", "zipcode": 75017, "geometry": {"coordinates": [2.324648, 48.884753], "type": "Point"}, "recordid": "d650c509a0aa8ed7b9c9b88861263f31463bbd0e", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.869519, 2.39339], "address": "108 rue de M\u00e9nilmontant", "name": "Chez Luna", "date": "2012-10-09", "zipcode": 75020, "geometry": {"coordinates": [2.39339, 48.869519], "type": "Point"}, "recordid": "736f4d996f1f8b7c3a0ce2abfeebfcce2a4bab13", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.877903, 2.385365], "address": "1 rue du Plateau", "name": "Le bar Fleuri", "date": "2012-03-07", "zipcode": 75019, "geometry": {"coordinates": [2.385365, 48.877903], "type": "Point"}, "recordid": "be55720646093788ec161c6cadc5ad8059f4b90b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.882939, 2.31809], "address": "101 rue des dames", "name": "Trois pi\u00e8ces cuisine", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.31809, 48.882939], "type": "Point"}, "recordid": "7bbbfb755020a2c25cce0067601994ce5ee4193f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.849497, 2.298855], "address": "61 avenue de la Motte Picquet", "name": "Le Zinc", "date": "2012-03-07", "zipcode": 75015, "geometry": {"coordinates": [2.298855, 48.849497], "type": "Point"}, "recordid": "e7c35a94454518de6de5bbecbc015fc37f7aea14", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.880669, 2.349964], "address": "136 rue du Faubourg poissonni\u00e8re", "name": "La cantine de Zo\u00e9", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.349964, 48.880669], "type": "Point"}, "recordid": "0edc473b3432a869b8ed66b6c4c989766b699947", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.844057, 2.328402], "address": "6/8 rue Stanislas", "name": "Les Vendangeurs", "date": "2012-03-07", "zipcode": 75006, "geometry": {"coordinates": [2.328402, 48.844057], "type": "Point"}, "recordid": "e9766ea36f6293bf670ed938bff02b975d012973", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.852053, 2.338779], "address": "3 carrefour de l'Od\u00e9on", "name": "L'avant comptoir", "date": "2012-03-07", "zipcode": 75006, "geometry": {"coordinates": [2.338779, 48.852053], "type": "Point"}, "recordid": "fe843d2f43dcaac9129f5b36dc367558dfd3b3e4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "1", "geoloc": [48.886504, 2.34498], "address": "1 rue Paul albert", "name": "Botak cafe", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.34498, 48.886504], "type": "Point"}, "recordid": "9e19c375e612f5fb803ec6a27881858619207812", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.872722, 2.354594], "address": "67 rue du Ch\u00e2teau d'eau", "name": "le chateau d'eau", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.354594, 48.872722], "type": "Point"}, "recordid": "05bb6a26ec5bfbba25da2d19a5f0e83d69800f38", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.85192, 2.373229], "address": "58 rue du Fbg Saint-Antoine", "name": "Bistrot Saint-Antoine", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.373229, 48.85192], "type": "Point"}, "recordid": "daa3908ddf69d378fec5b4548494727e1121adc4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.854685, 2.368487], "address": "11/13 boulevard Beaumarchais", "name": "Chez Oscar", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.368487, 48.854685], "type": "Point"}, "recordid": "c73be0483480c59e6ab6bc3a906c8d9dd474887f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.87226, 2.304441], "address": "63 rue de Ponthieu", "name": "Le Fronton", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.304441, 48.87226], "type": "Point"}, "recordid": "85a8200d3e3aed7724d3207ed8b1ee5ec50c1f90", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.851, 2.300378], "address": "48 avenue de la Motte Picquet", "name": "Le Piquet", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.300378, 48.851], "type": "Point"}, "recordid": "460e2adc95fd172f753b1b6ed296c2711639d49d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.841089, 2.349565], "address": "104 rue Mouffetard", "name": "Le Tournebride", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.349565, 48.841089], "type": "Point"}, "recordid": "8a7a23ed68366f70ab939c877cbdce46f19d75c7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.828704, 2.322074], "address": "52 rue des plantes", "name": "maison du vin", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.322074, 48.828704], "type": "Point"}, "recordid": "482507a8f0fe4960f94372b6fa12b16e7d4f2a93", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.842146, 2.375986], "address": "157 rue Bercy 75012 Paris", "name": "L'entrep\u00f4t", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.375986, 48.842146], "type": "Point"}, "recordid": "d8746118429eb118f38ecbee904636d9b33fa8ba", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.867092, 2.363288], "address": "Place de la R\u00e9publique", "name": "Le caf\u00e9 Monde et M\u00e9dias", "date": "2013-08-22", "zipcode": 75003, "geometry": {"coordinates": [2.363288, 48.867092], "type": "Point"}, "recordid": "af04c90f25e25daf7f5cbbab1bc740bac26541d4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.849821, 2.355337], "address": "11 Quai de la Tournelle", "name": "Caf\u00e9 rallye tournelles", "date": "2013-08-22", "zipcode": 75005, "geometry": {"coordinates": [2.355337, 48.849821], "type": "Point"}, "recordid": "91d88e321a75b6a8c4dea816c399fda77c41f9d1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.872498, 2.355136], "address": "61 rue du ch\u00e2teau d'eau", "name": "Brasserie le Morvan", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.355136, 48.872498], "type": "Point"}, "recordid": "6ec62058995948fc18d331f01a2d03acc0d9e0fa", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "1", "geoloc": [48.874879, 2.386064], "address": "6 rue M\u00e9lingue", "name": "Chez Miamophile", "date": "2013-08-22", "zipcode": 75019, "geometry": {"coordinates": [2.386064, 48.874879], "type": "Point"}, "recordid": "13924872737e4fc640a43da58937c3777c2ac753", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.864269, 2.36858], "address": "18 rue de Crussol", "name": "Panem", "date": "2012-05-11", "zipcode": 75011, "geometry": {"coordinates": [2.36858, 48.864269], "type": "Point"}, "recordid": "67bdf3a6989f80749a1ba33a17b1370de0a0e1cd", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.885662, 2.319591], "address": "47 rue de Batignolles", "name": "Petits Freres des Pauvres", "date": "2012-05-11", "zipcode": 75017, "geometry": {"coordinates": [2.319591, 48.885662], "type": "Point"}, "recordid": "e27fd00149514bbfad7dd7e8f9b0c677df2d3f25", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.837212, 2.296046], "address": "198 rue de la Convention", "name": "Caf\u00e9 Dupont", "date": "2012-05-11", "zipcode": 75015, "geometry": {"coordinates": [2.296046, 48.837212], "type": "Point"}, "recordid": "4d40e6d864dae81c152a05cb98e30933bde96aa1", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.871002, 2.30879], "address": "28 rue de Ponthieu", "name": "L'Angle", "date": "2012-10-22", "zipcode": 75008, "geometry": {"coordinates": [2.30879, 48.871002], "type": "Point"}, "recordid": "c40bd2d1f98b415e539c27cf68518d060ebab51e", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.888023, 2.353467], "address": "19-23 rue L\u00e9on", "name": "Institut des Cultures d'Islam", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.353467, 48.888023], "type": "Point"}, "recordid": "68d6d37b846e39bd8554e1f8f75974b486b0f27b", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.886044, 2.360781], "address": "19 rue Pajol", "name": "Canopy Caf\u00e9 associatif", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.360781, 48.886044], "type": "Point"}, "recordid": "ff73bafb514bb68eb925c81aee43c3a58ac3c70d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.870287, 2.332491], "address": "place de l'opera", "name": "L'Entracte", "date": "2012-10-09", "zipcode": 75002, "geometry": {"coordinates": [2.332491, 48.870287], "type": "Point"}, "recordid": "0039cd8bceb5e281677a158f832a660789088071", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.858709, 2.362701], "address": "15 rue du Parc Royal", "name": "Le S\u00e9vign\u00e9", "date": "2012-10-09", "zipcode": 75003, "geometry": {"coordinates": [2.362701, 48.858709], "type": "Point"}, "recordid": "adcc8b4f78f05ba7b24b0593e1516dfb7b415f91", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.839687, 2.347254], "address": "35 rue Claude Bernard", "name": "Le Caf\u00e9 d'avant", "date": "2012-10-09", "zipcode": 75005, "geometry": {"coordinates": [2.347254, 48.839687], "type": "Point"}, "recordid": "b904fc48763938eee2169ba25aad2ffcc0dd6a9f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.844244, 2.330407], "address": "53 rue Notre-Dame des Champs", "name": "Le Lucernaire", "date": "2012-10-09", "zipcode": 75006, "geometry": {"coordinates": [2.330407, 48.844244], "type": "Point"}, "recordid": "cc72af04314fd40e16ff611c799d378515043508", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.877599, 2.332111], "address": "12 rue Blanche", "name": "Le Brigadier", "date": "2012-10-09", "zipcode": 75009, "geometry": {"coordinates": [2.332111, 48.877599], "type": "Point"}, "recordid": "978d4bc68c9ebf81029d3e77274d2107777b8a75", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.826494, 2.359987], "address": "26 rue du Docteur Magnan", "name": "L'\u00e2ge d'or", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.359987, 48.826494], "type": "Point"}, "recordid": "40bffbdc0c9ed1cbce820fed875d7c21d8964640", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.883717, 2.326861], "address": "Place de Clichy", "name": "Bagels & Coffee Corner", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.326861, 48.883717], "type": "Point"}, "recordid": "262facde9b8c4568c9ba7fbce8f069ff8c76948d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.835843, 2.278501], "address": "10 boulevard Victor", "name": "Caf\u00e9 Victor", "date": "2012-03-07", "zipcode": 75015, "geometry": {"coordinates": [2.278501, 48.835843], "type": "Point"}, "recordid": "e5817ec44ac5a7ea2e4a34b6a2e13d535156642b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.845337, 2.379024], "address": "54, avenue Daumesnil", "name": "L'empreinte", "date": "2012-03-07", "zipcode": 75012, "geometry": {"coordinates": [2.379024, 48.845337], "type": "Point"}, "recordid": "b96ddd35cbbf5d93aaff79487afdf083b5ff0817", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.857312, 2.379055], "address": "93, rue de la Roquette", "name": "L'horizon", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.379055, 48.857312], "type": "Point"}, "recordid": "84c6b7335e7f82ac942c4f398723ec99076f148d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.835878, 2.395723], "address": "34 bis rue de Wattignies", "name": "Au pays de Vannes", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.395723, 48.835878], "type": "Point"}, "recordid": "a17869dbb9d0d5b1e5ed7bb288053900b04ee944", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.85413, 2.323539], "address": "36 rue de Varenne", "name": "Caf\u00e9 Varenne", "date": "2012-06-27", "zipcode": 75007, "geometry": {"coordinates": [2.323539, 48.85413], "type": "Point"}, "recordid": "a26ec0d5fca47b8de77d862ad8a99b75bb520a09", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.855161, 2.360218], "address": "125 Rue Saint-Antoine", "name": "l'El\u00e9phant du nil", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.360218, 48.855161], "type": "Point"}, "recordid": "7b7ceefd1f9ed85041265c9577e0dc8bee01d45a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.8357, 2.292961], "address": "354 bis rue Vaugirard", "name": "Le Comptoir", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.292961, 48.8357], "type": "Point"}, "recordid": "59d8fa304e535f4eb41f9746028034c9b30cbde4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.835451, 2.292515], "address": "358 rue de Vaugirard", "name": "Le Parc Vaugirard", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.292515, 48.835451], "type": "Point"}, "recordid": "19f655206a8446959c8e796c2b3cb9001890f985", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.834972, 2.327007], "address": "58 rue Daguerre", "name": "le Zango", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.327007, 48.834972], "type": "Point"}, "recordid": "e1b54109015316a822747f788128f997a3478050", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.848887, 2.399972], "address": "3 rue de Lagny", "name": "Melting Pot", "date": "2012-06-27", "zipcode": 75020, "geometry": {"coordinates": [2.399972, 48.848887], "type": "Point"}, "recordid": "fd0de2cbf73e0a728cd73e4e2a9a4a9c646f76f2", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, +{"prix_terasse": "-", "geoloc": [48.892366, 2.317359], "address": "174 avenue de Clichy", "name": "Pari's Caf\u00e9", "date": "2012-06-27", "zipcode": 75017, "geometry": {"coordinates": [2.317359, 48.892366], "type": "Point"}, "recordid": "831446ae203f89de26d3300e625c20717e82d40a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}] \ No newline at end of file diff --git a/bonobo/examples/datasets/coffeeshops.ldjson b/bonobo/examples/datasets/coffeeshops.ldjson index 30a5316..252c1a4 100644 --- a/bonobo/examples/datasets/coffeeshops.ldjson +++ b/bonobo/examples/datasets/coffeeshops.ldjson @@ -1,181 +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"} \ No newline at end of file +{"prix_terasse": "-", "geoloc": [48.839512, 2.303007], "address": "344Vrue Vaugirard", "name": "Coffee Chope", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.303007, 48.839512], "type": "Point"}, "recordid": "3c276428d45ad68ccdf6875e4ddcfe95d0c0d4cf", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.876737, 2.357601], "address": "5, rue d'Alsace", "name": "Ext\u00e9rieur Quai", "date": "2014-02-01", "zipcode": 75010, "geometry": {"coordinates": [2.357601, 48.876737], "type": "Point"}, "recordid": "97ad81cd1127a8566085ad796eeb44a06bec7514", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.850852, 2.362029], "address": "6 Bd henri IV", "name": "Le Sully", "date": "2014-02-01", "zipcode": 75004, "geometry": {"coordinates": [2.362029, 48.850852], "type": "Point"}, "recordid": "aa4294c1b8d660a23db0dc81321e509bae1dae68", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "1", "geoloc": [48.893517, 2.340271], "address": "53 rue du ruisseau", "name": "O q de poule", "date": "2013-08-22", "zipcode": 75018, "geometry": {"coordinates": [2.340271, 48.893517], "type": "Point"}, "recordid": "a81362dbed35247555fb105bd83ff2906904a66e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.864655, 2.350089], "address": "1 Passage du Grand Cerf", "name": "Le Pas Sage", "date": "2013-08-22", "zipcode": 75002, "geometry": {"coordinates": [2.350089, 48.864655], "type": "Point"}, "recordid": "7ced86acbd5ccfba229bcc07d70d0d117aee16a5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.895825, 2.339712], "address": "112 Rue Championnet", "name": "La Renaissance", "date": "2013-08-22", "zipcode": 75018, "geometry": {"coordinates": [2.339712, 48.895825], "type": "Point"}, "recordid": "5582c8572bd7637bf305b74c1c0bdb74a8e4247f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.868581, 2.373015], "address": "Rue de la Fontaine au Roi", "name": "La Caravane", "date": "2012-05-11", "zipcode": 75011, "geometry": {"coordinates": [2.373015, 48.868581], "type": "Point"}, "recordid": "50bb0fa06e562a242f115ddbdae2ed9c7df93d57", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.875155, 2.335536], "address": "51 Rue Victoire", "name": "Le chantereine", "date": "2012-10-22", "zipcode": 75009, "geometry": {"coordinates": [2.335536, 48.875155], "type": "Point"}, "recordid": "eb8a62feeedaf7ed8b8c912305270ee857068689", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.886536, 2.346525], "address": "11 rue Feutrier", "name": "Le M\u00fcller", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.346525, 48.886536], "type": "Point"}, "recordid": "62c552f167f671f88569c1f2d6a44098fb514c51", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.841494, 2.307117], "address": "21 rue Copreaux", "name": "Le drapeau de la fidelit\u00e9", "date": "2012-10-22", "zipcode": 75015, "geometry": {"coordinates": [2.307117, 48.841494], "type": "Point"}, "recordid": "5120ea0b9d7387766072b90655166486928e25c8", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.839743, 2.296898], "address": "125 rue Blomet", "name": "Le caf\u00e9 des amis", "date": "2012-10-22", "zipcode": 75015, "geometry": {"coordinates": [2.296898, 48.839743], "type": "Point"}, "recordid": "865f62415adc5c34e3ca38a1748b7a324dfba209", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.857728, 2.349641], "address": "10 rue Saint Martin", "name": "Le Caf\u00e9 Livres", "date": "2012-10-09", "zipcode": 75004, "geometry": {"coordinates": [2.349641, 48.857728], "type": "Point"}, "recordid": "7ef54a78802d49cafd2701458df2b0d0530d123b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.856003, 2.30457], "address": "46 avenue Bosquet", "name": "Le Bosquet", "date": "2012-10-09", "zipcode": 75007, "geometry": {"coordinates": [2.30457, 48.856003], "type": "Point"}, "recordid": "d701a759e08a71f4bbb01f29473274b0152135d0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.889426, 2.332954], "address": "12 rue Armand Carrel", "name": "Le Chaumontois", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.332954, 48.889426], "type": "Point"}, "recordid": "e12ff00a644c91ad910ddc63a770c190be93a393", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.838521, 2.370478], "address": "34 avenue Pierre Mend\u00e8s-France", "name": "Le Kleemend's", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.370478, 48.838521], "type": "Point"}, "recordid": "0f6cd1ee7751b00c9574efcfdcf66fa0e857d251", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.849861, 2.385342], "address": "202 rue du faubourg st antoine", "name": "Caf\u00e9 Pierre", "date": "2012-03-07", "zipcode": 75012, "geometry": {"coordinates": [2.385342, 48.849861], "type": "Point"}, "recordid": "f9de9d0fb5e92f047a6f1986a31f9dd4d38bcb36", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.872202, 2.304624], "address": "61 rue de Ponthieu", "name": "Les Arcades", "date": "2012-03-07", "zipcode": 75008, "geometry": {"coordinates": [2.304624, 48.872202], "type": "Point"}, "recordid": "67eaf58afc856077c0680601e453e75c0922c9c0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.859031, 2.320315], "address": "31 rue Saint-Dominique", "name": "Le Square", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.320315, 48.859031], "type": "Point"}, "recordid": "678558317bc9ad46652e5b1643e70c2142a76e7e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.850092, 2.37463], "address": "75, avenue Ledru-Rollin", "name": "Assaporare Dix sur Dix", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.37463, 48.850092], "type": "Point"}, "recordid": "667474321887d08a3cc636adf043ad354b65fa61", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.86805, 2.353313], "address": "129 boulevard sebastopol", "name": "Au cerceau d'or", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.353313, 48.86805], "type": "Point"}, "recordid": "c9ef52ba2fabe0286700329f18bbbbea9a10b474", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.845927, 2.373051], "address": "21 ter boulevard Diderot", "name": "Aux cadrans", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.373051, 48.845927], "type": "Point"}, "recordid": "ed5f98686856bf4ddd2b381b43ad229246741a90", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.851662, 2.273883], "address": "17 rue Jean de la Fontaine", "name": "Caf\u00e9 antoine", "date": "2012-06-27", "zipcode": 75016, "geometry": {"coordinates": [2.273883, 48.851662], "type": "Point"}, "recordid": "ab6d1e054e2e6ae7d6150013173f55e83c05ca23", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.877642, 2.312823], "address": "rue de Lisbonne", "name": "Caf\u00e9 de la Mairie (du VIII)", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.312823, 48.877642], "type": "Point"}, "recordid": "7de8a79b026ac63f453556612505b5bcd9229036", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.838633, 2.349916], "address": "5 rue Claude Bernard", "name": "Caf\u00e9 Lea", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.349916, 48.838633], "type": "Point"}, "recordid": "fecd8900cf83027f74ceced9fc4ad80ac73b63a7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.849293, 2.354486], "address": "11 boulevard Saint-Germain", "name": "Cardinal Saint-Germain", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.354486, 48.849293], "type": "Point"}, "recordid": "e4a078c30c98082896787f4e4b41a07554392529", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.869771, 2.342501], "address": "52 rue Notre-Dame des Victoires", "name": "D\u00e9d\u00e9 la frite", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.342501, 48.869771], "type": "Point"}, "recordid": "ccb2ba2f98043e8eefd5fda829dee1ea7f1d2c7a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.834051, 2.287345], "address": "36 rue du hameau", "name": "La Bauloise", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.287345, 48.834051], "type": "Point"}, "recordid": "c9fe10abd15ede7ccaeb55c309898d30d7b19d0e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.888165, 2.377387], "address": "71 quai de Seine", "name": "Le Bellerive", "date": "2012-06-27", "zipcode": 75019, "geometry": {"coordinates": [2.377387, 48.888165], "type": "Point"}, "recordid": "4e0b5c2d33d7c25fc54c51171f3d37e509959fc0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.864543, 2.340997], "address": "42 rue coquill\u00e8re", "name": "Le bistrot de Ma\u00eblle et Augustin", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.340997, 48.864543], "type": "Point"}, "recordid": "52acab12469af291984e9a70962e08c72b058e10", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.872103, 2.346161], "address": "14 rue Rougemont", "name": "Le Dellac", "date": "2012-06-27", "zipcode": 75009, "geometry": {"coordinates": [2.346161, 48.872103], "type": "Point"}, "recordid": "4d1d627ecea2ffa279bb862f8ba495d95ca75350", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.859645, 2.355598], "address": "1 rue Pecquay", "name": "Le Felteu", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.355598, 48.859645], "type": "Point"}, "recordid": "2c1fa55460af282266d86fd003af4f929fdf4e7d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.85763, 2.346101], "address": "2 bis quai de la m\u00e9gisserie", "name": "Le Reynou", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.346101, 48.85763], "type": "Point"}, "recordid": "d4ddd30ab3e721a317fc7ea89d5b9001255ce9f4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.884646, 2.337734], "address": "23 rue des abbesses", "name": "Le Saint Jean", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.337734, 48.884646], "type": "Point"}, "recordid": "51b47cf167b7f32eeebb108330956694d75d4268", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.841007, 2.31466], "address": "65 boulevard Pasteur", "name": "les montparnos", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.31466, 48.841007], "type": "Point"}, "recordid": "2aaca891ffd0694c657a43889516ab72afdfba07", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.850323, 2.33039], "address": "16 rue DE MEZIERES", "name": "L'antre d'eux", "date": "2014-02-01", "zipcode": 75006, "geometry": {"coordinates": [2.33039, 48.850323], "type": "Point"}, "recordid": "4ff4337934c66f61e00d1d9551f7cdddba03e544", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.864957, 2.346938], "address": "58 rue de Montorgueil", "name": "Drole d'endroit pour une rencontre", "date": "2014-02-01", "zipcode": 75002, "geometry": {"coordinates": [2.346938, 48.864957], "type": "Point"}, "recordid": "3451657f880abe75d0c7e386fc698405556c53e8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.889565, 2.339735], "address": "104 rue caulaincourt", "name": "Le pari's caf\u00e9", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.339735, 48.889565], "type": "Point"}, "recordid": "e8c34a537b673fcb26c76e02deca4f5a728929dc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.859115, 2.368871], "address": "60 rue saint-sabin", "name": "Le Poulailler", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.368871, 48.859115], "type": "Point"}, "recordid": "325ea74ba83f716dde87c08cffd36f7df7722a49", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.833595, 2.38604], "address": "33 Cour Saint Emilion", "name": "Chai 33", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.38604, 48.833595], "type": "Point"}, "recordid": "528de8d5d8780bee83145637e315483d48f5ae3c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.868741, 2.379969], "address": "99 rue Jean-Pierre Timbaud", "name": "L'Assassin", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.379969, 48.868741], "type": "Point"}, "recordid": "fac0483890ff8bdaeb3feddbdb032c5112f24678", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.851463, 2.398691], "address": "1 rue d'Avron", "name": "l'Usine", "date": "2014-02-01", "zipcode": 75020, "geometry": {"coordinates": [2.398691, 48.851463], "type": "Point"}, "recordid": "fee1e3eb103bbc98e19e45d34365da0f27166541", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.896305, 2.332898], "address": "52 rue Liebniz", "name": "La Bricole", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.332898, 48.896305], "type": "Point"}, "recordid": "4744e866c244c59eec43b3fe159542d2ef433065", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.850311, 2.34885], "address": "place maubert", "name": "le ronsard", "date": "2014-02-01", "zipcode": 75005, "geometry": {"coordinates": [2.34885, 48.850311], "type": "Point"}, "recordid": "49a390322b45246bc2c1e50fcd46815ad271bca0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.863038, 2.3604], "address": "82 rue des archives", "name": "Face Bar", "date": "2014-02-01", "zipcode": 75003, "geometry": {"coordinates": [2.3604, 48.863038], "type": "Point"}, "recordid": "d96e16ebf2460bb2f6c34198918a071233725cbc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.872746, 2.366392], "address": "49 rue bichat", "name": "American Kitchen", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.366392, 48.872746], "type": "Point"}, "recordid": "6b9395475cbbbbbacbaaeb070f71d31c2d183dc4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.870598, 2.365413], "address": "55 bis quai de valmy", "name": "La Marine", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.365413, 48.870598], "type": "Point"}, "recordid": "d4d2f92d27f38de59e57744f434781e61283551c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.889101, 2.318001], "address": "21 avenue Brochant", "name": "Le Bloc", "date": "2013-08-22", "zipcode": 75017, "geometry": {"coordinates": [2.318001, 48.889101], "type": "Point"}, "recordid": "e425882ee969d1e8bffe7234336ae40da88c8439", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.874697, 2.405421], "address": "229 avenue Gambetta", "name": "La Recoleta au Manoir", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.405421, 48.874697], "type": "Point"}, "recordid": "02de82cffb2918beafb740f4e924029d470b07a1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.847344, 2.286078], "address": "80 Rue Saint-Charles", "name": "Le Pareloup", "date": "2013-08-22", "zipcode": 75015, "geometry": {"coordinates": [2.286078, 48.847344], "type": "Point"}, "recordid": "0227ca95f76bb6097ae0a0e6f455af2624d49ae3", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} +{"prix_terasse": "1", "geoloc": [48.840771, 2.324589], "address": "3 rue de la Gait\u00e9", "name": "La Brasserie Gait\u00e9", "date": "2013-08-22", "zipcode": 75014, "geometry": {"coordinates": [2.324589, 48.840771], "type": "Point"}, "recordid": "e7c4cba08749c892a73db2715d06623d9e0c2f67", "prix_salle": "-", "prix_comptoir": null, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.875232, 2.336036], "address": "46 rue Victoire", "name": "Caf\u00e9 Zen", "date": "2012-05-11", "zipcode": 75009, "geometry": {"coordinates": [2.336036, 48.875232], "type": "Point"}, "recordid": "5e9a6172f8b64cd098a5f2cf9b1d42567fe0a894", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.872677, 2.315276], "address": "27 rue de Penthi\u00e8vre", "name": "O'Breizh", "date": "2012-10-22", "zipcode": 75008, "geometry": {"coordinates": [2.315276, 48.872677], "type": "Point"}, "recordid": "ead3108add27ef41bb92517aca834f7d7f632816", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.868838, 2.33605], "address": "23 rue saint augustin", "name": "Le Petit Choiseul", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.33605, 48.868838], "type": "Point"}, "recordid": "de601277ca00567b62fa4f5277e4a17679faa753", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.841526, 2.351012], "address": "7 rue Ep\u00e9e de Bois", "name": "Invitez vous chez nous", "date": "2012-10-22", "zipcode": 75005, "geometry": {"coordinates": [2.351012, 48.841526], "type": "Point"}, "recordid": "1d2373bdec8a07306298e3ee54894ac295ee1d55", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.86525, 2.350507], "address": "142 Rue Saint-Denis 75002 Paris", "name": "La Cordonnerie", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.350507, 48.86525], "type": "Point"}, "recordid": "5c9bf60617a99ad75445b454f98e75e1a104021d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.892244, 2.346973], "address": "3, rue Baudelique", "name": "Le Supercoin", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.346973, 48.892244], "type": "Point"}, "recordid": "68a4ee10f1fc4d2a659501e811d148420fa80e95", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.890043, 2.362241], "address": "86 bis rue Riquet", "name": "Populettes", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.362241, 48.890043], "type": "Point"}, "recordid": "8cc55d58d72621a7e91cf6b456731d2cb2863afc", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "1", "geoloc": [48.893017, 2.337776], "address": "49 rue des Cloys", "name": "Au bon coin", "date": "2012-10-18", "zipcode": 75018, "geometry": {"coordinates": [2.337776, 48.893017], "type": "Point"}, "recordid": "0408f272c08c52e3cae035ffdeb8928698787ea9", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.836919, 2.347003], "address": "69 rue Broca", "name": "Le Couvent", "date": "2012-10-18", "zipcode": 75013, "geometry": {"coordinates": [2.347003, 48.836919], "type": "Point"}, "recordid": "729b2f228d3fd2db6f78bb624d451f59555e4a04", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.840624, 2.349766], "address": "111 rue mouffetard", "name": "La Br\u00fblerie des Ternes", "date": "2012-10-18", "zipcode": 75005, "geometry": {"coordinates": [2.349766, 48.840624], "type": "Point"}, "recordid": "233dd2a17620cd5eae70fef11cc627748e3313d5", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.832825, 2.336116], "address": "59 Boulevard Saint-Jacques", "name": "L'\u00c9cir", "date": "2012-10-09", "zipcode": 75014, "geometry": {"coordinates": [2.336116, 48.832825], "type": "Point"}, "recordid": "4a44324a5a806801fd3e05af89cad7c0f1e69d1e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.850696, 2.378417], "address": "126, rue du Faubourg Saint Antoine", "name": "Le Chat bossu", "date": "2012-10-09", "zipcode": 75012, "geometry": {"coordinates": [2.378417, 48.850696], "type": "Point"}, "recordid": "d1d02463f7c90d38cccffd898e15f51e65910baf", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.834157, 2.33381], "address": "58 boulvevard Saint Jacques", "name": "Denfert caf\u00e9", "date": "2012-10-09", "zipcode": 75014, "geometry": {"coordinates": [2.33381, 48.834157], "type": "Point"}, "recordid": "f78f406e5ccb95cb902ce618ed54eba1d4776a3c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.867948, 2.343582], "address": "95 rue Montmartre", "name": "Le Caf\u00e9 frapp\u00e9", "date": "2012-10-09", "zipcode": 75002, "geometry": {"coordinates": [2.343582, 48.867948], "type": "Point"}, "recordid": "4dd2a924a7b2c5f061cecaba2f272548a8c83c6c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.859772, 2.360558], "address": "78 rue vieille du temple", "name": "La Perle", "date": "2012-10-09", "zipcode": 75003, "geometry": {"coordinates": [2.360558, 48.859772], "type": "Point"}, "recordid": "476c54e643613ec36a9b1c533f32122fd873f3c3", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.845047, 2.349583], "address": "1 rue Thouin", "name": "Le Descartes", "date": "2012-10-09", "zipcode": 75005, "geometry": {"coordinates": [2.349583, 48.845047], "type": "Point"}, "recordid": "e9cb0b0d6b6b512e9ecab889267ba342a4f0ea93", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.830151, 2.334213], "address": "55 rue de la tombe Issoire", "name": "Le petit club", "date": "2012-03-07", "zipcode": 75014, "geometry": {"coordinates": [2.334213, 48.830151], "type": "Point"}, "recordid": "c6ca1166fa7fd7050f5d1c626a1e17050116c91e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.865707, 2.374382], "address": "90 avenue Parmentier", "name": "Le Plein soleil", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.374382, 48.865707], "type": "Point"}, "recordid": "95cf5fb735bd19826db70a3af4fe72fce647d4e5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.875322, 2.312329], "address": "146, boulevard Haussmann", "name": "Le Relais Haussmann", "date": "2012-03-07", "zipcode": 75008, "geometry": {"coordinates": [2.312329, 48.875322], "type": "Point"}, "recordid": "6c5f68f47c916638342b77bd5edfc30bd7051303", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.859559, 2.30643], "address": "88 rue Saint-Dominique", "name": "Le Malar", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.30643, 48.859559], "type": "Point"}, "recordid": "c633cb01686aa5f3171bf59976dc9b7f23cbca54", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.864628, 2.408038], "address": "47 rue Belgrand", "name": "Au panini de la place", "date": "2012-03-07", "zipcode": 75020, "geometry": {"coordinates": [2.408038, 48.864628], "type": "Point"}, "recordid": "66dedd35fcbbbb24f328882d49098de7aa5f26ba", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.88435, 2.297978], "address": "182 rue de Courcelles", "name": "Le Village", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.297978, 48.88435], "type": "Point"}, "recordid": "1e07eaf8a93875906d0b18eb6a897c651943589a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.853381, 2.376706], "address": "41 rue de Charonne", "name": "Pause Caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.376706, 48.853381], "type": "Point"}, "recordid": "60d98c3236a70824df50e9aca83e7d7f13a310c5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.853253, 2.383415], "address": "14 rue Jean Mac\u00e9", "name": "Le Pure caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.383415, 48.853253], "type": "Point"}, "recordid": "66707fb2e707d2145fc2eb078a1b980a45921616", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.848873, 2.392859], "address": "307 fg saint Antoine", "name": "Extra old caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.392859, 48.848873], "type": "Point"}, "recordid": "039ec7dcb219cfc434547b06938ba497afeb83b4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.873227, 2.360787], "address": "44 rue Vinaigriers", "name": "Chez Fafa", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.360787, 48.873227], "type": "Point"}, "recordid": "1572d199f186bf86d7753fe71ac23477a7a8bd2c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.850836, 2.384069], "address": "3 rue Faidherbe", "name": "En attendant l'or", "date": "2012-06-27", "zipcode": 75011, "geometry": {"coordinates": [2.384069, 48.850836], "type": "Point"}, "recordid": "de5789bb4a4ffbd244cded8dc555639dbe7d2279", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.866993, 2.336006], "address": "30 rue des Petits-Champs", "name": "Br\u00fblerie San Jos\u00e9", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.336006, 48.866993], "type": "Point"}, "recordid": "b736e5fa17396ee56a642212ccd0ab29c7f2bef1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.856434, 2.342683], "address": "2 place Martin Nadaud", "name": "Caf\u00e9 Martin", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.342683, 48.856434], "type": "Point"}, "recordid": "25fbf857029c54d57909c158e3039349b77344ed", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.863675, 2.348701], "address": "14 rue Turbigo, Paris", "name": "Etienne", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.348701, 48.863675], "type": "Point"}, "recordid": "0190edd7b0766c6d3e43093deb41abe9446c1b22", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.854584, 2.385193], "address": "184 bd Voltaire", "name": "L'ing\u00e9nu", "date": "2012-06-27", "zipcode": 75011, "geometry": {"coordinates": [2.385193, 48.854584], "type": "Point"}, "recordid": "7fcef7475ec632d5c61c9c36c1d52a402ad2e9e8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.890605, 2.361349], "address": "8 rue L'Olive", "name": "L'Olive", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.361349, 48.890605], "type": "Point"}, "recordid": "35286273f281c8c5082b3d3bd17f6bbf207426f9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.871396, 2.338321], "address": "18 rue Favart", "name": "Le Biz", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.338321, 48.871396], "type": "Point"}, "recordid": "cf2f6b9e283aaeeca2214cc1fe57b45e45668e25", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.868109, 2.331785], "address": "1 rue Louis le Grand", "name": "Le Cap Bourbon", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.331785, 48.868109], "type": "Point"}, "recordid": "339030e95e0846b41a8e2b91045456c4e4a50043", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.84167, 2.303053], "address": "9 Place du General Beuret", "name": "Le General Beuret", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.303053, 48.84167], "type": "Point"}, "recordid": "12b37036d28d28b32ebe81756ad15eb68372947f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.846814, 2.289311], "address": "95 avenue Emile Zola", "name": "Le Germinal", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.289311, 48.846814], "type": "Point"}, "recordid": "4e03831a64e886a28a7232e54f2812c1ced23c5a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.862655, 2.337607], "address": "202 rue Saint-Honor\u00e9", "name": "Le Ragueneau", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.337607, 48.862655], "type": "Point"}, "recordid": "e303131b2600d4c2287749a36bf7193d2fa60bd7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.889982, 2.338933], "address": "72 rue lamarck", "name": "Le refuge", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.338933, 48.889982], "type": "Point"}, "recordid": "f64310461736a769c6854fdefb99b9f2e7b230a9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.870294, 2.352821], "address": "13 rue du Faubourg Saint Denis", "name": "Le sully", "date": "2012-06-27", "zipcode": 75010, "geometry": {"coordinates": [2.352821, 48.870294], "type": "Point"}, "recordid": "166be8588ff16e50838fa6a164d1e580497b795d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.842128, 2.280374], "address": "60 rue des bergers", "name": "Le bal du pirate", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.280374, 48.842128], "type": "Point"}, "recordid": "93ff6e35406a074a6ba2667d2b286abf91132f6a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.838769, 2.39609], "address": "95 rue claude decaen", "name": "zic zinc", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.39609, 48.838769], "type": "Point"}, "recordid": "8fdc739d64ff1f01973235301e3ec86791016759", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.870247, 2.376306], "address": "35 rue de l'orillon", "name": "l'orillon bar", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.376306, 48.870247], "type": "Point"}, "recordid": "6e6e9695ef04c3fdbd4cfa19f0cfb0c0f93f4933", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.869848, 2.394247], "address": "116 Rue de M\u00e9nilmontant", "name": "Le Zazabar", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.394247, 48.869848], "type": "Point"}, "recordid": "22edfe92a72ce1bb0eea972508b5caa7af2db2df", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.845458, 2.354796], "address": "22 rue Linn\u00e9", "name": "L'In\u00e9vitable", "date": "2013-08-22", "zipcode": 75005, "geometry": {"coordinates": [2.354796, 48.845458], "type": "Point"}, "recordid": "6893cb08e99319091de9ba80305f22e0ce4cc08d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.83336, 2.365782], "address": "77 rue Dunois", "name": "Le Dunois", "date": "2013-08-22", "zipcode": 75013, "geometry": {"coordinates": [2.365782, 48.83336], "type": "Point"}, "recordid": "c3be1246dbc4ca5734d5bbd569436ba655105248", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.862655, 2.337607], "address": "202 rue Saint Honor\u00e9", "name": "Ragueneau", "date": "2012-05-11", "zipcode": 75001, "geometry": {"coordinates": [2.337607, 48.862655], "type": "Point"}, "recordid": "e946d9e8f8c5a130f98eca945efadfd9eec40dcb", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": null, "geoloc": [48.826608, 2.374571], "address": "48 rue du Dessous des Berges", "name": "Le Caminito", "date": "2012-10-22", "zipcode": 75013, "geometry": {"coordinates": [2.374571, 48.826608], "type": "Point"}, "recordid": "5d9ad8bcfbbc20adaec2aa7dcdb71326868c7686", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.870598, 2.365413], "address": "55bis quai de Valmy", "name": "Epicerie Musicale", "date": "2012-10-22", "zipcode": 75010, "geometry": {"coordinates": [2.365413, 48.870598], "type": "Point"}, "recordid": "8ffea133d93c608d337bc0129f4f9f3d5cad8dae", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": null, "address": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital", "name": "Le petit Bretonneau", "date": "2012-10-22", "zipcode": 75018, "geometry": {}, "recordid": "dd1ffdbd55c2dc651201302b8015258f7d35fd35", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.862575, 2.367427], "address": "104 rue amelot", "name": "Le Centenaire", "date": "2012-10-22", "zipcode": 75011, "geometry": {"coordinates": [2.367427, 48.862575], "type": "Point"}, "recordid": "e47d25752c2c8bcab5efb0e3a41920ec7a8a766a", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.842833, 2.348314], "address": "13 Rue du Pot de Fer", "name": "La Montagne Sans Genevi\u00e8ve", "date": "2012-10-22", "zipcode": 75005, "geometry": {"coordinates": [2.348314, 48.842833], "type": "Point"}, "recordid": "314d170601f81e3a3f26d8801f0fbee39981c788", "prix_salle": "1", "prix_comptoir": null, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.851325, 2.40171], "address": "46 rue de Buzenval", "name": "Les P\u00e8res Populaires", "date": "2012-10-18", "zipcode": 75020, "geometry": {"coordinates": [2.40171, 48.851325], "type": "Point"}, "recordid": "e75e8a3fe6212a0e576beec82f0128dd394e56fa", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.857658, 2.305613], "address": "188 rue de Grenelle", "name": "Cafe de grenelle", "date": "2012-10-09", "zipcode": 75007, "geometry": {"coordinates": [2.305613, 48.857658], "type": "Point"}, "recordid": "72e274046d671e68bc7754808feacfc95b91b6ed", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.875207, 2.332944], "address": "73 rue de la Victoire", "name": "Le relais de la victoire", "date": "2012-10-09", "zipcode": 75009, "geometry": {"coordinates": [2.332944, 48.875207], "type": "Point"}, "recordid": "01c21abdf35484f9ad184782979ecd078de84523", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": null, "address": "Route de la Muette \u00e0 Neuilly\nClub hippique du Jardin d\u2019Acclimatation", "name": "La chaumi\u00e8re gourmande", "date": "2012-03-07", "zipcode": 75016, "geometry": {}, "recordid": "438ec18d35793d12eb6a137373c3fe4f3aa38a69", "prix_salle": "1", "prix_comptoir": null, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.891882, 2.33365], "address": "216, rue Marcadet", "name": "Le Brio", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.33365, 48.891882], "type": "Point"}, "recordid": "49e3584ce3d6a6a236b4a0db688865bdd3483fec", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.884753, 2.324648], "address": "22 rue des Dames", "name": "Caves populaires", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.324648, 48.884753], "type": "Point"}, "recordid": "af81e5eca2b84ea706ac2d379edf65b0fb2f879a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.827428, 2.325652], "address": "12 avenue Jean Moulin", "name": "Caprice caf\u00e9", "date": "2012-03-07", "zipcode": 75014, "geometry": {"coordinates": [2.325652, 48.827428], "type": "Point"}, "recordid": "88e07bdb723b49cd27c90403b5930bca1c93b458", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.832964, 2.369266], "address": "7 rue Clisson", "name": "Tamm Bara", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.369266, 48.832964], "type": "Point"}, "recordid": "baf59c98acafbe48ed9e91b64377da216a20cbcc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.876577, 2.348414], "address": "1 rue de Montholon", "name": "L'anjou", "date": "2012-03-07", "zipcode": 75009, "geometry": {"coordinates": [2.348414, 48.876577], "type": "Point"}, "recordid": "e6f5949dca40548aad296208c61c498f639f648c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.862599, 2.315086], "address": "2 rue Robert Esnault Pelterie", "name": "Caf\u00e9 dans l'aerogare Air France Invalides", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.315086, 48.862599], "type": "Point"}, "recordid": "6d175eb48c577fafdfc99df0ab55da468cf17164", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.844854, 2.345413], "address": "10 rue d\"Ulm", "name": "Waikiki", "date": "2012-03-07", "zipcode": 75005, "geometry": {"coordinates": [2.345413, 48.844854], "type": "Point"}, "recordid": "fb1e2bc2ae55d3d47da682c71093a12fa64fbd45", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.871576, 2.364499], "address": "36 rue Beaurepaire", "name": "Chez Prune", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.364499, 48.871576], "type": "Point"}, "recordid": "19399ede5b619761877822185bbb4c98b565974c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.833863, 2.329046], "address": "21 rue Boulard", "name": "Au Vin Des Rues", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.329046, 48.833863], "type": "Point"}, "recordid": "87263873b6f8346b5777844be0122a307f29fcab", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.838137, 2.301166], "address": "14 rue d'alleray", "name": "bistrot les timbr\u00e9s", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.301166, 48.838137], "type": "Point"}, "recordid": "74b6d7113e5b14eb8e890663f061208bf4ff6728", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.871799, 2.315985], "address": "9 rue de Miromesnil", "name": "Caf\u00e9 beauveau", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.315985, 48.871799], "type": "Point"}, "recordid": "2759f5cdda4bcb88ca3ae2f7299b37b8e62596c8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.866259, 2.338739], "address": "9 rue des petits champs", "name": "Caf\u00e9 Pistache", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.338739, 48.866259], "type": "Point"}, "recordid": "6d2675cdc912118d0376229be8e436feca9c8af7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.874605, 2.387738], "address": "13 Rue Jean-Baptiste Dumay", "name": "La Cagnotte", "date": "2012-06-27", "zipcode": 75020, "geometry": {"coordinates": [2.387738, 48.874605], "type": "Point"}, "recordid": "f7085c754c0c97e418d7e5213753f74bd396fc27", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.842462, 2.310919], "address": "172 rue de vaugirard", "name": "le 1 cinq", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.310919, 48.842462], "type": "Point"}, "recordid": "17e917723fc99d6e5bd77eb9633ac2e789a9a6d9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.84591, 2.375543], "address": "28 bis boulevard Diderot", "name": "Le Killy Jen", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.375543, 48.84591], "type": "Point"}, "recordid": "93132bd8b3ae67dfcc8cf8c1166e312ac4acb9b9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.842868, 2.303173], "address": "106 rue Lecourbe", "name": "Les Artisans", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.303173, 48.842868], "type": "Point"}, "recordid": "c37ef573b4cb2d1e61795d6a9ef11de433dc9a99", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.865684, 2.334416], "address": "83 avenue de Wagram", "name": "Peperoni", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.334416, 48.865684], "type": "Point"}, "recordid": "9461f859ca009ced25555fa6af1e6867dda9223e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.833146, 2.288834], "address": "380 rue de vaugirard", "name": "le lutece", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.288834, 48.833146], "type": "Point"}, "recordid": "ddd13990c1408700085366bd4ba313acd69a44ea", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.886431, 2.327429], "address": "16 rue Ganneron", "name": "Brasiloja", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.327429, 48.886431], "type": "Point"}, "recordid": "d679bb1642534278f4c0203d67be0bafd5306d81", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.855711, 2.359491], "address": "16 rue de Rivoli", "name": "Rivolux", "date": "2014-02-01", "zipcode": 75004, "geometry": {"coordinates": [2.359491, 48.855711], "type": "Point"}, "recordid": "bdd2b008cc765c7fe195c037b830cd2628420a2f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.845898, 2.372766], "address": "21 Bis Boulevard Diderot", "name": "L'europ\u00e9en", "date": "2013-08-22", "zipcode": 75012, "geometry": {"coordinates": [2.372766, 48.845898], "type": "Point"}, "recordid": "693c0da7d4db24781ed161c01a661c36074a94fa", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.867465, 2.357791], "address": "39 rue Notre Dame de Nazareth", "name": "NoMa", "date": "2013-08-22", "zipcode": 75003, "geometry": {"coordinates": [2.357791, 48.867465], "type": "Point"}, "recordid": "60d8b670810cc95eb0439dd0c238f8205ea8ef76", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.871595, 2.385858], "address": "1 Rue des Envierges", "name": "O'Paris", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.385858, 48.871595], "type": "Point"}, "recordid": "297c040284a05efe35c69bb621505e6acfdcdda4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.872402, 2.366532], "address": "16 avenue Richerand", "name": "Caf\u00e9 Clochette", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.366532, 48.872402], "type": "Point"}, "recordid": "a561a941f538e8a1d321bf8d98576d06be037962", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.856584, 2.368574], "address": "40 Boulevard Beaumarchais", "name": "La cantoche de Paname", "date": "2013-08-22", "zipcode": 75011, "geometry": {"coordinates": [2.368574, 48.856584], "type": "Point"}, "recordid": "5ba2aaec9f1de9d01e65be95215cab13c693cdf3", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.856496, 2.394874], "address": "148 Boulevard de Charonne", "name": "Le Saint Ren\u00e9", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.394874, 48.856496], "type": "Point"}, "recordid": "4d60b350d04d4b1bf4bfd4dd6cc59687dc792c74", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.850055, 2.383908], "address": "196 rue du faubourg saint-antoine", "name": "La Libert\u00e9", "date": "2012-10-22", "zipcode": 75012, "geometry": {"coordinates": [2.383908, 48.850055], "type": "Point"}, "recordid": "ee94e76326f8dcbe3500afec69f1a21eb1215ad0", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.866737, 2.33716], "address": "16 rue des Petits Champs", "name": "Chez Rutabaga", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.33716, 48.866737], "type": "Point"}, "recordid": "a420ea4608440b8dc8e0267fe8cc513daa950551", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.885367, 2.325325], "address": "2 rue Lemercier", "name": "Le BB (Bouchon des Batignolles)", "date": "2012-10-22", "zipcode": 75017, "geometry": {"coordinates": [2.325325, 48.885367], "type": "Point"}, "recordid": "20986cbfe11018bd0aba8150a49db1c435f7642d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.873175, 2.339193], "address": "10 rue Rossini", "name": "La Brocante", "date": "2012-10-22", "zipcode": 75009, "geometry": {"coordinates": [2.339193, 48.873175], "type": "Point"}, "recordid": "2e10601c35669394d43936a771b18408be0338ba", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.840771, 2.324589], "address": "3 rue Ga\u00eet\u00e9", "name": "Le Plomb du cantal", "date": "2012-10-22", "zipcode": 75014, "geometry": {"coordinates": [2.324589, 48.840771], "type": "Point"}, "recordid": "6fb510614e00b065bf16a5af8e2c0eaf561a5654", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.884753, 2.324648], "address": "22 rue des Dames", "name": "Les caves populaires", "date": "2012-10-22", "zipcode": 75017, "geometry": {"coordinates": [2.324648, 48.884753], "type": "Point"}, "recordid": "d650c509a0aa8ed7b9c9b88861263f31463bbd0e", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.869519, 2.39339], "address": "108 rue de M\u00e9nilmontant", "name": "Chez Luna", "date": "2012-10-09", "zipcode": 75020, "geometry": {"coordinates": [2.39339, 48.869519], "type": "Point"}, "recordid": "736f4d996f1f8b7c3a0ce2abfeebfcce2a4bab13", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.877903, 2.385365], "address": "1 rue du Plateau", "name": "Le bar Fleuri", "date": "2012-03-07", "zipcode": 75019, "geometry": {"coordinates": [2.385365, 48.877903], "type": "Point"}, "recordid": "be55720646093788ec161c6cadc5ad8059f4b90b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.882939, 2.31809], "address": "101 rue des dames", "name": "Trois pi\u00e8ces cuisine", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.31809, 48.882939], "type": "Point"}, "recordid": "7bbbfb755020a2c25cce0067601994ce5ee4193f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.849497, 2.298855], "address": "61 avenue de la Motte Picquet", "name": "Le Zinc", "date": "2012-03-07", "zipcode": 75015, "geometry": {"coordinates": [2.298855, 48.849497], "type": "Point"}, "recordid": "e7c35a94454518de6de5bbecbc015fc37f7aea14", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.880669, 2.349964], "address": "136 rue du Faubourg poissonni\u00e8re", "name": "La cantine de Zo\u00e9", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.349964, 48.880669], "type": "Point"}, "recordid": "0edc473b3432a869b8ed66b6c4c989766b699947", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.844057, 2.328402], "address": "6/8 rue Stanislas", "name": "Les Vendangeurs", "date": "2012-03-07", "zipcode": 75006, "geometry": {"coordinates": [2.328402, 48.844057], "type": "Point"}, "recordid": "e9766ea36f6293bf670ed938bff02b975d012973", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.852053, 2.338779], "address": "3 carrefour de l'Od\u00e9on", "name": "L'avant comptoir", "date": "2012-03-07", "zipcode": 75006, "geometry": {"coordinates": [2.338779, 48.852053], "type": "Point"}, "recordid": "fe843d2f43dcaac9129f5b36dc367558dfd3b3e4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "1", "geoloc": [48.886504, 2.34498], "address": "1 rue Paul albert", "name": "Botak cafe", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.34498, 48.886504], "type": "Point"}, "recordid": "9e19c375e612f5fb803ec6a27881858619207812", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.872722, 2.354594], "address": "67 rue du Ch\u00e2teau d'eau", "name": "le chateau d'eau", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.354594, 48.872722], "type": "Point"}, "recordid": "05bb6a26ec5bfbba25da2d19a5f0e83d69800f38", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.85192, 2.373229], "address": "58 rue du Fbg Saint-Antoine", "name": "Bistrot Saint-Antoine", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.373229, 48.85192], "type": "Point"}, "recordid": "daa3908ddf69d378fec5b4548494727e1121adc4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.854685, 2.368487], "address": "11/13 boulevard Beaumarchais", "name": "Chez Oscar", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.368487, 48.854685], "type": "Point"}, "recordid": "c73be0483480c59e6ab6bc3a906c8d9dd474887f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.87226, 2.304441], "address": "63 rue de Ponthieu", "name": "Le Fronton", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.304441, 48.87226], "type": "Point"}, "recordid": "85a8200d3e3aed7724d3207ed8b1ee5ec50c1f90", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.851, 2.300378], "address": "48 avenue de la Motte Picquet", "name": "Le Piquet", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.300378, 48.851], "type": "Point"}, "recordid": "460e2adc95fd172f753b1b6ed296c2711639d49d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.841089, 2.349565], "address": "104 rue Mouffetard", "name": "Le Tournebride", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.349565, 48.841089], "type": "Point"}, "recordid": "8a7a23ed68366f70ab939c877cbdce46f19d75c7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.828704, 2.322074], "address": "52 rue des plantes", "name": "maison du vin", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.322074, 48.828704], "type": "Point"}, "recordid": "482507a8f0fe4960f94372b6fa12b16e7d4f2a93", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.842146, 2.375986], "address": "157 rue Bercy 75012 Paris", "name": "L'entrep\u00f4t", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.375986, 48.842146], "type": "Point"}, "recordid": "d8746118429eb118f38ecbee904636d9b33fa8ba", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.867092, 2.363288], "address": "Place de la R\u00e9publique", "name": "Le caf\u00e9 Monde et M\u00e9dias", "date": "2013-08-22", "zipcode": 75003, "geometry": {"coordinates": [2.363288, 48.867092], "type": "Point"}, "recordid": "af04c90f25e25daf7f5cbbab1bc740bac26541d4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.849821, 2.355337], "address": "11 Quai de la Tournelle", "name": "Caf\u00e9 rallye tournelles", "date": "2013-08-22", "zipcode": 75005, "geometry": {"coordinates": [2.355337, 48.849821], "type": "Point"}, "recordid": "91d88e321a75b6a8c4dea816c399fda77c41f9d1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.872498, 2.355136], "address": "61 rue du ch\u00e2teau d'eau", "name": "Brasserie le Morvan", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.355136, 48.872498], "type": "Point"}, "recordid": "6ec62058995948fc18d331f01a2d03acc0d9e0fa", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "1", "geoloc": [48.874879, 2.386064], "address": "6 rue M\u00e9lingue", "name": "Chez Miamophile", "date": "2013-08-22", "zipcode": 75019, "geometry": {"coordinates": [2.386064, 48.874879], "type": "Point"}, "recordid": "13924872737e4fc640a43da58937c3777c2ac753", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.864269, 2.36858], "address": "18 rue de Crussol", "name": "Panem", "date": "2012-05-11", "zipcode": 75011, "geometry": {"coordinates": [2.36858, 48.864269], "type": "Point"}, "recordid": "67bdf3a6989f80749a1ba33a17b1370de0a0e1cd", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.885662, 2.319591], "address": "47 rue de Batignolles", "name": "Petits Freres des Pauvres", "date": "2012-05-11", "zipcode": 75017, "geometry": {"coordinates": [2.319591, 48.885662], "type": "Point"}, "recordid": "e27fd00149514bbfad7dd7e8f9b0c677df2d3f25", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.837212, 2.296046], "address": "198 rue de la Convention", "name": "Caf\u00e9 Dupont", "date": "2012-05-11", "zipcode": 75015, "geometry": {"coordinates": [2.296046, 48.837212], "type": "Point"}, "recordid": "4d40e6d864dae81c152a05cb98e30933bde96aa1", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.871002, 2.30879], "address": "28 rue de Ponthieu", "name": "L'Angle", "date": "2012-10-22", "zipcode": 75008, "geometry": {"coordinates": [2.30879, 48.871002], "type": "Point"}, "recordid": "c40bd2d1f98b415e539c27cf68518d060ebab51e", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.888023, 2.353467], "address": "19-23 rue L\u00e9on", "name": "Institut des Cultures d'Islam", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.353467, 48.888023], "type": "Point"}, "recordid": "68d6d37b846e39bd8554e1f8f75974b486b0f27b", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.886044, 2.360781], "address": "19 rue Pajol", "name": "Canopy Caf\u00e9 associatif", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.360781, 48.886044], "type": "Point"}, "recordid": "ff73bafb514bb68eb925c81aee43c3a58ac3c70d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.870287, 2.332491], "address": "place de l'opera", "name": "L'Entracte", "date": "2012-10-09", "zipcode": 75002, "geometry": {"coordinates": [2.332491, 48.870287], "type": "Point"}, "recordid": "0039cd8bceb5e281677a158f832a660789088071", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.858709, 2.362701], "address": "15 rue du Parc Royal", "name": "Le S\u00e9vign\u00e9", "date": "2012-10-09", "zipcode": 75003, "geometry": {"coordinates": [2.362701, 48.858709], "type": "Point"}, "recordid": "adcc8b4f78f05ba7b24b0593e1516dfb7b415f91", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.839687, 2.347254], "address": "35 rue Claude Bernard", "name": "Le Caf\u00e9 d'avant", "date": "2012-10-09", "zipcode": 75005, "geometry": {"coordinates": [2.347254, 48.839687], "type": "Point"}, "recordid": "b904fc48763938eee2169ba25aad2ffcc0dd6a9f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.844244, 2.330407], "address": "53 rue Notre-Dame des Champs", "name": "Le Lucernaire", "date": "2012-10-09", "zipcode": 75006, "geometry": {"coordinates": [2.330407, 48.844244], "type": "Point"}, "recordid": "cc72af04314fd40e16ff611c799d378515043508", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.877599, 2.332111], "address": "12 rue Blanche", "name": "Le Brigadier", "date": "2012-10-09", "zipcode": 75009, "geometry": {"coordinates": [2.332111, 48.877599], "type": "Point"}, "recordid": "978d4bc68c9ebf81029d3e77274d2107777b8a75", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.826494, 2.359987], "address": "26 rue du Docteur Magnan", "name": "L'\u00e2ge d'or", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.359987, 48.826494], "type": "Point"}, "recordid": "40bffbdc0c9ed1cbce820fed875d7c21d8964640", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.883717, 2.326861], "address": "Place de Clichy", "name": "Bagels & Coffee Corner", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.326861, 48.883717], "type": "Point"}, "recordid": "262facde9b8c4568c9ba7fbce8f069ff8c76948d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.835843, 2.278501], "address": "10 boulevard Victor", "name": "Caf\u00e9 Victor", "date": "2012-03-07", "zipcode": 75015, "geometry": {"coordinates": [2.278501, 48.835843], "type": "Point"}, "recordid": "e5817ec44ac5a7ea2e4a34b6a2e13d535156642b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.845337, 2.379024], "address": "54, avenue Daumesnil", "name": "L'empreinte", "date": "2012-03-07", "zipcode": 75012, "geometry": {"coordinates": [2.379024, 48.845337], "type": "Point"}, "recordid": "b96ddd35cbbf5d93aaff79487afdf083b5ff0817", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.857312, 2.379055], "address": "93, rue de la Roquette", "name": "L'horizon", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.379055, 48.857312], "type": "Point"}, "recordid": "84c6b7335e7f82ac942c4f398723ec99076f148d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.835878, 2.395723], "address": "34 bis rue de Wattignies", "name": "Au pays de Vannes", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.395723, 48.835878], "type": "Point"}, "recordid": "a17869dbb9d0d5b1e5ed7bb288053900b04ee944", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.85413, 2.323539], "address": "36 rue de Varenne", "name": "Caf\u00e9 Varenne", "date": "2012-06-27", "zipcode": 75007, "geometry": {"coordinates": [2.323539, 48.85413], "type": "Point"}, "recordid": "a26ec0d5fca47b8de77d862ad8a99b75bb520a09", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.855161, 2.360218], "address": "125 Rue Saint-Antoine", "name": "l'El\u00e9phant du nil", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.360218, 48.855161], "type": "Point"}, "recordid": "7b7ceefd1f9ed85041265c9577e0dc8bee01d45a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.8357, 2.292961], "address": "354 bis rue Vaugirard", "name": "Le Comptoir", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.292961, 48.8357], "type": "Point"}, "recordid": "59d8fa304e535f4eb41f9746028034c9b30cbde4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.835451, 2.292515], "address": "358 rue de Vaugirard", "name": "Le Parc Vaugirard", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.292515, 48.835451], "type": "Point"}, "recordid": "19f655206a8446959c8e796c2b3cb9001890f985", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.834972, 2.327007], "address": "58 rue Daguerre", "name": "le Zango", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.327007, 48.834972], "type": "Point"}, "recordid": "e1b54109015316a822747f788128f997a3478050", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.848887, 2.399972], "address": "3 rue de Lagny", "name": "Melting Pot", "date": "2012-06-27", "zipcode": 75020, "geometry": {"coordinates": [2.399972, 48.848887], "type": "Point"}, "recordid": "fd0de2cbf73e0a728cd73e4e2a9a4a9c646f76f2", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} +{"prix_terasse": "-", "geoloc": [48.892366, 2.317359], "address": "174 avenue de Clichy", "name": "Pari's Caf\u00e9", "date": "2012-06-27", "zipcode": 75017, "geometry": {"coordinates": [2.317359, 48.892366], "type": "Point"}, "recordid": "831446ae203f89de26d3300e625c20717e82d40a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} \ No newline at end of file diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index c4a9157..5177b82 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -3,7 +3,7 @@ appnope==0.1.0 bleach==2.1.1 decorator==4.1.2 entrypoints==0.2.3 -html5lib==1.0b10 +html5lib==0.999999999 ipykernel==4.6.1 ipython-genutils==0.2.0 ipython==6.2.1 @@ -28,11 +28,11 @@ prompt-toolkit==1.0.15 ptyprocess==0.5.2 pygments==2.2.0 python-dateutil==2.6.1 -pyzmq==17.0.0b3 +pyzmq==16.0.3 qtconsole==4.3.1 simplegeneric==0.8.1 six==1.11.0 -terminado==0.7 +terminado==0.8 testpath==0.3.1 tornado==4.5.2 traitlets==4.3.2 diff --git a/setup.py b/setup.py index c29e9ad..baa9b94 100644 --- a/setup.py +++ b/setup.py @@ -74,9 +74,10 @@ setup( }, entry_points={ 'bonobo.commands': [ - 'convert = bonobo.commands.convert:ConvertCommand', 'init = bonobo.commands.init:InitCommand', + 'convert = bonobo.commands.convert:ConvertCommand', 'download = bonobo.commands.download:DownloadCommand', + 'examples = bonobo.commands.examples:ExamplesCommand', 'init = bonobo.commands.init:InitCommand', 'inspect = bonobo.commands.inspect:InspectCommand', 'run = bonobo.commands.run:RunCommand', - 'version = bonobo.commands.version:VersionCommand', 'download = bonobo.commands.download:DownloadCommand' + 'version = bonobo.commands.version:VersionCommand' ], 'console_scripts': ['bonobo = bonobo.commands:entrypoint'] }, From c7ff06a7429fe53b9c5e76c75e3e8c0794a1eb90 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Tue, 28 Nov 2017 21:57:48 +0100 Subject: [PATCH 119/145] Documentation cosmetics. --- docs/_static/custom.css | 46 +++++++++++++++++++++++++++++++ docs/_templates/sidebarintro.html | 20 ++++++++------ docs/_templates/sidebarlogo.html | 4 +-- docs/conf.py | 2 +- docs/install.rst | 39 ++++++++++++++++++++------ 5 files changed, 91 insertions(+), 20 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index fa608d1..231ec92 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -16,4 +16,50 @@ div.related { .brand { font-family: 'Ubuntu', 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; + font-size: 0.9em; +} + +div.sphinxsidebar h1, +div.sphinxsidebar h2, +div.sphinxsidebar h3, +div.sphinxsidebar h4, +div.sphinxsidebar h5, +div.sphinxsidebar h6 { + font-family: 'Ubuntu', 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; +} + +div.sphinxsidebar h3 { + margin: 30px 0 10px 0; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Ubuntu', 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; +} + +div.admonition p.admonition-title { + font-family: 'Ubuntu', 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; +} + +div.sphinxsidebarwrapper { + padding: 0; +} + +div.note { + border: 0; +} + +div.admonition { + padding: 20px; +} + +.last { + margin-bottom: 0 !important; +} +pre { + padding: 6px 20px; } diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index ef4ad45..d4abd00 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -1,22 +1,24 @@

About Bonobo

- Bonobo is a data-processing toolkit for python 3.5+, with emphasis on simplicity, atomicity and testability. Oh, - and performances, too! + Bonobo is a data-processing toolkit for python 3.5+. +

+

+ It's a swiss-army knife for everyday's data.

Other Formats

- You can download the documentation in other formats as well: + Download the docs...

Useful Links

diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html index f46896c..f1947a7 100644 --- a/docs/_templates/sidebarlogo.html +++ b/docs/_templates/sidebarlogo.html @@ -1,12 +1,12 @@

- + Bonobo

-

+

Data processing for humans.

diff --git a/docs/conf.py b/docs/conf.py index 38a6dd9..46a9b55 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -189,7 +189,7 @@ intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} rst_epilog = """ .. |bonobo| replace:: **Bonobo** - + .. |longversion| replace:: v.{version} """.format( diff --git a/docs/install.rst b/docs/install.rst index 56f18ae..615e4bc 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,23 +1,47 @@ Installation ============ - Create an ETL project ::::::::::::::::::::: -Let's create a job. +First, install the framework: .. code-block:: shell-session $ pip install --upgrade bonobo - $ bonobo create my-etl.py + +Create a simple job: + +.. code-block:: shell-session + + $ bonobo init my-etl.py + +And let's go for a test drive: + +.. code-block:: shell-session + $ python my-etl.py -This job only uses one python file, and you can run it using the python interpreter. For bigger jobs or jobs that -relates to multiple files, you should create a python package. +Congratulations, you ran your first Bonobo ETL job. Now, you can head to :doc:`tutorial/index`. +.. note:: + + It's often best to start with a single file then move it into a project + (which, in python, needs to live in a package). + + You can read more about this topic in the :doc:`guide/packaging` section, + along with pointers on how to move this first file into an existing fully + featured python package. + + You can also avoid all the comments boilerplate by using `--bare` option + (which is a shorthand for `--template bare`): + + .. code-block:: shell-session + + $ bonobo init --bare my-bare-etl-job.py + Other installation options :::::::::::::::::::::::::: @@ -83,12 +107,12 @@ from the local clone.   $ git clone git@github.com:python-bonobo/bonobo.git $ cd bonobo $ pip install --editable . - + You can develop on this clone, but you probably want to add your own repository if you want to push code back and make pull requests. I usually name the git remote for the main bonobo repository "upstream", and my own repository "origin". .. code-block:: shell-session - + $ git remote rename origin upstream $ git remote add origin git@github.com:hartym/bonobo.git $ git fetch --all @@ -119,4 +143,3 @@ users. We're trying to look into that but energy available to provide serious support on windows is very limited. If you have experience in this domain and you're willing to help, you're more than welcome! - From d8c0dfe11a514dc24b43944ce73c751b1a174cce Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Tue, 28 Nov 2017 21:58:01 +0100 Subject: [PATCH 120/145] Inheritance of bags and better jupyter output for pretty printer. --- bonobo/constants.py | 15 +++++-- bonobo/examples/__init__.py | 4 +- bonobo/examples/datasets/__main__.py | 9 +++- bonobo/execution/contexts/__init__.py | 9 ++++ bonobo/execution/contexts/node.py | 62 ++++++++++++++++++--------- bonobo/nodes/basics.py | 62 +++++++++++++++++++-------- bonobo/settings.py | 3 ++ docs/conf.py | 4 +- tests/execution/contexts/test_node.py | 36 +++++++++++++++- tests/features/test_inherit.py | 27 ++++++++++++ 10 files changed, 180 insertions(+), 51 deletions(-) create mode 100644 tests/features/test_inherit.py diff --git a/bonobo/constants.py b/bonobo/constants.py index fceb8f9..b1a199c 100644 --- a/bonobo/constants.py +++ b/bonobo/constants.py @@ -11,9 +11,18 @@ class Token: BEGIN = Token('Begin') END = Token('End') -INHERIT_INPUT = Token('InheritInput') -LOOPBACK = Token('Loopback') -NOT_MODIFIED = Token('NotModified') + +class Flag(Token): + must_be_first = False + must_be_last = False + allows_data = True + + +INHERIT = Flag('Inherit') +NOT_MODIFIED = Flag('NotModified') +NOT_MODIFIED.must_be_first = True +NOT_MODIFIED.must_be_last = True +NOT_MODIFIED.allows_data = False EMPTY = tuple() diff --git a/bonobo/examples/__init__.py b/bonobo/examples/__init__.py index e1815f8..ec68fc5 100644 --- a/bonobo/examples/__init__.py +++ b/bonobo/examples/__init__.py @@ -27,6 +27,6 @@ def get_graph_options(options): _print = options.pop('print', False) return { - '_limit': (bonobo.Limit(_limit),) if _limit else (), - '_print': (bonobo.PrettyPrinter(),) if _print else (), + '_limit': (bonobo.Limit(_limit), ) if _limit else (), + '_print': (bonobo.PrettyPrinter(), ) if _print else (), } diff --git a/bonobo/examples/datasets/__main__.py b/bonobo/examples/datasets/__main__.py index 768ac5c..55a7fde 100644 --- a/bonobo/examples/datasets/__main__.py +++ b/bonobo/examples/datasets/__main__.py @@ -66,11 +66,16 @@ def get_services(): if __name__ == '__main__': parser = examples.get_argument_parser() - parser.add_argument('--target', '-t', choices=graphs.keys(), nargs='+') + parser.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(options['target'] if options['target'] else sorted(graphs.keys())) + graph_names = list( + options['target'] + if options['target'] else sorted(graphs.keys()) + ) graph = bonobo.Graph() for name in graph_names: diff --git a/bonobo/execution/contexts/__init__.py b/bonobo/execution/contexts/__init__.py index e69de29..4c462c5 100644 --- a/bonobo/execution/contexts/__init__.py +++ b/bonobo/execution/contexts/__init__.py @@ -0,0 +1,9 @@ +from bonobo.execution.contexts.graph import GraphExecutionContext +from bonobo.execution.contexts.node import NodeExecutionContext +from bonobo.execution.contexts.plugin import PluginExecutionContext + +__all__ = [ + 'GraphExecutionContext', + 'NodeExecutionContext', + 'PluginExecutionContext', +] diff --git a/bonobo/execution/contexts/node.py b/bonobo/execution/contexts/node.py index 194cf36..316d9b8 100644 --- a/bonobo/execution/contexts/node.py +++ b/bonobo/execution/contexts/node.py @@ -7,11 +7,11 @@ 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, Token +from bonobo.constants import NOT_MODIFIED, BEGIN, END, TICK_PERIOD, Token, Flag, INHERIT from bonobo.errors import InactiveReadableError, UnrecoverableError, UnrecoverableTypeError from bonobo.execution.contexts.base import BaseContext from bonobo.structs.inputs import Input -from bonobo.util import get_name, istuple, isconfigurabletype, ensure_tuple +from bonobo.util import get_name, isconfigurabletype, ensure_tuple from bonobo.util.bags import BagType from bonobo.util.statistics import WithStatistics @@ -292,20 +292,24 @@ class NodeExecutionContext(BaseContext, WithStatistics): def _cast(self, _input, _output): """ - Transforms a pair of input/output into what is the real output. + Transforms a pair of input/output into the real slim 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) + tokens, _output = split_token(_output) - return ensure_tuple(_output, cls=(self.output_type or tuple)) + if NOT_MODIFIED in tokens: + return ensure_tuple(_input, cls=(self.output_type or tuple)) + + if INHERIT in tokens: + if self._output_type is None: + self._output_type = concat_types(self._input_type, self._input_length, self._output_type, len(_output)) + _output = _input + ensure_tuple(_output) + + return ensure_tuple(_output, cls=(self._output_type or tuple)) def _send(self, value, _control=False): """ @@ -330,26 +334,44 @@ class NodeExecutionContext(BaseContext, WithStatistics): def isflag(param): - return isinstance(param, Token) and param in (NOT_MODIFIED, ) + return isinstance(param, Flag) -def split_tokens(output): +def split_token(output): """ Split an output into token tuple, real output tuple. :param output: :return: tuple, tuple """ - if isinstance(output, Token): - # just a flag - return (output, ), () - if not istuple(output): - # no flag - return (), (output, ) + output = ensure_tuple(output) - i = 0 - while isflag(output[i]): + flags, i, len_output, data_allowed = set(), 0, len(output), True + while i < len_output and isflag(output[i]): + if output[i].must_be_first and i: + raise ValueError('{} flag must be first.'.format(output[i])) + if i and output[i - 1].must_be_last: + raise ValueError('{} flag must be last.'.format(output[i - 1])) + if output[i] in flags: + raise ValueError('Duplicate flag {}.'.format(output[i])) + flags.add(output[i]) + data_allowed &= output[i].allows_data i += 1 - return output[:i], output[i:] + output = output[i:] + if not data_allowed and len(output): + raise ValueError('Output data provided after a flag that does not allow data.') + return flags, output + + +def concat_types(t1, l1, t2, l2): + t1, t2 = t1 or tuple, t2 or tuple + + if t1 == t2 == tuple: + return tuple + + f1 = t1._fields if hasattr(t1, '_fields') else tuple(range(l1)) + f2 = t2._fields if hasattr(t2, '_fields') else tuple(range(l2)) + + return BagType('Inherited', f1 + f2) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 9710ef7..ecf6827 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -1,11 +1,7 @@ import functools +import html 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, Method, use_raw_input, use_context, use_no_input @@ -14,6 +10,7 @@ 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 +from mondrian import term __all__ = [ 'FixedWindow', @@ -94,29 +91,41 @@ class PrettyPrinter(Configurable): @ContextProcessor def context(self, context): + context.setdefault('_jupyter_html', None) yield context + if context._jupyter_html is not None: + from IPython.display import display, HTML + display(HTML('\n'.join([''] + context._jupyter_html + ['
']))) 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)) + if not settings.QUIET: + if term.isjupyter: + self.print_jupyter(context, *args, **kwargs) + return NOT_MODIFIED + if term.istty: + self.print_console(context, *args, **kwargs) + return NOT_MODIFIED + self.print_quiet(context, *args, **kwargs) return NOT_MODIFIED - def _format_quiet(self, index, key, value, *, fields=None): + def print_quiet(self, context, *args, **kwargs): + for index, (key, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())): + if self.filter(index, key, value): + print(self.format_quiet(index, key, value, fields=context.get_input_fields())) + + def format_quiet(self, index, key, value, *, fields=None): # XXX should we implement argnames here ? return ' '.join(((' ' if index else '-'), str(key), ':', str(value).strip())) - def _format_console(self, index, key, value, *, fields=None): + def print_console(self, context, *args, **kwargs): + 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(self.format_console(index, key, value, fields=context.get_input_fields())) + print('\u2516' + '\u2500' * (self.max_width - 1)) + + 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]): @@ -136,6 +145,21 @@ class PrettyPrinter(Configurable): ).strip() return '{}{}{}'.format(prefix, repr_of_value.replace('\n', CLEAR_EOL + '\n'), CLEAR_EOL) + def print_jupyter(self, context, *args): + if not context._jupyter_html: + context._jupyter_html = [ + '', + *map('{}'.format, map(html.escape, map(str, + context.get_input_fields() or range(len(args))))), + '', + ] + + context._jupyter_html += [ + '', + *map('{}'.format, map(html.escape, map(repr, args))), + '', + ] + @use_no_input def noop(*args, **kwargs): diff --git a/bonobo/settings.py b/bonobo/settings.py index fdc4412..799ba3d 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -46,6 +46,9 @@ class Setting: def __eq__(self, other): return self.get() == other + def __bool__(self): + return bool(self.get()) + def set(self, value): value = self.formatter(value) if self.formatter else value if self.validator and not self.validator(value): diff --git a/docs/conf.py b/docs/conf.py index 46a9b55..53ed816 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -192,6 +192,4 @@ rst_epilog = """ .. |longversion| replace:: v.{version} -""".format( - version=version, -) +""".format(version=version, ) diff --git a/tests/execution/contexts/test_node.py b/tests/execution/contexts/test_node.py index 0ebae6d..c9c8f1f 100644 --- a/tests/execution/contexts/test_node.py +++ b/tests/execution/contexts/test_node.py @@ -3,8 +3,8 @@ from unittest.mock import MagicMock import pytest from bonobo import Graph -from bonobo.constants import EMPTY -from bonobo.execution.contexts.node import NodeExecutionContext +from bonobo.constants import EMPTY, NOT_MODIFIED, INHERIT +from bonobo.execution.contexts.node import NodeExecutionContext, split_token from bonobo.execution.strategies import NaiveStrategy from bonobo.util.testing import BufferingNodeExecutionContext, BufferingGraphExecutionContext @@ -224,3 +224,35 @@ def test_node_lifecycle_with_kill(): ctx.stop() assert all((ctx.started, ctx.killed, ctx.stopped)) and not ctx.alive + + +def test_split_token(): + assert split_token(('foo', 'bar')) == (set(), ('foo', 'bar')) + assert split_token(()) == (set(), ()) + assert split_token('') == (set(), ('', )) + + +def test_split_token_duplicate(): + with pytest.raises(ValueError): + split_token((NOT_MODIFIED, NOT_MODIFIED)) + with pytest.raises(ValueError): + split_token((INHERIT, INHERIT)) + with pytest.raises(ValueError): + split_token((INHERIT, NOT_MODIFIED, INHERIT)) + + +def test_split_token_not_modified(): + with pytest.raises(ValueError): + split_token((NOT_MODIFIED, 'foo', 'bar')) + with pytest.raises(ValueError): + split_token((NOT_MODIFIED, INHERIT)) + with pytest.raises(ValueError): + split_token((INHERIT, NOT_MODIFIED)) + assert split_token(NOT_MODIFIED) == ({NOT_MODIFIED}, ()) + assert split_token((NOT_MODIFIED, )) == ({NOT_MODIFIED}, ()) + + +def test_split_token_inherit(): + assert split_token(INHERIT) == ({INHERIT}, ()) + assert split_token((INHERIT, )) == ({INHERIT}, ()) + assert split_token((INHERIT, 'foo', 'bar')) == ({INHERIT}, ('foo', 'bar')) diff --git a/tests/features/test_inherit.py b/tests/features/test_inherit.py new file mode 100644 index 0000000..92b943b --- /dev/null +++ b/tests/features/test_inherit.py @@ -0,0 +1,27 @@ +from bonobo.constants import INHERIT +from bonobo.util.testing import BufferingNodeExecutionContext + +messages = [ + ('Hello', ), + ('Goodbye', ), +] + + +def append(*args): + return INHERIT, '!' + + +def test_inherit(): + with BufferingNodeExecutionContext(append) as context: + context.write_sync(*messages) + + assert context.get_buffer() == list(map(lambda x: x + ('!', ), messages)) + + +def test_inherit_bag_tuple(): + with BufferingNodeExecutionContext(append) as context: + context.set_input_fields(['message']) + context.write_sync(*messages) + + assert context.get_output_fields() == ('message', '0') + assert context.get_buffer() == list(map(lambda x: x + ('!', ), messages)) From db54ff41efa684f6d4e1920e6bb58af458dd32a7 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Fri, 1 Dec 2017 07:42:23 +0100 Subject: [PATCH 121/145] Fix PrettyPrinter, output verbosity is now slightly more discreete. --- Makefile | 2 +- Projectfile | 2 +- bonobo/commands/convert.py | 27 +++++++++++++++++++++------ bonobo/nodes/basics.py | 10 +++++----- requirements-dev.txt | 6 ++++-- requirements-jupyter.txt | 4 ++-- requirements.txt | 2 +- setup.py | 2 +- 8 files changed, 36 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index 50338ee..cf37115 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4.2 on 2017-11-27. +# Generated by Medikit 0.4.2 on 2017-12-01. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index 5d36b74..1beef5d 100644 --- a/Projectfile +++ b/Projectfile @@ -46,7 +46,7 @@ python.add_requirements( 'fs >=2.0,<2.1', 'graphviz >=0.8,<0.9', 'jinja2 >=2.9,<3', - 'mondrian >=0.5,<0.6', + 'mondrian >=0.6,<0.7', 'packaging >=16,<17', 'psutil >=5.4,<6', 'python-slugify >=1.2,<1.3', diff --git a/bonobo/commands/convert.py b/bonobo/commands/convert.py index f490213..d04e97b 100644 --- a/bonobo/commands/convert.py +++ b/bonobo/commands/convert.py @@ -19,6 +19,13 @@ class ConvertCommand(BaseCommand): help= 'Choose the writer factory if it cannot be detected from extension, or if detection is wrong (use - for console pretty print).' ) + parser.add_argument( + '--limit', + '-l', + type=int, + help='Adds a Limit() after the reader instance.', + default=None, + ) parser.add_argument( '--transformation', '-t', @@ -57,24 +64,32 @@ class ConvertCommand(BaseCommand): writer=None, writer_option=None, option=None, - transformation=None + limit=None, + transformation=None, ): reader_factory = default_registry.get_reader_factory_for(input_filename, format=reader) - reader_options = _resolve_options((option or []) + (reader_option or [])) + reader_kwargs = _resolve_options((option or []) + (reader_option or [])) if output_filename == '-': writer_factory = bonobo.PrettyPrinter + writer_args = () else: writer_factory = default_registry.get_writer_factory_for(output_filename, format=writer) - writer_options = _resolve_options((option or []) + (writer_option or [])) + writer_args = (output_filename, ) + writer_kwargs = _resolve_options((option or []) + (writer_option or [])) - transformations = _resolve_transformations(transformation) + transformations = () + + if limit: + transformations += (bonobo.Limit(limit), ) + + transformations += _resolve_transformations(transformation) graph = bonobo.Graph() graph.add_chain( - reader_factory(input_filename, **reader_options), + reader_factory(input_filename, **reader_kwargs), *transformations, - writer_factory(output_filename, **writer_options), + writer_factory(*writer_args, **writer_kwargs), ) return bonobo.run( diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index ecf6827..6a7adaf 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -119,21 +119,21 @@ class PrettyPrinter(Configurable): return ' '.join(((' ' if index else '-'), str(key), ':', str(value).strip())) def print_console(self, context, *args, **kwargs): - print('\u250e' + '\u2500' * (self.max_width - 1)) + print('\u250c') for index, (key, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())): if self.filter(index, key, value): print(self.format_console(index, key, value, fields=context.get_input_fields())) - print('\u2516' + '\u2500' * (self.max_width - 1)) + print('\u2514') 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]): + if len(fields) > key and str(key) != str(fields[key]): key = '{}{}'.format(fields[key], term.lightblack('[{}]'.format(key))) else: key = str(index) - prefix = '\u2503 {} = '.format(key) + prefix = '\u2502 {} = '.format(key) prefix_length = len(prefix) def indent(text, prefix): @@ -141,7 +141,7 @@ class PrettyPrinter(Configurable): 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)) + indent(pprint.pformat(value, width=self.max_width - prefix_length), '\u2502' + ' ' * (len(prefix) - 1)) ).strip() return '{}{}{}'.format(prefix, repr_of_value.replace('\n', CLEAR_EOL + '\n'), CLEAR_EOL) diff --git a/requirements-dev.txt b/requirements-dev.txt index cfd8c44..8d4d4e7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ -e .[dev] alabaster==0.7.10 +attrs==17.3.0 babel==2.5.1 certifi==2017.11.5 chardet==3.0.4 @@ -9,12 +10,13 @@ idna==2.6 imagesize==0.7.1 jinja2==2.10 markupsafe==1.0 +pluggy==0.6.0 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.5 +pytest-timeout==1.2.1 +pytest==3.3.0 pytz==2017.3 requests==2.18.4 six==1.11.0 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 5177b82..5ef711a 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -19,7 +19,7 @@ markupsafe==1.0 mistune==0.8.1 nbconvert==5.3.1 nbformat==4.4.0 -notebook==5.2.1 +notebook==5.2.2 pandocfilters==1.4.2 parso==0.1.0 pexpect==4.3.0 @@ -32,7 +32,7 @@ pyzmq==16.0.3 qtconsole==4.3.1 simplegeneric==0.8.1 six==1.11.0 -terminado==0.8 +terminado==0.8.1 testpath==0.3.1 tornado==4.5.2 traitlets==4.3.2 diff --git a/requirements.txt b/requirements.txt index f0ca1e4..f7a1d5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ graphviz==0.8.1 idna==2.6 jinja2==2.10 markupsafe==1.0 -mondrian==0.5.1 +mondrian==0.6.0 packaging==16.8 pbr==3.1.1 psutil==5.4.1 diff --git a/setup.py b/setup.py index baa9b94..b430f73 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,7 @@ 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.5, < 0.6)', 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6)', 'python-slugify (>= 1.2, < 1.3)', + 'mondrian (>= 0.6, < 0.7)', '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={ From 9e0834b0618e11c24fdf632de0a79d819ac825ef Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Fri, 1 Dec 2017 07:43:14 +0100 Subject: [PATCH 122/145] Check partially configured transformations that are function based (aka transformation factories) on execution context setup. wip: make sure all api is exported, raise an error when a function based transformation factory is (incorrectly) used as a transformation. --- bonobo/_api.py | 33 +++++++++---------------- bonobo/config/functools.py | 2 ++ bonobo/execution/strategies/executor.py | 7 ++++-- bonobo/nodes/basics.py | 5 +++- bonobo/util/inspect.py | 18 +++++++++++--- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index 2f5522c..830808e 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,25 +1,6 @@ from bonobo.execution.strategies import create_strategy -from bonobo.nodes import ( - CsvReader, - CsvWriter, - FileReader, - FileWriter, - Filter, - FixedWindow, - JsonReader, - JsonWriter, - Limit, - PickleReader, - PickleWriter, - PrettyPrinter, - RateLimited, - SetFields, - Tee, - count, - identity, - noop, -) -from bonobo.nodes import LdjsonReader, LdjsonWriter +from bonobo.nodes import __all__ as _all_nodes +from bonobo.nodes import * from bonobo.structs import Graph from bonobo.util import get_name from bonobo.util.environ import parse_args, get_argument_parser @@ -51,9 +32,13 @@ def register_graph_api(x, __all__=__all__): return register_api(x, __all__=__all__) -def register_api_group(*args): +def register_api_group(*args, check=None): + check = set(check) if check else None for attr in args: register_api(attr) + if check: + check.remove(get_name(attr)) + assert not (check and len(check)) @register_graph_api @@ -170,6 +155,7 @@ register_api_group( FileWriter, Filter, FixedWindow, + Format, JsonReader, JsonWriter, LdjsonReader, @@ -179,11 +165,14 @@ register_api_group( PickleWriter, PrettyPrinter, RateLimited, + Rename, SetFields, Tee, + UnpackItems, count, identity, noop, + check=_all_nodes, ) diff --git a/bonobo/config/functools.py b/bonobo/config/functools.py index 506306b..7beaf6f 100644 --- a/bonobo/config/functools.py +++ b/bonobo/config/functools.py @@ -12,4 +12,6 @@ def transformation_factory(f): ) return retval + _transformation_factory._partial = True + return _transformation_factory diff --git a/bonobo/execution/strategies/executor.py b/bonobo/execution/strategies/executor.py index fc72226..44a2160 100644 --- a/bonobo/execution/strategies/executor.py +++ b/bonobo/execution/strategies/executor.py @@ -50,8 +50,11 @@ class ExecutorStrategy(Strategy): def starter(node): @functools.wraps(node) def _runner(): - with node: - node.loop() + try: + with node: + node.loop() + except: + logging.getLogger(__name__).critical('Critical error in threadpool node starter.', exc_info=sys.exc_info()) try: futures.append(executor.submit(_runner)) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 6a7adaf..a1b1076 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -14,10 +14,13 @@ from mondrian import term __all__ = [ 'FixedWindow', + 'Format', 'Limit', 'PrettyPrinter', - 'Tee', + 'Rename', 'SetFields', + 'Tee', + 'UnpackItems', 'count', 'identity', 'noop', diff --git a/bonobo/util/inspect.py b/bonobo/util/inspect.py index f1463a4..a7f27c4 100644 --- a/bonobo/util/inspect.py +++ b/bonobo/util/inspect.py @@ -22,10 +22,20 @@ def isconfigurabletype(mixed, *, strict=False): :return: bool """ from bonobo.config.configurables import ConfigurableMeta, PartiallyConfigured - return isinstance(mixed, (ConfigurableMeta, ) if strict else ( - ConfigurableMeta, - PartiallyConfigured, - )) + + if isinstance(mixed, ConfigurableMeta): + return True + + if strict: + return False + + if isinstance(mixed, PartiallyConfigured): + return True + + if hasattr(mixed, '_partial') and mixed._partial: + return True + + return False def isoption(mixed): From 47c676a06845c878f3687cc53004792786fe6018 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 2 Dec 2017 14:18:24 +0100 Subject: [PATCH 123/145] Adds an OrderFields transformation factory, update examples. --- bonobo/_api.py | 1 + bonobo/examples/datasets/__main__.py | 71 +------- bonobo/examples/datasets/coffeeshops.csv | 183 -------------------- bonobo/examples/datasets/coffeeshops.json | 181 ------------------- bonobo/examples/datasets/coffeeshops.ldjson | 181 ------------------- bonobo/examples/datasets/coffeeshops.py | 61 +++++++ bonobo/examples/datasets/coffeeshops.txt | 183 -------------------- bonobo/examples/datasets/fablabs.py | 77 ++------ bonobo/examples/datasets/fablabs.txt | 135 --------------- bonobo/examples/datasets/services.py | 7 + bonobo/execution/strategies/executor.py | 4 +- bonobo/nodes/basics.py | 32 ++++ 12 files changed, 131 insertions(+), 985 deletions(-) delete mode 100644 bonobo/examples/datasets/coffeeshops.csv delete mode 100644 bonobo/examples/datasets/coffeeshops.json delete mode 100644 bonobo/examples/datasets/coffeeshops.ldjson create mode 100644 bonobo/examples/datasets/coffeeshops.py delete mode 100644 bonobo/examples/datasets/coffeeshops.txt delete mode 100644 bonobo/examples/datasets/fablabs.txt create mode 100644 bonobo/examples/datasets/services.py diff --git a/bonobo/_api.py b/bonobo/_api.py index 830808e..58aab3d 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -161,6 +161,7 @@ register_api_group( LdjsonReader, LdjsonWriter, Limit, + OrderFields, PickleReader, PickleWriter, PrettyPrinter, diff --git a/bonobo/examples/datasets/__main__.py b/bonobo/examples/datasets/__main__.py index 55a7fde..a066e07 100644 --- a/bonobo/examples/datasets/__main__.py +++ b/bonobo/examples/datasets/__main__.py @@ -1,84 +1,29 @@ import bonobo from bonobo import examples -from bonobo.contrib.opendatasoft import OpenDataSoftAPI as ODSReader -from bonobo.nodes.basics import UnpackItems, Rename, Format +from bonobo.examples.datasets.coffeeshops import get_graph as get_coffeeshops_graph +from bonobo.examples.datasets.fablabs import get_graph as get_fablabs_graph +from bonobo.examples.datasets.services import get_services - -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 = { +graph_factories = { 'coffeeshops': get_coffeeshops_graph, + 'fablabs': get_fablabs_graph, } - -def get_services(): - return {'fs': bonobo.open_fs(bonobo.get_examples_path('datasets'))} - - if __name__ == '__main__': parser = examples.get_argument_parser() parser.add_argument( - '--target', '-t', choices=graphs.keys(), nargs='+' + '--target', '-t', choices=graph_factories.keys(), nargs='+' ) with bonobo.parse_args(parser) as options: graph_options = examples.get_graph_options(options) graph_names = list( options['target'] - if options['target'] else sorted(graphs.keys()) + if options['target'] else sorted(graph_factories.keys()) ) graph = bonobo.Graph() for name in graph_names: - graph = graphs[name](graph, **graph_options) + graph = graph_factories[name](graph, **graph_options) bonobo.run(graph, services=get_services()) diff --git a/bonobo/examples/datasets/coffeeshops.csv b/bonobo/examples/datasets/coffeeshops.csv deleted file mode 100644 index b17f71a..0000000 --- a/bonobo/examples/datasets/coffeeshops.csv +++ /dev/null @@ -1,183 +0,0 @@ -prix_terasse,geoloc,address,name,date,zipcode,geometry,recordid,prix_salle,prix_comptoir,city,country --,"[48.839512, 2.303007]",344Vrue Vaugirard,Coffee Chope,2014-02-01,75015,"{'coordinates': [2.303007, 48.839512], 'type': 'Point'}",3c276428d45ad68ccdf6875e4ddcfe95d0c0d4cf,-,1,Paris,France --,"[48.876737, 2.357601]","5, rue d'Alsace",Extérieur Quai,2014-02-01,75010,"{'coordinates': [2.357601, 48.876737], 'type': 'Point'}",97ad81cd1127a8566085ad796eeb44a06bec7514,-,1,Paris,France --,"[48.850852, 2.362029]",6 Bd henri IV,Le Sully,2014-02-01,75004,"{'coordinates': [2.362029, 48.850852], 'type': 'Point'}",aa4294c1b8d660a23db0dc81321e509bae1dae68,-,1,Paris,France -1,"[48.893517, 2.340271]",53 rue du ruisseau,O q de poule,2013-08-22,75018,"{'coordinates': [2.340271, 48.893517], 'type': 'Point'}",a81362dbed35247555fb105bd83ff2906904a66e,-,1,Paris,France --,"[48.864655, 2.350089]",1 Passage du Grand Cerf,Le Pas Sage,2013-08-22,75002,"{'coordinates': [2.350089, 48.864655], 'type': 'Point'}",7ced86acbd5ccfba229bcc07d70d0d117aee16a5,-,1,Paris,France --,"[48.895825, 2.339712]",112 Rue Championnet,La Renaissance,2013-08-22,75018,"{'coordinates': [2.339712, 48.895825], 'type': 'Point'}",5582c8572bd7637bf305b74c1c0bdb74a8e4247f,-,1,Paris,France --,"[48.868581, 2.373015]",Rue de la Fontaine au Roi,La Caravane,2012-05-11,75011,"{'coordinates': [2.373015, 48.868581], 'type': 'Point'}",50bb0fa06e562a242f115ddbdae2ed9c7df93d57,-,0,Paris,France --,"[48.875155, 2.335536]",51 Rue Victoire,Le chantereine,2012-10-22,75009,"{'coordinates': [2.335536, 48.875155], 'type': 'Point'}",eb8a62feeedaf7ed8b8c912305270ee857068689,1,1,Paris,France --,"[48.886536, 2.346525]",11 rue Feutrier,Le Müller,2012-10-22,75018,"{'coordinates': [2.346525, 48.886536], 'type': 'Point'}",62c552f167f671f88569c1f2d6a44098fb514c51,1,1,Paris,France --,"[48.841494, 2.307117]",21 rue Copreaux,Le drapeau de la fidelité,2012-10-22,75015,"{'coordinates': [2.307117, 48.841494], 'type': 'Point'}",5120ea0b9d7387766072b90655166486928e25c8,1,1,Paris,France --,"[48.839743, 2.296898]",125 rue Blomet,Le café des amis,2012-10-22,75015,"{'coordinates': [2.296898, 48.839743], 'type': 'Point'}",865f62415adc5c34e3ca38a1748b7a324dfba209,1,1,Paris,France --,"[48.857728, 2.349641]",10 rue Saint Martin,Le Café Livres,2012-10-09,75004,"{'coordinates': [2.349641, 48.857728], 'type': 'Point'}",7ef54a78802d49cafd2701458df2b0d0530d123b,-,1,Paris,France --,"[48.856003, 2.30457]",46 avenue Bosquet,Le Bosquet,2012-10-09,75007,"{'coordinates': [2.30457, 48.856003], 'type': 'Point'}",d701a759e08a71f4bbb01f29473274b0152135d0,-,1,Paris,France --,"[48.889426, 2.332954]",12 rue Armand Carrel,Le Chaumontois,2012-03-07,75018,"{'coordinates': [2.332954, 48.889426], 'type': 'Point'}",e12ff00a644c91ad910ddc63a770c190be93a393,-,1,Paris,France --,"[48.838521, 2.370478]",34 avenue Pierre Mendès-France,Le Kleemend's,2012-03-07,75013,"{'coordinates': [2.370478, 48.838521], 'type': 'Point'}",0f6cd1ee7751b00c9574efcfdcf66fa0e857d251,-,1,Paris,France --,"[48.849861, 2.385342]",202 rue du faubourg st antoine,Café Pierre,2012-03-07,75012,"{'coordinates': [2.385342, 48.849861], 'type': 'Point'}",f9de9d0fb5e92f047a6f1986a31f9dd4d38bcb36,-,1,Paris,France --,"[48.872202, 2.304624]",61 rue de Ponthieu,Les Arcades,2012-03-07,75008,"{'coordinates': [2.304624, 48.872202], 'type': 'Point'}",67eaf58afc856077c0680601e453e75c0922c9c0,-,1,Paris,France --,"[48.859031, 2.320315]",31 rue Saint-Dominique,Le Square,2012-03-07,75007,"{'coordinates': [2.320315, 48.859031], 'type': 'Point'}",678558317bc9ad46652e5b1643e70c2142a76e7e,-,1,Paris,France --,"[48.850092, 2.37463]","75, avenue Ledru-Rollin",Assaporare Dix sur Dix,2012-06-27,75012,"{'coordinates': [2.37463, 48.850092], 'type': 'Point'}",667474321887d08a3cc636adf043ad354b65fa61,-,1,Paris,France --,"[48.86805, 2.353313]",129 boulevard sebastopol,Au cerceau d'or,2012-06-27,75002,"{'coordinates': [2.353313, 48.86805], 'type': 'Point'}",c9ef52ba2fabe0286700329f18bbbbea9a10b474,-,1,Paris,France --,"[48.845927, 2.373051]",21 ter boulevard Diderot,Aux cadrans,2012-06-27,75012,"{'coordinates': [2.373051, 48.845927], 'type': 'Point'}",ed5f98686856bf4ddd2b381b43ad229246741a90,-,1,Paris,France --,"[48.851662, 2.273883]",17 rue Jean de la Fontaine,Café antoine,2012-06-27,75016,"{'coordinates': [2.273883, 48.851662], 'type': 'Point'}",ab6d1e054e2e6ae7d6150013173f55e83c05ca23,-,1,Paris,France --,"[48.877642, 2.312823]",rue de Lisbonne,Café de la Mairie (du VIII),2012-06-27,75008,"{'coordinates': [2.312823, 48.877642], 'type': 'Point'}",7de8a79b026ac63f453556612505b5bcd9229036,-,1,Paris,France --,"[48.838633, 2.349916]",5 rue Claude Bernard,Café Lea,2012-06-27,75005,"{'coordinates': [2.349916, 48.838633], 'type': 'Point'}",fecd8900cf83027f74ceced9fc4ad80ac73b63a7,-,1,Paris,France --,"[48.849293, 2.354486]",11 boulevard Saint-Germain,Cardinal Saint-Germain,2012-06-27,75005,"{'coordinates': [2.354486, 48.849293], 'type': 'Point'}",e4a078c30c98082896787f4e4b41a07554392529,-,1,Paris,France --,"[48.869771, 2.342501]",52 rue Notre-Dame des Victoires,Dédé la frite,2012-06-27,75002,"{'coordinates': [2.342501, 48.869771], 'type': 'Point'}",ccb2ba2f98043e8eefd5fda829dee1ea7f1d2c7a,-,1,Paris,France --,"[48.834051, 2.287345]",36 rue du hameau,La Bauloise,2012-06-27,75015,"{'coordinates': [2.287345, 48.834051], 'type': 'Point'}",c9fe10abd15ede7ccaeb55c309898d30d7b19d0e,-,1,Paris,France --,"[48.888165, 2.377387]",71 quai de Seine,Le Bellerive,2012-06-27,75019,"{'coordinates': [2.377387, 48.888165], 'type': 'Point'}",4e0b5c2d33d7c25fc54c51171f3d37e509959fc0,-,1,Paris,France --,"[48.864543, 2.340997]",42 rue coquillère,Le bistrot de Maëlle et Augustin,2012-06-27,75001,"{'coordinates': [2.340997, 48.864543], 'type': 'Point'}",52acab12469af291984e9a70962e08c72b058e10,-,1,Paris,France --,"[48.872103, 2.346161]",14 rue Rougemont,Le Dellac,2012-06-27,75009,"{'coordinates': [2.346161, 48.872103], 'type': 'Point'}",4d1d627ecea2ffa279bb862f8ba495d95ca75350,-,1,Paris,France --,"[48.859645, 2.355598]",1 rue Pecquay,Le Felteu,2012-06-27,75004,"{'coordinates': [2.355598, 48.859645], 'type': 'Point'}",2c1fa55460af282266d86fd003af4f929fdf4e7d,-,1,Paris,France --,"[48.85763, 2.346101]",2 bis quai de la mégisserie,Le Reynou,2012-06-27,75001,"{'coordinates': [2.346101, 48.85763], 'type': 'Point'}",d4ddd30ab3e721a317fc7ea89d5b9001255ce9f4,-,1,Paris,France --,"[48.884646, 2.337734]",23 rue des abbesses,Le Saint Jean,2012-06-27,75018,"{'coordinates': [2.337734, 48.884646], 'type': 'Point'}",51b47cf167b7f32eeebb108330956694d75d4268,-,1,Paris,France --,"[48.841007, 2.31466]",65 boulevard Pasteur,les montparnos,2012-06-27,75015,"{'coordinates': [2.31466, 48.841007], 'type': 'Point'}",2aaca891ffd0694c657a43889516ab72afdfba07,-,1,Paris,France --,"[48.850323, 2.33039]",16 rue DE MEZIERES,L'antre d'eux,2014-02-01,75006,"{'coordinates': [2.33039, 48.850323], 'type': 'Point'}",4ff4337934c66f61e00d1d9551f7cdddba03e544,-,1,Paris,France --,"[48.864957, 2.346938]",58 rue de Montorgueil,Drole d'endroit pour une rencontre,2014-02-01,75002,"{'coordinates': [2.346938, 48.864957], 'type': 'Point'}",3451657f880abe75d0c7e386fc698405556c53e8,-,1,Paris,France --,"[48.889565, 2.339735]",104 rue caulaincourt,Le pari's café,2014-02-01,75018,"{'coordinates': [2.339735, 48.889565], 'type': 'Point'}",e8c34a537b673fcb26c76e02deca4f5a728929dc,-,1,Paris,France --,"[48.859115, 2.368871]",60 rue saint-sabin,Le Poulailler,2014-02-01,75011,"{'coordinates': [2.368871, 48.859115], 'type': 'Point'}",325ea74ba83f716dde87c08cffd36f7df7722a49,-,1,Paris,France --,"[48.833595, 2.38604]",33 Cour Saint Emilion,Chai 33,2014-02-01,75012,"{'coordinates': [2.38604, 48.833595], 'type': 'Point'}",528de8d5d8780bee83145637e315483d48f5ae3c,-,1,Paris,France --,"[48.868741, 2.379969]",99 rue Jean-Pierre Timbaud,L'Assassin,2014-02-01,75011,"{'coordinates': [2.379969, 48.868741], 'type': 'Point'}",fac0483890ff8bdaeb3feddbdb032c5112f24678,-,1,Paris,France --,"[48.851463, 2.398691]",1 rue d'Avron,l'Usine,2014-02-01,75020,"{'coordinates': [2.398691, 48.851463], 'type': 'Point'}",fee1e3eb103bbc98e19e45d34365da0f27166541,-,1,Paris,France --,"[48.896305, 2.332898]",52 rue Liebniz,La Bricole,2014-02-01,75018,"{'coordinates': [2.332898, 48.896305], 'type': 'Point'}",4744e866c244c59eec43b3fe159542d2ef433065,-,1,Paris,France --,"[48.850311, 2.34885]",place maubert,le ronsard,2014-02-01,75005,"{'coordinates': [2.34885, 48.850311], 'type': 'Point'}",49a390322b45246bc2c1e50fcd46815ad271bca0,-,1,Paris,France --,"[48.863038, 2.3604]",82 rue des archives,Face Bar,2014-02-01,75003,"{'coordinates': [2.3604, 48.863038], 'type': 'Point'}",d96e16ebf2460bb2f6c34198918a071233725cbc,-,1,Paris,France --,"[48.872746, 2.366392]",49 rue bichat,American Kitchen,2013-08-22,75010,"{'coordinates': [2.366392, 48.872746], 'type': 'Point'}",6b9395475cbbbbbacbaaeb070f71d31c2d183dc4,-,1,Paris,France --,"[48.870598, 2.365413]",55 bis quai de valmy,La Marine,2013-08-22,75010,"{'coordinates': [2.365413, 48.870598], 'type': 'Point'}",d4d2f92d27f38de59e57744f434781e61283551c,-,1,Paris,France --,"[48.889101, 2.318001]",21 avenue Brochant,Le Bloc,2013-08-22,75017,"{'coordinates': [2.318001, 48.889101], 'type': 'Point'}",e425882ee969d1e8bffe7234336ae40da88c8439,-,1,Paris,France --,"[48.874697, 2.405421]",229 avenue Gambetta,La Recoleta au Manoir,2013-08-22,75020,"{'coordinates': [2.405421, 48.874697], 'type': 'Point'}",02de82cffb2918beafb740f4e924029d470b07a1,-,1,Paris,France --,"[48.847344, 2.286078]",80 Rue Saint-Charles,Le Pareloup,2013-08-22,75015,"{'coordinates': [2.286078, 48.847344], 'type': 'Point'}",0227ca95f76bb6097ae0a0e6f455af2624d49ae3,-,0,Paris,France -1,"[48.840771, 2.324589]",3 rue de la Gaité,La Brasserie Gaité,2013-08-22,75014,"{'coordinates': [2.324589, 48.840771], 'type': 'Point'}",e7c4cba08749c892a73db2715d06623d9e0c2f67,-,,Paris,France --,"[48.875232, 2.336036]",46 rue Victoire,Café Zen,2012-05-11,75009,"{'coordinates': [2.336036, 48.875232], 'type': 'Point'}",5e9a6172f8b64cd098a5f2cf9b1d42567fe0a894,-,1,Paris,France --,"[48.872677, 2.315276]",27 rue de Penthièvre,O'Breizh,2012-10-22,75008,"{'coordinates': [2.315276, 48.872677], 'type': 'Point'}",ead3108add27ef41bb92517aca834f7d7f632816,1,1,Paris,France --,"[48.868838, 2.33605]",23 rue saint augustin,Le Petit Choiseul,2012-10-22,75002,"{'coordinates': [2.33605, 48.868838], 'type': 'Point'}",de601277ca00567b62fa4f5277e4a17679faa753,1,1,Paris,France --,"[48.841526, 2.351012]",7 rue Epée de Bois,Invitez vous chez nous,2012-10-22,75005,"{'coordinates': [2.351012, 48.841526], 'type': 'Point'}",1d2373bdec8a07306298e3ee54894ac295ee1d55,1,1,Paris,France --,"[48.86525, 2.350507]",142 Rue Saint-Denis 75002 Paris,La Cordonnerie,2012-10-22,75002,"{'coordinates': [2.350507, 48.86525], 'type': 'Point'}",5c9bf60617a99ad75445b454f98e75e1a104021d,1,1,Paris,France --,"[48.892244, 2.346973]","3, rue Baudelique",Le Supercoin,2012-10-22,75018,"{'coordinates': [2.346973, 48.892244], 'type': 'Point'}",68a4ee10f1fc4d2a659501e811d148420fa80e95,1,1,Paris,France --,"[48.890043, 2.362241]",86 bis rue Riquet,Populettes,2012-10-22,75018,"{'coordinates': [2.362241, 48.890043], 'type': 'Point'}",8cc55d58d72621a7e91cf6b456731d2cb2863afc,1,1,Paris,France -1,"[48.893017, 2.337776]",49 rue des Cloys,Au bon coin,2012-10-18,75018,"{'coordinates': [2.337776, 48.893017], 'type': 'Point'}",0408f272c08c52e3cae035ffdeb8928698787ea9,1,1,Paris,France --,"[48.836919, 2.347003]",69 rue Broca,Le Couvent,2012-10-18,75013,"{'coordinates': [2.347003, 48.836919], 'type': 'Point'}",729b2f228d3fd2db6f78bb624d451f59555e4a04,1,1,Paris,France --,"[48.840624, 2.349766]",111 rue mouffetard,La Brûlerie des Ternes,2012-10-18,75005,"{'coordinates': [2.349766, 48.840624], 'type': 'Point'}",233dd2a17620cd5eae70fef11cc627748e3313d5,1,1,Paris,France --,"[48.832825, 2.336116]",59 Boulevard Saint-Jacques,L'Écir,2012-10-09,75014,"{'coordinates': [2.336116, 48.832825], 'type': 'Point'}",4a44324a5a806801fd3e05af89cad7c0f1e69d1e,-,1,Paris,France --,"[48.850696, 2.378417]","126, rue du Faubourg Saint Antoine",Le Chat bossu,2012-10-09,75012,"{'coordinates': [2.378417, 48.850696], 'type': 'Point'}",d1d02463f7c90d38cccffd898e15f51e65910baf,-,1,Paris,France --,"[48.834157, 2.33381]",58 boulvevard Saint Jacques,Denfert café,2012-10-09,75014,"{'coordinates': [2.33381, 48.834157], 'type': 'Point'}",f78f406e5ccb95cb902ce618ed54eba1d4776a3c,-,1,Paris,France --,"[48.867948, 2.343582]",95 rue Montmartre,Le Café frappé,2012-10-09,75002,"{'coordinates': [2.343582, 48.867948], 'type': 'Point'}",4dd2a924a7b2c5f061cecaba2f272548a8c83c6c,-,1,Paris,France --,"[48.859772, 2.360558]",78 rue vieille du temple,La Perle,2012-10-09,75003,"{'coordinates': [2.360558, 48.859772], 'type': 'Point'}",476c54e643613ec36a9b1c533f32122fd873f3c3,-,1,Paris,France --,"[48.845047, 2.349583]",1 rue Thouin,Le Descartes,2012-10-09,75005,"{'coordinates': [2.349583, 48.845047], 'type': 'Point'}",e9cb0b0d6b6b512e9ecab889267ba342a4f0ea93,-,1,Paris,France --,"[48.830151, 2.334213]",55 rue de la tombe Issoire,Le petit club,2012-03-07,75014,"{'coordinates': [2.334213, 48.830151], 'type': 'Point'}",c6ca1166fa7fd7050f5d1c626a1e17050116c91e,-,1,Paris,France --,"[48.865707, 2.374382]",90 avenue Parmentier,Le Plein soleil,2012-03-07,75011,"{'coordinates': [2.374382, 48.865707], 'type': 'Point'}",95cf5fb735bd19826db70a3af4fe72fce647d4e5,-,1,Paris,France --,"[48.875322, 2.312329]","146, boulevard Haussmann",Le Relais Haussmann,2012-03-07,75008,"{'coordinates': [2.312329, 48.875322], 'type': 'Point'}",6c5f68f47c916638342b77bd5edfc30bd7051303,-,1,Paris,France --,"[48.859559, 2.30643]",88 rue Saint-Dominique,Le Malar,2012-03-07,75007,"{'coordinates': [2.30643, 48.859559], 'type': 'Point'}",c633cb01686aa5f3171bf59976dc9b7f23cbca54,-,1,Paris,France --,"[48.864628, 2.408038]",47 rue Belgrand,Au panini de la place,2012-03-07,75020,"{'coordinates': [2.408038, 48.864628], 'type': 'Point'}",66dedd35fcbbbb24f328882d49098de7aa5f26ba,-,1,Paris,France --,"[48.88435, 2.297978]",182 rue de Courcelles,Le Village,2012-03-07,75017,"{'coordinates': [2.297978, 48.88435], 'type': 'Point'}",1e07eaf8a93875906d0b18eb6a897c651943589a,-,1,Paris,France --,"[48.853381, 2.376706]",41 rue de Charonne,Pause Café,2012-03-07,75011,"{'coordinates': [2.376706, 48.853381], 'type': 'Point'}",60d98c3236a70824df50e9aca83e7d7f13a310c5,-,1,Paris,France --,"[48.853253, 2.383415]",14 rue Jean Macé,Le Pure café,2012-03-07,75011,"{'coordinates': [2.383415, 48.853253], 'type': 'Point'}",66707fb2e707d2145fc2eb078a1b980a45921616,-,1,Paris,France --,"[48.848873, 2.392859]",307 fg saint Antoine,Extra old café,2012-03-07,75011,"{'coordinates': [2.392859, 48.848873], 'type': 'Point'}",039ec7dcb219cfc434547b06938ba497afeb83b4,-,1,Paris,France --,"[48.873227, 2.360787]",44 rue Vinaigriers,Chez Fafa,2012-03-07,75010,"{'coordinates': [2.360787, 48.873227], 'type': 'Point'}",1572d199f186bf86d7753fe71ac23477a7a8bd2c,-,1,Paris,France --,"[48.850836, 2.384069]",3 rue Faidherbe,En attendant l'or,2012-06-27,75011,"{'coordinates': [2.384069, 48.850836], 'type': 'Point'}",de5789bb4a4ffbd244cded8dc555639dbe7d2279,-,1,Paris,France --,"[48.866993, 2.336006]",30 rue des Petits-Champs,Brûlerie San José,2012-06-27,75002,"{'coordinates': [2.336006, 48.866993], 'type': 'Point'}",b736e5fa17396ee56a642212ccd0ab29c7f2bef1,-,1,Paris,France --,"[48.856434, 2.342683]",2 place Martin Nadaud,Café Martin,2012-06-27,75001,"{'coordinates': [2.342683, 48.856434], 'type': 'Point'}",25fbf857029c54d57909c158e3039349b77344ed,-,1,Paris,France --,"[48.863675, 2.348701]","14 rue Turbigo, Paris",Etienne,2012-06-27,75001,"{'coordinates': [2.348701, 48.863675], 'type': 'Point'}",0190edd7b0766c6d3e43093deb41abe9446c1b22,-,1,Paris,France --,"[48.854584, 2.385193]",184 bd Voltaire,L'ingénu,2012-06-27,75011,"{'coordinates': [2.385193, 48.854584], 'type': 'Point'}",7fcef7475ec632d5c61c9c36c1d52a402ad2e9e8,-,1,Paris,France --,"[48.890605, 2.361349]",8 rue L'Olive,L'Olive,2012-06-27,75018,"{'coordinates': [2.361349, 48.890605], 'type': 'Point'}",35286273f281c8c5082b3d3bd17f6bbf207426f9,-,1,Paris,France --,"[48.871396, 2.338321]",18 rue Favart,Le Biz,2012-06-27,75002,"{'coordinates': [2.338321, 48.871396], 'type': 'Point'}",cf2f6b9e283aaeeca2214cc1fe57b45e45668e25,-,1,Paris,France --,"[48.868109, 2.331785]",1 rue Louis le Grand,Le Cap Bourbon,2012-06-27,75002,"{'coordinates': [2.331785, 48.868109], 'type': 'Point'}",339030e95e0846b41a8e2b91045456c4e4a50043,-,1,Paris,France --,"[48.84167, 2.303053]",9 Place du General Beuret,Le General Beuret,2012-06-27,75015,"{'coordinates': [2.303053, 48.84167], 'type': 'Point'}",12b37036d28d28b32ebe81756ad15eb68372947f,-,1,Paris,France --,"[48.846814, 2.289311]",95 avenue Emile Zola,Le Germinal,2012-06-27,75015,"{'coordinates': [2.289311, 48.846814], 'type': 'Point'}",4e03831a64e886a28a7232e54f2812c1ced23c5a,-,1,Paris,France --,"[48.862655, 2.337607]",202 rue Saint-Honoré,Le Ragueneau,2012-06-27,75001,"{'coordinates': [2.337607, 48.862655], 'type': 'Point'}",e303131b2600d4c2287749a36bf7193d2fa60bd7,-,1,Paris,France --,"[48.889982, 2.338933]",72 rue lamarck,Le refuge,2012-06-27,75018,"{'coordinates': [2.338933, 48.889982], 'type': 'Point'}",f64310461736a769c6854fdefb99b9f2e7b230a9,-,1,Paris,France --,"[48.870294, 2.352821]",13 rue du Faubourg Saint Denis,Le sully,2012-06-27,75010,"{'coordinates': [2.352821, 48.870294], 'type': 'Point'}",166be8588ff16e50838fa6a164d1e580497b795d,-,1,Paris,France --,"[48.842128, 2.280374]",60 rue des bergers,Le bal du pirate,2014-02-01,75015,"{'coordinates': [2.280374, 48.842128], 'type': 'Point'}",93ff6e35406a074a6ba2667d2b286abf91132f6a,-,1,Paris,France --,"[48.838769, 2.39609]",95 rue claude decaen,zic zinc,2014-02-01,75012,"{'coordinates': [2.39609, 48.838769], 'type': 'Point'}",8fdc739d64ff1f01973235301e3ec86791016759,-,1,Paris,France --,"[48.870247, 2.376306]",35 rue de l'orillon,l'orillon bar,2014-02-01,75011,"{'coordinates': [2.376306, 48.870247], 'type': 'Point'}",6e6e9695ef04c3fdbd4cfa19f0cfb0c0f93f4933,-,1,Paris,France --,"[48.869848, 2.394247]",116 Rue de Ménilmontant,Le Zazabar,2013-08-22,75020,"{'coordinates': [2.394247, 48.869848], 'type': 'Point'}",22edfe92a72ce1bb0eea972508b5caa7af2db2df,-,1,Paris,France --,"[48.845458, 2.354796]",22 rue Linné,L'Inévitable,2013-08-22,75005,"{'coordinates': [2.354796, 48.845458], 'type': 'Point'}",6893cb08e99319091de9ba80305f22e0ce4cc08d,-,1,Paris,France --,"[48.83336, 2.365782]",77 rue Dunois,Le Dunois,2013-08-22,75013,"{'coordinates': [2.365782, 48.83336], 'type': 'Point'}",c3be1246dbc4ca5734d5bbd569436ba655105248,-,1,Paris,France --,"[48.862655, 2.337607]",202 rue Saint Honoré,Ragueneau,2012-05-11,75001,"{'coordinates': [2.337607, 48.862655], 'type': 'Point'}",e946d9e8f8c5a130f98eca945efadfd9eec40dcb,-,1,Paris,France -,"[48.826608, 2.374571]",48 rue du Dessous des Berges,Le Caminito,2012-10-22,75013,"{'coordinates': [2.374571, 48.826608], 'type': 'Point'}",5d9ad8bcfbbc20adaec2aa7dcdb71326868c7686,1,1,Paris,France --,"[48.870598, 2.365413]",55bis quai de Valmy,Epicerie Musicale,2012-10-22,75010,"{'coordinates': [2.365413, 48.870598], 'type': 'Point'}",8ffea133d93c608d337bc0129f4f9f3d5cad8dae,1,1,Paris,France --,,Le petit Bretonneau - à l'intérieur de l'Hôpital,Le petit Bretonneau,2012-10-22,75018,{},dd1ffdbd55c2dc651201302b8015258f7d35fd35,1,1,Paris,France --,"[48.862575, 2.367427]",104 rue amelot,Le Centenaire,2012-10-22,75011,"{'coordinates': [2.367427, 48.862575], 'type': 'Point'}",e47d25752c2c8bcab5efb0e3a41920ec7a8a766a,1,1,Paris,France --,"[48.842833, 2.348314]",13 Rue du Pot de Fer,La Montagne Sans Geneviève,2012-10-22,75005,"{'coordinates': [2.348314, 48.842833], 'type': 'Point'}",314d170601f81e3a3f26d8801f0fbee39981c788,1,,Paris,France --,"[48.851325, 2.40171]",46 rue de Buzenval,Les Pères Populaires,2012-10-18,75020,"{'coordinates': [2.40171, 48.851325], 'type': 'Point'}",e75e8a3fe6212a0e576beec82f0128dd394e56fa,1,1,Paris,France --,"[48.857658, 2.305613]",188 rue de Grenelle,Cafe de grenelle,2012-10-09,75007,"{'coordinates': [2.305613, 48.857658], 'type': 'Point'}",72e274046d671e68bc7754808feacfc95b91b6ed,-,1,Paris,France --,"[48.875207, 2.332944]",73 rue de la Victoire,Le relais de la victoire,2012-10-09,75009,"{'coordinates': [2.332944, 48.875207], 'type': 'Point'}",01c21abdf35484f9ad184782979ecd078de84523,-,1,Paris,France --,,"Route de la Muette à Neuilly -Club hippique du Jardin d’Acclimatation",La chaumière gourmande,2012-03-07,75016,{},438ec18d35793d12eb6a137373c3fe4f3aa38a69,1,,Paris,France --,"[48.891882, 2.33365]","216, rue Marcadet",Le Brio,2012-03-07,75018,"{'coordinates': [2.33365, 48.891882], 'type': 'Point'}",49e3584ce3d6a6a236b4a0db688865bdd3483fec,-,1,Paris,France --,"[48.884753, 2.324648]",22 rue des Dames,Caves populaires,2012-03-07,75017,"{'coordinates': [2.324648, 48.884753], 'type': 'Point'}",af81e5eca2b84ea706ac2d379edf65b0fb2f879a,-,1,Paris,France --,"[48.827428, 2.325652]",12 avenue Jean Moulin,Caprice café,2012-03-07,75014,"{'coordinates': [2.325652, 48.827428], 'type': 'Point'}",88e07bdb723b49cd27c90403b5930bca1c93b458,-,1,Paris,France --,"[48.832964, 2.369266]",7 rue Clisson,Tamm Bara,2012-03-07,75013,"{'coordinates': [2.369266, 48.832964], 'type': 'Point'}",baf59c98acafbe48ed9e91b64377da216a20cbcc,-,1,Paris,France --,"[48.876577, 2.348414]",1 rue de Montholon,L'anjou,2012-03-07,75009,"{'coordinates': [2.348414, 48.876577], 'type': 'Point'}",e6f5949dca40548aad296208c61c498f639f648c,-,1,Paris,France --,"[48.862599, 2.315086]",2 rue Robert Esnault Pelterie,Café dans l'aerogare Air France Invalides,2012-03-07,75007,"{'coordinates': [2.315086, 48.862599], 'type': 'Point'}",6d175eb48c577fafdfc99df0ab55da468cf17164,-,1,Paris,France --,"[48.844854, 2.345413]","10 rue d""Ulm",Waikiki,2012-03-07,75005,"{'coordinates': [2.345413, 48.844854], 'type': 'Point'}",fb1e2bc2ae55d3d47da682c71093a12fa64fbd45,-,1,Paris,France --,"[48.871576, 2.364499]",36 rue Beaurepaire,Chez Prune,2012-03-07,75010,"{'coordinates': [2.364499, 48.871576], 'type': 'Point'}",19399ede5b619761877822185bbb4c98b565974c,-,1,Paris,France --,"[48.833863, 2.329046]",21 rue Boulard,Au Vin Des Rues,2012-06-27,75014,"{'coordinates': [2.329046, 48.833863], 'type': 'Point'}",87263873b6f8346b5777844be0122a307f29fcab,-,1,Paris,France --,"[48.838137, 2.301166]",14 rue d'alleray,bistrot les timbrés,2012-06-27,75015,"{'coordinates': [2.301166, 48.838137], 'type': 'Point'}",74b6d7113e5b14eb8e890663f061208bf4ff6728,-,1,Paris,France --,"[48.871799, 2.315985]",9 rue de Miromesnil,Café beauveau,2012-06-27,75008,"{'coordinates': [2.315985, 48.871799], 'type': 'Point'}",2759f5cdda4bcb88ca3ae2f7299b37b8e62596c8,-,1,Paris,France --,"[48.866259, 2.338739]",9 rue des petits champs,Café Pistache,2012-06-27,75001,"{'coordinates': [2.338739, 48.866259], 'type': 'Point'}",6d2675cdc912118d0376229be8e436feca9c8af7,-,1,Paris,France --,"[48.874605, 2.387738]",13 Rue Jean-Baptiste Dumay,La Cagnotte,2012-06-27,75020,"{'coordinates': [2.387738, 48.874605], 'type': 'Point'}",f7085c754c0c97e418d7e5213753f74bd396fc27,-,1,Paris,France --,"[48.842462, 2.310919]",172 rue de vaugirard,le 1 cinq,2012-06-27,75015,"{'coordinates': [2.310919, 48.842462], 'type': 'Point'}",17e917723fc99d6e5bd77eb9633ac2e789a9a6d9,-,1,Paris,France --,"[48.84591, 2.375543]",28 bis boulevard Diderot,Le Killy Jen,2012-06-27,75012,"{'coordinates': [2.375543, 48.84591], 'type': 'Point'}",93132bd8b3ae67dfcc8cf8c1166e312ac4acb9b9,-,1,Paris,France --,"[48.842868, 2.303173]",106 rue Lecourbe,Les Artisans,2012-06-27,75015,"{'coordinates': [2.303173, 48.842868], 'type': 'Point'}",c37ef573b4cb2d1e61795d6a9ef11de433dc9a99,-,1,Paris,France --,"[48.865684, 2.334416]",83 avenue de Wagram,Peperoni,2012-06-27,75001,"{'coordinates': [2.334416, 48.865684], 'type': 'Point'}",9461f859ca009ced25555fa6af1e6867dda9223e,-,1,Paris,France --,"[48.833146, 2.288834]",380 rue de vaugirard,le lutece,2014-02-01,75015,"{'coordinates': [2.288834, 48.833146], 'type': 'Point'}",ddd13990c1408700085366bd4ba313acd69a44ea,-,1,Paris,France --,"[48.886431, 2.327429]",16 rue Ganneron,Brasiloja,2014-02-01,75018,"{'coordinates': [2.327429, 48.886431], 'type': 'Point'}",d679bb1642534278f4c0203d67be0bafd5306d81,-,1,Paris,France --,"[48.855711, 2.359491]",16 rue de Rivoli,Rivolux,2014-02-01,75004,"{'coordinates': [2.359491, 48.855711], 'type': 'Point'}",bdd2b008cc765c7fe195c037b830cd2628420a2f,-,1,Paris,France --,"[48.845898, 2.372766]",21 Bis Boulevard Diderot,L'européen,2013-08-22,75012,"{'coordinates': [2.372766, 48.845898], 'type': 'Point'}",693c0da7d4db24781ed161c01a661c36074a94fa,-,1,Paris,France --,"[48.867465, 2.357791]",39 rue Notre Dame de Nazareth,NoMa,2013-08-22,75003,"{'coordinates': [2.357791, 48.867465], 'type': 'Point'}",60d8b670810cc95eb0439dd0c238f8205ea8ef76,-,1,Paris,France --,"[48.871595, 2.385858]",1 Rue des Envierges,O'Paris,2013-08-22,75020,"{'coordinates': [2.385858, 48.871595], 'type': 'Point'}",297c040284a05efe35c69bb621505e6acfdcdda4,-,1,Paris,France --,"[48.872402, 2.366532]",16 avenue Richerand,Café Clochette,2013-08-22,75010,"{'coordinates': [2.366532, 48.872402], 'type': 'Point'}",a561a941f538e8a1d321bf8d98576d06be037962,-,1,Paris,France --,"[48.856584, 2.368574]",40 Boulevard Beaumarchais,La cantoche de Paname,2013-08-22,75011,"{'coordinates': [2.368574, 48.856584], 'type': 'Point'}",5ba2aaec9f1de9d01e65be95215cab13c693cdf3,-,0,Paris,France --,"[48.856496, 2.394874]",148 Boulevard de Charonne,Le Saint René,2013-08-22,75020,"{'coordinates': [2.394874, 48.856496], 'type': 'Point'}",4d60b350d04d4b1bf4bfd4dd6cc59687dc792c74,-,1,Paris,France --,"[48.850055, 2.383908]",196 rue du faubourg saint-antoine,La Liberté,2012-10-22,75012,"{'coordinates': [2.383908, 48.850055], 'type': 'Point'}",ee94e76326f8dcbe3500afec69f1a21eb1215ad0,1,1,Paris,France --,"[48.866737, 2.33716]",16 rue des Petits Champs,Chez Rutabaga,2012-10-22,75002,"{'coordinates': [2.33716, 48.866737], 'type': 'Point'}",a420ea4608440b8dc8e0267fe8cc513daa950551,1,1,Paris,France --,"[48.885367, 2.325325]",2 rue Lemercier,Le BB (Bouchon des Batignolles),2012-10-22,75017,"{'coordinates': [2.325325, 48.885367], 'type': 'Point'}",20986cbfe11018bd0aba8150a49db1c435f7642d,1,1,Paris,France --,"[48.873175, 2.339193]",10 rue Rossini,La Brocante,2012-10-22,75009,"{'coordinates': [2.339193, 48.873175], 'type': 'Point'}",2e10601c35669394d43936a771b18408be0338ba,1,1,Paris,France --,"[48.840771, 2.324589]",3 rue Gaîté,Le Plomb du cantal,2012-10-22,75014,"{'coordinates': [2.324589, 48.840771], 'type': 'Point'}",6fb510614e00b065bf16a5af8e2c0eaf561a5654,1,1,Paris,France --,"[48.884753, 2.324648]",22 rue des Dames,Les caves populaires,2012-10-22,75017,"{'coordinates': [2.324648, 48.884753], 'type': 'Point'}",d650c509a0aa8ed7b9c9b88861263f31463bbd0e,1,1,Paris,France --,"[48.869519, 2.39339]",108 rue de Ménilmontant,Chez Luna,2012-10-09,75020,"{'coordinates': [2.39339, 48.869519], 'type': 'Point'}",736f4d996f1f8b7c3a0ce2abfeebfcce2a4bab13,-,1,Paris,France --,"[48.877903, 2.385365]",1 rue du Plateau,Le bar Fleuri,2012-03-07,75019,"{'coordinates': [2.385365, 48.877903], 'type': 'Point'}",be55720646093788ec161c6cadc5ad8059f4b90b,-,1,Paris,France --,"[48.882939, 2.31809]",101 rue des dames,Trois pièces cuisine,2012-03-07,75017,"{'coordinates': [2.31809, 48.882939], 'type': 'Point'}",7bbbfb755020a2c25cce0067601994ce5ee4193f,-,1,Paris,France --,"[48.849497, 2.298855]",61 avenue de la Motte Picquet,Le Zinc,2012-03-07,75015,"{'coordinates': [2.298855, 48.849497], 'type': 'Point'}",e7c35a94454518de6de5bbecbc015fc37f7aea14,-,1,Paris,France --,"[48.880669, 2.349964]",136 rue du Faubourg poissonnière,La cantine de Zoé,2012-03-07,75010,"{'coordinates': [2.349964, 48.880669], 'type': 'Point'}",0edc473b3432a869b8ed66b6c4c989766b699947,-,1,Paris,France --,"[48.844057, 2.328402]",6/8 rue Stanislas,Les Vendangeurs,2012-03-07,75006,"{'coordinates': [2.328402, 48.844057], 'type': 'Point'}",e9766ea36f6293bf670ed938bff02b975d012973,-,1,Paris,France --,"[48.852053, 2.338779]",3 carrefour de l'Odéon,L'avant comptoir,2012-03-07,75006,"{'coordinates': [2.338779, 48.852053], 'type': 'Point'}",fe843d2f43dcaac9129f5b36dc367558dfd3b3e4,-,1,Paris,France -1,"[48.886504, 2.34498]",1 rue Paul albert,Botak cafe,2012-03-07,75018,"{'coordinates': [2.34498, 48.886504], 'type': 'Point'}",9e19c375e612f5fb803ec6a27881858619207812,1,1,Paris,France --,"[48.872722, 2.354594]",67 rue du Château d'eau,le chateau d'eau,2012-03-07,75010,"{'coordinates': [2.354594, 48.872722], 'type': 'Point'}",05bb6a26ec5bfbba25da2d19a5f0e83d69800f38,-,1,Paris,France --,"[48.85192, 2.373229]",58 rue du Fbg Saint-Antoine,Bistrot Saint-Antoine,2012-06-27,75012,"{'coordinates': [2.373229, 48.85192], 'type': 'Point'}",daa3908ddf69d378fec5b4548494727e1121adc4,-,1,Paris,France --,"[48.854685, 2.368487]",11/13 boulevard Beaumarchais,Chez Oscar,2012-06-27,75004,"{'coordinates': [2.368487, 48.854685], 'type': 'Point'}",c73be0483480c59e6ab6bc3a906c8d9dd474887f,-,1,Paris,France --,"[48.87226, 2.304441]",63 rue de Ponthieu,Le Fronton,2012-06-27,75008,"{'coordinates': [2.304441, 48.87226], 'type': 'Point'}",85a8200d3e3aed7724d3207ed8b1ee5ec50c1f90,-,1,Paris,France --,"[48.851, 2.300378]",48 avenue de la Motte Picquet,Le Piquet,2012-06-27,75015,"{'coordinates': [2.300378, 48.851], 'type': 'Point'}",460e2adc95fd172f753b1b6ed296c2711639d49d,-,1,Paris,France --,"[48.841089, 2.349565]",104 rue Mouffetard,Le Tournebride,2012-06-27,75005,"{'coordinates': [2.349565, 48.841089], 'type': 'Point'}",8a7a23ed68366f70ab939c877cbdce46f19d75c7,-,1,Paris,France --,"[48.828704, 2.322074]",52 rue des plantes,maison du vin,2012-06-27,75014,"{'coordinates': [2.322074, 48.828704], 'type': 'Point'}",482507a8f0fe4960f94372b6fa12b16e7d4f2a93,-,1,Paris,France --,"[48.842146, 2.375986]",157 rue Bercy 75012 Paris,L'entrepôt,2014-02-01,75012,"{'coordinates': [2.375986, 48.842146], 'type': 'Point'}",d8746118429eb118f38ecbee904636d9b33fa8ba,-,1,Paris,France --,"[48.867092, 2.363288]",Place de la République,Le café Monde et Médias,2013-08-22,75003,"{'coordinates': [2.363288, 48.867092], 'type': 'Point'}",af04c90f25e25daf7f5cbbab1bc740bac26541d4,-,1,Paris,France --,"[48.849821, 2.355337]",11 Quai de la Tournelle,Café rallye tournelles,2013-08-22,75005,"{'coordinates': [2.355337, 48.849821], 'type': 'Point'}",91d88e321a75b6a8c4dea816c399fda77c41f9d1,-,1,Paris,France --,"[48.872498, 2.355136]",61 rue du château d'eau,Brasserie le Morvan,2013-08-22,75010,"{'coordinates': [2.355136, 48.872498], 'type': 'Point'}",6ec62058995948fc18d331f01a2d03acc0d9e0fa,-,1,Paris,France -1,"[48.874879, 2.386064]",6 rue Mélingue,Chez Miamophile,2013-08-22,75019,"{'coordinates': [2.386064, 48.874879], 'type': 'Point'}",13924872737e4fc640a43da58937c3777c2ac753,1,1,Paris,France --,"[48.864269, 2.36858]",18 rue de Crussol,Panem,2012-05-11,75011,"{'coordinates': [2.36858, 48.864269], 'type': 'Point'}",67bdf3a6989f80749a1ba33a17b1370de0a0e1cd,-,0,Paris,France --,"[48.885662, 2.319591]",47 rue de Batignolles,Petits Freres des Pauvres,2012-05-11,75017,"{'coordinates': [2.319591, 48.885662], 'type': 'Point'}",e27fd00149514bbfad7dd7e8f9b0c677df2d3f25,-,0,Paris,France --,"[48.837212, 2.296046]",198 rue de la Convention,Café Dupont,2012-05-11,75015,"{'coordinates': [2.296046, 48.837212], 'type': 'Point'}",4d40e6d864dae81c152a05cb98e30933bde96aa1,-,0,Paris,France --,"[48.871002, 2.30879]",28 rue de Ponthieu,L'Angle,2012-10-22,75008,"{'coordinates': [2.30879, 48.871002], 'type': 'Point'}",c40bd2d1f98b415e539c27cf68518d060ebab51e,1,1,Paris,France --,"[48.888023, 2.353467]",19-23 rue Léon,Institut des Cultures d'Islam,2012-10-22,75018,"{'coordinates': [2.353467, 48.888023], 'type': 'Point'}",68d6d37b846e39bd8554e1f8f75974b486b0f27b,1,1,Paris,France --,"[48.886044, 2.360781]",19 rue Pajol,Canopy Café associatif,2012-10-22,75018,"{'coordinates': [2.360781, 48.886044], 'type': 'Point'}",ff73bafb514bb68eb925c81aee43c3a58ac3c70d,1,1,Paris,France --,"[48.870287, 2.332491]",place de l'opera,L'Entracte,2012-10-09,75002,"{'coordinates': [2.332491, 48.870287], 'type': 'Point'}",0039cd8bceb5e281677a158f832a660789088071,-,1,Paris,France --,"[48.858709, 2.362701]",15 rue du Parc Royal,Le Sévigné,2012-10-09,75003,"{'coordinates': [2.362701, 48.858709], 'type': 'Point'}",adcc8b4f78f05ba7b24b0593e1516dfb7b415f91,-,1,Paris,France --,"[48.839687, 2.347254]",35 rue Claude Bernard,Le Café d'avant,2012-10-09,75005,"{'coordinates': [2.347254, 48.839687], 'type': 'Point'}",b904fc48763938eee2169ba25aad2ffcc0dd6a9f,-,1,Paris,France --,"[48.844244, 2.330407]",53 rue Notre-Dame des Champs,Le Lucernaire,2012-10-09,75006,"{'coordinates': [2.330407, 48.844244], 'type': 'Point'}",cc72af04314fd40e16ff611c799d378515043508,-,1,Paris,France --,"[48.877599, 2.332111]",12 rue Blanche,Le Brigadier,2012-10-09,75009,"{'coordinates': [2.332111, 48.877599], 'type': 'Point'}",978d4bc68c9ebf81029d3e77274d2107777b8a75,-,1,Paris,France --,"[48.826494, 2.359987]",26 rue du Docteur Magnan,L'âge d'or,2012-03-07,75013,"{'coordinates': [2.359987, 48.826494], 'type': 'Point'}",40bffbdc0c9ed1cbce820fed875d7c21d8964640,-,1,Paris,France --,"[48.883717, 2.326861]",Place de Clichy,Bagels & Coffee Corner,2012-03-07,75017,"{'coordinates': [2.326861, 48.883717], 'type': 'Point'}",262facde9b8c4568c9ba7fbce8f069ff8c76948d,-,1,Paris,France --,"[48.835843, 2.278501]",10 boulevard Victor,Café Victor,2012-03-07,75015,"{'coordinates': [2.278501, 48.835843], 'type': 'Point'}",e5817ec44ac5a7ea2e4a34b6a2e13d535156642b,-,1,Paris,France --,"[48.845337, 2.379024]","54, avenue Daumesnil",L'empreinte,2012-03-07,75012,"{'coordinates': [2.379024, 48.845337], 'type': 'Point'}",b96ddd35cbbf5d93aaff79487afdf083b5ff0817,-,1,Paris,France --,"[48.857312, 2.379055]","93, rue de la Roquette",L'horizon,2012-03-07,75011,"{'coordinates': [2.379055, 48.857312], 'type': 'Point'}",84c6b7335e7f82ac942c4f398723ec99076f148d,-,1,Paris,France --,"[48.835878, 2.395723]",34 bis rue de Wattignies,Au pays de Vannes,2012-06-27,75012,"{'coordinates': [2.395723, 48.835878], 'type': 'Point'}",a17869dbb9d0d5b1e5ed7bb288053900b04ee944,-,1,Paris,France --,"[48.85413, 2.323539]",36 rue de Varenne,Café Varenne,2012-06-27,75007,"{'coordinates': [2.323539, 48.85413], 'type': 'Point'}",a26ec0d5fca47b8de77d862ad8a99b75bb520a09,-,1,Paris,France --,"[48.855161, 2.360218]",125 Rue Saint-Antoine,l'Eléphant du nil,2012-06-27,75004,"{'coordinates': [2.360218, 48.855161], 'type': 'Point'}",7b7ceefd1f9ed85041265c9577e0dc8bee01d45a,-,1,Paris,France --,"[48.8357, 2.292961]",354 bis rue Vaugirard,Le Comptoir,2012-06-27,75015,"{'coordinates': [2.292961, 48.8357], 'type': 'Point'}",59d8fa304e535f4eb41f9746028034c9b30cbde4,-,1,Paris,France --,"[48.835451, 2.292515]",358 rue de Vaugirard,Le Parc Vaugirard,2012-06-27,75015,"{'coordinates': [2.292515, 48.835451], 'type': 'Point'}",19f655206a8446959c8e796c2b3cb9001890f985,-,1,Paris,France --,"[48.834972, 2.327007]",58 rue Daguerre,le Zango,2012-06-27,75014,"{'coordinates': [2.327007, 48.834972], 'type': 'Point'}",e1b54109015316a822747f788128f997a3478050,-,1,Paris,France --,"[48.848887, 2.399972]",3 rue de Lagny,Melting Pot,2012-06-27,75020,"{'coordinates': [2.399972, 48.848887], 'type': 'Point'}",fd0de2cbf73e0a728cd73e4e2a9a4a9c646f76f2,-,1,Paris,France --,"[48.892366, 2.317359]",174 avenue de Clichy,Pari's Café,2012-06-27,75017,"{'coordinates': [2.317359, 48.892366], 'type': 'Point'}",831446ae203f89de26d3300e625c20717e82d40a,-,1,Paris,France diff --git a/bonobo/examples/datasets/coffeeshops.json b/bonobo/examples/datasets/coffeeshops.json deleted file mode 100644 index 30b20ee..0000000 --- a/bonobo/examples/datasets/coffeeshops.json +++ /dev/null @@ -1,181 +0,0 @@ -[{"prix_terasse": "-", "geoloc": [48.839512, 2.303007], "address": "344Vrue Vaugirard", "name": "Coffee Chope", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.303007, 48.839512], "type": "Point"}, "recordid": "3c276428d45ad68ccdf6875e4ddcfe95d0c0d4cf", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.876737, 2.357601], "address": "5, rue d'Alsace", "name": "Ext\u00e9rieur Quai", "date": "2014-02-01", "zipcode": 75010, "geometry": {"coordinates": [2.357601, 48.876737], "type": "Point"}, "recordid": "97ad81cd1127a8566085ad796eeb44a06bec7514", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.850852, 2.362029], "address": "6 Bd henri IV", "name": "Le Sully", "date": "2014-02-01", "zipcode": 75004, "geometry": {"coordinates": [2.362029, 48.850852], "type": "Point"}, "recordid": "aa4294c1b8d660a23db0dc81321e509bae1dae68", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "1", "geoloc": [48.893517, 2.340271], "address": "53 rue du ruisseau", "name": "O q de poule", "date": "2013-08-22", "zipcode": 75018, "geometry": {"coordinates": [2.340271, 48.893517], "type": "Point"}, "recordid": "a81362dbed35247555fb105bd83ff2906904a66e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.864655, 2.350089], "address": "1 Passage du Grand Cerf", "name": "Le Pas Sage", "date": "2013-08-22", "zipcode": 75002, "geometry": {"coordinates": [2.350089, 48.864655], "type": "Point"}, "recordid": "7ced86acbd5ccfba229bcc07d70d0d117aee16a5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.895825, 2.339712], "address": "112 Rue Championnet", "name": "La Renaissance", "date": "2013-08-22", "zipcode": 75018, "geometry": {"coordinates": [2.339712, 48.895825], "type": "Point"}, "recordid": "5582c8572bd7637bf305b74c1c0bdb74a8e4247f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.868581, 2.373015], "address": "Rue de la Fontaine au Roi", "name": "La Caravane", "date": "2012-05-11", "zipcode": 75011, "geometry": {"coordinates": [2.373015, 48.868581], "type": "Point"}, "recordid": "50bb0fa06e562a242f115ddbdae2ed9c7df93d57", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.875155, 2.335536], "address": "51 Rue Victoire", "name": "Le chantereine", "date": "2012-10-22", "zipcode": 75009, "geometry": {"coordinates": [2.335536, 48.875155], "type": "Point"}, "recordid": "eb8a62feeedaf7ed8b8c912305270ee857068689", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.886536, 2.346525], "address": "11 rue Feutrier", "name": "Le M\u00fcller", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.346525, 48.886536], "type": "Point"}, "recordid": "62c552f167f671f88569c1f2d6a44098fb514c51", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.841494, 2.307117], "address": "21 rue Copreaux", "name": "Le drapeau de la fidelit\u00e9", "date": "2012-10-22", "zipcode": 75015, "geometry": {"coordinates": [2.307117, 48.841494], "type": "Point"}, "recordid": "5120ea0b9d7387766072b90655166486928e25c8", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.839743, 2.296898], "address": "125 rue Blomet", "name": "Le caf\u00e9 des amis", "date": "2012-10-22", "zipcode": 75015, "geometry": {"coordinates": [2.296898, 48.839743], "type": "Point"}, "recordid": "865f62415adc5c34e3ca38a1748b7a324dfba209", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.857728, 2.349641], "address": "10 rue Saint Martin", "name": "Le Caf\u00e9 Livres", "date": "2012-10-09", "zipcode": 75004, "geometry": {"coordinates": [2.349641, 48.857728], "type": "Point"}, "recordid": "7ef54a78802d49cafd2701458df2b0d0530d123b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.856003, 2.30457], "address": "46 avenue Bosquet", "name": "Le Bosquet", "date": "2012-10-09", "zipcode": 75007, "geometry": {"coordinates": [2.30457, 48.856003], "type": "Point"}, "recordid": "d701a759e08a71f4bbb01f29473274b0152135d0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.889426, 2.332954], "address": "12 rue Armand Carrel", "name": "Le Chaumontois", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.332954, 48.889426], "type": "Point"}, "recordid": "e12ff00a644c91ad910ddc63a770c190be93a393", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.838521, 2.370478], "address": "34 avenue Pierre Mend\u00e8s-France", "name": "Le Kleemend's", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.370478, 48.838521], "type": "Point"}, "recordid": "0f6cd1ee7751b00c9574efcfdcf66fa0e857d251", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.849861, 2.385342], "address": "202 rue du faubourg st antoine", "name": "Caf\u00e9 Pierre", "date": "2012-03-07", "zipcode": 75012, "geometry": {"coordinates": [2.385342, 48.849861], "type": "Point"}, "recordid": "f9de9d0fb5e92f047a6f1986a31f9dd4d38bcb36", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.872202, 2.304624], "address": "61 rue de Ponthieu", "name": "Les Arcades", "date": "2012-03-07", "zipcode": 75008, "geometry": {"coordinates": [2.304624, 48.872202], "type": "Point"}, "recordid": "67eaf58afc856077c0680601e453e75c0922c9c0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.859031, 2.320315], "address": "31 rue Saint-Dominique", "name": "Le Square", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.320315, 48.859031], "type": "Point"}, "recordid": "678558317bc9ad46652e5b1643e70c2142a76e7e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.850092, 2.37463], "address": "75, avenue Ledru-Rollin", "name": "Assaporare Dix sur Dix", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.37463, 48.850092], "type": "Point"}, "recordid": "667474321887d08a3cc636adf043ad354b65fa61", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.86805, 2.353313], "address": "129 boulevard sebastopol", "name": "Au cerceau d'or", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.353313, 48.86805], "type": "Point"}, "recordid": "c9ef52ba2fabe0286700329f18bbbbea9a10b474", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.845927, 2.373051], "address": "21 ter boulevard Diderot", "name": "Aux cadrans", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.373051, 48.845927], "type": "Point"}, "recordid": "ed5f98686856bf4ddd2b381b43ad229246741a90", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.851662, 2.273883], "address": "17 rue Jean de la Fontaine", "name": "Caf\u00e9 antoine", "date": "2012-06-27", "zipcode": 75016, "geometry": {"coordinates": [2.273883, 48.851662], "type": "Point"}, "recordid": "ab6d1e054e2e6ae7d6150013173f55e83c05ca23", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.877642, 2.312823], "address": "rue de Lisbonne", "name": "Caf\u00e9 de la Mairie (du VIII)", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.312823, 48.877642], "type": "Point"}, "recordid": "7de8a79b026ac63f453556612505b5bcd9229036", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.838633, 2.349916], "address": "5 rue Claude Bernard", "name": "Caf\u00e9 Lea", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.349916, 48.838633], "type": "Point"}, "recordid": "fecd8900cf83027f74ceced9fc4ad80ac73b63a7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.849293, 2.354486], "address": "11 boulevard Saint-Germain", "name": "Cardinal Saint-Germain", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.354486, 48.849293], "type": "Point"}, "recordid": "e4a078c30c98082896787f4e4b41a07554392529", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.869771, 2.342501], "address": "52 rue Notre-Dame des Victoires", "name": "D\u00e9d\u00e9 la frite", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.342501, 48.869771], "type": "Point"}, "recordid": "ccb2ba2f98043e8eefd5fda829dee1ea7f1d2c7a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.834051, 2.287345], "address": "36 rue du hameau", "name": "La Bauloise", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.287345, 48.834051], "type": "Point"}, "recordid": "c9fe10abd15ede7ccaeb55c309898d30d7b19d0e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.888165, 2.377387], "address": "71 quai de Seine", "name": "Le Bellerive", "date": "2012-06-27", "zipcode": 75019, "geometry": {"coordinates": [2.377387, 48.888165], "type": "Point"}, "recordid": "4e0b5c2d33d7c25fc54c51171f3d37e509959fc0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.864543, 2.340997], "address": "42 rue coquill\u00e8re", "name": "Le bistrot de Ma\u00eblle et Augustin", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.340997, 48.864543], "type": "Point"}, "recordid": "52acab12469af291984e9a70962e08c72b058e10", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.872103, 2.346161], "address": "14 rue Rougemont", "name": "Le Dellac", "date": "2012-06-27", "zipcode": 75009, "geometry": {"coordinates": [2.346161, 48.872103], "type": "Point"}, "recordid": "4d1d627ecea2ffa279bb862f8ba495d95ca75350", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.859645, 2.355598], "address": "1 rue Pecquay", "name": "Le Felteu", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.355598, 48.859645], "type": "Point"}, "recordid": "2c1fa55460af282266d86fd003af4f929fdf4e7d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.85763, 2.346101], "address": "2 bis quai de la m\u00e9gisserie", "name": "Le Reynou", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.346101, 48.85763], "type": "Point"}, "recordid": "d4ddd30ab3e721a317fc7ea89d5b9001255ce9f4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.884646, 2.337734], "address": "23 rue des abbesses", "name": "Le Saint Jean", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.337734, 48.884646], "type": "Point"}, "recordid": "51b47cf167b7f32eeebb108330956694d75d4268", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.841007, 2.31466], "address": "65 boulevard Pasteur", "name": "les montparnos", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.31466, 48.841007], "type": "Point"}, "recordid": "2aaca891ffd0694c657a43889516ab72afdfba07", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.850323, 2.33039], "address": "16 rue DE MEZIERES", "name": "L'antre d'eux", "date": "2014-02-01", "zipcode": 75006, "geometry": {"coordinates": [2.33039, 48.850323], "type": "Point"}, "recordid": "4ff4337934c66f61e00d1d9551f7cdddba03e544", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.864957, 2.346938], "address": "58 rue de Montorgueil", "name": "Drole d'endroit pour une rencontre", "date": "2014-02-01", "zipcode": 75002, "geometry": {"coordinates": [2.346938, 48.864957], "type": "Point"}, "recordid": "3451657f880abe75d0c7e386fc698405556c53e8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.889565, 2.339735], "address": "104 rue caulaincourt", "name": "Le pari's caf\u00e9", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.339735, 48.889565], "type": "Point"}, "recordid": "e8c34a537b673fcb26c76e02deca4f5a728929dc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.859115, 2.368871], "address": "60 rue saint-sabin", "name": "Le Poulailler", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.368871, 48.859115], "type": "Point"}, "recordid": "325ea74ba83f716dde87c08cffd36f7df7722a49", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.833595, 2.38604], "address": "33 Cour Saint Emilion", "name": "Chai 33", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.38604, 48.833595], "type": "Point"}, "recordid": "528de8d5d8780bee83145637e315483d48f5ae3c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.868741, 2.379969], "address": "99 rue Jean-Pierre Timbaud", "name": "L'Assassin", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.379969, 48.868741], "type": "Point"}, "recordid": "fac0483890ff8bdaeb3feddbdb032c5112f24678", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.851463, 2.398691], "address": "1 rue d'Avron", "name": "l'Usine", "date": "2014-02-01", "zipcode": 75020, "geometry": {"coordinates": [2.398691, 48.851463], "type": "Point"}, "recordid": "fee1e3eb103bbc98e19e45d34365da0f27166541", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.896305, 2.332898], "address": "52 rue Liebniz", "name": "La Bricole", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.332898, 48.896305], "type": "Point"}, "recordid": "4744e866c244c59eec43b3fe159542d2ef433065", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.850311, 2.34885], "address": "place maubert", "name": "le ronsard", "date": "2014-02-01", "zipcode": 75005, "geometry": {"coordinates": [2.34885, 48.850311], "type": "Point"}, "recordid": "49a390322b45246bc2c1e50fcd46815ad271bca0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.863038, 2.3604], "address": "82 rue des archives", "name": "Face Bar", "date": "2014-02-01", "zipcode": 75003, "geometry": {"coordinates": [2.3604, 48.863038], "type": "Point"}, "recordid": "d96e16ebf2460bb2f6c34198918a071233725cbc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.872746, 2.366392], "address": "49 rue bichat", "name": "American Kitchen", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.366392, 48.872746], "type": "Point"}, "recordid": "6b9395475cbbbbbacbaaeb070f71d31c2d183dc4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.870598, 2.365413], "address": "55 bis quai de valmy", "name": "La Marine", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.365413, 48.870598], "type": "Point"}, "recordid": "d4d2f92d27f38de59e57744f434781e61283551c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.889101, 2.318001], "address": "21 avenue Brochant", "name": "Le Bloc", "date": "2013-08-22", "zipcode": 75017, "geometry": {"coordinates": [2.318001, 48.889101], "type": "Point"}, "recordid": "e425882ee969d1e8bffe7234336ae40da88c8439", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.874697, 2.405421], "address": "229 avenue Gambetta", "name": "La Recoleta au Manoir", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.405421, 48.874697], "type": "Point"}, "recordid": "02de82cffb2918beafb740f4e924029d470b07a1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.847344, 2.286078], "address": "80 Rue Saint-Charles", "name": "Le Pareloup", "date": "2013-08-22", "zipcode": 75015, "geometry": {"coordinates": [2.286078, 48.847344], "type": "Point"}, "recordid": "0227ca95f76bb6097ae0a0e6f455af2624d49ae3", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, -{"prix_terasse": "1", "geoloc": [48.840771, 2.324589], "address": "3 rue de la Gait\u00e9", "name": "La Brasserie Gait\u00e9", "date": "2013-08-22", "zipcode": 75014, "geometry": {"coordinates": [2.324589, 48.840771], "type": "Point"}, "recordid": "e7c4cba08749c892a73db2715d06623d9e0c2f67", "prix_salle": "-", "prix_comptoir": null, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.875232, 2.336036], "address": "46 rue Victoire", "name": "Caf\u00e9 Zen", "date": "2012-05-11", "zipcode": 75009, "geometry": {"coordinates": [2.336036, 48.875232], "type": "Point"}, "recordid": "5e9a6172f8b64cd098a5f2cf9b1d42567fe0a894", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.872677, 2.315276], "address": "27 rue de Penthi\u00e8vre", "name": "O'Breizh", "date": "2012-10-22", "zipcode": 75008, "geometry": {"coordinates": [2.315276, 48.872677], "type": "Point"}, "recordid": "ead3108add27ef41bb92517aca834f7d7f632816", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.868838, 2.33605], "address": "23 rue saint augustin", "name": "Le Petit Choiseul", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.33605, 48.868838], "type": "Point"}, "recordid": "de601277ca00567b62fa4f5277e4a17679faa753", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.841526, 2.351012], "address": "7 rue Ep\u00e9e de Bois", "name": "Invitez vous chez nous", "date": "2012-10-22", "zipcode": 75005, "geometry": {"coordinates": [2.351012, 48.841526], "type": "Point"}, "recordid": "1d2373bdec8a07306298e3ee54894ac295ee1d55", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.86525, 2.350507], "address": "142 Rue Saint-Denis 75002 Paris", "name": "La Cordonnerie", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.350507, 48.86525], "type": "Point"}, "recordid": "5c9bf60617a99ad75445b454f98e75e1a104021d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.892244, 2.346973], "address": "3, rue Baudelique", "name": "Le Supercoin", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.346973, 48.892244], "type": "Point"}, "recordid": "68a4ee10f1fc4d2a659501e811d148420fa80e95", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.890043, 2.362241], "address": "86 bis rue Riquet", "name": "Populettes", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.362241, 48.890043], "type": "Point"}, "recordid": "8cc55d58d72621a7e91cf6b456731d2cb2863afc", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "1", "geoloc": [48.893017, 2.337776], "address": "49 rue des Cloys", "name": "Au bon coin", "date": "2012-10-18", "zipcode": 75018, "geometry": {"coordinates": [2.337776, 48.893017], "type": "Point"}, "recordid": "0408f272c08c52e3cae035ffdeb8928698787ea9", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.836919, 2.347003], "address": "69 rue Broca", "name": "Le Couvent", "date": "2012-10-18", "zipcode": 75013, "geometry": {"coordinates": [2.347003, 48.836919], "type": "Point"}, "recordid": "729b2f228d3fd2db6f78bb624d451f59555e4a04", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.840624, 2.349766], "address": "111 rue mouffetard", "name": "La Br\u00fblerie des Ternes", "date": "2012-10-18", "zipcode": 75005, "geometry": {"coordinates": [2.349766, 48.840624], "type": "Point"}, "recordid": "233dd2a17620cd5eae70fef11cc627748e3313d5", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.832825, 2.336116], "address": "59 Boulevard Saint-Jacques", "name": "L'\u00c9cir", "date": "2012-10-09", "zipcode": 75014, "geometry": {"coordinates": [2.336116, 48.832825], "type": "Point"}, "recordid": "4a44324a5a806801fd3e05af89cad7c0f1e69d1e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.850696, 2.378417], "address": "126, rue du Faubourg Saint Antoine", "name": "Le Chat bossu", "date": "2012-10-09", "zipcode": 75012, "geometry": {"coordinates": [2.378417, 48.850696], "type": "Point"}, "recordid": "d1d02463f7c90d38cccffd898e15f51e65910baf", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.834157, 2.33381], "address": "58 boulvevard Saint Jacques", "name": "Denfert caf\u00e9", "date": "2012-10-09", "zipcode": 75014, "geometry": {"coordinates": [2.33381, 48.834157], "type": "Point"}, "recordid": "f78f406e5ccb95cb902ce618ed54eba1d4776a3c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.867948, 2.343582], "address": "95 rue Montmartre", "name": "Le Caf\u00e9 frapp\u00e9", "date": "2012-10-09", "zipcode": 75002, "geometry": {"coordinates": [2.343582, 48.867948], "type": "Point"}, "recordid": "4dd2a924a7b2c5f061cecaba2f272548a8c83c6c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.859772, 2.360558], "address": "78 rue vieille du temple", "name": "La Perle", "date": "2012-10-09", "zipcode": 75003, "geometry": {"coordinates": [2.360558, 48.859772], "type": "Point"}, "recordid": "476c54e643613ec36a9b1c533f32122fd873f3c3", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.845047, 2.349583], "address": "1 rue Thouin", "name": "Le Descartes", "date": "2012-10-09", "zipcode": 75005, "geometry": {"coordinates": [2.349583, 48.845047], "type": "Point"}, "recordid": "e9cb0b0d6b6b512e9ecab889267ba342a4f0ea93", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.830151, 2.334213], "address": "55 rue de la tombe Issoire", "name": "Le petit club", "date": "2012-03-07", "zipcode": 75014, "geometry": {"coordinates": [2.334213, 48.830151], "type": "Point"}, "recordid": "c6ca1166fa7fd7050f5d1c626a1e17050116c91e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.865707, 2.374382], "address": "90 avenue Parmentier", "name": "Le Plein soleil", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.374382, 48.865707], "type": "Point"}, "recordid": "95cf5fb735bd19826db70a3af4fe72fce647d4e5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.875322, 2.312329], "address": "146, boulevard Haussmann", "name": "Le Relais Haussmann", "date": "2012-03-07", "zipcode": 75008, "geometry": {"coordinates": [2.312329, 48.875322], "type": "Point"}, "recordid": "6c5f68f47c916638342b77bd5edfc30bd7051303", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.859559, 2.30643], "address": "88 rue Saint-Dominique", "name": "Le Malar", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.30643, 48.859559], "type": "Point"}, "recordid": "c633cb01686aa5f3171bf59976dc9b7f23cbca54", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.864628, 2.408038], "address": "47 rue Belgrand", "name": "Au panini de la place", "date": "2012-03-07", "zipcode": 75020, "geometry": {"coordinates": [2.408038, 48.864628], "type": "Point"}, "recordid": "66dedd35fcbbbb24f328882d49098de7aa5f26ba", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.88435, 2.297978], "address": "182 rue de Courcelles", "name": "Le Village", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.297978, 48.88435], "type": "Point"}, "recordid": "1e07eaf8a93875906d0b18eb6a897c651943589a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.853381, 2.376706], "address": "41 rue de Charonne", "name": "Pause Caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.376706, 48.853381], "type": "Point"}, "recordid": "60d98c3236a70824df50e9aca83e7d7f13a310c5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.853253, 2.383415], "address": "14 rue Jean Mac\u00e9", "name": "Le Pure caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.383415, 48.853253], "type": "Point"}, "recordid": "66707fb2e707d2145fc2eb078a1b980a45921616", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.848873, 2.392859], "address": "307 fg saint Antoine", "name": "Extra old caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.392859, 48.848873], "type": "Point"}, "recordid": "039ec7dcb219cfc434547b06938ba497afeb83b4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.873227, 2.360787], "address": "44 rue Vinaigriers", "name": "Chez Fafa", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.360787, 48.873227], "type": "Point"}, "recordid": "1572d199f186bf86d7753fe71ac23477a7a8bd2c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.850836, 2.384069], "address": "3 rue Faidherbe", "name": "En attendant l'or", "date": "2012-06-27", "zipcode": 75011, "geometry": {"coordinates": [2.384069, 48.850836], "type": "Point"}, "recordid": "de5789bb4a4ffbd244cded8dc555639dbe7d2279", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.866993, 2.336006], "address": "30 rue des Petits-Champs", "name": "Br\u00fblerie San Jos\u00e9", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.336006, 48.866993], "type": "Point"}, "recordid": "b736e5fa17396ee56a642212ccd0ab29c7f2bef1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.856434, 2.342683], "address": "2 place Martin Nadaud", "name": "Caf\u00e9 Martin", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.342683, 48.856434], "type": "Point"}, "recordid": "25fbf857029c54d57909c158e3039349b77344ed", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.863675, 2.348701], "address": "14 rue Turbigo, Paris", "name": "Etienne", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.348701, 48.863675], "type": "Point"}, "recordid": "0190edd7b0766c6d3e43093deb41abe9446c1b22", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.854584, 2.385193], "address": "184 bd Voltaire", "name": "L'ing\u00e9nu", "date": "2012-06-27", "zipcode": 75011, "geometry": {"coordinates": [2.385193, 48.854584], "type": "Point"}, "recordid": "7fcef7475ec632d5c61c9c36c1d52a402ad2e9e8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.890605, 2.361349], "address": "8 rue L'Olive", "name": "L'Olive", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.361349, 48.890605], "type": "Point"}, "recordid": "35286273f281c8c5082b3d3bd17f6bbf207426f9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.871396, 2.338321], "address": "18 rue Favart", "name": "Le Biz", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.338321, 48.871396], "type": "Point"}, "recordid": "cf2f6b9e283aaeeca2214cc1fe57b45e45668e25", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.868109, 2.331785], "address": "1 rue Louis le Grand", "name": "Le Cap Bourbon", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.331785, 48.868109], "type": "Point"}, "recordid": "339030e95e0846b41a8e2b91045456c4e4a50043", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.84167, 2.303053], "address": "9 Place du General Beuret", "name": "Le General Beuret", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.303053, 48.84167], "type": "Point"}, "recordid": "12b37036d28d28b32ebe81756ad15eb68372947f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.846814, 2.289311], "address": "95 avenue Emile Zola", "name": "Le Germinal", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.289311, 48.846814], "type": "Point"}, "recordid": "4e03831a64e886a28a7232e54f2812c1ced23c5a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.862655, 2.337607], "address": "202 rue Saint-Honor\u00e9", "name": "Le Ragueneau", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.337607, 48.862655], "type": "Point"}, "recordid": "e303131b2600d4c2287749a36bf7193d2fa60bd7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.889982, 2.338933], "address": "72 rue lamarck", "name": "Le refuge", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.338933, 48.889982], "type": "Point"}, "recordid": "f64310461736a769c6854fdefb99b9f2e7b230a9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.870294, 2.352821], "address": "13 rue du Faubourg Saint Denis", "name": "Le sully", "date": "2012-06-27", "zipcode": 75010, "geometry": {"coordinates": [2.352821, 48.870294], "type": "Point"}, "recordid": "166be8588ff16e50838fa6a164d1e580497b795d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.842128, 2.280374], "address": "60 rue des bergers", "name": "Le bal du pirate", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.280374, 48.842128], "type": "Point"}, "recordid": "93ff6e35406a074a6ba2667d2b286abf91132f6a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.838769, 2.39609], "address": "95 rue claude decaen", "name": "zic zinc", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.39609, 48.838769], "type": "Point"}, "recordid": "8fdc739d64ff1f01973235301e3ec86791016759", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.870247, 2.376306], "address": "35 rue de l'orillon", "name": "l'orillon bar", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.376306, 48.870247], "type": "Point"}, "recordid": "6e6e9695ef04c3fdbd4cfa19f0cfb0c0f93f4933", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.869848, 2.394247], "address": "116 Rue de M\u00e9nilmontant", "name": "Le Zazabar", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.394247, 48.869848], "type": "Point"}, "recordid": "22edfe92a72ce1bb0eea972508b5caa7af2db2df", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.845458, 2.354796], "address": "22 rue Linn\u00e9", "name": "L'In\u00e9vitable", "date": "2013-08-22", "zipcode": 75005, "geometry": {"coordinates": [2.354796, 48.845458], "type": "Point"}, "recordid": "6893cb08e99319091de9ba80305f22e0ce4cc08d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.83336, 2.365782], "address": "77 rue Dunois", "name": "Le Dunois", "date": "2013-08-22", "zipcode": 75013, "geometry": {"coordinates": [2.365782, 48.83336], "type": "Point"}, "recordid": "c3be1246dbc4ca5734d5bbd569436ba655105248", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.862655, 2.337607], "address": "202 rue Saint Honor\u00e9", "name": "Ragueneau", "date": "2012-05-11", "zipcode": 75001, "geometry": {"coordinates": [2.337607, 48.862655], "type": "Point"}, "recordid": "e946d9e8f8c5a130f98eca945efadfd9eec40dcb", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": null, "geoloc": [48.826608, 2.374571], "address": "48 rue du Dessous des Berges", "name": "Le Caminito", "date": "2012-10-22", "zipcode": 75013, "geometry": {"coordinates": [2.374571, 48.826608], "type": "Point"}, "recordid": "5d9ad8bcfbbc20adaec2aa7dcdb71326868c7686", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.870598, 2.365413], "address": "55bis quai de Valmy", "name": "Epicerie Musicale", "date": "2012-10-22", "zipcode": 75010, "geometry": {"coordinates": [2.365413, 48.870598], "type": "Point"}, "recordid": "8ffea133d93c608d337bc0129f4f9f3d5cad8dae", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": null, "address": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital", "name": "Le petit Bretonneau", "date": "2012-10-22", "zipcode": 75018, "geometry": {}, "recordid": "dd1ffdbd55c2dc651201302b8015258f7d35fd35", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.862575, 2.367427], "address": "104 rue amelot", "name": "Le Centenaire", "date": "2012-10-22", "zipcode": 75011, "geometry": {"coordinates": [2.367427, 48.862575], "type": "Point"}, "recordid": "e47d25752c2c8bcab5efb0e3a41920ec7a8a766a", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.842833, 2.348314], "address": "13 Rue du Pot de Fer", "name": "La Montagne Sans Genevi\u00e8ve", "date": "2012-10-22", "zipcode": 75005, "geometry": {"coordinates": [2.348314, 48.842833], "type": "Point"}, "recordid": "314d170601f81e3a3f26d8801f0fbee39981c788", "prix_salle": "1", "prix_comptoir": null, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.851325, 2.40171], "address": "46 rue de Buzenval", "name": "Les P\u00e8res Populaires", "date": "2012-10-18", "zipcode": 75020, "geometry": {"coordinates": [2.40171, 48.851325], "type": "Point"}, "recordid": "e75e8a3fe6212a0e576beec82f0128dd394e56fa", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.857658, 2.305613], "address": "188 rue de Grenelle", "name": "Cafe de grenelle", "date": "2012-10-09", "zipcode": 75007, "geometry": {"coordinates": [2.305613, 48.857658], "type": "Point"}, "recordid": "72e274046d671e68bc7754808feacfc95b91b6ed", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.875207, 2.332944], "address": "73 rue de la Victoire", "name": "Le relais de la victoire", "date": "2012-10-09", "zipcode": 75009, "geometry": {"coordinates": [2.332944, 48.875207], "type": "Point"}, "recordid": "01c21abdf35484f9ad184782979ecd078de84523", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": null, "address": "Route de la Muette \u00e0 Neuilly\nClub hippique du Jardin d\u2019Acclimatation", "name": "La chaumi\u00e8re gourmande", "date": "2012-03-07", "zipcode": 75016, "geometry": {}, "recordid": "438ec18d35793d12eb6a137373c3fe4f3aa38a69", "prix_salle": "1", "prix_comptoir": null, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.891882, 2.33365], "address": "216, rue Marcadet", "name": "Le Brio", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.33365, 48.891882], "type": "Point"}, "recordid": "49e3584ce3d6a6a236b4a0db688865bdd3483fec", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.884753, 2.324648], "address": "22 rue des Dames", "name": "Caves populaires", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.324648, 48.884753], "type": "Point"}, "recordid": "af81e5eca2b84ea706ac2d379edf65b0fb2f879a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.827428, 2.325652], "address": "12 avenue Jean Moulin", "name": "Caprice caf\u00e9", "date": "2012-03-07", "zipcode": 75014, "geometry": {"coordinates": [2.325652, 48.827428], "type": "Point"}, "recordid": "88e07bdb723b49cd27c90403b5930bca1c93b458", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.832964, 2.369266], "address": "7 rue Clisson", "name": "Tamm Bara", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.369266, 48.832964], "type": "Point"}, "recordid": "baf59c98acafbe48ed9e91b64377da216a20cbcc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.876577, 2.348414], "address": "1 rue de Montholon", "name": "L'anjou", "date": "2012-03-07", "zipcode": 75009, "geometry": {"coordinates": [2.348414, 48.876577], "type": "Point"}, "recordid": "e6f5949dca40548aad296208c61c498f639f648c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.862599, 2.315086], "address": "2 rue Robert Esnault Pelterie", "name": "Caf\u00e9 dans l'aerogare Air France Invalides", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.315086, 48.862599], "type": "Point"}, "recordid": "6d175eb48c577fafdfc99df0ab55da468cf17164", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.844854, 2.345413], "address": "10 rue d\"Ulm", "name": "Waikiki", "date": "2012-03-07", "zipcode": 75005, "geometry": {"coordinates": [2.345413, 48.844854], "type": "Point"}, "recordid": "fb1e2bc2ae55d3d47da682c71093a12fa64fbd45", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.871576, 2.364499], "address": "36 rue Beaurepaire", "name": "Chez Prune", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.364499, 48.871576], "type": "Point"}, "recordid": "19399ede5b619761877822185bbb4c98b565974c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.833863, 2.329046], "address": "21 rue Boulard", "name": "Au Vin Des Rues", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.329046, 48.833863], "type": "Point"}, "recordid": "87263873b6f8346b5777844be0122a307f29fcab", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.838137, 2.301166], "address": "14 rue d'alleray", "name": "bistrot les timbr\u00e9s", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.301166, 48.838137], "type": "Point"}, "recordid": "74b6d7113e5b14eb8e890663f061208bf4ff6728", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.871799, 2.315985], "address": "9 rue de Miromesnil", "name": "Caf\u00e9 beauveau", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.315985, 48.871799], "type": "Point"}, "recordid": "2759f5cdda4bcb88ca3ae2f7299b37b8e62596c8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.866259, 2.338739], "address": "9 rue des petits champs", "name": "Caf\u00e9 Pistache", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.338739, 48.866259], "type": "Point"}, "recordid": "6d2675cdc912118d0376229be8e436feca9c8af7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.874605, 2.387738], "address": "13 Rue Jean-Baptiste Dumay", "name": "La Cagnotte", "date": "2012-06-27", "zipcode": 75020, "geometry": {"coordinates": [2.387738, 48.874605], "type": "Point"}, "recordid": "f7085c754c0c97e418d7e5213753f74bd396fc27", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.842462, 2.310919], "address": "172 rue de vaugirard", "name": "le 1 cinq", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.310919, 48.842462], "type": "Point"}, "recordid": "17e917723fc99d6e5bd77eb9633ac2e789a9a6d9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.84591, 2.375543], "address": "28 bis boulevard Diderot", "name": "Le Killy Jen", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.375543, 48.84591], "type": "Point"}, "recordid": "93132bd8b3ae67dfcc8cf8c1166e312ac4acb9b9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.842868, 2.303173], "address": "106 rue Lecourbe", "name": "Les Artisans", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.303173, 48.842868], "type": "Point"}, "recordid": "c37ef573b4cb2d1e61795d6a9ef11de433dc9a99", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.865684, 2.334416], "address": "83 avenue de Wagram", "name": "Peperoni", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.334416, 48.865684], "type": "Point"}, "recordid": "9461f859ca009ced25555fa6af1e6867dda9223e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.833146, 2.288834], "address": "380 rue de vaugirard", "name": "le lutece", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.288834, 48.833146], "type": "Point"}, "recordid": "ddd13990c1408700085366bd4ba313acd69a44ea", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.886431, 2.327429], "address": "16 rue Ganneron", "name": "Brasiloja", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.327429, 48.886431], "type": "Point"}, "recordid": "d679bb1642534278f4c0203d67be0bafd5306d81", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.855711, 2.359491], "address": "16 rue de Rivoli", "name": "Rivolux", "date": "2014-02-01", "zipcode": 75004, "geometry": {"coordinates": [2.359491, 48.855711], "type": "Point"}, "recordid": "bdd2b008cc765c7fe195c037b830cd2628420a2f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.845898, 2.372766], "address": "21 Bis Boulevard Diderot", "name": "L'europ\u00e9en", "date": "2013-08-22", "zipcode": 75012, "geometry": {"coordinates": [2.372766, 48.845898], "type": "Point"}, "recordid": "693c0da7d4db24781ed161c01a661c36074a94fa", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.867465, 2.357791], "address": "39 rue Notre Dame de Nazareth", "name": "NoMa", "date": "2013-08-22", "zipcode": 75003, "geometry": {"coordinates": [2.357791, 48.867465], "type": "Point"}, "recordid": "60d8b670810cc95eb0439dd0c238f8205ea8ef76", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.871595, 2.385858], "address": "1 Rue des Envierges", "name": "O'Paris", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.385858, 48.871595], "type": "Point"}, "recordid": "297c040284a05efe35c69bb621505e6acfdcdda4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.872402, 2.366532], "address": "16 avenue Richerand", "name": "Caf\u00e9 Clochette", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.366532, 48.872402], "type": "Point"}, "recordid": "a561a941f538e8a1d321bf8d98576d06be037962", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.856584, 2.368574], "address": "40 Boulevard Beaumarchais", "name": "La cantoche de Paname", "date": "2013-08-22", "zipcode": 75011, "geometry": {"coordinates": [2.368574, 48.856584], "type": "Point"}, "recordid": "5ba2aaec9f1de9d01e65be95215cab13c693cdf3", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.856496, 2.394874], "address": "148 Boulevard de Charonne", "name": "Le Saint Ren\u00e9", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.394874, 48.856496], "type": "Point"}, "recordid": "4d60b350d04d4b1bf4bfd4dd6cc59687dc792c74", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.850055, 2.383908], "address": "196 rue du faubourg saint-antoine", "name": "La Libert\u00e9", "date": "2012-10-22", "zipcode": 75012, "geometry": {"coordinates": [2.383908, 48.850055], "type": "Point"}, "recordid": "ee94e76326f8dcbe3500afec69f1a21eb1215ad0", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.866737, 2.33716], "address": "16 rue des Petits Champs", "name": "Chez Rutabaga", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.33716, 48.866737], "type": "Point"}, "recordid": "a420ea4608440b8dc8e0267fe8cc513daa950551", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.885367, 2.325325], "address": "2 rue Lemercier", "name": "Le BB (Bouchon des Batignolles)", "date": "2012-10-22", "zipcode": 75017, "geometry": {"coordinates": [2.325325, 48.885367], "type": "Point"}, "recordid": "20986cbfe11018bd0aba8150a49db1c435f7642d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.873175, 2.339193], "address": "10 rue Rossini", "name": "La Brocante", "date": "2012-10-22", "zipcode": 75009, "geometry": {"coordinates": [2.339193, 48.873175], "type": "Point"}, "recordid": "2e10601c35669394d43936a771b18408be0338ba", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.840771, 2.324589], "address": "3 rue Ga\u00eet\u00e9", "name": "Le Plomb du cantal", "date": "2012-10-22", "zipcode": 75014, "geometry": {"coordinates": [2.324589, 48.840771], "type": "Point"}, "recordid": "6fb510614e00b065bf16a5af8e2c0eaf561a5654", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.884753, 2.324648], "address": "22 rue des Dames", "name": "Les caves populaires", "date": "2012-10-22", "zipcode": 75017, "geometry": {"coordinates": [2.324648, 48.884753], "type": "Point"}, "recordid": "d650c509a0aa8ed7b9c9b88861263f31463bbd0e", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.869519, 2.39339], "address": "108 rue de M\u00e9nilmontant", "name": "Chez Luna", "date": "2012-10-09", "zipcode": 75020, "geometry": {"coordinates": [2.39339, 48.869519], "type": "Point"}, "recordid": "736f4d996f1f8b7c3a0ce2abfeebfcce2a4bab13", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.877903, 2.385365], "address": "1 rue du Plateau", "name": "Le bar Fleuri", "date": "2012-03-07", "zipcode": 75019, "geometry": {"coordinates": [2.385365, 48.877903], "type": "Point"}, "recordid": "be55720646093788ec161c6cadc5ad8059f4b90b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.882939, 2.31809], "address": "101 rue des dames", "name": "Trois pi\u00e8ces cuisine", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.31809, 48.882939], "type": "Point"}, "recordid": "7bbbfb755020a2c25cce0067601994ce5ee4193f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.849497, 2.298855], "address": "61 avenue de la Motte Picquet", "name": "Le Zinc", "date": "2012-03-07", "zipcode": 75015, "geometry": {"coordinates": [2.298855, 48.849497], "type": "Point"}, "recordid": "e7c35a94454518de6de5bbecbc015fc37f7aea14", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.880669, 2.349964], "address": "136 rue du Faubourg poissonni\u00e8re", "name": "La cantine de Zo\u00e9", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.349964, 48.880669], "type": "Point"}, "recordid": "0edc473b3432a869b8ed66b6c4c989766b699947", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.844057, 2.328402], "address": "6/8 rue Stanislas", "name": "Les Vendangeurs", "date": "2012-03-07", "zipcode": 75006, "geometry": {"coordinates": [2.328402, 48.844057], "type": "Point"}, "recordid": "e9766ea36f6293bf670ed938bff02b975d012973", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.852053, 2.338779], "address": "3 carrefour de l'Od\u00e9on", "name": "L'avant comptoir", "date": "2012-03-07", "zipcode": 75006, "geometry": {"coordinates": [2.338779, 48.852053], "type": "Point"}, "recordid": "fe843d2f43dcaac9129f5b36dc367558dfd3b3e4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "1", "geoloc": [48.886504, 2.34498], "address": "1 rue Paul albert", "name": "Botak cafe", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.34498, 48.886504], "type": "Point"}, "recordid": "9e19c375e612f5fb803ec6a27881858619207812", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.872722, 2.354594], "address": "67 rue du Ch\u00e2teau d'eau", "name": "le chateau d'eau", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.354594, 48.872722], "type": "Point"}, "recordid": "05bb6a26ec5bfbba25da2d19a5f0e83d69800f38", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.85192, 2.373229], "address": "58 rue du Fbg Saint-Antoine", "name": "Bistrot Saint-Antoine", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.373229, 48.85192], "type": "Point"}, "recordid": "daa3908ddf69d378fec5b4548494727e1121adc4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.854685, 2.368487], "address": "11/13 boulevard Beaumarchais", "name": "Chez Oscar", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.368487, 48.854685], "type": "Point"}, "recordid": "c73be0483480c59e6ab6bc3a906c8d9dd474887f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.87226, 2.304441], "address": "63 rue de Ponthieu", "name": "Le Fronton", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.304441, 48.87226], "type": "Point"}, "recordid": "85a8200d3e3aed7724d3207ed8b1ee5ec50c1f90", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.851, 2.300378], "address": "48 avenue de la Motte Picquet", "name": "Le Piquet", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.300378, 48.851], "type": "Point"}, "recordid": "460e2adc95fd172f753b1b6ed296c2711639d49d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.841089, 2.349565], "address": "104 rue Mouffetard", "name": "Le Tournebride", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.349565, 48.841089], "type": "Point"}, "recordid": "8a7a23ed68366f70ab939c877cbdce46f19d75c7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.828704, 2.322074], "address": "52 rue des plantes", "name": "maison du vin", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.322074, 48.828704], "type": "Point"}, "recordid": "482507a8f0fe4960f94372b6fa12b16e7d4f2a93", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.842146, 2.375986], "address": "157 rue Bercy 75012 Paris", "name": "L'entrep\u00f4t", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.375986, 48.842146], "type": "Point"}, "recordid": "d8746118429eb118f38ecbee904636d9b33fa8ba", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.867092, 2.363288], "address": "Place de la R\u00e9publique", "name": "Le caf\u00e9 Monde et M\u00e9dias", "date": "2013-08-22", "zipcode": 75003, "geometry": {"coordinates": [2.363288, 48.867092], "type": "Point"}, "recordid": "af04c90f25e25daf7f5cbbab1bc740bac26541d4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.849821, 2.355337], "address": "11 Quai de la Tournelle", "name": "Caf\u00e9 rallye tournelles", "date": "2013-08-22", "zipcode": 75005, "geometry": {"coordinates": [2.355337, 48.849821], "type": "Point"}, "recordid": "91d88e321a75b6a8c4dea816c399fda77c41f9d1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.872498, 2.355136], "address": "61 rue du ch\u00e2teau d'eau", "name": "Brasserie le Morvan", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.355136, 48.872498], "type": "Point"}, "recordid": "6ec62058995948fc18d331f01a2d03acc0d9e0fa", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "1", "geoloc": [48.874879, 2.386064], "address": "6 rue M\u00e9lingue", "name": "Chez Miamophile", "date": "2013-08-22", "zipcode": 75019, "geometry": {"coordinates": [2.386064, 48.874879], "type": "Point"}, "recordid": "13924872737e4fc640a43da58937c3777c2ac753", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.864269, 2.36858], "address": "18 rue de Crussol", "name": "Panem", "date": "2012-05-11", "zipcode": 75011, "geometry": {"coordinates": [2.36858, 48.864269], "type": "Point"}, "recordid": "67bdf3a6989f80749a1ba33a17b1370de0a0e1cd", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.885662, 2.319591], "address": "47 rue de Batignolles", "name": "Petits Freres des Pauvres", "date": "2012-05-11", "zipcode": 75017, "geometry": {"coordinates": [2.319591, 48.885662], "type": "Point"}, "recordid": "e27fd00149514bbfad7dd7e8f9b0c677df2d3f25", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.837212, 2.296046], "address": "198 rue de la Convention", "name": "Caf\u00e9 Dupont", "date": "2012-05-11", "zipcode": 75015, "geometry": {"coordinates": [2.296046, 48.837212], "type": "Point"}, "recordid": "4d40e6d864dae81c152a05cb98e30933bde96aa1", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.871002, 2.30879], "address": "28 rue de Ponthieu", "name": "L'Angle", "date": "2012-10-22", "zipcode": 75008, "geometry": {"coordinates": [2.30879, 48.871002], "type": "Point"}, "recordid": "c40bd2d1f98b415e539c27cf68518d060ebab51e", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.888023, 2.353467], "address": "19-23 rue L\u00e9on", "name": "Institut des Cultures d'Islam", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.353467, 48.888023], "type": "Point"}, "recordid": "68d6d37b846e39bd8554e1f8f75974b486b0f27b", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.886044, 2.360781], "address": "19 rue Pajol", "name": "Canopy Caf\u00e9 associatif", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.360781, 48.886044], "type": "Point"}, "recordid": "ff73bafb514bb68eb925c81aee43c3a58ac3c70d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.870287, 2.332491], "address": "place de l'opera", "name": "L'Entracte", "date": "2012-10-09", "zipcode": 75002, "geometry": {"coordinates": [2.332491, 48.870287], "type": "Point"}, "recordid": "0039cd8bceb5e281677a158f832a660789088071", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.858709, 2.362701], "address": "15 rue du Parc Royal", "name": "Le S\u00e9vign\u00e9", "date": "2012-10-09", "zipcode": 75003, "geometry": {"coordinates": [2.362701, 48.858709], "type": "Point"}, "recordid": "adcc8b4f78f05ba7b24b0593e1516dfb7b415f91", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.839687, 2.347254], "address": "35 rue Claude Bernard", "name": "Le Caf\u00e9 d'avant", "date": "2012-10-09", "zipcode": 75005, "geometry": {"coordinates": [2.347254, 48.839687], "type": "Point"}, "recordid": "b904fc48763938eee2169ba25aad2ffcc0dd6a9f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.844244, 2.330407], "address": "53 rue Notre-Dame des Champs", "name": "Le Lucernaire", "date": "2012-10-09", "zipcode": 75006, "geometry": {"coordinates": [2.330407, 48.844244], "type": "Point"}, "recordid": "cc72af04314fd40e16ff611c799d378515043508", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.877599, 2.332111], "address": "12 rue Blanche", "name": "Le Brigadier", "date": "2012-10-09", "zipcode": 75009, "geometry": {"coordinates": [2.332111, 48.877599], "type": "Point"}, "recordid": "978d4bc68c9ebf81029d3e77274d2107777b8a75", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.826494, 2.359987], "address": "26 rue du Docteur Magnan", "name": "L'\u00e2ge d'or", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.359987, 48.826494], "type": "Point"}, "recordid": "40bffbdc0c9ed1cbce820fed875d7c21d8964640", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.883717, 2.326861], "address": "Place de Clichy", "name": "Bagels & Coffee Corner", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.326861, 48.883717], "type": "Point"}, "recordid": "262facde9b8c4568c9ba7fbce8f069ff8c76948d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.835843, 2.278501], "address": "10 boulevard Victor", "name": "Caf\u00e9 Victor", "date": "2012-03-07", "zipcode": 75015, "geometry": {"coordinates": [2.278501, 48.835843], "type": "Point"}, "recordid": "e5817ec44ac5a7ea2e4a34b6a2e13d535156642b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.845337, 2.379024], "address": "54, avenue Daumesnil", "name": "L'empreinte", "date": "2012-03-07", "zipcode": 75012, "geometry": {"coordinates": [2.379024, 48.845337], "type": "Point"}, "recordid": "b96ddd35cbbf5d93aaff79487afdf083b5ff0817", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.857312, 2.379055], "address": "93, rue de la Roquette", "name": "L'horizon", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.379055, 48.857312], "type": "Point"}, "recordid": "84c6b7335e7f82ac942c4f398723ec99076f148d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.835878, 2.395723], "address": "34 bis rue de Wattignies", "name": "Au pays de Vannes", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.395723, 48.835878], "type": "Point"}, "recordid": "a17869dbb9d0d5b1e5ed7bb288053900b04ee944", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.85413, 2.323539], "address": "36 rue de Varenne", "name": "Caf\u00e9 Varenne", "date": "2012-06-27", "zipcode": 75007, "geometry": {"coordinates": [2.323539, 48.85413], "type": "Point"}, "recordid": "a26ec0d5fca47b8de77d862ad8a99b75bb520a09", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.855161, 2.360218], "address": "125 Rue Saint-Antoine", "name": "l'El\u00e9phant du nil", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.360218, 48.855161], "type": "Point"}, "recordid": "7b7ceefd1f9ed85041265c9577e0dc8bee01d45a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.8357, 2.292961], "address": "354 bis rue Vaugirard", "name": "Le Comptoir", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.292961, 48.8357], "type": "Point"}, "recordid": "59d8fa304e535f4eb41f9746028034c9b30cbde4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.835451, 2.292515], "address": "358 rue de Vaugirard", "name": "Le Parc Vaugirard", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.292515, 48.835451], "type": "Point"}, "recordid": "19f655206a8446959c8e796c2b3cb9001890f985", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.834972, 2.327007], "address": "58 rue Daguerre", "name": "le Zango", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.327007, 48.834972], "type": "Point"}, "recordid": "e1b54109015316a822747f788128f997a3478050", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.848887, 2.399972], "address": "3 rue de Lagny", "name": "Melting Pot", "date": "2012-06-27", "zipcode": 75020, "geometry": {"coordinates": [2.399972, 48.848887], "type": "Point"}, "recordid": "fd0de2cbf73e0a728cd73e4e2a9a4a9c646f76f2", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}, -{"prix_terasse": "-", "geoloc": [48.892366, 2.317359], "address": "174 avenue de Clichy", "name": "Pari's Caf\u00e9", "date": "2012-06-27", "zipcode": 75017, "geometry": {"coordinates": [2.317359, 48.892366], "type": "Point"}, "recordid": "831446ae203f89de26d3300e625c20717e82d40a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"}] \ No newline at end of file diff --git a/bonobo/examples/datasets/coffeeshops.ldjson b/bonobo/examples/datasets/coffeeshops.ldjson deleted file mode 100644 index 252c1a4..0000000 --- a/bonobo/examples/datasets/coffeeshops.ldjson +++ /dev/null @@ -1,181 +0,0 @@ -{"prix_terasse": "-", "geoloc": [48.839512, 2.303007], "address": "344Vrue Vaugirard", "name": "Coffee Chope", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.303007, 48.839512], "type": "Point"}, "recordid": "3c276428d45ad68ccdf6875e4ddcfe95d0c0d4cf", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.876737, 2.357601], "address": "5, rue d'Alsace", "name": "Ext\u00e9rieur Quai", "date": "2014-02-01", "zipcode": 75010, "geometry": {"coordinates": [2.357601, 48.876737], "type": "Point"}, "recordid": "97ad81cd1127a8566085ad796eeb44a06bec7514", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.850852, 2.362029], "address": "6 Bd henri IV", "name": "Le Sully", "date": "2014-02-01", "zipcode": 75004, "geometry": {"coordinates": [2.362029, 48.850852], "type": "Point"}, "recordid": "aa4294c1b8d660a23db0dc81321e509bae1dae68", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "1", "geoloc": [48.893517, 2.340271], "address": "53 rue du ruisseau", "name": "O q de poule", "date": "2013-08-22", "zipcode": 75018, "geometry": {"coordinates": [2.340271, 48.893517], "type": "Point"}, "recordid": "a81362dbed35247555fb105bd83ff2906904a66e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.864655, 2.350089], "address": "1 Passage du Grand Cerf", "name": "Le Pas Sage", "date": "2013-08-22", "zipcode": 75002, "geometry": {"coordinates": [2.350089, 48.864655], "type": "Point"}, "recordid": "7ced86acbd5ccfba229bcc07d70d0d117aee16a5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.895825, 2.339712], "address": "112 Rue Championnet", "name": "La Renaissance", "date": "2013-08-22", "zipcode": 75018, "geometry": {"coordinates": [2.339712, 48.895825], "type": "Point"}, "recordid": "5582c8572bd7637bf305b74c1c0bdb74a8e4247f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.868581, 2.373015], "address": "Rue de la Fontaine au Roi", "name": "La Caravane", "date": "2012-05-11", "zipcode": 75011, "geometry": {"coordinates": [2.373015, 48.868581], "type": "Point"}, "recordid": "50bb0fa06e562a242f115ddbdae2ed9c7df93d57", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.875155, 2.335536], "address": "51 Rue Victoire", "name": "Le chantereine", "date": "2012-10-22", "zipcode": 75009, "geometry": {"coordinates": [2.335536, 48.875155], "type": "Point"}, "recordid": "eb8a62feeedaf7ed8b8c912305270ee857068689", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.886536, 2.346525], "address": "11 rue Feutrier", "name": "Le M\u00fcller", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.346525, 48.886536], "type": "Point"}, "recordid": "62c552f167f671f88569c1f2d6a44098fb514c51", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.841494, 2.307117], "address": "21 rue Copreaux", "name": "Le drapeau de la fidelit\u00e9", "date": "2012-10-22", "zipcode": 75015, "geometry": {"coordinates": [2.307117, 48.841494], "type": "Point"}, "recordid": "5120ea0b9d7387766072b90655166486928e25c8", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.839743, 2.296898], "address": "125 rue Blomet", "name": "Le caf\u00e9 des amis", "date": "2012-10-22", "zipcode": 75015, "geometry": {"coordinates": [2.296898, 48.839743], "type": "Point"}, "recordid": "865f62415adc5c34e3ca38a1748b7a324dfba209", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.857728, 2.349641], "address": "10 rue Saint Martin", "name": "Le Caf\u00e9 Livres", "date": "2012-10-09", "zipcode": 75004, "geometry": {"coordinates": [2.349641, 48.857728], "type": "Point"}, "recordid": "7ef54a78802d49cafd2701458df2b0d0530d123b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.856003, 2.30457], "address": "46 avenue Bosquet", "name": "Le Bosquet", "date": "2012-10-09", "zipcode": 75007, "geometry": {"coordinates": [2.30457, 48.856003], "type": "Point"}, "recordid": "d701a759e08a71f4bbb01f29473274b0152135d0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.889426, 2.332954], "address": "12 rue Armand Carrel", "name": "Le Chaumontois", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.332954, 48.889426], "type": "Point"}, "recordid": "e12ff00a644c91ad910ddc63a770c190be93a393", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.838521, 2.370478], "address": "34 avenue Pierre Mend\u00e8s-France", "name": "Le Kleemend's", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.370478, 48.838521], "type": "Point"}, "recordid": "0f6cd1ee7751b00c9574efcfdcf66fa0e857d251", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.849861, 2.385342], "address": "202 rue du faubourg st antoine", "name": "Caf\u00e9 Pierre", "date": "2012-03-07", "zipcode": 75012, "geometry": {"coordinates": [2.385342, 48.849861], "type": "Point"}, "recordid": "f9de9d0fb5e92f047a6f1986a31f9dd4d38bcb36", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.872202, 2.304624], "address": "61 rue de Ponthieu", "name": "Les Arcades", "date": "2012-03-07", "zipcode": 75008, "geometry": {"coordinates": [2.304624, 48.872202], "type": "Point"}, "recordid": "67eaf58afc856077c0680601e453e75c0922c9c0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.859031, 2.320315], "address": "31 rue Saint-Dominique", "name": "Le Square", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.320315, 48.859031], "type": "Point"}, "recordid": "678558317bc9ad46652e5b1643e70c2142a76e7e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.850092, 2.37463], "address": "75, avenue Ledru-Rollin", "name": "Assaporare Dix sur Dix", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.37463, 48.850092], "type": "Point"}, "recordid": "667474321887d08a3cc636adf043ad354b65fa61", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.86805, 2.353313], "address": "129 boulevard sebastopol", "name": "Au cerceau d'or", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.353313, 48.86805], "type": "Point"}, "recordid": "c9ef52ba2fabe0286700329f18bbbbea9a10b474", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.845927, 2.373051], "address": "21 ter boulevard Diderot", "name": "Aux cadrans", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.373051, 48.845927], "type": "Point"}, "recordid": "ed5f98686856bf4ddd2b381b43ad229246741a90", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.851662, 2.273883], "address": "17 rue Jean de la Fontaine", "name": "Caf\u00e9 antoine", "date": "2012-06-27", "zipcode": 75016, "geometry": {"coordinates": [2.273883, 48.851662], "type": "Point"}, "recordid": "ab6d1e054e2e6ae7d6150013173f55e83c05ca23", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.877642, 2.312823], "address": "rue de Lisbonne", "name": "Caf\u00e9 de la Mairie (du VIII)", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.312823, 48.877642], "type": "Point"}, "recordid": "7de8a79b026ac63f453556612505b5bcd9229036", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.838633, 2.349916], "address": "5 rue Claude Bernard", "name": "Caf\u00e9 Lea", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.349916, 48.838633], "type": "Point"}, "recordid": "fecd8900cf83027f74ceced9fc4ad80ac73b63a7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.849293, 2.354486], "address": "11 boulevard Saint-Germain", "name": "Cardinal Saint-Germain", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.354486, 48.849293], "type": "Point"}, "recordid": "e4a078c30c98082896787f4e4b41a07554392529", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.869771, 2.342501], "address": "52 rue Notre-Dame des Victoires", "name": "D\u00e9d\u00e9 la frite", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.342501, 48.869771], "type": "Point"}, "recordid": "ccb2ba2f98043e8eefd5fda829dee1ea7f1d2c7a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.834051, 2.287345], "address": "36 rue du hameau", "name": "La Bauloise", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.287345, 48.834051], "type": "Point"}, "recordid": "c9fe10abd15ede7ccaeb55c309898d30d7b19d0e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.888165, 2.377387], "address": "71 quai de Seine", "name": "Le Bellerive", "date": "2012-06-27", "zipcode": 75019, "geometry": {"coordinates": [2.377387, 48.888165], "type": "Point"}, "recordid": "4e0b5c2d33d7c25fc54c51171f3d37e509959fc0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.864543, 2.340997], "address": "42 rue coquill\u00e8re", "name": "Le bistrot de Ma\u00eblle et Augustin", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.340997, 48.864543], "type": "Point"}, "recordid": "52acab12469af291984e9a70962e08c72b058e10", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.872103, 2.346161], "address": "14 rue Rougemont", "name": "Le Dellac", "date": "2012-06-27", "zipcode": 75009, "geometry": {"coordinates": [2.346161, 48.872103], "type": "Point"}, "recordid": "4d1d627ecea2ffa279bb862f8ba495d95ca75350", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.859645, 2.355598], "address": "1 rue Pecquay", "name": "Le Felteu", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.355598, 48.859645], "type": "Point"}, "recordid": "2c1fa55460af282266d86fd003af4f929fdf4e7d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.85763, 2.346101], "address": "2 bis quai de la m\u00e9gisserie", "name": "Le Reynou", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.346101, 48.85763], "type": "Point"}, "recordid": "d4ddd30ab3e721a317fc7ea89d5b9001255ce9f4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.884646, 2.337734], "address": "23 rue des abbesses", "name": "Le Saint Jean", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.337734, 48.884646], "type": "Point"}, "recordid": "51b47cf167b7f32eeebb108330956694d75d4268", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.841007, 2.31466], "address": "65 boulevard Pasteur", "name": "les montparnos", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.31466, 48.841007], "type": "Point"}, "recordid": "2aaca891ffd0694c657a43889516ab72afdfba07", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.850323, 2.33039], "address": "16 rue DE MEZIERES", "name": "L'antre d'eux", "date": "2014-02-01", "zipcode": 75006, "geometry": {"coordinates": [2.33039, 48.850323], "type": "Point"}, "recordid": "4ff4337934c66f61e00d1d9551f7cdddba03e544", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.864957, 2.346938], "address": "58 rue de Montorgueil", "name": "Drole d'endroit pour une rencontre", "date": "2014-02-01", "zipcode": 75002, "geometry": {"coordinates": [2.346938, 48.864957], "type": "Point"}, "recordid": "3451657f880abe75d0c7e386fc698405556c53e8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.889565, 2.339735], "address": "104 rue caulaincourt", "name": "Le pari's caf\u00e9", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.339735, 48.889565], "type": "Point"}, "recordid": "e8c34a537b673fcb26c76e02deca4f5a728929dc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.859115, 2.368871], "address": "60 rue saint-sabin", "name": "Le Poulailler", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.368871, 48.859115], "type": "Point"}, "recordid": "325ea74ba83f716dde87c08cffd36f7df7722a49", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.833595, 2.38604], "address": "33 Cour Saint Emilion", "name": "Chai 33", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.38604, 48.833595], "type": "Point"}, "recordid": "528de8d5d8780bee83145637e315483d48f5ae3c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.868741, 2.379969], "address": "99 rue Jean-Pierre Timbaud", "name": "L'Assassin", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.379969, 48.868741], "type": "Point"}, "recordid": "fac0483890ff8bdaeb3feddbdb032c5112f24678", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.851463, 2.398691], "address": "1 rue d'Avron", "name": "l'Usine", "date": "2014-02-01", "zipcode": 75020, "geometry": {"coordinates": [2.398691, 48.851463], "type": "Point"}, "recordid": "fee1e3eb103bbc98e19e45d34365da0f27166541", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.896305, 2.332898], "address": "52 rue Liebniz", "name": "La Bricole", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.332898, 48.896305], "type": "Point"}, "recordid": "4744e866c244c59eec43b3fe159542d2ef433065", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.850311, 2.34885], "address": "place maubert", "name": "le ronsard", "date": "2014-02-01", "zipcode": 75005, "geometry": {"coordinates": [2.34885, 48.850311], "type": "Point"}, "recordid": "49a390322b45246bc2c1e50fcd46815ad271bca0", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.863038, 2.3604], "address": "82 rue des archives", "name": "Face Bar", "date": "2014-02-01", "zipcode": 75003, "geometry": {"coordinates": [2.3604, 48.863038], "type": "Point"}, "recordid": "d96e16ebf2460bb2f6c34198918a071233725cbc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.872746, 2.366392], "address": "49 rue bichat", "name": "American Kitchen", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.366392, 48.872746], "type": "Point"}, "recordid": "6b9395475cbbbbbacbaaeb070f71d31c2d183dc4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.870598, 2.365413], "address": "55 bis quai de valmy", "name": "La Marine", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.365413, 48.870598], "type": "Point"}, "recordid": "d4d2f92d27f38de59e57744f434781e61283551c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.889101, 2.318001], "address": "21 avenue Brochant", "name": "Le Bloc", "date": "2013-08-22", "zipcode": 75017, "geometry": {"coordinates": [2.318001, 48.889101], "type": "Point"}, "recordid": "e425882ee969d1e8bffe7234336ae40da88c8439", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.874697, 2.405421], "address": "229 avenue Gambetta", "name": "La Recoleta au Manoir", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.405421, 48.874697], "type": "Point"}, "recordid": "02de82cffb2918beafb740f4e924029d470b07a1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.847344, 2.286078], "address": "80 Rue Saint-Charles", "name": "Le Pareloup", "date": "2013-08-22", "zipcode": 75015, "geometry": {"coordinates": [2.286078, 48.847344], "type": "Point"}, "recordid": "0227ca95f76bb6097ae0a0e6f455af2624d49ae3", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} -{"prix_terasse": "1", "geoloc": [48.840771, 2.324589], "address": "3 rue de la Gait\u00e9", "name": "La Brasserie Gait\u00e9", "date": "2013-08-22", "zipcode": 75014, "geometry": {"coordinates": [2.324589, 48.840771], "type": "Point"}, "recordid": "e7c4cba08749c892a73db2715d06623d9e0c2f67", "prix_salle": "-", "prix_comptoir": null, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.875232, 2.336036], "address": "46 rue Victoire", "name": "Caf\u00e9 Zen", "date": "2012-05-11", "zipcode": 75009, "geometry": {"coordinates": [2.336036, 48.875232], "type": "Point"}, "recordid": "5e9a6172f8b64cd098a5f2cf9b1d42567fe0a894", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.872677, 2.315276], "address": "27 rue de Penthi\u00e8vre", "name": "O'Breizh", "date": "2012-10-22", "zipcode": 75008, "geometry": {"coordinates": [2.315276, 48.872677], "type": "Point"}, "recordid": "ead3108add27ef41bb92517aca834f7d7f632816", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.868838, 2.33605], "address": "23 rue saint augustin", "name": "Le Petit Choiseul", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.33605, 48.868838], "type": "Point"}, "recordid": "de601277ca00567b62fa4f5277e4a17679faa753", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.841526, 2.351012], "address": "7 rue Ep\u00e9e de Bois", "name": "Invitez vous chez nous", "date": "2012-10-22", "zipcode": 75005, "geometry": {"coordinates": [2.351012, 48.841526], "type": "Point"}, "recordid": "1d2373bdec8a07306298e3ee54894ac295ee1d55", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.86525, 2.350507], "address": "142 Rue Saint-Denis 75002 Paris", "name": "La Cordonnerie", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.350507, 48.86525], "type": "Point"}, "recordid": "5c9bf60617a99ad75445b454f98e75e1a104021d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.892244, 2.346973], "address": "3, rue Baudelique", "name": "Le Supercoin", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.346973, 48.892244], "type": "Point"}, "recordid": "68a4ee10f1fc4d2a659501e811d148420fa80e95", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.890043, 2.362241], "address": "86 bis rue Riquet", "name": "Populettes", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.362241, 48.890043], "type": "Point"}, "recordid": "8cc55d58d72621a7e91cf6b456731d2cb2863afc", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "1", "geoloc": [48.893017, 2.337776], "address": "49 rue des Cloys", "name": "Au bon coin", "date": "2012-10-18", "zipcode": 75018, "geometry": {"coordinates": [2.337776, 48.893017], "type": "Point"}, "recordid": "0408f272c08c52e3cae035ffdeb8928698787ea9", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.836919, 2.347003], "address": "69 rue Broca", "name": "Le Couvent", "date": "2012-10-18", "zipcode": 75013, "geometry": {"coordinates": [2.347003, 48.836919], "type": "Point"}, "recordid": "729b2f228d3fd2db6f78bb624d451f59555e4a04", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.840624, 2.349766], "address": "111 rue mouffetard", "name": "La Br\u00fblerie des Ternes", "date": "2012-10-18", "zipcode": 75005, "geometry": {"coordinates": [2.349766, 48.840624], "type": "Point"}, "recordid": "233dd2a17620cd5eae70fef11cc627748e3313d5", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.832825, 2.336116], "address": "59 Boulevard Saint-Jacques", "name": "L'\u00c9cir", "date": "2012-10-09", "zipcode": 75014, "geometry": {"coordinates": [2.336116, 48.832825], "type": "Point"}, "recordid": "4a44324a5a806801fd3e05af89cad7c0f1e69d1e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.850696, 2.378417], "address": "126, rue du Faubourg Saint Antoine", "name": "Le Chat bossu", "date": "2012-10-09", "zipcode": 75012, "geometry": {"coordinates": [2.378417, 48.850696], "type": "Point"}, "recordid": "d1d02463f7c90d38cccffd898e15f51e65910baf", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.834157, 2.33381], "address": "58 boulvevard Saint Jacques", "name": "Denfert caf\u00e9", "date": "2012-10-09", "zipcode": 75014, "geometry": {"coordinates": [2.33381, 48.834157], "type": "Point"}, "recordid": "f78f406e5ccb95cb902ce618ed54eba1d4776a3c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.867948, 2.343582], "address": "95 rue Montmartre", "name": "Le Caf\u00e9 frapp\u00e9", "date": "2012-10-09", "zipcode": 75002, "geometry": {"coordinates": [2.343582, 48.867948], "type": "Point"}, "recordid": "4dd2a924a7b2c5f061cecaba2f272548a8c83c6c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.859772, 2.360558], "address": "78 rue vieille du temple", "name": "La Perle", "date": "2012-10-09", "zipcode": 75003, "geometry": {"coordinates": [2.360558, 48.859772], "type": "Point"}, "recordid": "476c54e643613ec36a9b1c533f32122fd873f3c3", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.845047, 2.349583], "address": "1 rue Thouin", "name": "Le Descartes", "date": "2012-10-09", "zipcode": 75005, "geometry": {"coordinates": [2.349583, 48.845047], "type": "Point"}, "recordid": "e9cb0b0d6b6b512e9ecab889267ba342a4f0ea93", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.830151, 2.334213], "address": "55 rue de la tombe Issoire", "name": "Le petit club", "date": "2012-03-07", "zipcode": 75014, "geometry": {"coordinates": [2.334213, 48.830151], "type": "Point"}, "recordid": "c6ca1166fa7fd7050f5d1c626a1e17050116c91e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.865707, 2.374382], "address": "90 avenue Parmentier", "name": "Le Plein soleil", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.374382, 48.865707], "type": "Point"}, "recordid": "95cf5fb735bd19826db70a3af4fe72fce647d4e5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.875322, 2.312329], "address": "146, boulevard Haussmann", "name": "Le Relais Haussmann", "date": "2012-03-07", "zipcode": 75008, "geometry": {"coordinates": [2.312329, 48.875322], "type": "Point"}, "recordid": "6c5f68f47c916638342b77bd5edfc30bd7051303", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.859559, 2.30643], "address": "88 rue Saint-Dominique", "name": "Le Malar", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.30643, 48.859559], "type": "Point"}, "recordid": "c633cb01686aa5f3171bf59976dc9b7f23cbca54", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.864628, 2.408038], "address": "47 rue Belgrand", "name": "Au panini de la place", "date": "2012-03-07", "zipcode": 75020, "geometry": {"coordinates": [2.408038, 48.864628], "type": "Point"}, "recordid": "66dedd35fcbbbb24f328882d49098de7aa5f26ba", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.88435, 2.297978], "address": "182 rue de Courcelles", "name": "Le Village", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.297978, 48.88435], "type": "Point"}, "recordid": "1e07eaf8a93875906d0b18eb6a897c651943589a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.853381, 2.376706], "address": "41 rue de Charonne", "name": "Pause Caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.376706, 48.853381], "type": "Point"}, "recordid": "60d98c3236a70824df50e9aca83e7d7f13a310c5", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.853253, 2.383415], "address": "14 rue Jean Mac\u00e9", "name": "Le Pure caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.383415, 48.853253], "type": "Point"}, "recordid": "66707fb2e707d2145fc2eb078a1b980a45921616", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.848873, 2.392859], "address": "307 fg saint Antoine", "name": "Extra old caf\u00e9", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.392859, 48.848873], "type": "Point"}, "recordid": "039ec7dcb219cfc434547b06938ba497afeb83b4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.873227, 2.360787], "address": "44 rue Vinaigriers", "name": "Chez Fafa", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.360787, 48.873227], "type": "Point"}, "recordid": "1572d199f186bf86d7753fe71ac23477a7a8bd2c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.850836, 2.384069], "address": "3 rue Faidherbe", "name": "En attendant l'or", "date": "2012-06-27", "zipcode": 75011, "geometry": {"coordinates": [2.384069, 48.850836], "type": "Point"}, "recordid": "de5789bb4a4ffbd244cded8dc555639dbe7d2279", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.866993, 2.336006], "address": "30 rue des Petits-Champs", "name": "Br\u00fblerie San Jos\u00e9", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.336006, 48.866993], "type": "Point"}, "recordid": "b736e5fa17396ee56a642212ccd0ab29c7f2bef1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.856434, 2.342683], "address": "2 place Martin Nadaud", "name": "Caf\u00e9 Martin", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.342683, 48.856434], "type": "Point"}, "recordid": "25fbf857029c54d57909c158e3039349b77344ed", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.863675, 2.348701], "address": "14 rue Turbigo, Paris", "name": "Etienne", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.348701, 48.863675], "type": "Point"}, "recordid": "0190edd7b0766c6d3e43093deb41abe9446c1b22", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.854584, 2.385193], "address": "184 bd Voltaire", "name": "L'ing\u00e9nu", "date": "2012-06-27", "zipcode": 75011, "geometry": {"coordinates": [2.385193, 48.854584], "type": "Point"}, "recordid": "7fcef7475ec632d5c61c9c36c1d52a402ad2e9e8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.890605, 2.361349], "address": "8 rue L'Olive", "name": "L'Olive", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.361349, 48.890605], "type": "Point"}, "recordid": "35286273f281c8c5082b3d3bd17f6bbf207426f9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.871396, 2.338321], "address": "18 rue Favart", "name": "Le Biz", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.338321, 48.871396], "type": "Point"}, "recordid": "cf2f6b9e283aaeeca2214cc1fe57b45e45668e25", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.868109, 2.331785], "address": "1 rue Louis le Grand", "name": "Le Cap Bourbon", "date": "2012-06-27", "zipcode": 75002, "geometry": {"coordinates": [2.331785, 48.868109], "type": "Point"}, "recordid": "339030e95e0846b41a8e2b91045456c4e4a50043", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.84167, 2.303053], "address": "9 Place du General Beuret", "name": "Le General Beuret", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.303053, 48.84167], "type": "Point"}, "recordid": "12b37036d28d28b32ebe81756ad15eb68372947f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.846814, 2.289311], "address": "95 avenue Emile Zola", "name": "Le Germinal", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.289311, 48.846814], "type": "Point"}, "recordid": "4e03831a64e886a28a7232e54f2812c1ced23c5a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.862655, 2.337607], "address": "202 rue Saint-Honor\u00e9", "name": "Le Ragueneau", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.337607, 48.862655], "type": "Point"}, "recordid": "e303131b2600d4c2287749a36bf7193d2fa60bd7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.889982, 2.338933], "address": "72 rue lamarck", "name": "Le refuge", "date": "2012-06-27", "zipcode": 75018, "geometry": {"coordinates": [2.338933, 48.889982], "type": "Point"}, "recordid": "f64310461736a769c6854fdefb99b9f2e7b230a9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.870294, 2.352821], "address": "13 rue du Faubourg Saint Denis", "name": "Le sully", "date": "2012-06-27", "zipcode": 75010, "geometry": {"coordinates": [2.352821, 48.870294], "type": "Point"}, "recordid": "166be8588ff16e50838fa6a164d1e580497b795d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.842128, 2.280374], "address": "60 rue des bergers", "name": "Le bal du pirate", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.280374, 48.842128], "type": "Point"}, "recordid": "93ff6e35406a074a6ba2667d2b286abf91132f6a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.838769, 2.39609], "address": "95 rue claude decaen", "name": "zic zinc", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.39609, 48.838769], "type": "Point"}, "recordid": "8fdc739d64ff1f01973235301e3ec86791016759", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.870247, 2.376306], "address": "35 rue de l'orillon", "name": "l'orillon bar", "date": "2014-02-01", "zipcode": 75011, "geometry": {"coordinates": [2.376306, 48.870247], "type": "Point"}, "recordid": "6e6e9695ef04c3fdbd4cfa19f0cfb0c0f93f4933", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.869848, 2.394247], "address": "116 Rue de M\u00e9nilmontant", "name": "Le Zazabar", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.394247, 48.869848], "type": "Point"}, "recordid": "22edfe92a72ce1bb0eea972508b5caa7af2db2df", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.845458, 2.354796], "address": "22 rue Linn\u00e9", "name": "L'In\u00e9vitable", "date": "2013-08-22", "zipcode": 75005, "geometry": {"coordinates": [2.354796, 48.845458], "type": "Point"}, "recordid": "6893cb08e99319091de9ba80305f22e0ce4cc08d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.83336, 2.365782], "address": "77 rue Dunois", "name": "Le Dunois", "date": "2013-08-22", "zipcode": 75013, "geometry": {"coordinates": [2.365782, 48.83336], "type": "Point"}, "recordid": "c3be1246dbc4ca5734d5bbd569436ba655105248", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.862655, 2.337607], "address": "202 rue Saint Honor\u00e9", "name": "Ragueneau", "date": "2012-05-11", "zipcode": 75001, "geometry": {"coordinates": [2.337607, 48.862655], "type": "Point"}, "recordid": "e946d9e8f8c5a130f98eca945efadfd9eec40dcb", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": null, "geoloc": [48.826608, 2.374571], "address": "48 rue du Dessous des Berges", "name": "Le Caminito", "date": "2012-10-22", "zipcode": 75013, "geometry": {"coordinates": [2.374571, 48.826608], "type": "Point"}, "recordid": "5d9ad8bcfbbc20adaec2aa7dcdb71326868c7686", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.870598, 2.365413], "address": "55bis quai de Valmy", "name": "Epicerie Musicale", "date": "2012-10-22", "zipcode": 75010, "geometry": {"coordinates": [2.365413, 48.870598], "type": "Point"}, "recordid": "8ffea133d93c608d337bc0129f4f9f3d5cad8dae", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": null, "address": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital", "name": "Le petit Bretonneau", "date": "2012-10-22", "zipcode": 75018, "geometry": {}, "recordid": "dd1ffdbd55c2dc651201302b8015258f7d35fd35", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.862575, 2.367427], "address": "104 rue amelot", "name": "Le Centenaire", "date": "2012-10-22", "zipcode": 75011, "geometry": {"coordinates": [2.367427, 48.862575], "type": "Point"}, "recordid": "e47d25752c2c8bcab5efb0e3a41920ec7a8a766a", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.842833, 2.348314], "address": "13 Rue du Pot de Fer", "name": "La Montagne Sans Genevi\u00e8ve", "date": "2012-10-22", "zipcode": 75005, "geometry": {"coordinates": [2.348314, 48.842833], "type": "Point"}, "recordid": "314d170601f81e3a3f26d8801f0fbee39981c788", "prix_salle": "1", "prix_comptoir": null, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.851325, 2.40171], "address": "46 rue de Buzenval", "name": "Les P\u00e8res Populaires", "date": "2012-10-18", "zipcode": 75020, "geometry": {"coordinates": [2.40171, 48.851325], "type": "Point"}, "recordid": "e75e8a3fe6212a0e576beec82f0128dd394e56fa", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.857658, 2.305613], "address": "188 rue de Grenelle", "name": "Cafe de grenelle", "date": "2012-10-09", "zipcode": 75007, "geometry": {"coordinates": [2.305613, 48.857658], "type": "Point"}, "recordid": "72e274046d671e68bc7754808feacfc95b91b6ed", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.875207, 2.332944], "address": "73 rue de la Victoire", "name": "Le relais de la victoire", "date": "2012-10-09", "zipcode": 75009, "geometry": {"coordinates": [2.332944, 48.875207], "type": "Point"}, "recordid": "01c21abdf35484f9ad184782979ecd078de84523", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": null, "address": "Route de la Muette \u00e0 Neuilly\nClub hippique du Jardin d\u2019Acclimatation", "name": "La chaumi\u00e8re gourmande", "date": "2012-03-07", "zipcode": 75016, "geometry": {}, "recordid": "438ec18d35793d12eb6a137373c3fe4f3aa38a69", "prix_salle": "1", "prix_comptoir": null, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.891882, 2.33365], "address": "216, rue Marcadet", "name": "Le Brio", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.33365, 48.891882], "type": "Point"}, "recordid": "49e3584ce3d6a6a236b4a0db688865bdd3483fec", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.884753, 2.324648], "address": "22 rue des Dames", "name": "Caves populaires", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.324648, 48.884753], "type": "Point"}, "recordid": "af81e5eca2b84ea706ac2d379edf65b0fb2f879a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.827428, 2.325652], "address": "12 avenue Jean Moulin", "name": "Caprice caf\u00e9", "date": "2012-03-07", "zipcode": 75014, "geometry": {"coordinates": [2.325652, 48.827428], "type": "Point"}, "recordid": "88e07bdb723b49cd27c90403b5930bca1c93b458", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.832964, 2.369266], "address": "7 rue Clisson", "name": "Tamm Bara", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.369266, 48.832964], "type": "Point"}, "recordid": "baf59c98acafbe48ed9e91b64377da216a20cbcc", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.876577, 2.348414], "address": "1 rue de Montholon", "name": "L'anjou", "date": "2012-03-07", "zipcode": 75009, "geometry": {"coordinates": [2.348414, 48.876577], "type": "Point"}, "recordid": "e6f5949dca40548aad296208c61c498f639f648c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.862599, 2.315086], "address": "2 rue Robert Esnault Pelterie", "name": "Caf\u00e9 dans l'aerogare Air France Invalides", "date": "2012-03-07", "zipcode": 75007, "geometry": {"coordinates": [2.315086, 48.862599], "type": "Point"}, "recordid": "6d175eb48c577fafdfc99df0ab55da468cf17164", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.844854, 2.345413], "address": "10 rue d\"Ulm", "name": "Waikiki", "date": "2012-03-07", "zipcode": 75005, "geometry": {"coordinates": [2.345413, 48.844854], "type": "Point"}, "recordid": "fb1e2bc2ae55d3d47da682c71093a12fa64fbd45", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.871576, 2.364499], "address": "36 rue Beaurepaire", "name": "Chez Prune", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.364499, 48.871576], "type": "Point"}, "recordid": "19399ede5b619761877822185bbb4c98b565974c", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.833863, 2.329046], "address": "21 rue Boulard", "name": "Au Vin Des Rues", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.329046, 48.833863], "type": "Point"}, "recordid": "87263873b6f8346b5777844be0122a307f29fcab", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.838137, 2.301166], "address": "14 rue d'alleray", "name": "bistrot les timbr\u00e9s", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.301166, 48.838137], "type": "Point"}, "recordid": "74b6d7113e5b14eb8e890663f061208bf4ff6728", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.871799, 2.315985], "address": "9 rue de Miromesnil", "name": "Caf\u00e9 beauveau", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.315985, 48.871799], "type": "Point"}, "recordid": "2759f5cdda4bcb88ca3ae2f7299b37b8e62596c8", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.866259, 2.338739], "address": "9 rue des petits champs", "name": "Caf\u00e9 Pistache", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.338739, 48.866259], "type": "Point"}, "recordid": "6d2675cdc912118d0376229be8e436feca9c8af7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.874605, 2.387738], "address": "13 Rue Jean-Baptiste Dumay", "name": "La Cagnotte", "date": "2012-06-27", "zipcode": 75020, "geometry": {"coordinates": [2.387738, 48.874605], "type": "Point"}, "recordid": "f7085c754c0c97e418d7e5213753f74bd396fc27", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.842462, 2.310919], "address": "172 rue de vaugirard", "name": "le 1 cinq", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.310919, 48.842462], "type": "Point"}, "recordid": "17e917723fc99d6e5bd77eb9633ac2e789a9a6d9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.84591, 2.375543], "address": "28 bis boulevard Diderot", "name": "Le Killy Jen", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.375543, 48.84591], "type": "Point"}, "recordid": "93132bd8b3ae67dfcc8cf8c1166e312ac4acb9b9", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.842868, 2.303173], "address": "106 rue Lecourbe", "name": "Les Artisans", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.303173, 48.842868], "type": "Point"}, "recordid": "c37ef573b4cb2d1e61795d6a9ef11de433dc9a99", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.865684, 2.334416], "address": "83 avenue de Wagram", "name": "Peperoni", "date": "2012-06-27", "zipcode": 75001, "geometry": {"coordinates": [2.334416, 48.865684], "type": "Point"}, "recordid": "9461f859ca009ced25555fa6af1e6867dda9223e", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.833146, 2.288834], "address": "380 rue de vaugirard", "name": "le lutece", "date": "2014-02-01", "zipcode": 75015, "geometry": {"coordinates": [2.288834, 48.833146], "type": "Point"}, "recordid": "ddd13990c1408700085366bd4ba313acd69a44ea", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.886431, 2.327429], "address": "16 rue Ganneron", "name": "Brasiloja", "date": "2014-02-01", "zipcode": 75018, "geometry": {"coordinates": [2.327429, 48.886431], "type": "Point"}, "recordid": "d679bb1642534278f4c0203d67be0bafd5306d81", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.855711, 2.359491], "address": "16 rue de Rivoli", "name": "Rivolux", "date": "2014-02-01", "zipcode": 75004, "geometry": {"coordinates": [2.359491, 48.855711], "type": "Point"}, "recordid": "bdd2b008cc765c7fe195c037b830cd2628420a2f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.845898, 2.372766], "address": "21 Bis Boulevard Diderot", "name": "L'europ\u00e9en", "date": "2013-08-22", "zipcode": 75012, "geometry": {"coordinates": [2.372766, 48.845898], "type": "Point"}, "recordid": "693c0da7d4db24781ed161c01a661c36074a94fa", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.867465, 2.357791], "address": "39 rue Notre Dame de Nazareth", "name": "NoMa", "date": "2013-08-22", "zipcode": 75003, "geometry": {"coordinates": [2.357791, 48.867465], "type": "Point"}, "recordid": "60d8b670810cc95eb0439dd0c238f8205ea8ef76", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.871595, 2.385858], "address": "1 Rue des Envierges", "name": "O'Paris", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.385858, 48.871595], "type": "Point"}, "recordid": "297c040284a05efe35c69bb621505e6acfdcdda4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.872402, 2.366532], "address": "16 avenue Richerand", "name": "Caf\u00e9 Clochette", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.366532, 48.872402], "type": "Point"}, "recordid": "a561a941f538e8a1d321bf8d98576d06be037962", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.856584, 2.368574], "address": "40 Boulevard Beaumarchais", "name": "La cantoche de Paname", "date": "2013-08-22", "zipcode": 75011, "geometry": {"coordinates": [2.368574, 48.856584], "type": "Point"}, "recordid": "5ba2aaec9f1de9d01e65be95215cab13c693cdf3", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.856496, 2.394874], "address": "148 Boulevard de Charonne", "name": "Le Saint Ren\u00e9", "date": "2013-08-22", "zipcode": 75020, "geometry": {"coordinates": [2.394874, 48.856496], "type": "Point"}, "recordid": "4d60b350d04d4b1bf4bfd4dd6cc59687dc792c74", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.850055, 2.383908], "address": "196 rue du faubourg saint-antoine", "name": "La Libert\u00e9", "date": "2012-10-22", "zipcode": 75012, "geometry": {"coordinates": [2.383908, 48.850055], "type": "Point"}, "recordid": "ee94e76326f8dcbe3500afec69f1a21eb1215ad0", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.866737, 2.33716], "address": "16 rue des Petits Champs", "name": "Chez Rutabaga", "date": "2012-10-22", "zipcode": 75002, "geometry": {"coordinates": [2.33716, 48.866737], "type": "Point"}, "recordid": "a420ea4608440b8dc8e0267fe8cc513daa950551", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.885367, 2.325325], "address": "2 rue Lemercier", "name": "Le BB (Bouchon des Batignolles)", "date": "2012-10-22", "zipcode": 75017, "geometry": {"coordinates": [2.325325, 48.885367], "type": "Point"}, "recordid": "20986cbfe11018bd0aba8150a49db1c435f7642d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.873175, 2.339193], "address": "10 rue Rossini", "name": "La Brocante", "date": "2012-10-22", "zipcode": 75009, "geometry": {"coordinates": [2.339193, 48.873175], "type": "Point"}, "recordid": "2e10601c35669394d43936a771b18408be0338ba", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.840771, 2.324589], "address": "3 rue Ga\u00eet\u00e9", "name": "Le Plomb du cantal", "date": "2012-10-22", "zipcode": 75014, "geometry": {"coordinates": [2.324589, 48.840771], "type": "Point"}, "recordid": "6fb510614e00b065bf16a5af8e2c0eaf561a5654", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.884753, 2.324648], "address": "22 rue des Dames", "name": "Les caves populaires", "date": "2012-10-22", "zipcode": 75017, "geometry": {"coordinates": [2.324648, 48.884753], "type": "Point"}, "recordid": "d650c509a0aa8ed7b9c9b88861263f31463bbd0e", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.869519, 2.39339], "address": "108 rue de M\u00e9nilmontant", "name": "Chez Luna", "date": "2012-10-09", "zipcode": 75020, "geometry": {"coordinates": [2.39339, 48.869519], "type": "Point"}, "recordid": "736f4d996f1f8b7c3a0ce2abfeebfcce2a4bab13", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.877903, 2.385365], "address": "1 rue du Plateau", "name": "Le bar Fleuri", "date": "2012-03-07", "zipcode": 75019, "geometry": {"coordinates": [2.385365, 48.877903], "type": "Point"}, "recordid": "be55720646093788ec161c6cadc5ad8059f4b90b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.882939, 2.31809], "address": "101 rue des dames", "name": "Trois pi\u00e8ces cuisine", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.31809, 48.882939], "type": "Point"}, "recordid": "7bbbfb755020a2c25cce0067601994ce5ee4193f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.849497, 2.298855], "address": "61 avenue de la Motte Picquet", "name": "Le Zinc", "date": "2012-03-07", "zipcode": 75015, "geometry": {"coordinates": [2.298855, 48.849497], "type": "Point"}, "recordid": "e7c35a94454518de6de5bbecbc015fc37f7aea14", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.880669, 2.349964], "address": "136 rue du Faubourg poissonni\u00e8re", "name": "La cantine de Zo\u00e9", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.349964, 48.880669], "type": "Point"}, "recordid": "0edc473b3432a869b8ed66b6c4c989766b699947", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.844057, 2.328402], "address": "6/8 rue Stanislas", "name": "Les Vendangeurs", "date": "2012-03-07", "zipcode": 75006, "geometry": {"coordinates": [2.328402, 48.844057], "type": "Point"}, "recordid": "e9766ea36f6293bf670ed938bff02b975d012973", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.852053, 2.338779], "address": "3 carrefour de l'Od\u00e9on", "name": "L'avant comptoir", "date": "2012-03-07", "zipcode": 75006, "geometry": {"coordinates": [2.338779, 48.852053], "type": "Point"}, "recordid": "fe843d2f43dcaac9129f5b36dc367558dfd3b3e4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "1", "geoloc": [48.886504, 2.34498], "address": "1 rue Paul albert", "name": "Botak cafe", "date": "2012-03-07", "zipcode": 75018, "geometry": {"coordinates": [2.34498, 48.886504], "type": "Point"}, "recordid": "9e19c375e612f5fb803ec6a27881858619207812", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.872722, 2.354594], "address": "67 rue du Ch\u00e2teau d'eau", "name": "le chateau d'eau", "date": "2012-03-07", "zipcode": 75010, "geometry": {"coordinates": [2.354594, 48.872722], "type": "Point"}, "recordid": "05bb6a26ec5bfbba25da2d19a5f0e83d69800f38", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.85192, 2.373229], "address": "58 rue du Fbg Saint-Antoine", "name": "Bistrot Saint-Antoine", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.373229, 48.85192], "type": "Point"}, "recordid": "daa3908ddf69d378fec5b4548494727e1121adc4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.854685, 2.368487], "address": "11/13 boulevard Beaumarchais", "name": "Chez Oscar", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.368487, 48.854685], "type": "Point"}, "recordid": "c73be0483480c59e6ab6bc3a906c8d9dd474887f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.87226, 2.304441], "address": "63 rue de Ponthieu", "name": "Le Fronton", "date": "2012-06-27", "zipcode": 75008, "geometry": {"coordinates": [2.304441, 48.87226], "type": "Point"}, "recordid": "85a8200d3e3aed7724d3207ed8b1ee5ec50c1f90", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.851, 2.300378], "address": "48 avenue de la Motte Picquet", "name": "Le Piquet", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.300378, 48.851], "type": "Point"}, "recordid": "460e2adc95fd172f753b1b6ed296c2711639d49d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.841089, 2.349565], "address": "104 rue Mouffetard", "name": "Le Tournebride", "date": "2012-06-27", "zipcode": 75005, "geometry": {"coordinates": [2.349565, 48.841089], "type": "Point"}, "recordid": "8a7a23ed68366f70ab939c877cbdce46f19d75c7", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.828704, 2.322074], "address": "52 rue des plantes", "name": "maison du vin", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.322074, 48.828704], "type": "Point"}, "recordid": "482507a8f0fe4960f94372b6fa12b16e7d4f2a93", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.842146, 2.375986], "address": "157 rue Bercy 75012 Paris", "name": "L'entrep\u00f4t", "date": "2014-02-01", "zipcode": 75012, "geometry": {"coordinates": [2.375986, 48.842146], "type": "Point"}, "recordid": "d8746118429eb118f38ecbee904636d9b33fa8ba", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.867092, 2.363288], "address": "Place de la R\u00e9publique", "name": "Le caf\u00e9 Monde et M\u00e9dias", "date": "2013-08-22", "zipcode": 75003, "geometry": {"coordinates": [2.363288, 48.867092], "type": "Point"}, "recordid": "af04c90f25e25daf7f5cbbab1bc740bac26541d4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.849821, 2.355337], "address": "11 Quai de la Tournelle", "name": "Caf\u00e9 rallye tournelles", "date": "2013-08-22", "zipcode": 75005, "geometry": {"coordinates": [2.355337, 48.849821], "type": "Point"}, "recordid": "91d88e321a75b6a8c4dea816c399fda77c41f9d1", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.872498, 2.355136], "address": "61 rue du ch\u00e2teau d'eau", "name": "Brasserie le Morvan", "date": "2013-08-22", "zipcode": 75010, "geometry": {"coordinates": [2.355136, 48.872498], "type": "Point"}, "recordid": "6ec62058995948fc18d331f01a2d03acc0d9e0fa", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "1", "geoloc": [48.874879, 2.386064], "address": "6 rue M\u00e9lingue", "name": "Chez Miamophile", "date": "2013-08-22", "zipcode": 75019, "geometry": {"coordinates": [2.386064, 48.874879], "type": "Point"}, "recordid": "13924872737e4fc640a43da58937c3777c2ac753", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.864269, 2.36858], "address": "18 rue de Crussol", "name": "Panem", "date": "2012-05-11", "zipcode": 75011, "geometry": {"coordinates": [2.36858, 48.864269], "type": "Point"}, "recordid": "67bdf3a6989f80749a1ba33a17b1370de0a0e1cd", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.885662, 2.319591], "address": "47 rue de Batignolles", "name": "Petits Freres des Pauvres", "date": "2012-05-11", "zipcode": 75017, "geometry": {"coordinates": [2.319591, 48.885662], "type": "Point"}, "recordid": "e27fd00149514bbfad7dd7e8f9b0c677df2d3f25", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.837212, 2.296046], "address": "198 rue de la Convention", "name": "Caf\u00e9 Dupont", "date": "2012-05-11", "zipcode": 75015, "geometry": {"coordinates": [2.296046, 48.837212], "type": "Point"}, "recordid": "4d40e6d864dae81c152a05cb98e30933bde96aa1", "prix_salle": "-", "prix_comptoir": 0, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.871002, 2.30879], "address": "28 rue de Ponthieu", "name": "L'Angle", "date": "2012-10-22", "zipcode": 75008, "geometry": {"coordinates": [2.30879, 48.871002], "type": "Point"}, "recordid": "c40bd2d1f98b415e539c27cf68518d060ebab51e", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.888023, 2.353467], "address": "19-23 rue L\u00e9on", "name": "Institut des Cultures d'Islam", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.353467, 48.888023], "type": "Point"}, "recordid": "68d6d37b846e39bd8554e1f8f75974b486b0f27b", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.886044, 2.360781], "address": "19 rue Pajol", "name": "Canopy Caf\u00e9 associatif", "date": "2012-10-22", "zipcode": 75018, "geometry": {"coordinates": [2.360781, 48.886044], "type": "Point"}, "recordid": "ff73bafb514bb68eb925c81aee43c3a58ac3c70d", "prix_salle": "1", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.870287, 2.332491], "address": "place de l'opera", "name": "L'Entracte", "date": "2012-10-09", "zipcode": 75002, "geometry": {"coordinates": [2.332491, 48.870287], "type": "Point"}, "recordid": "0039cd8bceb5e281677a158f832a660789088071", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.858709, 2.362701], "address": "15 rue du Parc Royal", "name": "Le S\u00e9vign\u00e9", "date": "2012-10-09", "zipcode": 75003, "geometry": {"coordinates": [2.362701, 48.858709], "type": "Point"}, "recordid": "adcc8b4f78f05ba7b24b0593e1516dfb7b415f91", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.839687, 2.347254], "address": "35 rue Claude Bernard", "name": "Le Caf\u00e9 d'avant", "date": "2012-10-09", "zipcode": 75005, "geometry": {"coordinates": [2.347254, 48.839687], "type": "Point"}, "recordid": "b904fc48763938eee2169ba25aad2ffcc0dd6a9f", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.844244, 2.330407], "address": "53 rue Notre-Dame des Champs", "name": "Le Lucernaire", "date": "2012-10-09", "zipcode": 75006, "geometry": {"coordinates": [2.330407, 48.844244], "type": "Point"}, "recordid": "cc72af04314fd40e16ff611c799d378515043508", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.877599, 2.332111], "address": "12 rue Blanche", "name": "Le Brigadier", "date": "2012-10-09", "zipcode": 75009, "geometry": {"coordinates": [2.332111, 48.877599], "type": "Point"}, "recordid": "978d4bc68c9ebf81029d3e77274d2107777b8a75", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.826494, 2.359987], "address": "26 rue du Docteur Magnan", "name": "L'\u00e2ge d'or", "date": "2012-03-07", "zipcode": 75013, "geometry": {"coordinates": [2.359987, 48.826494], "type": "Point"}, "recordid": "40bffbdc0c9ed1cbce820fed875d7c21d8964640", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.883717, 2.326861], "address": "Place de Clichy", "name": "Bagels & Coffee Corner", "date": "2012-03-07", "zipcode": 75017, "geometry": {"coordinates": [2.326861, 48.883717], "type": "Point"}, "recordid": "262facde9b8c4568c9ba7fbce8f069ff8c76948d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.835843, 2.278501], "address": "10 boulevard Victor", "name": "Caf\u00e9 Victor", "date": "2012-03-07", "zipcode": 75015, "geometry": {"coordinates": [2.278501, 48.835843], "type": "Point"}, "recordid": "e5817ec44ac5a7ea2e4a34b6a2e13d535156642b", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.845337, 2.379024], "address": "54, avenue Daumesnil", "name": "L'empreinte", "date": "2012-03-07", "zipcode": 75012, "geometry": {"coordinates": [2.379024, 48.845337], "type": "Point"}, "recordid": "b96ddd35cbbf5d93aaff79487afdf083b5ff0817", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.857312, 2.379055], "address": "93, rue de la Roquette", "name": "L'horizon", "date": "2012-03-07", "zipcode": 75011, "geometry": {"coordinates": [2.379055, 48.857312], "type": "Point"}, "recordid": "84c6b7335e7f82ac942c4f398723ec99076f148d", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.835878, 2.395723], "address": "34 bis rue de Wattignies", "name": "Au pays de Vannes", "date": "2012-06-27", "zipcode": 75012, "geometry": {"coordinates": [2.395723, 48.835878], "type": "Point"}, "recordid": "a17869dbb9d0d5b1e5ed7bb288053900b04ee944", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.85413, 2.323539], "address": "36 rue de Varenne", "name": "Caf\u00e9 Varenne", "date": "2012-06-27", "zipcode": 75007, "geometry": {"coordinates": [2.323539, 48.85413], "type": "Point"}, "recordid": "a26ec0d5fca47b8de77d862ad8a99b75bb520a09", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.855161, 2.360218], "address": "125 Rue Saint-Antoine", "name": "l'El\u00e9phant du nil", "date": "2012-06-27", "zipcode": 75004, "geometry": {"coordinates": [2.360218, 48.855161], "type": "Point"}, "recordid": "7b7ceefd1f9ed85041265c9577e0dc8bee01d45a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.8357, 2.292961], "address": "354 bis rue Vaugirard", "name": "Le Comptoir", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.292961, 48.8357], "type": "Point"}, "recordid": "59d8fa304e535f4eb41f9746028034c9b30cbde4", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.835451, 2.292515], "address": "358 rue de Vaugirard", "name": "Le Parc Vaugirard", "date": "2012-06-27", "zipcode": 75015, "geometry": {"coordinates": [2.292515, 48.835451], "type": "Point"}, "recordid": "19f655206a8446959c8e796c2b3cb9001890f985", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.834972, 2.327007], "address": "58 rue Daguerre", "name": "le Zango", "date": "2012-06-27", "zipcode": 75014, "geometry": {"coordinates": [2.327007, 48.834972], "type": "Point"}, "recordid": "e1b54109015316a822747f788128f997a3478050", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.848887, 2.399972], "address": "3 rue de Lagny", "name": "Melting Pot", "date": "2012-06-27", "zipcode": 75020, "geometry": {"coordinates": [2.399972, 48.848887], "type": "Point"}, "recordid": "fd0de2cbf73e0a728cd73e4e2a9a4a9c646f76f2", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} -{"prix_terasse": "-", "geoloc": [48.892366, 2.317359], "address": "174 avenue de Clichy", "name": "Pari's Caf\u00e9", "date": "2012-06-27", "zipcode": 75017, "geometry": {"coordinates": [2.317359, 48.892366], "type": "Point"}, "recordid": "831446ae203f89de26d3300e625c20717e82d40a", "prix_salle": "-", "prix_comptoir": 1, "city": "Paris", "country": "France"} \ No newline at end of file diff --git a/bonobo/examples/datasets/coffeeshops.py b/bonobo/examples/datasets/coffeeshops.py new file mode 100644 index 0000000..85883d7 --- /dev/null +++ b/bonobo/examples/datasets/coffeeshops.py @@ -0,0 +1,61 @@ +import bonobo +from bonobo import examples +from bonobo.contrib.opendatasoft import OpenDataSoftAPI as ODSReader +from bonobo.examples.datasets.services import get_services + + +def get_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, + bonobo.UnpackItems(0), + bonobo.Rename( + name='nom_du_cafe', + address='adresse', + zipcode='arrondissement' + ), + bonobo.Format(city='Paris', country='France'), + bonobo.OrderFields( + [ + 'name', 'address', 'zipcode', 'city', 'country', + 'geometry', 'geoloc' + ] + ), + *_print, + ) + + # Comma separated values. + graph.add_chain( + bonobo.CsvWriter( + 'coffeeshops.csv', + fields=['name', 'address', 'zipcode', 'city'], + delimiter=',' + ), + _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 + + +if __name__ == '__main__': + parser = examples.get_argument_parser() + + with bonobo.parse_args(parser) as options: + bonobo.run(get_graph(**examples.get_graph_options(options)), services=get_services()) diff --git a/bonobo/examples/datasets/coffeeshops.txt b/bonobo/examples/datasets/coffeeshops.txt deleted file mode 100644 index 8382078..0000000 --- a/bonobo/examples/datasets/coffeeshops.txt +++ /dev/null @@ -1,183 +0,0 @@ -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 d’Acclimatation",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 diff --git a/bonobo/examples/datasets/fablabs.py b/bonobo/examples/datasets/fablabs.py index 71b7da3..086bdfc 100644 --- a/bonobo/examples/datasets/fablabs.py +++ b/bonobo/examples/datasets/fablabs.py @@ -16,11 +16,10 @@ and a flat txt file. import json -from colorama import Fore, Style - import bonobo -from bonobo.commands import get_default_services +from bonobo import examples from bonobo.contrib.opendatasoft import OpenDataSoftAPI +from bonobo.examples.datasets.services import get_services try: import pycountry @@ -29,8 +28,7 @@ except ImportError as exc: 'You must install package "pycountry" to run this example.' ) from exc -API_DATASET = 'fablabs-in-the-world' -API_NETLOC = 'datanova.laposte.fr' +API_DATASET = 'fablabs@public-us' ROWS = 100 @@ -40,65 +38,28 @@ def _getlink(x): def normalize(row): result = { - ** - row, + **row, 'links': list(filter(None, map(_getlink, json.loads(row.get('links'))))), 'country': pycountry.countries.get(alpha_2=row.get('country_code', '').upper()).name, } return result -def display(row): - print(Style.BRIGHT, row.get('name'), Style.RESET_ALL, sep='') +def get_graph(graph=None, *, _limit=(), _print=()): + graph = graph or bonobo.Graph() + graph.add_chain( + OpenDataSoftAPI(dataset=API_DATASET), + *_limit, + normalize, + bonobo.UnpackItems(0), + *_print, + bonobo.JsonWriter(path='fablabs.json'), + ) + return graph - address = list( - filter( - None, ( - ' '.join( - filter( - None, ( - row.get('postal_code', None), - row.get('city', None) - ) - ) - ), - row.get('county', None), - row.get('country'), - ) - ) - ) - - print( - ' - {}address{}: {address}'.format( - Fore.BLUE, Style.RESET_ALL, address=', '.join(address) - ) - ) - print( - ' - {}links{}: {links}'.format( - Fore.BLUE, Style.RESET_ALL, links=', '.join(row['links']) - ) - ) - print( - ' - {}geometry{}: {geometry}'.format( - Fore.BLUE, Style.RESET_ALL, **row - ) - ) - print( - ' - {}source{}: {source}'.format( - Fore.BLUE, Style.RESET_ALL, source='datanova/' + API_DATASET - ) - ) - - -graph = bonobo.Graph( - OpenDataSoftAPI( - dataset=API_DATASET, netloc=API_NETLOC, timezone='Europe/Paris' - ), - normalize, - bonobo.Filter(filter=lambda row: row.get('country') == 'France'), - bonobo.JsonWriter(path='fablabs.txt', ioformat='arg0'), - bonobo.Tee(display), -) 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()) diff --git a/bonobo/examples/datasets/fablabs.txt b/bonobo/examples/datasets/fablabs.txt deleted file mode 100644 index 9333578..0000000 --- a/bonobo/examples/datasets/fablabs.txt +++ /dev/null @@ -1,135 +0,0 @@ -[{"city": "Lannion", "kind_name": "fab_lab", "links": ["http://fablab-lannion.org"], "capabilities": "three_d_printing;cnc_milling;circuit_production", "url": "https://www.fablabs.io/labs/fablablannion", "coordinates": [48.7317261, -3.4509764], "name": "Fablab Lannion - KerNEL", "phone": "+33 2 96 37 84 46", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/27/c6c015ba-26c6-4620-833f-8441123a4afc/Fablab Lannion - KerNEL.jpg", "postal_code": "22300", "longitude": -3.45097639999994, "country_code": "fr", "latitude": 48.7317261, "address_notes": "Use the small portal", "email": "contact@fablab-lannion.org", "address_1": "14 Rue de Beauchamp", "geometry": {"type": "Point", "coordinates": [-3.4509764, 48.7317261]}, "country": "France"}, -{"city": "Villeneuve-d'Ascq", "kind_name": "fab_lab", "links": ["http://www.flickr.com/photos/fablablille/", "https://twitter.com/FabLab_Lille", "http://www.fablablille.fr"], "url": "https://www.fablabs.io/labs/fablablille", "coordinates": [50.642869867, 3.1386641], "county": "Nord-Pas-de-Calais", "phone": "+33 9 72 29 47 65", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/10/34/147c88ca-2acd-42a4-aeb0-17b2dc830903/FabLab Lille.jpg", "postal_code": "59650", "longitude": 3.13866410000003, "country_code": "fr", "latitude": 50.6428698670218, "address_1": "2 All\u00e9e Lakanal", "name": "FabLab Lille", "geometry": {"type": "Point", "coordinates": [3.1386641, 50.642869867]}, "country": "France"}, -{"city": "Dijon", "name": "L'abscisse", "links": ["http://fablab.coagul.org"], "url": "https://www.fablabs.io/labs/lab6", "longitude": 5.04147999999998, "county": "France", "parent_id": 545, "kind_name": "mini_fab_lab", "postal_code": "2100", "coordinates": [47.322047, 5.04148], "address_2": "6, impasse Quentin", "latitude": 47.322047, "country_code": "fr", "email": "c-bureau@outils.coagul.org", "address_1": "Dijon", "geometry": {"type": "Point", "coordinates": [5.04148, 47.322047]}, "country": "France"}, -{"city": "Montreuil", "kind_name": "fab_lab", "links": ["http://www.apedec.org ", "http://webtv.montreuil.fr/festival-m.u.s.i.c-et-fablab-video-415-12.html", "http://www.wedemain.fr/A-Montreuil-un-fab-lab-circulaire-dans-une-usine-verticale_a421.html"], "capabilities": "three_d_printing", "url": "https://www.fablabs.io/labs/ecodesignfablab", "name": "ECODESIGN FAB LAB", "email": "contact@apedec.org", "coordinates": [48.8693157, 2.4564764], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/51/53/74898eb4-e94d-49fc-9e57-18246d1901c8/ECODESIGN FAB LAB.jpg", "phone": "+33 1 (0)9.81.29.17.31", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/13/33b98c6f-b6c1-4cfd-8b63-401c4441f964/ECODESIGN FAB LAB.jpg", "postal_code": "93106", "longitude": 2.45647640000004, "country_code": "fr", "latitude": 48.8693157, "address_1": "Montreuil", "address_notes": "lot 38 D", "address_2": "2 \u00e0 20 avenue Allende, MOZINOR", "blurb": "FAB LAB specialized in upcycling and ecodesign with furniture production based on diverted source of industrial waste, located in a industrial zone, in the heart of a popular city.", "description": "Based on the roof of an industrial zone of 50 SMEs (and 500 workers), Ecodesign Fab Lab is now open to address upcycling and eco-innovation, thanks waste collection, designers and classical wood equipment, but also 3D printers (and CNC equipment in the next weeks).", "geometry": {"type": "Point", "coordinates": [2.4564764, 48.8693157]}, "country": "France"}, -{"city": "Parthenay", "coordinates": [46.6466301, -0.2493703], "kind_name": "fab_lab", "links": ["http://parthlab.cc-parthenay.fr/"], "url": "https://www.fablabs.io/labs/parthlab", "name": "Parthlab", "longitude": -0.24937030000001, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/52/14/b7ab616b-ab7a-4c6f-b09f-7261e2c64526/Parthlab.jpg", "phone": "0549710870", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/41/c500e484-7d21-47cf-bf5d-fe27dba14783/Parthlab.jpg", "postal_code": "79200", "capabilities": "three_d_printing;cnc_milling;laser", "country_code": "fr", "latitude": 46.6466301, "address_1": "5 Rue Jean Mac\u00e9", "address_notes": "dans l'epn armand jubien", "email": "parthlab@cc-parthenay.fr", "blurb": "ParthLab est une association de parthenay. La fablab est pr\u00e9sent dans un espace numerique", "description": "Le fablab dispose divers locaux et de diff\u00e9rents mat\u00e9riels (carte arduino, imprimante makerbot, une cnc en cours de montage et d\u00e9coupeuse laser).\r\nPour apprendre et nous am\u00e9liorer on se lance sur des projets comme le montage d'une EggBot, d'un scanner 3d et d'un kritzler.. \r\nOn teste aussi des moteurs pour faire avancer une voiture par diff\u00e9rentes commandes.", "geometry": {"type": "Point", "coordinates": [-0.2493703, 46.6466301]}, "country": "France"}, -{"city": "Folschviller", "coordinates": [49.0709033, 6.6865092], "kind_name": "fab_lab", "links": ["http://wiki.fablab.is/wiki/OpenEdge", "http://openedge.cc"], "url": "https://www.fablabs.io/labs/openedge", "name": "Open Edge", "longitude": 6.68650920000005, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/52/03/ede3bd93-24a4-4173-8ca3-ce18db4cf3a6/Open Edge.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/31/1d60e6cc-2535-4b44-85d8-b1b184695b64/Open Edge.jpg", "postal_code": "57730", "capabilities": "three_d_printing;laser", "country_code": "fr", "latitude": 49.0709033, "address_1": "6 Avenue Foch", "address_notes": "Look for the wooden garage door, a big sign is on it", "email": "openedge@openedge.cc", "blurb": "A rural FabLab, manufacturing the FoldaRap a portable RepRap", "description": "The rural FabLab that is making the FoldaRap, the Mondrian and many other cool open-hardware projects at larger scale production (while being open to the general public).", "geometry": {"type": "Point", "coordinates": [6.6865092, 49.0709033]}, "country": "France"}, -{"city": "Blois", "coordinates": [47.5879436, 1.3362879], "kind_name": "fab_lab", "links": ["http://fablab-robert-houdin.org/"], "url": "https://www.fablabs.io/labs/roberthoudinfablab", "name": "FabLab Robert-Houdin", "longitude": 1.3362879, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/48/57/e2f9f15e-7686-42ac-a3d5-834a9e713fe5/FabLab Robert-Houdin.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/12/47/bf25dd10-1761-4412-b6bc-853429d17dbb/FabLab Robert-Houdin.jpg", "postal_code": "41000", "capabilities": "three_d_printing;cnc_milling", "country_code": "fr", "latitude": 47.5879436, "address_notes": "B\u00e2timent 39D", "email": "fablabs41@gmail.com", "blurb": "The FabLab of Blois", "address_1": "39D All\u00e9e des Pins", "geometry": {"type": "Point", "coordinates": [1.3362879, 47.5879436]}, "country": "France"}, -{"city": "Dijon", "kind_name": "fab_lab", "links": ["https://www.facebook.com/KelleFabriK/", "https://kellefabrik.org/", "https://kellefabrik.wordpress.com/"], "capabilities": "three_d_printing;laser;vinyl_cutting", "url": "https://www.fablabs.io/labs/fablabkellefabrik", "coordinates": [47.3223509, 5.04165], "name": "fablab Kelle FabriK", "phone": "0674711777", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/18/adf22710-81f5-41bf-a430-e3c9522292b2/fablab Kelle FabriK.jpg", "postal_code": "21000", "longitude": 5.04165, "country_code": "fr", "latitude": 47.3223509, "address_1": "2 avenue Junot", "address_notes": "le fablab Kelle FabriK est situ\u00e9 dans l'ancienne gare de Dijon Porte Neuve, au 2 avenue Junot.", "email": "contact@kellefabrik.org", "blurb": "le fablab Kelle FabriK est un fablab associatif, ouvert \u00e0 tous.", "description": "Le Fablab Kelle FabriK est un espace collaboratif dijonnais, permettant la rencontre de personnes et la mise en \u0153uvre de projets personnels ou professionnels, ayant en commun d\u2019allier cr\u00e9ativit\u00e9 et innovation.\r\nLe fablab Kelle FabriK collabore avec des acteurs locaux sur des projets \r\n- d'\u00e9ducation : fablab solidaire avec le soutiend e la Fondation Orange, mission locale, ecole de la deuxi\u00e8me chance, m\u00e9diath\u00e8ques de Chen\u00f4ves, mansart...\r\n- li\u00e9s aux PME : \u00e0 travers des partenariats avec la SNCF, le CEA Valduc, Schneider Electric...\r\n- projet de labs \u00e9tendus sur la m\u00e9tropole dijonnaise avec la ville de Dijon.", "geometry": {"type": "Point", "coordinates": [5.04165, 47.3223509]}, "country": "France"}, -{"city": "Granville", "kind_name": "fab_lab", "links": ["https://twitter.com/Goldorhack50", "https://www.facebook.com/goldorhack", "http://www.goldorhack.org"], "url": "https://www.fablabs.io/labs/goldorhack", "capabilities": "three_d_printing", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/26/21/cbbfae59-b9cd-4cd3-96dc-f0633f1d1c10/Goldorhack.jpg", "postal_code": "50400", "address_1": "466 rue de la parfonterie", "country_code": "fr", "email": "contact@goldorhack.org", "name": "Goldorhack", "geometry": {}, "country": "France"}, -{"city": "Nantes", "kind_name": "fab_lab", "links": ["http://fablab.pingbase.net"], "url": "https://www.fablabs.io/labs/ping", "coordinates": [47.218371, -1.553621], "county": "Pays de la Loire", "postal_code": "44000", "longitude": -1.55362100000002, "country_code": "fr", "latitude": 47.218371, "name": "PiNG", "geometry": {"type": "Point", "coordinates": [-1.553621, 47.218371]}, "country": "France"}, -{"city": "Millau", "description": "Cr\u00e9aLab - MillauLab\r\nCr\u00e9aLab est une association loi 1901 qui g\u00e8re le fablab MillauLab. \r\nCe fablab est est le fruit d'une collaboration entre la Communaut\u00e9 de Communes Millau Grands Causses, la Ville de Millau et l'association Cr\u00e9aLab. \r\nLes locaux se situent au CREA (Centre de Rencontres, d'Echanges et d'Animations) au centre ville de Millau. \r\nParce que le mouvement makers vient de la base, ce fablab est ouvert au plus grand nombre afin de cr\u00e9er, exp\u00e9rimenter, partager, collaborer, \u00e9changer. \r\nNous souhaitons cr\u00e9er, aider \u00e0 la cr\u00e9ation et promouvoir le partage de connaissance tant au niveau local qu'au niveau global. En m\u00eame temps, notre atelier permet la production local de ce qui a pu \u00eatre pens\u00e9 et con\u00e7u au niveau global, notamment dans le r\u00e9seau des fablabs.", "links": ["https://www.millaulab.fr/"], "parent_id": 211, "url": "https://www.fablabs.io/labs/Millaulab", "longitude": 3.07917550000002, "name": "MillauLab", "county": "Aveyron", "phone": "+33565600800", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/31/16/aca64444-99c3-412d-a443-a4d5ef8d0bf8/MillauLab.jpg", "postal_code": "12100", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 44.0984651, "address_1": "10 Boulevard Sadi Carnot", "coordinates": [44.0984651, 3.0791755], "email": "contact@millaulab.fr", "blurb": "Fablab de Millau", "kind_name": "fab_lab", "geometry": {"type": "Point", "coordinates": [3.0791755, 44.0984651]}, "country": "France"}, -{"city": "Saint-Denis", "kind_name": "supernode", "links": ["http://www.pointcarre.info"], "url": "https://www.fablabs.io/labs/pointcarre", "coordinates": [48.9363676, 2.3570765], "name": "Point Carr\u00e9", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/53/8e96819d-40fa-4992-97f1-32c6e9109d65/Point Carr\u00e9.jpg", "postal_code": "93200", "longitude": 2.35707649999995, "country_code": "fr", "latitude": 48.9363676, "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "email": "lacceslab@gmail.com", "blurb": "Le Point Carr\u00e9 is a project of fablab for the bigining of 2015 in Saint-Denis, France. It will be also a coworking place and an store for craftmen.", "address_1": "Saint-Denis", "geometry": {"type": "Point", "coordinates": [2.3570765, 48.9363676]}, "country": "France"}, -{"city": "Rennes", "kind_name": "fab_lab", "links": ["http://labfab.fr"], "url": "https://www.fablabs.io/labs/labfabderennes", "coordinates": [48.1135035, -1.6755769], "county": "Brittany", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/10/26/103efb61-8832-40ca-a165-977dd081778f/labfab de Rennes.jpg", "postal_code": "35000", "longitude": -1.67557690000001, "country_code": "fr", "latitude": 48.1135035, "capabilities": "three_d_printing;cnc_milling;laser;precision_milling;vinyl_cutting", "name": "labfab de Rennes", "geometry": {"type": "Point", "coordinates": [-1.6755769, 48.1135035]}, "country": "France"}, -{"city": "Ferney-Voltaire", "coordinates": [46.2590156, 6.1073924], "kind_name": "fab_lab", "links": ["http://facebook.com/panglosslabs", "http://panglosslabs.org"], "url": "https://www.fablabs.io/labs/panglosslabs1ferneyvoltaire", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "name": "Pangloss Labs #1 - Ferney-Voltaire", "county": "Ain", "phone": "+33-4-50590783", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/24/25/6e1efc3b-2896-4ee3-bc02-9b16c4d9c9de/Pangloss Labs #1 - Ferney-Voltaire.jpg", "postal_code": "01210", "longitude": 6.10739239999998, "country_code": "fr", "latitude": 46.2590156, "address_1": "12bis Rue de Gex", "address_notes": "Head to the back of the building. Look for the FabLab sign.", "email": "contacteznous@panglosslabs.org", "blurb": "Pangloss Labs #1 is an open innovation centre for Grand Geneve. An ecological fablab is one of the tools we will use to kickstart entrepreneurial activities in the region. .com + .org + .edu", "description": "Welcome. Pangloss Labs is now two non-profit associations, one in France and one in Geneva, Switzerland. Both were founded in 2014. It aims to create experimental laboratories, and to prototype activities across various sectors, called Pangloss Labs. The French association is headquartered in Prevessin, France.\r\n\r\nJoin us\r\nWho are we?\r\n\r\nWe are a team of socially minded entrepreneurs from the Greater Geneva area (both Switzerland and France). We have a shared passion for innovation. We came together to co-create products and services, create links, identify synergies and contribute to endogenous growth in the region.\r\n\r\nWhat do we do?\r\n\r\nWe create and we animate third-spaces and eco-fablabs to support the development of local entrepreneurial activities. Our group is non-political, non-religious, non-discriminatory and open to all who love to innovate and co-create together.\r\n\r\nThis place will be a welcoming space, warm, open, practical, multicultural, ideally located in a city center, and connected with other areas of the same type.\r\n\r\nHow can I learn more about Pangloss Labs?\r\n\r\nWe communicate mainly through our Facebook group and we will be happy to send a newsletter with our activities. \r\n\r\n\r\nMachines?\r\n\r\nWe have many more machines than this web site shows including: Printrbot Simple Metal 3D Printer, Filafab filament extruder, 80W Chinese Laser Cutter (1000mmx400mm), OX Openbuilds CNC 1500x1000mm, iTopie Rainbow 3D Printer, Syntratec Laser Sintering Printer, ATLAS 3D Scanner, Fuel 3D Scanner, Pixmap Vinyl Cutter, Lemantek Giant 3D Printer", "geometry": {"type": "Point", "coordinates": [6.1073924, 46.2590156]}, "country": "France"}, -{"city": "Nancy", "kind_name": "fab_lab", "links": ["https://nybi.slack.com/", "http://nybi.cc"], "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "url": "https://www.fablabs.io/labs/nybicc", "coordinates": [48.6936291, 6.1991858], "name": "Nybi.cc", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/17/9c35bc8b-cb09-49b0-9523-a234abe5a5d5/Nybi.cc.jpg", "postal_code": "54000", "longitude": 6.19918580000001, "country_code": "fr", "latitude": 48.6936291, "address_1": "49 boulevard d'Austrasie", "address_notes": "Fablab associatif situ\u00e9 au sein du Lorraine Fab Living Lab de l'universit\u00e9 de Lorraine.", "email": "association@nybi.cc", "blurb": "NancY BIdouille Cr\u00e9ation Construction Makerspace", "description": "NYBI.CC est un espace de cr\u00e9ation et de fabrication \u00e0 Nancy. NYBI.CC vise le partage des connaissances et la mutualisation des moyens de production : au local, chacun est libre d'utiliser les machines pour exp\u00e9rimenter, apprendre, fabriquer. L'association est ouverte \u00e0 tous.", "geometry": {"type": "Point", "coordinates": [6.1991858, 48.6936291]}, "country": "France"}, -{"city": "Marseille", "kind_name": "fab_lab", "links": ["http://reso-nance.org/wiki/projets/machines/accueil", "http://reso-nance.org/lfo", "http://reso-nance.org/wiki"], "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "url": "https://www.fablabs.io/labs/lfo", "name": "Lieu de Fabrication Ouvert", "email": "contact@lfofablab.org", "coordinates": [43.3101074, 5.3898011], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/53/11/d746ef5f-c972-489a-8d14-f64e03d68de8/Lieu de Fabrication Ouvert.jpg", "phone": "33 (0)4 95 04 95 12", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/16/45/92380786-b42a-4299-8ed7-4cfe1feefc2b/Lieu de Fabrication Ouvert.jpg", "postal_code": "13003", "longitude": 5.3898011, "country_code": "fr", "latitude": 43.3101074, "address_1": "31 Rue Jobin", "address_notes": "2nd floor, near Zinc and Radio Grenouille", "address_2": "Friche Belle de Mai", "blurb": "Les acteurs du Lieu de Fabrication Ouvert (LFO) veulent stimuler l\u2019\u00e9mergence d\u2019une communaut\u00e9 apprenante pour produire des objets, des savoirs, des \u0153uvres artistiques, des partages.", "description": "Le Lieu de Fabrication Ouvert (LFO) est un fablab situ\u00e9 \u00e0 la Friche la Belle de Mai \u00e0 Marseille depuis novembre 2013. L'espace est anim\u00e9 par les assocations Zinc, producteurs et diffuseurs de pi\u00e8ces artistiques et Reso-nance num\u00e9rique, collectif d'artistes et formateurs.\r\n\r\nLIEU : L\u2019espace est \u00e9quip\u00e9 d\u2019outils, de machines et de mat\u00e9riels \u00e9lectroniques permettant \u00e0 chacun d\u2019\u00e9changer, d\u2019exp\u00e9rimenter et de prototyper ses id\u00e9es. Il est situ\u00e9 \u00e0 la Friche la Belle de Mai, dans un quartier populaire et artistique, l\u2019objectif \u00e9tant de favoriser l\u2019\u00e9mergence d\u2019une communaut\u00e9 apprenante impliquant ing\u00e9nieurs, artisans, artistes, amateurs, \u00e9tudiants, curieux, \u2026\r\n\r\nFABRICATION : Conscients des enjeux mat\u00e9riels, politiques et \u00e9thiques li\u00e9s aux nouvelles technologies, notre approche se base sur la pratique, en associant anciennes et nouvelles techniques. En faisant par nous-m\u00eames, nous apprenons \u00e0 les d\u00e9cortiquer, les modifier pour questionner les processus qui fa\u00e7onnent les objets que nous consommons et qui nous entourent.\r\n\r\nOUVERT : Le mouvement du libre et de l\u2019open source est \u00e0 l\u2019origine d\u2019outils largement r\u00e9pandus. Nous les utilisons et nous nous inspirons de ces m\u00e9thodes de travail collaboratif pour faire vivre le LFO : auto-apprentissage, apprentissage au sein d\u2019ateliers th\u00e9matiques, accompagnement de projets, production de ressources, contributions au sein d\u2019une plateforme de documentation (wiki), etc.\r\n\r\nNous sommes ouvert les samedis autour d'une th\u00e9matique et sur rendez-vous pour les projets. Nous organisons aussi des rencontres et des festivals \u00e0 Marseille pour \u00e9changer, apprendre et fabriquer \u00e0 plusieurs, notamment en conviant les fablabs et les ateliers de la r\u00e9gion. R\u00e9cemment, nous avons organis\u00e9 le festival Machines (http://reso-nance.org/wiki/projets/machines/accueil) sur la question de l'\u00e9nergie et des techniques Low Tech. Nous portons aussi la proposition de \"Soci\u00e9t\u00e9 des ateliers\"(http://reso-nance.org/wiki/culture/societesdesateliers/accueil) pour stimuler les \u00e9changes en circuits courts entre d'ateliers dans divers domaines.", "geometry": {"type": "Point", "coordinates": [5.3898011, 43.3101074]}, "country": "France"}, -{"city": "La Rochelle", "description": "Fablab anim\u00e9 par une communaut\u00e9 b\u00e9n\u00e9vole. Ouvert tous les jeudis apr\u00e8s-midi \u00e0 partir de 14h\r\nPlus d'infos sur www.rupellab.org", "links": ["http://rupellab.org"], "url": "https://www.fablabs.io/labs/rupellab", "coordinates": [46.1486724, -1.1561185], "name": "Rupellab - fablab La Rochelle", "phone": "0619315125", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/20/27/bddcc574-aa17-4df5-bb57-993dc977bffb/Rupellab - fablab La Rochelle.jpg", "postal_code": "17000", "longitude": -1.15611850000005, "country_code": "fr", "latitude": 46.1486724, "address_1": "17 rue Newton", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "email": "contact@rupellab.org", "blurb": "Le Rupellab - fablab La Rochelle est une association loi 1901 cr\u00e9\u00e9e en juin 2014.", "kind_name": "fab_lab", "geometry": {"type": "Point", "coordinates": [-1.1561185, 46.1486724]}, "country": "France"}, -{"city": "Auray", "kind_name": "fab_lab", "links": ["https://www.lafabriqueduloch.org/"], "parent_id": 179, "url": "https://www.fablabs.io/labs/lafabriqueduloch", "coordinates": [47.6683953, -2.9860108], "name": "la FABrique du Loch", "phone": "00 33 2 97 58 47 04", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/29/38/ac237f67-051e-42ed-b050-d3a2bd9b8fc3/la FABrique du Loch.jpg", "postal_code": "56400", "longitude": -2.98601080000003, "country_code": "fr", "latitude": 47.6683953, "capabilities": "three_d_printing;laser;precision_milling;vinyl_cutting", "email": "lafabriqueduloch@gmail.com", "blurb": "Atelier partag\u00e9, la FABrique du Loch est accessible \u00e0 tous pour presque tout faire : apprendre, inventer, fabriquer, r\u00e9parer. Des outils classiques et num\u00e9riques sont mis \u00e0 disposition des membres.", "address_1": "8 Rue Georges Clemenceau", "geometry": {"type": "Point", "coordinates": [-2.9860108, 47.6683953]}, "country": "France"}, -{"city": "Viroflay", "coordinates": [48.8000736637, 2.16120527361], "kind_name": "fab_lab", "links": ["http://www.meetup.com/fr/FabLab-Versailles/", "https://www.facebook.com/mysunlabface", "https://twitter.com/le_hatlab", "http://mysunlab.org"], "url": "https://www.fablabs.io/labs/sunlab", "name": "Sunlab", "longitude": 2.16120527361068, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/02/50/f05c4fb2-7b56-4906-8ddd-37e2f436638b/Sunlab.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/26/00/a70246c0-7796-4c60-ba65-4afb420cb704/Sunlab.jpg", "postal_code": "78220", "capabilities": "three_d_printing", "country_code": "fr", "latitude": 48.8000736637036, "address_1": "185 Avenue du G\u00e9n\u00e9ral Leclerc", "address_notes": "Si la porte de devant est ferm\u00e9e, faites le tour par la droite pour entrer par la cour arri\u00e8re.\r\n(horaires sur le site)", "blurb": "Fablab associatif \u00e0 Versailles Grand Parc. Pour curieux, entrepreuneurs, artistes... Vous \u00eates le bienvenue!", "description": "Fablab construit autour de l'envie de cr\u00e9er/construire/inventer/partager.", "geometry": {"type": "Point", "coordinates": [2.16120527361, 48.8000736637]}, "country": "France"}, -{"capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "city": "Albi", "kind_name": "fab_lab", "links": ["https://www.instagram.com/fablab.albi/", "https://twitter.com/FabLabAlbi", "https://www.facebook.com/fablab.albi", "http://numerique-albi.fr"], "parent_id": 21, "url": "https://www.fablabs.io/labs/albilab", "name": "Albilab", "coordinates": [43.920103, 2.181445], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/50/14/d852ff57-8da2-4870-be6a-71d309f44955/Albilab.jpg", "phone": "0652894502", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/30/02/3f2a5673-670a-4264-8acd-e433dd8bcc4a/Albilab.jpg", "postal_code": "81000", "longitude": 2.18144499999994, "country_code": "fr", "latitude": 43.920103, "address_1": "8 Rue Pierre gilles de Gennes", "address_notes": "Pour acc\u00e9der au FabLab, il faut se diriger vers le parking d'Innoprod, derri\u00e8re le b\u00e2timent. Albilab se situe au fond.", "email": "asso.acne@gmail.com", "blurb": "Albilab est un espace d\u00e9di\u00e9 \u00e0 la cr\u00e9ation, la rencontre, le partage et l'apprentissage \u00e0 travers les nouvelles technologies. Il a pour ambition de permettre \u00e0 tous l'acc\u00e8s \u00e0 des outils num\u00e9riques.", "description": "Suite \u00e0 sa cr\u00e9ation en mars 2015, l'association pour la culture num\u00e9rique et l'environnement s'est donn\u00e9 pour mission premi\u00e8re la cr\u00e9ation d'un lieu ouvert et accessible \u00e0 tous qui permet de faire un lien entre le num\u00e9rique et les nouvelles technologies et la rencontre et l'apprentissage. Au fil de ses rencontres avec des FabLabs comme Artilect \u00e0 Toulouse, ainsi que lors de son implication au FabLab Festival, l'association a eu la volont\u00e9 de monter un FabLab dans la ville d'Albi. Les mois qui ont suivi ont privil\u00e9gi\u00e9 la rencontre avec de nombreuses personnes int\u00e9ress\u00e9es qui se sont impliqu\u00e9es au fur et \u00e0 mesure et ont constitu\u00e9 la premi\u00e8re communaut\u00e9 du futur FabLab.\r\n\r\nApr\u00e8s plusieurs mois de travail et de r\u00e9flexion, de soutiens comme celui de la Fondation Orange et de participation \u00e0 de nombreux \u00e9v\u00e9nements, l'association a pu installer le lab au sein du parc technopolitain de la ville d'Albi en octobre 2015, avec le soutien des \u00e9lus locaux. C'est dans un petit local d'une quarantaine de m\u00e8tres carr\u00e9s que la communaut\u00e9 du FabLab a commenc\u00e9 \u00e0 s'\u00e9quiper d'outils, \u00e0 accueillir le public de divers horizons et \u00e0 diffuser l'esprit du lieu sur le territoire. \u00c0 la suite de son installation, ACNE a lanc\u00e9 une campagne de financement participatif avec pour objectif d'acqu\u00e9rir une d\u00e9coupeuse laser et indirectement de donner une assise au projet. Le FabLab, lors de l'assembl\u00e9e g\u00e9n\u00e9rale d'ACNE en mars 2016, a \u00e9t\u00e9 renomm\u00e9 Albilab.\r\n\r\nAinsi, depuis son installation, l'association compte plus de 170 membres qui ont acc\u00e8s aux machines du FabLab et sont form\u00e9s gratuitement pour pouvoir les utiliser. Plusieurs projets ont vu le jour et sont en r\u00e9alisation au FabLab. Par exemple, nous accueillons depuis fin octobre un projet d'orth\u00e8se, d'une personne n'ayant pas r\u00e9ussi \u00e0 trouver un mod\u00e8le qui lui convienne depuis son accident il y a une quinzaine d'ann\u00e9e. Ce projet a r\u00e9uni une dizaine de personnes qui se regroupent au FabLab toutes les deux semaines pour travailler ensemble. L'\u00e9quipe inclut des personnes venant d'horizons diff\u00e9rents, allant du g\u00e9nie m\u00e9canique \u00e0 l'ergoth\u00e9rapeute, en passant par le retrait\u00e9 passionn\u00e9 de nouvelles technologies. Ils re\u00e7oivent le soutien et les conseils des membres actifs du lieu. Depuis le mois d'avril un premier prototype a vu le jour. D'autres projets sont r\u00e9alis\u00e9s au FabLab, comme un projet de d\u00e9co/architecture, un projet autour des jeux vid\u00e9os, un projet drones, etc. Chaque projet s'organise comme il le souhaite en fonction de son \u00e9quipe et des disponibilit\u00e9s du FabLab.\r\n\r\nAvec l'aide de partenaires comme la Fondation Orange et l'association technopolitaine Albi-Innoprod, ainsi que gr\u00e2ce \u00e0 la r\u00e9ussite de sa campagne de financement participatif, le FabLab a pu s'\u00e9quiper de diff\u00e9rents outils. Actuellement, les outils disponibles sont :\r\n- trois imprimantes 3D\r\n- une fraiseuse num\u00e9rique\r\n- une d\u00e9coupeuse laser\r\n- une d\u00e9coupeuse vinyle\r\n- des cartes Arduino\r\n- des cartes Thingz\r\n- un scanner 3D\r\n- une extrudeuse\r\n- des outils divers apport\u00e9s par la communaut\u00e9 : machine \u00e0 coudre, outils de bricolage, perceuse \u00e0 colonne...\r\n\r\nAfin de pouvoir rendre le projet p\u00e9renne et \u00e9galitaire, l'association a mis en place un mode de fonctionnement, en prenant exemple dans les autres FabLabs. Des cartes pr\u00e9pay\u00e9es ont \u00e9t\u00e9 cr\u00e9es afin de faciliter l'utilisation des outils (\"cartes spots\"). Seuls le mat\u00e9riau et l'utilisation des principales machines \u00e0 commande num\u00e9rique (imprimante 3D, d\u00e9coupeuse laser, fraiseuse num\u00e9rique, d\u00e9coupeuse vinyle) sont payants. Le FabLab accueille tout type de projets dans le but de ne pas restreindre l'acc\u00e8s au lieu et les possibilit\u00e9s de cr\u00e9ation.\r\nL'association organise une fois par mois des initiations sur les outils. Elle a \u00e9galement une programmation destin\u00e9e \u00e0 permettre \u00e0 plus de monde de s'approprier le lieu : ateliers DIY (bijoux, cuisine, robotique, produits m\u00e9nagers, etc.), soir\u00e9es projets, \u00e9v\u00e9nements ext\u00e9rieurs... Elle travaille, \u00e0 travers ses actions, sur la rencontre et le travail en r\u00e9seau avec les acteurs locaux.", "geometry": {"type": "Point", "coordinates": [2.181445, 43.920103]}, "country": "France"}, -{"capabilities": "three_d_printing;cnc_milling;vinyl_cutting", "city": "Val-de-Reuil", "kind_name": "fab_lab", "links": ["http://www.wiki.fablab276.org", "http://www.fablab276.org", "https://www.facebook.com/FabLab-276-440132109521107/", "http://www.paris-normandie.fr/actualites/politique/impression-3d-et-tarifs-municipaux-au-menu-du-conseil-municipal-de-val-de-reuil-YB4726463#.V808lPmLTGg", "http://www.paris-normandie.fr/loisirs/val-de-reuil--grace-a-fablab276-creez-vos-objets-en-3d-AC5150398#.V808jfmLTGg", "http://www.paris-normandie.fr/breves/normandie/a-val-de-reuil-fablab-276-cree-en-trois-dimensions-GK5149866#.V808jfmLTGg"], "parent_id": 339, "url": "https://www.fablabs.io/labs/fablab276valdereuil", "name": "FabLab 276 Val-de-Reuil", "coordinates": [49.2741976, 1.2120202], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/52/27/da499236-73c9-46a4-9f22-6092a90aa0fa/FabLab 276 Val-de-Reuil.jpg", "county": "Normandie", "phone": "+33 (0)9 83 55 96 05", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/31/59/3f4340dd-a0cf-48f4-8f32-421e95043020/FabLab 276 Val-de-Reuil.jpg", "postal_code": "27100", "longitude": 1.21202019999998, "country_code": "fr", "latitude": 49.2741976, "address_1": "Voie de la Palestre", "address_notes": "L'entr\u00e9e du FabLab est situ\u00e9e sous l'escalier ext\u00e9rieur.", "email": "contact@fablab276.org", "blurb": "Atelier num\u00e9rique ouvert \u00e0 tous, les samedis de 10h00 \u00e0 18h00.", "description": "FabLab associatif install\u00e9 en F\u00e9vrier 2016 dans la commune de Val-de-Reuil.\r\n\r\nNous sommes actuellement \u00e9quip\u00e9s de trois imprimantes 3D FDM, une d\u00e9coupeuse vinyle, des stations de conception, scanners 3D, fraiseuse num\u00e9rique CharlyRobot (en cours de restauration), paillasses d'\u00e9lectronique, tables communes et supports pour formations et conf\u00e9rences.\r\n\r\nNous travaillons beaucoup autour de la conception et l'impression 3D", "geometry": {"type": "Point", "coordinates": [1.2120202, 49.2741976]}, "country": "France"}, -{"city": "toulouse", "kind_name": "fab_lab", "links": ["http://campusfab.univ-tlse3.fr"], "url": "https://www.fablabs.io/labs/campusfab", "name": "CampusFab Universit\u00e9 Toulouse 3 - Toulouse - Midi-Pyr\u00e9n\u00e9es - France", "longitude": 1.46737247990109, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/03/16/15/04/55/8be2bd91-2093-4646-a7b3-7ebcc6838d70/Exterieur.jpg", "email": "fablab@univ-tlse3.fr", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/03/16/15/04/55/77e50d2f-f213-457e-b8b7-276601d67766/vignette_campusfab.jpg", "postal_code": "31062", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "country_code": "fr", "latitude": 43.5618226019239, "address_1": "118 route de Narbonne", "coordinates": [43.5618226019, 1.4673724799], "address_2": "b\u00e2timent U4", "blurb": "FabLab universitaire, destin\u00e9 aux \u00e9tudiants et personnels de l'universit\u00e9 de Toulouse.", "description": "CampusFab is the local antenna of the fabLab of the University of Toulouse 3. This FabLab is opened to students and to the staff of the federal university of Toulouse. Its aim is to support development of student's projets, and pedagogic evolutions in teaching of sciences. Researchers are also welcome to prototype new devices and to share their experience with students and others staff members.", "geometry": {"type": "Point", "coordinates": [1.4673724799, 43.5618226019]}, "country": "France"}, -{"city": "Aix-en-Provence", "description": "Le Fab Lab Provence est une action commune de deux acteurs locaux fortement impliqu\u00e9s dans la communaut\u00e9 du DIY/DIT: Design the Future Now et le Laboratoire d'Aix-p\u00e9rimentation et de Bidouille. Cette action a pour objectif principal de faire \u00e9merger un Fab Lab de taille assez importante pour rayonner sur toute la Provence. Ce rayonnement sera bas\u00e9 sur une strat\u00e9gie de diss\u00e9mination de la fabrication num\u00e9rique au plus pr\u00e8s des citoyens.", "links": ["http://fablab-provence.com/"], "url": "https://www.fablabs.io/labs/fablabprovence", "coordinates": [43.293476, 5.389933], "name": "Fab Lab Provence", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/47/85ee09b9-8e27-42d4-a4c5-23986a5d3119/Fab Lab Provence.jpg", "longitude": 5.38993300000004, "country_code": "fr", "latitude": 43.293476, "address_1": "Aix-en-Provence", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "email": "contact@fablab-provence.com", "blurb": "Agents pollinisateurs de l\u2019open innovation et du DIY/DIT", "kind_name": "fab_lab", "geometry": {"type": "Point", "coordinates": [5.389933, 43.293476]}, "country": "France"}, -{"city": "Biarne", "coordinates": [47.1459073117, 5.45618141164], "kind_name": "fab_lab", "links": ["http://www.fablab-net-iki.org "], "url": "https://www.fablabs.io/labs/fablabnetiki", "name": "FabLab Net-IKi", "longitude": 5.45618141163936, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/48/52/63958125-ff96-492b-85e7-c98f862c135c/FabLab Net-IKi.jpg", "county": "Franche-Comt\u00e9", "phone": "33660324386", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/12/44/4fab0087-d70e-441a-ac9b-d6ede144748d/FabLab Net-IKi.jpg", "postal_code": "39290", "capabilities": "three_d_printing;circuit_production", "country_code": "fr", "latitude": 47.1459073117437, "address_1": "3 Rue de l'\u00c9glise", "address_notes": "Parking gratuit derri\u00e8re le FabLab \r\n5 km de Dole (Gare TGV)", "email": "fablab.netiki@gmail.com", "blurb": "FabLab rural et cotois en France - dans le Jura depuis 2012 - association Net-IKi - village de 350 habitants", "description": "1er FabLab rural fran\u00e7ais depuis juin 2012. A l'origine des FabLabs Comtois (r\u00e9gion Franche-Comt\u00e9) en France. \r\nFabLab intervillage, accessible \u00e0 tous via l'association Net-IKi (\"l'Internet de chez nous depuis 2009).\r\n\r\nFabLab int\u00e9gr\u00e9 dans la r\u00e9gion : Universit\u00e9, Lyc\u00e9es, Coll\u00e8ges, p\u00f4les de comp\u00e9titivit\u00e9 (microtechniques, platipolis)... \r\n\r\nEssaimage : FabLab Champagnole et d'autres projets en Bourgogne, Franche-Comt\u00e9....", "geometry": {"type": "Point", "coordinates": [5.45618141164, 47.1459073117]}, "country": "France"}, -{"city": "Charleville-M\u00e9zi\u00e8res", "kind_name": "fab_lab", "links": ["http://fablab.ifts.net/"], "url": "https://www.fablabs.io/labs/smartmaterials", "coordinates": [49.7397084, 4.7178623], "name": "Smart Materials", "phone": "03.24.59.64.93", "postal_code": "08000", "longitude": 4.71786229999998, "country_code": "fr", "latitude": 49.7397084, "capabilities": "three_d_printing", "email": "fablab@ifts.net", "address_1": "Boulevard Jean Delautre", "geometry": {"type": "Point", "coordinates": [4.7178623, 49.7397084]}, "country": "France"}, -{"city": "Ajaccio", "description": "In the center of the City of Ajaccio in Corsica, we have 360 square meters of space. We provide workshops for 2D and 3D digital design, digital fabrication and robotic programs for youth.", "links": ["https://instagram.com/fablabajaccio/", "https://twitter.com/fablabajaccio", "https://www.facebook.com/pages/Fab-Lab-Ajaccio/682469011898431"], "parent_id": 16, "url": "https://www.fablabs.io/labs/fablabajaccio", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "name": "Fab Lab Ajaccio", "county": "Corsica", "phone": "04.95.52.33.37 ou 09 67 52 37 50", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/25/12/643b243d-10c3-407a-b193-11428e3b8514/Fab Lab Ajaccio.jpg", "postal_code": "20193", "address_1": "1 Avenue Napoleon 3", "country_code": "fr", "email": "info@fablabajaccio.com", "blurb": "Fab Lab Ajaccio provides digital fabrication, machine access, workshops and project collaboration.", "kind_name": "fab_lab", "geometry": {}, "country": "France"}, -{"city": "Champs-sur-Marne", "kind_name": "fab_lab", "links": ["https://www.facebook.com/Fablabdescartes/", "https://twitter.com/FablabDescartes", "http://www.fablab-descartes.com"], "url": "https://www.fablabs.io/labs/fablabdescartes", "name": "Fablab Descartes", "longitude": 2.58977100000004, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/03/35/d8861584-e4cb-4c5d-a4b0-81972b338450/Fablab Descartes.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/26/59/afd2b5e8-f388-4a05-ba7e-53983fbd68ec/Fablab Descartes.jpg", "postal_code": "77420", "coordinates": [48.8380105, 2.589771], "country_code": "fr", "latitude": 48.8380105, "address_1": "23 Rue Alfred Nobel", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "email": "contact@fablab-descartes.com", "blurb": "La Fablab Descartes est directement li\u00e9 \u00e0 l'Incubateur d'entreprise. Au-del\u00e0 de ses ressources propres le Fablab s'est forg\u00e9 un r\u00e9seau de partenaires au sein de la cit\u00e9 universitaire Descartes.", "description": "La Fablab Descartes travaille sur 4 volets principaux que sont : l'Innovation Technique en acc\u00e9l\u00e9rant le passage de l'id\u00e9e au prototype et en cr\u00e9ant une atmosph\u00e8re propice \u00e0 la cr\u00e9ativit\u00e9, la Formation pour stimuler la mont\u00e9e en comp\u00e9tence des personnes, le D\u00e9veloppement Economique en soutenant les startups en synergie avec l'Incubateur Descartes et enfin, le volet Social en favorisant l'insertion par la cr\u00e9ation d'un lien social et par la sensibilisation du public aux technologies de fabrication num\u00e9rique.", "geometry": {"type": "Point", "coordinates": [2.589771, 48.8380105]}, "country": "France"}, -{"city": "Saint-Loup-sur-Semouse", "kind_name": "fab_lab", "links": ["http://labhautcomtois.fr/"], "url": "https://www.fablabs.io/labs/labhautcomtois", "name": "LAB' HAUT COMTOIS", "longitude": 6.27228730000002, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/49/11/ebfff7c5-47b0-4d88-8a58-b80e1153cb31/LAB' HAUT COMTOIS.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/29/10/4ef944be-579c-4bcb-bd02-f4467735e449/LAB' HAUT COMTOIS.jpg", "postal_code": "70800", "coordinates": [47.8861736, 6.2722873], "country_code": "fr", "latitude": 47.8861736, "address_1": "3 Rue de l'Abattoir", "capabilities": "three_d_printing;circuit_production;laser;vinyl_cutting", "email": "labhautcomtois@gmail.com", "blurb": "Le LAB' HAUT COMTOIS est un lieu innovant, ouvert \u00e0 tous, qui regroupe un espace COWORKING, et un FAB LAB.", "description": "Situ\u00e9 dans le d\u00e9partement de la Haut-Sa\u00f4ne (70), \u00e0 Saint-Loup-Sur-Semouse, le LAB' HAUT COMTOIS, \u00e0 \u00e9t\u00e9 cr\u00e9\u00e9 \u00e0 l'initiative de la Communaut\u00e9 de Communes de la Haute Comt\u00e9.\r\n\r\n\u00c9tabli comme association en avril 2016, ce lui innovant rassemble toutes les valeurs, et moyens de partage, pour cr\u00e9er, innover, r\u00e9parer, \u00e9changer, d\u00e9velopper, collaborer, imaginer et entreprendre.\r\n\r\nDans cet esprit, vous y trouverez non seulement un Fab Lab, mais aussi un espace de Coworking, destin\u00e9s tous deux \u00e0 ouvrir le champ des possibles \u00e0 tous.\r\n\r\nToute l'\u00e9quipe sera heureuse de vous rencontrer et de s'agrandir, alors n'attendez plus ! Partagez, aimez, visitez...", "geometry": {"type": "Point", "coordinates": [6.2722873, 47.8861736]}, "country": "France"}, -{"city": "Paris", "kind_name": "fab_lab", "links": ["http://www.villettemakerz.com"], "url": "https://www.fablabs.io/labs/villettemakerzbywoma", "coordinates": [48.8905882, 2.3917407], "name": "Villette Makerz by woma", "county": "France", "parent_id": 587, "postal_code": "75019", "longitude": 2.39174070000001, "address_2": "Folie L5 parc de la villette", "latitude": 48.8905882, "address_1": "211 Avenue Jean Jaur\u00e8s", "country_code": "fr", "capabilities": "three_d_printing;circuit_production;laser;vinyl_cutting", "email": "hello@villettemakerz.com", "blurb": "Impuls\u00e9 par WoMa, fabrique de quartier, et soutenu par la Ville de Paris et l\u2019Etablissement Public du Parc et de la Grande Halle de la Villette (EPPGHV), VILLETTE MAKERZ est un tiers-lieu pour relier", "description": "ESPACE CR\u00c9ATIF POUR TOUS \r\nCe nouvel espace s\u2019adresse \u00e0 tous ceux (jeune public, adulte, entrepreneur, entreprise, etc.) qui veulent d\u00e9couvrir et exp\u00e9rimenter les technologies de la cr\u00e9ation contemporaine telles que : le design, la 3D, le code, l\u2019\u00e9lectronique, l\u2019audiovisuel, l'internet des objets, etc. \r\n\r\nEXP\u00c9RIMENTER ET TRANSMETTRE \r\nLaboratoire collaboratif de conception et de fabrication - Fablab - dot\u00e9 d\u2019une boutique, VILLETTE MAKERZ est un espace de travail, d'exp\u00e9rimentation et de diffusion pour les co-makers et co-workers. Devenez autonome en vous formant sur des machines professionnelles, concr\u00e9tisez votre projet entrepreneurial entour\u00e9 par une \u00e9quipe d\u2019experts, \u00e9changez et partagez avec d\u2019autres pour enrichir vos connaissances, fabriquez et diffusez localement. \r\n\r\nD\u00c9COUVRIR ET SOUTENIR \r\nDans ce lieu-outil en perp\u00e9tuel mouvement, exp\u00e9rimentez le Do It Yourself (\u2018Fais-le toi-m\u00eame\u2019) inspirez-vous des cr\u00e9ations locales & d\u00e9couvrez les technologies d\u2019un Fablab gr\u00e2ce \u00e0 l\u2019\u00c9cole des Makerz, accessible d\u00e8s l\u2019\u00e2ge de 6 ans. La programmation culturelle de l\u2019espace, en \u00e9cho avec le Parc de La Villette, valorise l\u2019expertise de la communaut\u00e9 maker gr\u00e2ce \u00e0 la mise en place \u2018d\u2019ateliers d\u00e9couverte\u2019 gratuits ou \u00e0 prix libre les week-ends, pour soutenir les makers et leurs initiatives. L\u2019\u00e9quipe VILLETTE MAKERZ propose aussi des services Fablab : du prototypage \u00e0 la fabrication sur-mesure, d\u2019animations \u00e9v\u00e8nementielles \u00e0 l\u2019accompagnement \u00e0 l\u2019innovation.", "geometry": {"type": "Point", "coordinates": [2.3917407, 48.8905882]}, "country": "France"}, -{"city": "Antibes", "kind_name": "fab_lab", "links": ["http://navlab.avitys.com/projets", "http://navlab.avitys.com/boutique", "https://www.youtube.com/playlist?list=PLxk8EtJl46CluaL47zJRbjvDHkp11_YL1", "http://imaginationforpeople.org/fr/project/le-navlab-un-fablab-nautique-a-antibes/", "http://www.viadeo.com/v/company/navlab", "http://www.linkedin.com/company/navlab?trk=company_name", "https://twitter.com/NavLabAntibes", "https://www.facebook.com/navlab?ref=hl", "https://www.facebook.com/pages/Navlab-English/455206721245993?ref=hl", "http://navlab.avitys.com"], "capabilities": "three_d_printing;cnc_milling;vinyl_cutting", "url": "https://www.fablabs.io/labs/navlab", "name": "Antibes NavLab", "email": "fablab@navlab.fr", "coordinates": [43.5794445, 7.1206983], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/48/48/770cfbbd-4b39-4338-bdd1-ac04f8f6858d/Antibes NavLab.jpg", "county": "Provence-Alpes-C\u00f4te d'Azur", "phone": "33972472768", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/12/41/026a85f9-e7f8-48ab-84e9-1c4f86b36dc5/Antibes NavLab.jpg", "postal_code": "06600", "longitude": 7.12069829999996, "country_code": "fr", "latitude": 43.5794445, "address_1": "Antibes", "address_notes": "First floor (above the flower shop !)", "address_2": "3 Boulevard Wilson", "blurb": "The NavLab is a FabLab specialized in maritime projects and luxury yacht electronics maintenance. We are now open... come for a visit!", "description": "The NavLab is a digital manufacturing community workshop specialized in maritime projects. It is a mix between a FabLab, a co-working open space and a nautical laboratory.\r\n\r\nThere you will be able to use 3D printers, vinyle cutters and CNC milling machines, among other cool equipment to work on your projects, using the dedicated openSpace or individual workshops depending on your needs.\r\n\r\nLike other FabLabs these \u00ab fabrication laboratories \u00bb, it aims to provide a place for meeting and sharing knowledge about digital manufacturing technologies, while providing workspace and tools. It is open to all, to experiment, learn, build together and share each other skills.\r\n\r\nWhatever your level of technology and your project are, you can come to the NavLab to \u00ab learn by making \u00bb, using tools such as 3D printers, digital milling machines, vinyle cutting machines and other computer controlled tools, working with wood, fabrics, plastics, paper and even metal.\r\n\r\nWe are open since July 2014. You're welcome to pass by and have a look :)", "geometry": {"type": "Point", "coordinates": [7.1206983, 43.5794445]}, "country": "France"}, -{"city": "Soustons", "kind_name": "fab_lab", "links": ["http://letabli.net"], "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "url": "https://www.fablabs.io/labs/letabli", "coordinates": [43.7486121, -1.3254706], "name": "L'ETABLI", "county": "Aquitaine/Landes", "phone": "+33558412366 - +33637 20 21 53", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/25/5a9b0639-9b3f-4a90-93be-8d76d29cb4b7/L'ETABLI.jpg", "postal_code": "40140", "longitude": -1.32547060000002, "address_2": "rue de Moscou", "latitude": 43.7486121, "address_1": "Pole Associatif R\u00e9sano-Lap\u00e8gue", "country_code": "fr", "address_notes": "2\u00e8me \u00e9tage du p\u00f4le associatif.", "email": "contact@letabli.net mathias@letabli.net", "blurb": "Projet de Lab sur 4 axes : \u00e9ducation, jeunes, professionnels, artistes Connexion Entreprises/Universit\u00e9s sur l'identification de projets de R&D", "description": "Projet port\u00e9 par l'Universit\u00e9 du Temps Libre Landes C\u00f4te sud, appuy\u00e9 par la communaut\u00e9 de communes MACS et la Ville de Soustons. Accompagnement d'un projet d'un groupe de jeunes autour de l'impression 3D depuis f\u00e9vrier 2014. Initiation \u00e0 la programmation de robots en milieu scolaire (TAP : Temps d'Accueil P\u00e9riscolaire). Actions de sensibilisations tous publics depuis mars 2015 (Concept Fab Lab, impression 3D, scanner, CAO, plotter). Engag\u00e9 dans la mise en place d'un r\u00e9seau aquitain des Fab Labs.Mise \u00e0 disposition d'un local de 120 m2 par la Ville de Soustons,en cours d'am\u00e9nagement. Mise en service pleinement op\u00e9rationnel pour mi 2016.", "geometry": {"type": "Point", "coordinates": [-1.3254706, 43.7486121]}, "country": "France"}, -{"city": "Champagnole", "kind_name": "fab_lab", "links": ["https://www.facebook.com/FabLabChampagnole", "http://www.netvibes.com/fablabchampagnole "], "capabilities": "three_d_printing", "url": "https://www.fablabs.io/labs/fablabchampagnole", "name": "FabLab CHAMPAGNOLE", "email": "fablabchampagnole@gmail.com", "coordinates": [46.7427149, 5.9229598], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/52/16/f5fc1bd6-e971-4361-8b93-7741866dacd3/FabLab CHAMPAGNOLE.jpg", "county": "Jura/Franche-Comt\u00e9/FRANCE", "parent_id": 80, "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/44/9a8a5143-3be4-4318-816e-45ecb202536a/FabLab CHAMPAGNOLE.jpg", "postal_code": "39300", "longitude": 5.92295979999994, "country_code": "fr", "latitude": 46.7427149, "address_1": "Lyc\u00e9e Paul Emile Victor de CHAMPAGNOLE 625 Rue de Gottmadingen", "address_notes": "Salle 237 du Lyc\u00e9e", "address_2": "8 rue Marandet Le Pasquier 39300", "blurb": "2i\u00e8me FabLab COMTOIS, sp\u00e9cialis\u00e9 dans le liens social, install\u00e9 au lyc\u00e9e Paul-Emile VICTOR de Champagnole dans le jura (39) (Franche-Comt\u00e9/FRANCE/EUROPE/Terre/VoieLact\u00e9e) REUSSIR = PRATIQUE", "description": "2i\u00e8me FabLab COMTOIS, sp\u00e9cialis\u00e9 dans le liens social, install\u00e9 au lyc\u00e9e Paul-Emile VICTOR de Champagnole", "geometry": {"type": "Point", "coordinates": [5.9229598, 46.7427149]}, "country": "France"}, -{"city": "B\u00e9ziers", "address_notes": "Une fois entr\u00e9 dans l'IUT, prenez l'escalier m\u00e9talique, au 1er \u00e9tage, prenez le couloir et un autre escalier sur votre droite, montez au 2\u00e8me \u00e9tage, tournez \u00e0 gauche dans le couloir en sortant de l'escalier, marchez jusqu'au bout et l\u00e0, ce sera la premi\u00e8re porte \u00e0 droite.", "kind_name": "fab_lab", "links": ["http://fablab.web-5.org"], "url": "https://www.fablabs.io/labs/fablabweb5", "capabilities": "three_d_printing;circuit_production", "name": "Fablab Web-5", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/51/ab5c4861-a2dd-4b9d-bda9-1336c6668946/Fablab Web-5.jpg", "postal_code": "34500", "longitude": 3.22226095651854, "address_2": "IUT de B\u00e9ziers", "latitude": 43.3464805836195, "country_code": "fr", "coordinates": [43.3464805836, 3.22226095652], "email": "mailto:fablab@web-5.org", "address_1": "3, place du 14 juillet", "geometry": {"type": "Point", "coordinates": [3.22226095652, 43.3464805836]}, "country": "France"}, -{"city": "Lagardelle-sur-L\u00e8ze", "kind_name": "supernode", "links": ["http://facebook.fr/infoaleze"], "url": "https://www.fablabs.io/labs/infoleze", "name": "Info@Leze", "longitude": 1.39028161006468, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/45/24/51c5b94a-6a4c-4f82-8778-d555cdd240cb/Info@Leze.jpg", "phone": "05 34 47 54 95", "postal_code": "31870", "coordinates": [43.4130813818, 1.39028161006], "country_code": "fr", "latitude": 43.4130813818105, "address_1": "7 Chemin Neuf", "email": "infoaleze@gmail.com", "blurb": "Tout pour bidouiller, d\u00e9couvrir et faire en \u00e9lectronique et informatique", "description": "Nouveau petit FabLab !\r\nLe FabLab dispose d'\u00e9quipement pour la r\u00e9alisation de projet \u00e9lectronique et informatique. Nous disposons \u00e9galement d'Internet, d'un r\u00e9seau local et de laboratoire.\r\nActuellement les Th\u00e9matiques : Apprendre la programmation avec Python, R\u00e9paration de son ordinateur, Recyclage des \u00e9quipement num\u00e9rique. Nous disposerons \u00e0 court terme d'une imprimante 3d", "geometry": {"type": "Point", "coordinates": [1.39028161006, 43.4130813818]}, "country": "France"}, -{"city": "Beauvais", "description": "L'Atelier FAB LAB PEDAGO \u00ab un lieu, une semaine, un groupe\u2026 pour produire ensemble \u00bb.\r\nLes enseignants peuvent se former en apportant leurs projet et leurs exp\u00e9riences au sein d\u2019ateliers de production : blog p\u00e9dagogique, journal scolaire num\u00e9rique, tablettes, web radio scolaire, r\u00e9seaux sociaux, cartes mentales, etc", "links": ["http://crdp.ac-amiens.fr/cddpoise/blog_mediatheque/?p=14304"], "url": "https://www.fablabs.io/labs/fablabpedago", "longitude": 2.07796440000004, "name": "FAB LAB PEDAGO", "county": "Oise", "phone": "0344063118", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/19/25/183ccf12-8a06-436d-af19-0da3eb98fe92/FAB LAB PEDAGO.jpg", "postal_code": "60000", "coordinates": [49.4360212, 2.0779644], "address_2": "22, avenue victor-hugo", "latitude": 49.4360212, "address_1": "22 Avenue Victor Hugo", "country_code": "fr", "email": "cddp.oise@ac-amiens.fr", "blurb": "L'Atelier FAB LAB PEDAGO \u00ab un lieu, une semaine, un groupe\u2026 pour produire ensemble \u00bb.", "kind_name": "fab_lab", "geometry": {"type": "Point", "coordinates": [2.0779644, 49.4360212]}, "country": "France"}, -{"city": "Cintegabelle", "kind_name": "fab_lab", "links": ["https://plus.google.com/+Fablab-sud31Fr", "https://www.facebook.com/FabLabSud31", "https://twitter.com/FabLabSud31", "http://www.fablab-sud31.fr/"], "url": "https://www.fablabs.io/labs/fablabsud31", "name": "Fab Lab Sud31-Val d'Ari\u00e8ge", "longitude": 1.53192119999994, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/57/39/748305e4-90de-4237-8e5e-d2f5e818daa9/Fab Lab Sud31-Val d'Ari\u00e8ge.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/21/08/3fa301ab-c31f-464f-a19f-d857772603bf/Fab Lab Sud31-Val d'Ari\u00e8ge.jpg", "postal_code": "31550", "coordinates": [43.313836, 1.5319212], "country_code": "fr", "latitude": 43.313836, "address_1": "10 rue de la R\u00e9publique", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "email": "contact@fablab-sud31.fr", "blurb": "Fab Lab Sud31-Val d'Ari\u00e8ge est un Fab Lab rural sur la r\u00e9gion de Cintegabelle, Auterive et la vall\u00e9e de l'Ari\u00e8ge.", "description": "L'association Fab Lab Sud31-Val d'Ari\u00e8ge a pour objet de contribuer au d\u00e9veloppement du savoir et de la culture pour tous par l'acc\u00e8s aux moyens de fabrication (num\u00e9rique et classique).\r\n\r\nL'association veut permettre et faciliter la d\u00e9couverte, l'innovation, et le partage de connaissances par la pratique, et au travers de la collaboration de chacun, dans un esprit de transversalit\u00e9 et de respect de l'environnement.\r\n\r\nL'association poursuit un but non lucratif.", "geometry": {"type": "Point", "coordinates": [1.5319212, 43.313836]}, "country": "France"}, -{"city": "Toulouse", "kind_name": "mini_fab_lab", "links": ["http://www.facebook.com/fabric.insa", "http://www.fabric-insa.fr"], "url": "https://www.fablabs.io/labs/fabricinsa", "name": "Fabric'INSA", "longitude": 1.46888639999997, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/00/05/7d90fb58-ead9-4a9c-a2b7-e9a3832b0e2d/Fabric'INSA.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/23/37/dddef457-3869-40dd-b482-45364fbcd9c2/Fabric'INSA.jpg", "postal_code": "31400", "coordinates": [43.57194, 1.4688864], "country_code": "fr", "latitude": 43.57194, "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;vinyl_cutting", "email": "contact@fabric-insa.fr", "blurb": "FabLab de l'INSA Toulouse", "address_1": "135 Avenue de Rangueil", "geometry": {"type": "Point", "coordinates": [1.4688864, 43.57194]}, "country": "France"}, -{"city": "Castres", "kind_name": "fab_lab", "links": ["https://twitter.com/InnoFabCastres", "https://www.facebook.com/innofabcastres", "http://www.innofab.fr/"], "url": "https://www.fablabs.io/labs/innofab", "name": "INNOFAB", "longitude": 2.26158669999995, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/04/09/0253af6d-53fa-460a-89fa-8149a1b3a69c/INNOFAB.jpg", "parent_id": 21, "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/27/28/497925e2-7d00-4f5f-ba3f-ef89fc71221d/INNOFAB.jpg", "postal_code": "81100", "capabilities": "three_d_printing", "country_code": "fr", "latitude": 43.6220449, "coordinates": [43.6220449, 2.2615867], "email": "contact@innofab.fr", "blurb": "Innofab is a FabLab founded on public-private partnership, promoting innovation, collaborative projects' emergence and new business, and located in a university.", "address_1": "Avenue Georges Pompidou", "geometry": {"type": "Point", "coordinates": [2.2615867, 43.6220449]}, "country": "France"}, -{"capabilities": "three_d_printing;laser;vinyl_cutting", "city": "Gonesse", "kind_name": "fab_lab", "links": ["https://www.facebook.com/lafabriquenumerique.gonesse/"], "parent_id": 514, "url": "https://www.fablabs.io/labs/lafabriquenumeriquedegonesse", "coordinates": [49.0022342231, 2.42293977434], "name": "La Fabrique Numerique de Gonesse", "county": "France", "phone": "+33 615487657", "postal_code": "95500", "longitude": 2.42293977434383, "address_2": "Centre socio-Culturel Marc sangnier", "latitude": 49.002234223134, "address_1": "17 rue marc sangnier", "country_code": "fr", "address_notes": "In the social center Marc Sangnier, next to cinema.", "email": "vivien@co-dev.org", "blurb": "for children who drop-out to school in Gonesse (fr)", "description": "The training center \"La fabrique Numerique de Gonesse\" is a social fablab, open in the \"la Fauconniere\" area at Gonesse, a popular district at less 30 km of Paris to North Est. Here, the social context is difficult, the unemployment rate is high (nineteen point two pourcent on Val d\u2019Oise territory), so lot of young people have nothing to do, risking to fall in delinquency. There are social criterias for the selection of profiles that we discuss with the city administration. The curriculum of La Fab Num is free and open at twelve to fifteen students, aged between sixteen - twenty five years old, and who have dropped out to school and/or non-degree . During five half month, they follow a training on digital fabrication for learn skills and individual's self-identification in social group.\u00a0The time of training is of four hundred thirty hours, this is divided in twenty hours by week - four days, to reason of five hours by day.\u00a0We start at nine past half am until twelve past half pm, with one break between. And the afternoon is as follow : one past half pm at four past half pm with one pause.\u00a0We have doing the choice of leave free two days ( friday and saturday) for those that want to find a mini job or to approach potential employers. This structure can't delivering any diplomas, but it's a device for re-engagement and a work on identity code. We have received the french label \"Grande Ecole du Num\u00e9rique\" (Great Digital School) who certify the training device by the Ministry of National Education, and in addition to grants granted on social grounds. We are five supervising staff : one for global partnership, one expert in training framework, two for media education, one for fablab education. Sometimes, others experts intervene for special pattern- work.", "geometry": {"type": "Point", "coordinates": [2.42293977434, 49.0022342231]}, "country": "France"}, -{"city": "Strasbourg", "kind_name": "fab_lab", "links": ["http://www.ideaslab.fr"], "url": "https://www.fablabs.io/labs/fablabinsastrasbourg", "coordinates": [48.583148, 7.747882], "county": "Alsace", "postal_code": "67000", "longitude": 7.747882, "country_code": "fr", "latitude": 48.583148, "name": "FabLab INSA Strasbourg", "geometry": {"type": "Point", "coordinates": [7.747882, 48.583148]}, "country": "France"}, -{"city": "Montreuil", "coordinates": [48.8587828, 2.4259452], "kind_name": "fab_lab", "links": ["http://www.icimontreuil.com"], "url": "https://www.fablabs.io/labs/icimontreuil", "name": "ICI MONTREUIL", "longitude": 2.4259452, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/45/39/631594e7-26c2-4f93-afc2-253c6c5c7a40/ICI MONTREUIL.jpg", "phone": "+33 6 33 78 38 49", "postal_code": "93100", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 48.8587828, "address_1": "135 Boulevard Chanzy", "address_notes": "Just Push the Big Black Door", "email": "pierric@madeinmontreuil.com", "blurb": "We are dedicated to the Creative Industries entrepreneurs. We help Creatives & Makers to innovate with the help of our digital fabrication tools and our communities of 58 savoir faire.", "description": "Art(isanat) + Design + Techno \r\n\r\nOpened since end of 2012, ICI Montreuil is a 1.700 m2 Collaborative and Social MakerSpace with a community of 165 makers and 58 savoir-faire. We provide access to 15 workshops (Wood, Metal, CNC Tools, Jewelry, Leather, Prototype studio, Photo Studio, Textile, Open Spaces etc) and to a community of doers & makers that help our users to give birth to their projects.\r\n\r\nWe especially love projects that place Art, Design & Craft at the heart of their activity.", "geometry": {"type": "Point", "coordinates": [2.4259452, 48.8587828]}, "country": "France"}, -{"city": "Coudures", "kind_name": "supernode", "links": ["https://forums.fabriques-alternatives.org", "https://plus.google.com/114201372084258307863", "https://www.facebook.com/pages/Fabriques-Alternatives/712873455394908", "http://www.fabriques-alternatives.org"], "url": "https://www.fablabs.io/labs/fabriquesalternatives", "coordinates": [43.6892663, -0.5199144], "name": "Fabriques Alternatives", "phone": "00 33 6 86 97 82 56", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/04/94f1203c-2592-41f5-9678-693b93b58f08/Fabriques Alternatives.jpg", "postal_code": "40500", "longitude": -0.519914400000062, "country_code": "fr", "latitude": 43.6892663, "capabilities": "three_d_printing;circuit_production;precision_milling", "email": "fabriques.alternatives@gmail.com", "blurb": "A small countryside lab built in a RepLab approach. It specializes in simulation, wearable and gamification fields.", "address_1": "Coudures", "geometry": {"type": "Point", "coordinates": [-0.5199144, 43.6892663]}, "country": "France"}, -{"city": "Havre (Le)", "kind_name": "fab_lab", "links": ["https://www.facebook.com/LH3Dfablab/", "https://twitter.com/LH3Dfablab", "http://www.lh3d.fr"], "url": "https://www.fablabs.io/labs/lhfablab", "name": "LH3D fablab", "longitude": 0.123766799999999, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/52/01/b74362dd-0482-479f-9c9b-aa8cc94170b3/LH3D fablab.jpg", "county": "Haute Normandie", "phone": "0602360075", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/30/7eb2b338-cff9-431c-9c71-14f85b4099fc/LH3D fablab.jpg", "postal_code": "76600", "capabilities": "three_d_printing;cnc_milling", "country_code": "fr", "latitude": 49.4977263, "address_1": "1 Rue Dum\u00e9 d'Aplemont", "coordinates": [49.4977263, 0.1237668], "email": "contact@lh3d.fr", "blurb": "Fab lab en plein coeur du Havre, sp\u00e9cialis\u00e9 dans l'impression 3D", "description": "Nous sommes un fab lab sp\u00e9cialis\u00e9 dans l'impression 3D. \r\nVenez nous rejoindre au lyc\u00e9e Jules Siegfried pour d\u00e9couvrir le fab lab.\r\nPour plus d'informations concernant les services que nous proposons visitez notre site web : www.lh3d.fr", "geometry": {"type": "Point", "coordinates": [0.1237668, 49.4977263]}, "country": "France"}, -{"capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "city": "Amiens", "kind_name": "fab_lab", "links": ["https://twitter.com/Machinerie", "https://www.facebook.com/LaMachinerieAmiens", "http://lamachinerie.org"], "parent_id": 143, "url": "https://www.fablabs.io/labs/lamachinerie", "name": "La Machinerie", "coordinates": [49.890166, 2.3014764], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/49/52/c23604a8-a203-409c-81f5-a4569b805f83/La Machinerie.jpg", "county": "Somme", "phone": "+33966851851", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/41/cafae3ec-04ad-4ce5-99a0-fa637c801d44/La Machinerie.jpg", "postal_code": "80000", "longitude": 2.30147639999996, "country_code": "fr", "latitude": 49.890166, "address_1": "70 rue des Jacobins", "address_notes": "2nd floor in the building", "email": "contact@lamachinerie.org", "blurb": "We are now hosting a Full Featured Lab with the foundation named La Machinerie that also include a Coworking space in Amiens !", "description": "Located near the famous Cathedral of Amiens, The lab describes itself as a 200m2 place full of tools and people.\r\nOut of The Weekly Open session, We already propose some commercial services to companies & schools.\r\nWe plan to host Fab Academy Students in 2015 so feel free to visit / contact us !", "geometry": {"type": "Point", "coordinates": [2.3014764, 49.890166]}, "country": "France"}, -{"city": "Grenoble", "kind_name": "fab_lab", "links": ["https://fablab.lacasemate.fr", "http://fablab.ccsti-grenoble.org"], "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "url": "https://www.fablabs.io/labs/fablabgrenoble", "name": "Fab Lab La Casemate", "email": "fablab@ccsti-grenoble.org", "coordinates": [45.1976679, 5.7322305], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/46/01/ef1bfda3-f175-4d9d-8385-941e3ae2c96c/Fab Lab La Casemate.jpg", "county": "Rhone-Alpes", "phone": "+33 4 76 44 88 76", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/10/24/5aef4df6-29bf-4e66-8099-c321c3a172ac/Fab Lab La Casemate.jpg", "postal_code": "38000", "longitude": 5.73223050000001, "country_code": "fr", "latitude": 45.1976679, "address_notes": "Tram B arr\u00eat \u00cele Verte. Ou \u00e0 20 minutes \u00e0 pied de la gare de Grenoble.", "address_2": "2 place Saint Laurent", "blurb": "Situated in the French Alps, in an old stone building and open to the public 6 days a week. Workshops, conferences, education.. As a science center we show the general public what Fab labs are about!", "address_1": "CCSTI Grenoble La Casemate", "geometry": {"type": "Point", "coordinates": [5.7322305, 45.1976679]}, "country": "France"}, -{"city": "Orsay", "kind_name": "fab_lab", "links": ["http://www.le503.institutoptique.fr/", "http://bit.ly/ProtoListes"], "capabilities": "three_d_printing;circuit_production;precision_milling", "url": "https://www.fablabs.io/labs/photonicfablab", "name": "Photonic FabLab", "email": "camille.resseguier@institutoptique.fr", "coordinates": [48.7068033, 2.1741725], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/49/25/ae0fe292-b920-4db7-bae1-11a1fa2b6f5f/Photonic FabLab.jpg", "phone": "0164533228", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/17/0d0b4158-8eb2-4c2b-996e-ab68ff2bbec2/Photonic FabLab.jpg", "postal_code": "91400", "longitude": 2.17417249999994, "country_code": "fr", "latitude": 48.7068033, "address_1": "B\u00e2timent 503", "address_notes": "Au deuxi\u00e8me \u00e9tage \u00e0 droite du b\u00e2timent 503", "address_2": "Rue du Belv\u00e9d\u00e8re", "blurb": "FabLab d\u00e9di\u00e9 \u00e0 l'entrepreneuriat et \u00e0 la photonique", "description": "Le Photonic FabLab a \u00e9t\u00e9 cr\u00e9e \u00e0 l'origine par l'Institut d'Optique (Sup'Optique) pour ses \u00e9tudiants en \"fili\u00e8re innovation entrepreneur\" (FIE), leur permettant ainsi d'acc\u00e9der \u00e0 des moyens de prototypage. Ce FabLab est maintenant aussi accessible aux entreprises qui souhaitent acc\u00e9l\u00e9rer leur R&D, et sera bient\u00f4t ouvert aux \u00e9tudiants de tout horizon.", "geometry": {"type": "Point", "coordinates": [2.1741725, 48.7068033]}, "country": "France"}, -{"city": "Liancourt", "description": "La cr\u00e9ation d\u2019un FabLAB \u00e0 Liancourt est n\u00e9e de la volont\u00e9 conjointe de la FONDATION Arts et M\u00e9tiers et d\u2019un Dirigeant d\u2019entreprise Fabien MADORE de rester au c\u0153ur de l\u2019\u00e9volution des technologies.", "links": ["http://fablab-am-liancourt.fr/"], "parent_id": 678, "url": "https://www.fablabs.io/labs/liancourt", "coordinates": [49.329572, 2.4681282], "name": "FabLAB Arts et M\u00e9tiers Liancourt", "county": "oise", "phone": "0601826073", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/22/34/0d2e352b-9f62-4ec1-bd08-5dadb5199db1/FabLAB Arts et M\u00e9tiers Liancourt.jpg", "longitude": 2.46812820000002, "country_code": "fr", "latitude": 49.329572, "address_1": "Liancourt", "capabilities": "three_d_printing;cnc_milling;vinyl_cutting", "email": "fablabliancourt@gmail.com", "kind_name": "fab_lab", "geometry": {"type": "Point", "coordinates": [2.4681282, 49.329572]}, "country": "France"}, -{"city": "Gradignan", "kind_name": "fab_lab", "links": ["http://www.cohabit.fr/"], "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "url": "https://www.fablabs.io/labs/cohabit", "coordinates": [44.7908300945, -0.611331155017], "name": "Fablab Coh@bit IUT Bordeaux", "postal_code": "33170", "longitude": -0.611331155017069, "country_code": "fr", "latitude": 44.7908300945351, "address_1": "15 Rue de Naudet", "address_notes": "IUT bordeaux near bat 10A", "email": "bastien.dupuy@u-bordeaux.fr", "blurb": "we are a technologic fablab", "description": "100m2 with working place", "geometry": {"type": "Point", "coordinates": [-0.611331155017, 44.7908300945]}, "country": "France"}, -{"links": ["https://www.youtube.com/channel/UCGowETqxhJ-9xvk9Ezd252A", "https://twitter.com/QuaiLab", "http://www.quai-lab.com"], "county": "France", "postal_code": "86240", "capabilities": "three_d_printing;circuit_production", "country_code": "fr", "kind_name": "fab_lab", "city": "Ligug\u00e9", "coordinates": [46.5213643, 0.3105438], "parent_id": 288, "latitude": 46.5213643, "email": "info@quai-lab.com", "blurb": "Quai-Lab est une association qui \u00e0 pour vocation de faire d\u00e9couvrir et d'\u00e9duquer aux technologies du num\u00e9rique. Sp\u00e9cialisation en Robotique, objets connect\u00e9s, \u00e9lectronique et impression 3D.", "description": "L'objectif de l'association QUAI-LAB est :\r\n* De promouvoir l\u2019exp\u00e9rimentation, la cr\u00e9ation, la conception et la r\u00e9alisation de projets gr\u00e2ce aux \u00e9changes de savoir et \u00e0 la mise \u00e0 disposition de moyens techniques, que ces projets aient une vocation scientifique, technique, artistique, culturelle, industrielle ou \u00e9conomique;\r\n* De favoriser l\u2019apprentissage des technologies (\u00e9lectronique, informatique etc.) gr\u00e2ce au partage d\u2019exp\u00e9rience et des connaissances, en particulier \u00e0 destination du jeune public;\r\n* De promouvoir la r\u00e9appropriation par le grand public des capacit\u00e9s d\u2019analyse, de conception, de fabrication et de modification d\u2019objets technologiques;\r\n* De promouvoir les contenus libres qu\u2019ils soient logiciels ou mat\u00e9riels par l\u2019usage de ces contenus et une contribution \u00e0 leur enrichissement \r\n* De promouvoir les actions visant \u00e0 l\u2019utilisation consciente des mat\u00e9riaux et des \u00e9nergies ayant pour objectif la r\u00e9duction de la consommation des ressources naturelles et la pr\u00e9servation de l\u2019environnement, en appliquant notamment la strat\u00e9gie des Trois R (R\u00e9duire, R\u00e9utiliser, Recycler) ; l\u2019accent sera tout particuli\u00e8rement mis sur la r\u00e9utilisation d\u2019objets et mat\u00e9riaux existants, et sur leur recyclage ;\r\n* De g\u00e9rer le lieu qui h\u00e9berge les activit\u00e9s de l\u2019association ainsi que les \u00e9quipements qu\u2019il contient\r\n*De proposer aux entreprises locales, aux associations et institutions des services favorisant leur d\u00e9veloppement (prototypage rapide, exp\u00e9rimentation de services, produits et outils innovants, etc.)\r\n* De dispenser des formations sur des th\u00e9matiques li\u00e9es aux activit\u00e9s de l\u2019association, \u00e0 titre gratuit ou on\u00e9reux.*\r\n* De revendre \u00e0 ses membres, mati\u00e8res premi\u00e8res, consommables, outillages \u00e0 des conditions pr\u00e9f\u00e9rentielles.\r\n* D\u2019entretenir des r\u00e9seaux de relations destin\u00e9s \u00e0 la cr\u00e9ation d\u2019entreprises et d\u2019opportunit\u00e9s commerciales sur la base des projets issus du FabLab\u00a9.", "phone": "0615897279", "name": "QUAILAB", "url": "https://www.fablabs.io/labs/quailab86", "longitude": 0.310543800000005, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/03/21/0d900429-a5aa-465f-83d8-5fd47d5c92d0/QUAILAB.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/26/38/0455a51b-0288-497b-b5fb-2da3b09a0318/QUAILAB.jpg", "address_1": "ZA les Erondi\u00e8res", "address_2": "Batiment 1", "address_notes": "Bat 1 - ZA Les Erondieres", "geometry": {"type": "Point", "coordinates": [0.3105438, 46.5213643]}, "country": "France"}, -{"city": "Amanlis", "coordinates": [48.0033603, -1.4750607], "kind_name": "fab_lab", "links": ["http://twitter.com/antoine_fablab", "http://facebook.com/lafabriqueccprf", "http://lafabriqueccprf.wordpress.com"], "url": "https://www.fablabs.io/labs/lafabriqueccprf", "name": "La Fab'rique", "longitude": -1.47506069999997, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/48/25/9318a890-cf50-4710-92f9-abe0585cf77d/La Fab'rique.jpg", "phone": "0626594675", "postal_code": "35150", "capabilities": "three_d_printing;cnc_milling;laser;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 48.0033603, "address_1": "1 Rue Jacques de Corbi\u00e8re", "address_notes": "Au sein de l'espace jeunes d'Amanlis (35)", "email": "fablab@ccprf.fr", "blurb": "La Fab'rique, le FabLab de la CCPRF (35)", "description": "La Fab'rique, le FabLab de la CCPRF (35) ouvert tous les samedis de 10H \u00e0 12H30 (hors vacances scolaires)", "geometry": {"type": "Point", "coordinates": [-1.4750607, 48.0033603]}, "country": "France"}, -{"capabilities": "three_d_printing;cnc_milling;laser;precision_milling", "city": "Marseille", "kind_name": "fab_lab", "links": ["http://www.lacharbonnerie.com"], "parent_id": 874, "url": "https://www.fablabs.io/labs/lacharbonnerie", "name": "La Charbonnerie", "coordinates": [43.2989573, 5.3662317], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/48/38/41dd980a-71e6-44fa-8523-6ef1afbcc846/La Charbonnerie.jpg", "phone": "0952767920", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/28/42/2e6aae22-a881-419b-9a22-f18effa20f82/La Charbonnerie.jpg", "postal_code": "13002", "longitude": 5.36623170000007, "country_code": "fr", "latitude": 43.2989573, "address_1": "38 Rue de l'\u00c9v\u00each\u00e9", "address_notes": "en voiture sortie tunnel joliette, a pied: Metro Joliette, Tram Sadi Carnot", "email": "contact@lacharbonnerie.com", "blurb": "Un \u00e9cosyst\u00e8me professionnel pour passer rapidement de l'id\u00e9e \u00e0 l'objet. Un lieu innovant, stimulant la cr\u00e9ativit\u00e9 et le partage de comp\u00e9tences", "description": "Un espace concept \u00e0 la sauce marseillaise combinant des postes de travail en coworking et des ateliers de fabrication partag\u00e9s au service des start-ups, PMEs, travailleurs ind\u00e9pendants, cr\u00e9ateurs, salari\u00e9s nomades, \u00e9tudiants.... Un \u00e9cosyst\u00e8me professionnel pour passer rapidement de l'id\u00e9e \u00e0 l'objet. Un lieu innovant, stimulant la cr\u00e9ativit\u00e9 et le partage de comp\u00e9tences", "geometry": {"type": "Point", "coordinates": [5.3662317, 43.2989573]}, "country": "France"}, -{"city": "Rennes", "description": "En. \r\nThe fabrication laboratory of Rennes 1 University is a place available for the design and creation and realization of innovation objects in the various fields of technique/scientifique practice proposed by the university. This, proposed under a philosophy of social projection and collaboratif work with partners of local gouvernement, the extended network of labfabs, among others.", "links": ["https://twitter.com/LabFab_UR1"], "url": "https://www.fablabs.io/labs/labfabur1", "name": "LabFab UR1", "county": "Bretagne", "parent_id": 151, "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/29/16/207104c0-56b5-44cd-896f-2ec8da096d9f/LabFab UR1.jpg", "postal_code": "35042", "address_1": "263 Avenue G\u00e9n\u00e9ral Leclerc, 35042 Rennes", "country_code": "fr", "email": "labfab@univ-rennes1.fr", "blurb": "The Laboiratoire de Fabrication of Rennes 1 University has the aim of creating, innovating in line with the current economic context and in conjunction with its public / private partners.", "kind_name": "supernode", "geometry": {}, "country": "France"}, -{"city": "75005", "kind_name": "fab_lab", "links": ["http://fablab.sorbonne-universites.fr/"], "parent_id": 355, "url": "https://www.fablabs.io/labs/fablabsu", "name": "FablabSU", "email": "contact.fablabsu@gmail.com", "coordinates": [48.847640858, 2.35668479472], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/52/56/5dfcd1ed-e3da-43d6-9a37-7ccc1bfd126d/FablabSU.jpg", "county": "France", "phone": "+33662821701", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/32/21/700356cf-d0da-4db9-8115-3c4503de97a8/FablabSU.jpg", "postal_code": "75012", "longitude": 2.35668479472042, "country_code": "fr", "latitude": 48.8476408579993, "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "address_notes": "Tower 32-33, 1st floor, room 111", "address_2": "4 place Jussieu", "blurb": "FablabSU is a fablab created by the university cluster Sorbonne-Universit\u00e9s. One of its main mission is to promote the fablab tools and spirit to students and to the public in general.", "address_1": "University Pierre and Marie Curie", "geometry": {"type": "Point", "coordinates": [2.35668479472, 48.847640858]}, "country": "France"}, -{"capabilities": "three_d_printing;vinyl_cutting", "city": "Manosque", "kind_name": "fab_lab", "links": ["http://www.lespetitsdebrouillardspaca.org/-04-Alpes-de-Hautes-Provence-.html", "https://www.facebook.com/lespetitsdebrouillards04/"], "parent_id": 179, "url": "https://www.fablabs.io/labs/dcliclab", "name": "D'Clic Lab", "email": "d.cliclab@debrouillonet.org", "coordinates": [43.8332664, 5.7824618], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/56/09/1f073a3e-6235-4efd-910a-d788b9624a8d/D'Clic Lab.jpg", "phone": "0492726709", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/34/48/c88e7d0a-efe5-43df-9755-f4551c46dc2a/D'Clic Lab.jpg", "postal_code": "04100", "longitude": 5.78246179999996, "country_code": "fr", "latitude": 43.8332664, "address_1": "10 Rue Arthur Robert", "address_notes": "1er \u00e9tage", "address_2": "1er \u00e9tage", "blurb": "Le D\u2019Clic Lab est un espace partag\u00e9, anim\u00e9 par l'association Les Petits D\u00e9brouillards avec des outils et des ressources mutualis\u00e9s permettant de fabriquer ou d'inventer presque tout ce que l'on veut.", "description": "Le D\u2019Clic Lab est un espace partag\u00e9, anim\u00e9 par l'association Les Petits D\u00e9brouillards avec des outils et des ressources mutualis\u00e9s permettant de fabriquer ou d'inventer presque tout ce que l'on veut. On partage, on \u00e9change, on invente, on innove. \r\nPour tous les curieux, les bidouilleurs, les cr\u00e9atifs, ceux qui on envie de s'engager dans des projets citoyens, ludiques, ou d\u2019actualit\u00e9 sur des th\u00e8mes vari\u00e9s comme la transition \u00e9cologique (climat, d\u00e9veloppement durable...), les enjeux du num\u00e9rique et d\u2019internet (usages, r\u00e9seaux sociaux, s\u00e9curit\u00e9, objets connect\u00e9s...). Le D\u2019Clic Lab est ouvert \u00e0 tous ! \r\n\r\nLe Fablab organise des Open Lab (temps de rencontres et de bidouille) un soir par semaine et est ouvert sur demande en semaine. Des temps d\u00e9di\u00e9s aux plus jeunes (9-15 ans) se tiennent chaque mercredi apr\u00e8s midi. \r\n\r\nRetrouvez les horaires d\u2019ouverture et le programme des ateliers, en direct, \r\nvia notre page facebook publique : www.facebook.com/lespetitsdebrouillards04", "geometry": {"type": "Point", "coordinates": [5.7824618, 43.8332664]}, "country": "France"}, -{"city": "Brest", "kind_name": "fab_lab", "links": ["http://tyfab.fr", "http://twitter.com/TyFabBrest", "http://mdl29.net"], "url": "https://www.fablabs.io/labs/tyfabbrest", "name": "TyFab", "longitude": -4.47244069999999, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/48/59/8e946a67-1cae-42b6-8a35-667650271882/TyFab.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/12/48/346d39d8-41c3-448c-9a17-51215da8774b/TyFab.jpg", "postal_code": "29200", "coordinates": [48.3995478, -4.4724407], "country_code": "fr", "latitude": 48.3995478, "address_1": "214 rue Jean Jaur\u00e8s", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "email": "tyfab@mdl29.net", "blurb": "TyFab is the first fablab in Brest since 2012.", "description": "TyFab was created by the non-profit organisation Maison du Libre.", "geometry": {"type": "Point", "coordinates": [-4.4724407, 48.3995478]}, "country": "France"}, -{"city": "Marseille", "kind_name": "mini_fab_lab", "links": ["http://www.lafabulerie.com"], "url": "https://www.fablabs.io/labs/lafabulerie", "name": "La Fabulerie", "longitude": 5.38385859999994, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/52/07/3a6a93b8-f974-4d4d-863c-a43a1a414485/La Fabulerie.jpg", "phone": "04 13 63 68 30", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/35/73525d56-5962-4e89-b20c-04b88014e28a/La Fabulerie.jpg", "postal_code": "13001", "coordinates": [43.2953058, 5.3838586], "country_code": "fr", "latitude": 43.2953058, "address_1": "4 Rue de la Biblioth\u00e8que", "capabilities": "three_d_printing;cnc_milling;vinyl_cutting", "email": "contact@lafabulerie.com", "blurb": "Mediation, pedagogy", "description": "Design The Future Now is a non profit-making organization, working in the DIY and Maker culture.\r\nOur space is localised in center town with a surface of 150m2. There is an active community around the project : \"Les Fabuleu(x)(ses).", "geometry": {"type": "Point", "coordinates": [5.3838586, 43.2953058]}, "country": "France"}, -{"city": "Gennevilliers", "kind_name": "fab_lab", "links": ["http://www.youtube.com/user/Faclabucp/", "https://twitter.com/FacLabUcp", "https://www.facebook.com/faclab", "http://www.faclab.org"], "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "url": "https://www.fablabs.io/labs/faclab", "name": "FacLab", "coordinates": [48.9355223, 2.3033937], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/46/00/3c427c79-731f-49cb-8321-2206d2982a9a/FacLab.jpg", "county": "\u00cele-de-France", "email": "contact@faclab.org", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/10/22/5d0ba3ef-15f3-4e5f-bbfc-4f51cb3dfbe9/FacLab.jpg", "postal_code": "92230", "longitude": 2.30339370000002, "country_code": "fr", "latitude": 48.9355223, "address_1": "Avenue Marcel Paul", "address_notes": "Enter the University main building, then the corridor in front of you, then turn into the first corridor to your left.", "address_2": "All\u00e9e des Pierres Mayettes", "blurb": "\"Create, Document, Share\". Tuesday-Friday, 1 to 6pm (7:45 on Tuesdays). No fees, machines available in exchange of contributing and sharing (knowledge, goodwill, food!). Open workshops.", "description": "Open to all, the FacLab aims at providing access to knowledge, technology, arts and crafts through the exchange of competencies. All projects and contributions are welcome, in a spirit of goodwill.\r\nThere are 5 rooms open to the public, over almost 200 square meters, including a sofa, microwave oven, coffee machine, fridge, fast internet and library.\r\nWe regularly organize workshops (Arduino, CNC, jewelry, sewing...) and once a month share food, experiences, drinks and laughs around a participative lunch.\r\nThe FacLab also delivers University Diplomas (\"Become a FabManager\", \"Create a FabLab\", \"Digital Fabrication\").", "geometry": {"type": "Point", "coordinates": [2.3033937, 48.9355223]}, "country": "France"}, -{"city": "Paris", "kind_name": "fab_lab", "links": ["http://www.cite-sciences.fr/fr/au-programme/lieux-ressources/carrefour-numerique2/presentation/fab-lab/"], "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "url": "https://www.fablabs.io/labs/carrefournumerique", "coordinates": [48.895985, 2.387059], "name": "Carrefour Num\u00e9rique\u00b2", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/16/17/e8e89377-02dd-49d2-90b6-6bd0cbc47c9e/Carrefour Num\u00e9rique\u00b2.jpg", "postal_code": "75019", "longitude": 2.38705900000002, "country_code": "fr", "latitude": 48.895985, "address_1": "30 Avenue Corentin Cariou", "address_notes": "Level -1", "email": "carrefour-numerique@universcience.fr", "blurb": "The Carrefour Num\u00e9rique\u00b2 aims to involve users into science and technics by empowering them into digital fabrication.", "description": "This FabLab takes place in La Cit\u00e9 des sciences, a science center in Paris.\r\n\r\nThe FabLab is open to everyone : \r\nTuesday ->Thursday 17h ->18h30\r\nFriday / Saturday 14h -> 18h30", "geometry": {"type": "Point", "coordinates": [2.387059, 48.895985]}, "country": "France"}, -{"city": "Thionville", "kind_name": "fab_lab", "links": ["http://www.thilab.fr"], "capabilities": "three_d_printing;laser", "url": "https://www.fablabs.io/labs/thilab", "name": "Thilab", "longitude": 6.16001459252925, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/55/22/291d1720-1f89-4bef-a3bf-da5990393261/Thilab.jpg", "county": "Moselle", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/18/50/d726e97f-b02b-4da5-943c-aabd75dcad8d/Thilab.jpg", "postal_code": "57100", "coordinates": [49.343550913, 6.16001459253], "country_code": "fr", "latitude": 49.3435509129817, "address_notes": "1er \u00e9tage.", "email": "thilab@techtic-co.eu", "address_1": "5, impasse des anciens hauts-fourneaux", "geometry": {"type": "Point", "coordinates": [6.16001459253, 49.343550913]}, "country": "France"}, -{"city": "CLERMONT-FERRAND", "description": "L'ESACM, l'Ecole d'Art de Clermont m\u00e9tropole ouvre son fablab le ]ProtoLab[ , au public, courant janvier 2015.\r\nChaque Fablab a des sp\u00e9cificit\u00e9s qui le caract\u00e9risent. Le ]ProtoLab[ est ax\u00e9 sur le d\u00e9veloppement de pratiques et de projets artistiques, ce qui en fait le premier fablab de l'hexagone d\u00e9di\u00e9 \u00e0 l'art num\u00e9rique.\r\nL\u2019appellation ]ProtoLab[ viens du grec pr\u00f4tos, exprimant le premier rang, la priorit\u00e9 ; en arch\u00e9ologie, il d\u00e9signe l'\u00e9tape d'une r\u00e9alisation proche de l'accomplissement et de lab, laboratoire.\r\nLe ]ProtoLab[ est un espace, qui fait partie int\u00e9grante de l'\u00e9cole depuis 2013. Nous l'inventons et le concevons comme un atelier de fabrication d\u00e9di\u00e9 aux arts du num\u00e9rique. S'y trouvent machines de d\u00e9coupe, imprimante 3D, brodeuse, Arduino, mat\u00e9riel \u00e9lectronique qui permettent de cr\u00e9er gr\u00e2ce \u00e0 la conception assist\u00e9e par ordinateur.\r\nL'ESACM a vocation \u00e0 d\u00e9velopper la cr\u00e9ation, au travers de projets ambitieux, en invitant notamment des artistes \u00e0 travailler dans l'espace du ]ProtoLab[. Ils pourront d\u00e9velopper leurs recherches sur les questions du num\u00e9rique et r\u00e9aliser des \u0153uvres in situ en collaboration avec les \u00e9tudiants de l'\u00e9cole ; dans l'esprit de partage des connaissances, qui au travers du r\u00e9seau des Fab Labs, rends les utopies tangibles.\r\nLe ]ProtoLab[ est en lien avec l'Imal et The Printlab (Fab Lab de l'\u00e9cole d'art du septentecinq) \u00e0 Bruxelles", "links": ["http://www.esacm.fr"], "parent_id": 116, "url": "https://www.fablabs.io/labs/leprotolab", "capabilities": "three_d_printing;laser;vinyl_cutting", "name": "le ]ProtoLab[", "phone": "04 73 17 36 10", "postal_code": "63000", "address_1": "25 RUE KESSLER", "country_code": "fr", "address_notes": "\u00c9COLE SUP\u00c9RIEURE D\u2019ART DE CLERMONT M\u00c9TROPOLE\r\n\r\n25 RUE KESSLER\r\n63 000 CLERMONT-FERRAND\r\n\r\nles bus suivants s'arr\u00eatent \u00e0 proximit\u00e9 de l'\u00e9cole :\r\n\r\nLe 3 arr\u00eat Lecoq\r\nLe 8 arr\u00eat Maison de la Culture \r\nLe 12 arr\u00eat Rabanesse\r\n\r\n\r\n\u00c0 l\u2019accueil du b\u00e2timent, Marc Champomier ou\r\nMabidingao Guedingao, vous orienterons.", "email": "arrieu@gmx.fr", "blurb": "Le ]ProtoLab[ de l'ESACM est ax\u00e9 sur le d\u00e9veloppement de pratiques et de projets artistiques, ce qui en fait le premier fablab de l'hexagone d\u00e9di\u00e9 \u00e0 l'art num\u00e9rique.", "kind_name": "fab_lab", "geometry": {}, "country": "France"}, -{"city": "Besan\u00e7on", "coordinates": [47.2419928, 6.007478], "kind_name": "fab_lab", "links": ["http://www.frenchmakers.com"], "url": "https://www.fablabs.io/labs/frenchmakers", "name": "FRENCHMAKERS", "longitude": 6.00747799999999, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/57/26/54fc0dab-9055-4764-801a-49e3a9757860/FRENCHMAKERS.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/20/49/889ff4fe-ca67-4c0e-bf68-9eee421887e7/FRENCHMAKERS.jpg", "postal_code": "25000", "capabilities": "three_d_printing;laser;vinyl_cutting", "country_code": "fr", "latitude": 47.2419928, "address_1": "17 Rue Xavier Marmier", "address_notes": "1er \u00e9tage", "email": "contact@frenchmakers.com", "blurb": "FRENCHMAKERS FABLAB BESANCON FRANCE", "description": "FabLab Besancon- Impression 3D, d\u00e9coupe laser, prototypage, incubateur d'id\u00e9es et de projets...", "geometry": {"type": "Point", "coordinates": [6.007478, 47.2419928]}, "country": "France"}, -{"city": "Paris", "coordinates": [48.8442169, 2.3121423], "kind_name": "mini_fab_lab", "links": ["http://www.le17bis.com"], "url": "https://www.fablabs.io/labs/le17bis", "name": "Le 17 bis", "longitude": 2.3121423, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/59/10/ff40945b-8e40-4201-8e0f-68e2071678f9/Le 17 bis.jpg", "county": "Ile de France", "phone": "0033760397524", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/22/48/4a7d6a07-e0cf-4c7f-8476-bc901e403104/Le 17 bis.jpg", "postal_code": "75015", "capabilities": "three_d_printing;vinyl_cutting", "country_code": "fr", "latitude": 48.8442169, "address_1": "17 bis Boulevard Pasteur", "address_notes": "Metro Pasteur", "email": "contact@le17bis.com", "blurb": "Le 17 bis is a coworking space about design, with tools and machines to quickly prototype.", "description": "Le 17 bis is a coworking space, with projects around design. We are opening on the 7th May 2015 (http://www.eventbrite.fr/e/billets-soiree-crepes-16457961205) and will have a 3D printer a vinyl cutter, a sewing machine, a soldering station and wood working tools. We help our members for their projects with a Design Thinking and Human Centered approach.", "geometry": {"type": "Point", "coordinates": [2.3121423, 48.8442169]}, "country": "France"}, -{"city": "Chalon-sur-Sa\u00f4ne", "description": "At Nic\u00e9phore Cit\u00e9, digital engineering center of Burgundy, we believe the new forms of collaboration with, first creating our incubator and the opening of the Raffinerie, coworking space for connected self-employed people. The creation of a FabLab in this public structure is therefore a logical consequence, a further step towards change.\r\nThe fablab is a tool designed to provide digital manufacturing capabilities for different audiences: individuals, students, job seekers, companies, ...", "links": ["http://www.nicephorelabs.com"], "parent_id": 216, "url": "https://www.fablabs.io/labs/nicephorelabs", "coordinates": [46.7764133, 4.8466683], "name": "Nic\u00e9phore Labs", "county": "Burgundy", "phone": "+33 (0)385908303", "postal_code": "71100", "longitude": 4.84666830000003, "address_2": "All\u00e9e de la sucrerie", "latitude": 46.7764133, "address_1": "34 Quai Saint-Cosme", "country_code": "fr", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "email": "nicephorelabs@gmail.com", "blurb": "The Nic\u00e9phore Labs was created in February 2016. The goal is to welcome all public with projects in mind", "kind_name": "fab_lab", "geometry": {"type": "Point", "coordinates": [4.8466683, 46.7764133]}, "country": "France"}, -{"capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "city": "Perpignan", "kind_name": "fab_lab", "links": ["http://www.squaregolab.com"], "parent_id": 21, "url": "https://www.fablabs.io/labs/squaregolab", "name": "SquaregoLab", "coordinates": [42.6889663, 2.8502832], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/49/39/b524a3d5-a93d-4865-9f74-9bfa24607d8f/SquaregoLab.jpg", "phone": "+33468981332", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/29/33/baf95047-2ead-47d4-8edf-73183c701b78/SquaregoLab.jpg", "postal_code": "66000", "longitude": 2.85028320000004, "country_code": "fr", "latitude": 42.6889663, "address_1": "96 Rue de Zurich", "address_notes": "Bus stop : Line 17, Stop name : Bruxelles", "email": "contact@squaregolab.com", "blurb": "Based in Perpignan, south of France, SquaregoLab FabLab Perpignan is an open lab that aims to promote entrepreneurship, innovation and learning for all.", "description": "Built as a non profit company, SquaregoLab is a third place, where all makers can learn, make and share on every domains regardless theirs resources. Makers can access Fablab resources for free for open source projects, or not for a commercial use.\r\nThis lab is a collaborative learing and making place. Our main goals are to stimulate innovation in order to serve local economy, and to give access to our facilities for all.", "geometry": {"type": "Point", "coordinates": [2.8502832, 42.6889663]}, "country": "France"}, -{"city": "Gourdon", "description": "FabLab sur la Communaut\u00e9 de Commune Quercy Bouriane\r\n\r\nNous proposons tout au long de l'ann\u00e9e des atelier de d\u00e9mocratisation aux nouvelles technologies.", "links": ["http://www.fablabgourdon.fr", "http://www.polenumerique.net"], "parent_id": 21, "url": "https://www.fablabs.io/labs/techfactory", "coordinates": [44.737572, 1.384484], "name": "TechF@ctory", "phone": "0565371022", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/31/38/bc1823cd-9586-4c3a-998d-4a5b84ac3965/TechF@ctory.jpg", "postal_code": "46300", "longitude": 1.38448399999993, "country_code": "fr", "latitude": 44.737572, "address_1": "20 boulevard des Martyrs", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "email": "fab-lab@polenumerique.net", "blurb": "FabLab sur la Communaut\u00e9 de Commune Quercy Bouriane", "kind_name": "fab_lab", "geometry": {"type": "Point", "coordinates": [1.384484, 44.737572]}, "country": "France"}, -{"city": "Vannes-le-Ch\u00e2tel", "kind_name": "fab_lab", "links": ["https://twitter.com/TheGlassFablab", "https://twitter.com/CerfavFablab", "http://www.cerfav.fr/fablab/"], "url": "https://www.fablabs.io/labs/theglassfablab", "name": "The Glass Fablab", "longitude": 5.77789519999999, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/49/59/9ffd94d2-414b-49c0-bf26-561d1e1ec555/The Glass Fablab.jpg", "phone": "0033.383254993", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/48/81e2f2af-2605-470d-810d-e2a59ff0eead/The Glass Fablab.jpg", "postal_code": "54112", "coordinates": [48.5482028, 5.7778952], "country_code": "fr", "latitude": 48.5482028, "address_1": "Rue de la Libert\u00e9", "capabilities": "three_d_printing;cnc_milling;laser;precision_milling;vinyl_cutting", "email": "fablab@cerfav.fr", "blurb": "Based at CERFAV, Center for Researches and Training in Glassworks in Vannes-le-Ch\u00e2tel (Lorraine, France)", "description": "Our Fablab is opened for students, inhabitants of this rural/sub-urban area. It's sticken to a multitude of studios for glass-crafting. This Fablab should help digital revolution in arts & crafts such as glassblowing, stained-glass, kiln-casting, sand-casting, slumping, lost-wax process, etc. Since 2008, the Glass Fablab set in Cerfav - France, helps day after day glassworkers to manage with glass media matter. We're also used to work with various profiles of customers, from kids to industrial engineers.", "geometry": {"type": "Point", "coordinates": [5.7778952, 48.5482028]}, "country": "France"}, -{"city": "Mulhouse", "coordinates": [47.74235, 7.3394436], "kind_name": "fab_lab", "links": ["https://twitter.com/Technistub", "https://www.facebook.com/technistub/", "http://www.technistub.org"], "url": "https://www.fablabs.io/labs/Technistub", "name": "Technistub", "longitude": 7.33944359999998, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/50/11/f10298bc-6db7-4f95-a9d3-a329ba166823/Technistub.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/53/36acd392-e98f-4dea-bc2d-01691d6f504b/Technistub.jpg", "postal_code": "68100", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 47.74235, "address_1": "5 rue Jules Ehrmann", "address_notes": "More details about access to our lab are available here: http://www.technistub.org/?page_id=511", "email": "contact@technistub.org", "blurb": "Fablab & Makerspace @ Mulhouse, France.", "description": "Technistub is member of french Fablabs network (#RFFLabs).\r\nWe have most of the equipment prescribed in official Fablab inventory. Not always the exact same brand or model but we have the main capabilities.", "geometry": {"type": "Point", "coordinates": [7.3394436, 47.74235]}, "country": "France"}, -{"city": "Beauvais", "description": "L\u2019Atelier FabLab promeut les nouvelles \u00e9conomies participatives et est un lieu ouvert \u00e0 tous. Il b\u00e9n\u00e9ficie tant \u00e0 des particuliers (\u00e9tudiants, amateurs, public d'experts, professionnels, entreprises) qu\u2019\u00e0 des acteurs institutionnels (collectivit\u00e9s territoriales, enseignement). \r\n\r\nL\u2019Atelier FabLab est :\r\n- accompagnateur du DIY (\u201cDo It Yourself\u201d, en fran\u00e7ais \u00ab faites le par vous-m\u00eame \u00bb),\r\n- plateforme d\u2019innovation ascendante, \r\n- promoteur de la culture FabLab dans l\u2019Oise.\r\n \r\nIl est aussi un lieu o\u00f9 l'on peut :\r\n- partager et \u00e9changer ses connaissances ou son savoir-faire sur un logiciel ou une machine par exemple,\r\n- recevoir des formations et des expertises,\r\n- organiser des \u00e9v\u00e8nements (AfterWorks, BootCamps, ateliers s\u00e9minaires, etc.).\r\n\r\nL\u2019adh\u00e9sion \u00e0 L\u2019Atelier FabLab est gratuite mais conditionn\u00e9e par l\u2019acceptation de partager ses cr\u00e9ations avec le plus grand nombre et par le respect de la charte MIT des FabLabs.", "links": ["https://www.facebook.com/pages/Latelier/724079104347745?ref=br_tf", "https://twitter.com/atelier60", "http://atelier.oise.fr"], "parent_id": 15, "url": "https://www.fablabs.io/labs/latelierfablab", "longitude": 2.08185871667172, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/57/07/59bcbe41-7da0-40e3-8fa6-a55dccd10c9d/L'Atelier FabLab.jpg", "phone": "+33 (0)344066408", "kind_name": "fab_lab", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/20/24/de6cd1de-fefd-4e94-85dc-e71f390bd394/L'Atelier FabLab.jpg", "postal_code": "60000", "capabilities": "three_d_printing;laser;vinyl_cutting", "country_code": "fr", "latitude": 49.4278455853673, "address_1": "Rue du Pont de Paris", "coordinates": [49.4278455854, 2.08185871667], "email": "contact@atelier.oise.fr", "blurb": "L\u2019Atelier FabLab permet aux habitants de l'Oise d'acc\u00e9der aux sciences et aux techniques de fabrication num\u00e9rique", "name": "L'Atelier FabLab", "geometry": {"type": "Point", "coordinates": [2.08185871667, 49.4278455854]}, "country": "France"}, -{"city": "clamecy", "kind_name": "fab_lab", "links": ["https://www.facebook.com/fabnlab", "http://www.cg58.fr/services-numeriques/la-nievre-numerique/les-fablab.html"], "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "url": "https://www.fablabs.io/labs/fabnlabdeclamecy", "name": "FabNLab de Clamecy", "coordinates": [47.4610935, 3.5149945], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/04/16/d38d502e-6d98-4702-9149-960f999af1d2/FabNLab de Clamecy.jpg", "county": "Ni\u00e8vre", "parent_id": 280, "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/27/34/ab418a42-596b-4638-a1ed-e1da98768fb5/FabNLab de Clamecy.jpg", "postal_code": "58500", "longitude": 3.51499450000006, "country_code": "fr", "latitude": 47.4610935, "address_1": "rue de druyes", "address_notes": "le Fablab est situ\u00e9 entre la maison du d\u00e9veloppement \u00e9conomique et la maison de la formation", "email": "fabnlab@nievre.fr", "blurb": "Le FabNLab de Clamecy est une Fablab destin\u00e9 essentiellement \u00e0 une population rurale, aux artisans et aux commer\u00e7ants", "description": "Le FabNLab de Clamecy a ouvert ses portes au public en octobre 2015. Il est port\u00e9 par le d\u00e9partement de la Ni\u00e8vre", "geometry": {"type": "Point", "coordinates": [3.5149945, 47.4610935]}, "country": "France"}, -{"city": "La Verri\u00e8re", "kind_name": "fab_lab", "links": ["http://sqylab.org/"], "capabilities": "three_d_printing;laser", "url": "https://www.fablabs.io/labs/sqylab", "name": "SQYLAB", "coordinates": [48.757052, 1.944001], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/53/18/f214a9d7-d231-4262-8dd1-58d1f6927dbb/SQYLAB.jpg", "parent_id": 355, "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/32/39/9b79bd26-d341-4e14-815c-c6c50a42b67a/SQYLAB.jpg", "postal_code": "78320", "longitude": 1.94400100000007, "country_code": "fr", "latitude": 48.757052, "address_1": "4 rue Louis Lormand", "address_notes": "Longer la salle de danse, l'entr\u00e9e est au bout, porte marqu\u00e9e SECMAT", "email": "bureau@hatlab.fr", "blurb": "SQYLAB wants to be a classical FabLab with an ecological flavour. It wants to promote eco-conception, re-use and recycling of materials.", "description": "SQYLAB includes typical tools like a laser-cutter, a 3D printer, an electronics workshop, but also a wood workshop and a sewing workshop. It wishes to be inclusive, diverse, educative, and opened.", "geometry": {"type": "Point", "coordinates": [1.944001, 48.757052]}, "country": "France"}, -{"city": "Strasbourg", "kind_name": "fab_lab", "links": ["http://www.av-exciters.com/AV-Lab"], "url": "https://www.fablabs.io/labs/avlab", "coordinates": [48.5831305849, 7.75406274233], "name": "AV-Lab", "county": "Alsace", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/10/28/4e9f3cfb-fb76-4d35-936b-5b3b85d15bbb/AV-Lab.jpg", "postal_code": "67000", "longitude": 7.75406274232796, "country_code": "fr", "latitude": 48.5831305848726, "address_1": "37 rue des fr\u00e8res", "email": "av.exciters@gmail.com", "description": "AV Lab is the Fablab of Strasbourg. It's a laboratory of experimentation where everybody can come and build whatever he wants using prototyping Machines.", "geometry": {"type": "Point", "coordinates": [7.75406274233, 48.5831305849]}, "country": "France"}, -{"city": "Limoges", "kind_name": "fab_lab", "links": ["http://twitter.com/limouzilab", "http://www.fb.me/limouzilab", "http://lab.limouzi.org"], "parent_id": 15, "url": "https://www.fablabs.io/labs/limouzilab", "coordinates": [45.8396337, 1.2670267], "name": "LimouziLab", "phone": "+33684756553 +33670068492", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/14/19/c4bb5805-9e30-4b97-b3e5-e56fd81495c4/LimouziLab.jpg", "postal_code": "87100", "longitude": 1.26702669999997, "country_code": "fr", "latitude": 45.8396337, "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "email": "contact@limouzi.org", "blurb": "doing almost anything.", "address_1": "2 bis impasse daguerre", "geometry": {"type": "Point", "coordinates": [1.2670267, 45.8396337]}, "country": "France"}, -{"city": "Montpellier", "description": "Our Fab-Lab consists of a community of scientists and students involved in public research in the field of biology.\r\n\r\nNotre Fab-Lab est constitu\u00e9 d'une communaut\u00e9 de scientifiques et d'\u00e9tudiants qui font de la recherche publique dans le domaine de la biologie.", "links": ["http://www.crbm.cnrs.fr/index.php/fr/news-du-s-e-m/417-bio-fab"], "url": "https://www.fablabs.io/labs/biofab", "coordinates": [43.6369394, 3.8661468], "name": "Bio-Fab", "phone": "0434359571", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/16/23/25716975-91df-4122-bb5a-5269ac63eb65/Bio-Fab.jpg", "postal_code": "34090", "longitude": 3.86614680000002, "country_code": "fr", "latitude": 43.6369394, "address_1": "CRBM-IGMM-CPBS - CNRS 1919 Route de Mende", "capabilities": "three_d_printing;precision_milling", "email": "jean.casanova@crbm.cnrs.fr", "blurb": "We make application for biology research", "kind_name": "fab_lab", "geometry": {"type": "Point", "coordinates": [3.8661468, 43.6369394]}, "country": "France"}, -{"city": "Cannes", "kind_name": "mini_fab_lab", "links": ["http://www.la-refabrique.fr"], "url": "https://www.fablabs.io/labs/larefabrique", "name": "la refabrique", "email": "larefabrique@sitespros.fr", "longitude": 6.96050220000006, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/48/55/71c06836-6328-47b9-b89b-0f66d6c729aa/la refabrique.jpg", "county": "Provence-Alpes-Cote d'Azur", "phone": "0493489582", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/12/46/5cc218ca-40ec-451e-be76-368015278128/la refabrique.jpg", "postal_code": "06150", "capabilities": "three_d_printing;cnc_milling;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 43.5708477, "address_1": "14 rue Jean Giono", "coordinates": [43.5708477, 6.9605022], "address_2": "les olivarelles 2 - Villa 8", "blurb": "l\u2019atelier de d\u00e9couverte, d\u2019exp\u00e9rimentation, de cr\u00e9ation et de fabrication personnel mais pas priv\u00e9 d\u2019un bricoleur/bidouilleur geek.", "description": "\u00ab la refabrique \u00bb, est au d\u00e9part, l\u2019atelier de d\u00e9couverte, d\u2019exp\u00e9rimentation, de cr\u00e9ation et de fabrication personnel d\u2019un bricoleur/bidouilleur geek : j\u2019ai nomm\u00e9 MOI m\u00eame. \r\n\r\nAu fur des ann\u00e9es, petit \u00e0 petit, je me suis \u00e9quip\u00e9 d\u2019un bon nombre d\u2019outils de fabrication (d\u00e9coupage, pon\u00e7age, d\u00e9fon\u00e7age, pliage, assemblage, \u2026).pour rentabiliser l\u2019achat (ou la cr\u00e9ation) des machines et des outils de plus en plus sophistiqu\u00e9 et couteux, j\u2019ai eu l\u2019id\u00e9e \u00ab d\u2019ouvrir \u00bb cet atelier a d\u2019autres personnes, comme moi (bricoleur/ bidouilleur geek), susceptible d\u2019avoir besoin de mes machines num\u00e9riques, de mes outils et/ou de mes comp\u00e9tences techniques pour la r\u00e9alisation de leur projet de fabrication personnel.", "geometry": {"type": "Point", "coordinates": [6.9605022, 43.5708477]}, "country": "France"}, -{"city": "Auxerre", "kind_name": "fab_lab", "links": ["http://beauxboulons.org/"], "url": "https://www.fablabs.io/labs/beauxboulons", "coordinates": [47.8054064, 3.5787246], "name": "Atelier des Beaux Boulons", "phone": "33623537734", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/18/01/c3ac89ed-d055-467f-8517-06de9cfcb007/Atelier des Beaux Boulons.jpg", "postal_code": "89000", "longitude": 3.57872459999999, "country_code": "fr", "latitude": 47.8054064, "capabilities": "three_d_printing;circuit_production;precision_milling;vinyl_cutting", "email": "atelierdesbeauxboulons@gmail.com", "blurb": "Fablab associatif \u00e0 Auxerre", "address_1": "24 Rue des Champoulains", "geometry": {"type": "Point", "coordinates": [3.5787246, 47.8054064]}, "country": "France"}, -{"city": "Tremblay-en-France", "coordinates": [48.9449558, 2.5790067], "kind_name": "mini_fab_lab", "links": ["http://mjccaussimon.fr/?Fabrik-numerique"], "url": "https://www.fablabs.io/labs/fabriknumerique", "name": "Fabrik'numerique", "longitude": 2.57900670000004, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/54/48/86f8d2a4-4cf1-4ce3-8428-c3799b96c780/Fabrik'numerique.jpg", "phone": "01 48 61 09 85", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/18/12/70d8d722-b75d-4edc-a0fe-d2835678f59f/Fabrik'numerique.jpg", "postal_code": "93290", "capabilities": "three_d_printing", "country_code": "fr", "latitude": 48.9449558, "address_1": "6 Rue des Alpes", "address_notes": "La Fabrik'num\u00e9rique fait partie des ateliers propos\u00e9s par l'Espace Jean-Roger Caussimon.", "email": "bienvenue@mjccaussimon.fr", "blurb": "Atelier de partage des savoirs / Exp\u00e9rimentations / Impression 3D / Mini Formations, les samedis de 14h \u00e0 17h30 (hors vacances scolaires)", "description": "Bidouilleurs(euses) du dimanche, \u00e9lectronicien(ne)s \u00e0 la retraite, adolescents touche-\u00e0-tout et curieux de tout bord se rejoignent le temps de cet atelier pour partager leurs d\u00e9couvertes et tester tout ce qui leur passe par la t\u00eate.\r\n\u00c7a construit, \u00e7a d\u00e9boulonne, \u00e7a soude, \u00e7a programme et surtout, \u00e7a invente ou r\u00e9invente.\r\n\r\nVenez nous rejoindre pour d\u00e9couvrir que le monde se fait surtout avec vos id\u00e9es !\r\n\r\nDes P'tits d\u00e8j' \u00e9lectriques, les samedis de 10h30 \u00e012h30, vous sont propos\u00e9s en cours d'ann\u00e9e pour vous former aux outils num\u00e9riques (caf\u00e9 & croissants offerts !)\r\n\r\n> Samedi 4 Octobre 2014, Arduino\r\n> Samedi 8 Novembre 2014, Arduino\r\n> Samedi 10 Janvier 2015, Arduino\r\n\r\n> Samedi 24 Janvier 2015, Sketch'Up\r\n> Samedi 7 F\u00e9vrier 2015, Sketch'Up\r\n> Samedi 14 Mars 2015, Sketch'Up\r\n\r\nLes places sont limit\u00e9es, inscrivez-vous au 01.48.61.09.85", "geometry": {"type": "Point", "coordinates": [2.5790067, 48.9449558]}, "country": "France"}, -{"city": "Corti", "kind_name": "fab_lab", "links": ["http://facebook.com/fablabcorti"], "url": "https://www.fablabs.io/labs/fablabcorti", "coordinates": [42.309525, 9.14927], "name": "Fab Lab Corti", "county": "Corsica", "postal_code": "20250", "longitude": 9.14927, "address_2": "La Citadelle BP 52, 20250 Corte, France", "latitude": 42.309525, "address_1": "Universit\u00e0 di Corsica Pasquale Paoli - Palazzu Naziunale", "country_code": "fr", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "email": "simeoni_l@univ-corse.fr", "blurb": "Fab Lab Corti est le Fab Lab de l'Universit\u00e9 de Corse", "description": "Le Fab Lab Corti est port\u00e9 par l\u2019Universit\u00e9 de Corse et sa Fondation. Il occupe 250 m2 au sein du Palazzu Naziunale. Il permet d\u2019enrichir les formations et la recherche universitaires; il favorise la conversion des comp\u00e9tences en projets ; enfin, il permet de consolider la r\u00e9cente strat\u00e9gie d\u00e9velopp\u00e9e autour de la r\u00e9sidence de Designer Fabbrica Design.", "geometry": {"type": "Point", "coordinates": [9.14927, 42.309525]}, "country": "France"}, -{"city": "Douai", "description": "Fab Lab by Mines Douai is the FabLab of the Ecole Nationale Superieure des Mines de Douai, including five teaching and research departments (Polymer and Composites Technology, and Mechanical Engineering, Atmospheric and Environment Sciences, Industrial Energetic, Civil Engineering, and Computer Science and Automation).\r\n\r\nThis place was created to emerge the creativity and innovation of any age and any structure, through our 7 thematics : tool box for your projects, prototype your project, collective creativity, and robotics connected devices, recycling and re-use, arts / heritage / archeology and open lab.\r\n\r\nOpen sessions for free lab access are organized to discover the Lab; learning sessions are planned to discover our team, our machines and their potential; sessions are dedicated to members in order to produce themselves theirs prototypes; co-design and co-production sessions also give you the opportunity to exchange, develop and produce collectively. For experts wishing to transfer their know-how, open lab sessions are also planned!\r\n\r\nWe will be happy to welcome you in our lab, see your creativity carry through prototypes and talk with you about concrete issues of territory, company or business.", "links": ["http://facebook.com/FabLabBYMinesDouai", "http://@FabLaBYMinesD", "http:// http://fablabby.mines-douai.fr"], "parent_id": 254, "url": "https://www.fablabs.io/labs/fablabbyminesdouai", "coordinates": [50.367874, 3.080602], "name": "FabLab by Mines Douai", "county": "France", "phone": "0033 327712143", "postal_code": "59500", "longitude": 3.080602, "country_code": "fr", "latitude": 50.367874, "address_1": "941 rue Charles Bourseul", "capabilities": "three_d_printing;cnc_milling;laser;precision_milling", "email": "fablab@mines-douai.fr", "blurb": "7 themes in our lab: the toolbox for your projects, prototype your project, collective creativity, robotics and connected objects, recycling and re-use, art / heritage / archeology, and open lab!", "kind_name": "fab_lab", "geometry": {"type": "Point", "coordinates": [3.080602, 50.367874]}, "country": "France"}, -{"city": "Bures-sur-Yvette", "kind_name": "mini_fab_lab", "links": ["http://smalllab.proto204.co/"], "url": "https://www.fablabs.io/labs/proto204smalllab", "name": "Proto204 - SmallLab", "longitude": 2.17244070000004, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/48/44/50a96260-0d26-4ffa-8039-ad37cfa2aac0/Proto204 - SmallLab.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/28/47/378397e0-5854-4148-9046-282fbde64638/Proto204 - SmallLab.jpg", "postal_code": "91440", "coordinates": [48.6992634, 2.1724407], "country_code": "fr", "latitude": 48.6992634, "address_1": "204 Rue Andr\u00e9 Amp\u00e8re", "capabilities": "three_d_printing", "email": "contact@proto204.co", "blurb": "The SmallLab is the hacker space of the Proto204, a research-dedicated brownfield now reconverted in a 'Third Place' which gather on a daily basis students, inhabitants, researchers of all ages.", "description": "The SmallLab is the hacker space of the Proto204, a research-dedicated brownfield now reconverted in a 'Third Place' which gather on a daily basis students, inhabitants, researchers of all ages.\r\n\r\nWe make tools availables to the users for their DIY projects. Arduino boards, electronic equipements and knowledge are ready to be shared. The 'Coding Sessions' takes place in the smalllab each monday thus allowing the users to meet up around a particular topic.\r\n\r\nVisit the website to be informed on the upcoming events !\r\nhttp://proto204.co/\r\nhttp://smalllab.proto204.co/", "geometry": {"type": "Point", "coordinates": [2.1724407, 48.6992634]}, "country": "France"}, -{"city": "Maz\u00e8res-sur-Salat", "description": "Un Fablab est un atelier ouvert au public dans lequel on peut utiliser des outils de fabrication innovants notamment des machines-outils pilot\u00e9es par ordinateur tels que imprimantes 3D, d\u00e9coupeuses laser, fraiseuses num\u00e9riques \u2026", "links": ["http://labtop.syv.fr/"], "parent_id": 21, "url": "https://www.fablabs.io/labs/labtopinnovation", "email": "sylvaindr@laposte.net", "longitude": 0.973469399999999, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/49/43/5242e37e-a5a5-496b-8bd0-b88a48eb8966/Lab Top Innovation.jpg", "phone": "+33615821573", "kind_name": "fab_lab", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/29/37/212eca16-70e5-4b72-895a-0f75ec3dd27f/Lab Top Innovation.jpg", "postal_code": "31260", "capabilities": "three_d_printing;laser", "country_code": "fr", "latitude": 43.1326708, "address_1": "Maz\u00e8res-sur-Salat", "coordinates": [43.1326708, 0.9734694], "address_2": "usine lacroix", "blurb": "Fablab Comminges : des lieux d'\u00e9change d\u00e9di\u00e9s \u00e0 la fabrication et \u00e0 l'innovation, en milieu rural", "name": "Lab Top Innovation", "geometry": {"type": "Point", "coordinates": [0.9734694, 43.1326708]}, "country": "France"}, -{"city": "Vallauris", "description": "Le Village Graphic is the resourceful place to print and work on your medias. Thanks to the training day we'll provide you, you'll get accustomed to the material use. You'll just need to prepare your files in order to print them, whether it be in small or big format. \r\nThe facturation only focuses on your real consumption of products so that you can make great savings regarding the online offer. Plus, you can deliver your productions on the very day basis!", "links": ["https://www.linkedin.com/company/le-village-graphic?trk=biz-companies-cym", "https://twitter.com/Village_Graphic", "https://www.facebook.com/Le-Village-Graphic-474468392744210/", "http://www.levillagegraphic.com/"], "parent_id": 212, "url": "https://www.fablabs.io/labs/levillagegraphic", "email": "contact@levillagegraphic.com", "capabilities": "vinyl_cutting", "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/50/06/8e1a0f14-e208-48ec-a47c-b2a6af2e273e/Le Village Graphic - FabLab.jpg", "phone": "04.93.33.33.33.", "kind_name": "fab_lab", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/29/53/c786af34-7f7c-4586-9436-55e5c5e0773c/Le Village Graphic - FabLab.jpg", "postal_code": "06220", "address_1": "1856 Chemin Saint Bernard, Norma Color", "country_code": "fr", "address_notes": "Le Village Graphic is located in Norma Color premises.", "address_2": "Porte n\u00b08", "blurb": "The 1st FabLab specialized in digital printing and working for graphic arts professionnals looking for tailor-made services to ensure their clients satisfaction.", "name": "Le Village Graphic - FabLab", "geometry": {}, "country": "France"}, -{"city": "Ch\u00e2teauroux", "kind_name": "fab_lab", "links": ["http:// http://www.facebook.com/berrylab36", "http://www.berrylab36.org"], "url": "https://www.fablabs.io/labs/berrylab36", "name": "BerryLab36", "longitude": 1.67516519986111, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/50/42/75c7efd5-52a6-4cad-87e3-1b4048b60ac9/BerryLab36.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/30/26/c13b4603-fa38-420c-8bb8-1bd241752d1a/BerryLab36.jpg", "postal_code": "36000", "coordinates": [46.812097168, 1.67516519986], "country_code": "fr", "latitude": 46.8120971679806, "capabilities": "three_d_printing;cnc_milling;laser", "email": "contact@berrylab36.org", "blurb": "Berrylab36 was born in february 2015 and was oppened to public in september the same year. We actauly have around 45 membres. Material : - lasercut (trotec speedy 100) - CNC - 3D printers (makerb", "address_1": "All\u00e9e Jean Vaill\u00e9", "geometry": {"type": "Point", "coordinates": [1.67516519986, 46.812097168]}, "country": "France"}, -{"city": "Amb\u00e9rieu-en-Bugey", "kind_name": "fab_lab", "links": ["https://openagenda.com/lab01", "http://www.lab01.fr/"], "url": "https://www.fablabs.io/labs/Lab01", "name": "LAB01", "longitude": 5.34531000000004, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/53/08/61c7e060-f13d-418c-9553-1399f3610146/LAB01.jpg", "phone": "+336 71 34 90 24", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/32/31/4dc21af8-fde0-4335-81e8-ca6a1d03e8c5/LAB01.jpg", "postal_code": "01500", "coordinates": [45.9545285, 5.34531], "country_code": "fr", "latitude": 45.9545285, "address_1": "48 Rue Gustave Noblemaire", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "email": "info@lab01.fr", "blurb": "C\u2019est un outil collectif qui va permettre aux usagers et particuli\u00e8rement aux PME du territoire, de tester, d\u2019exp\u00e9rimenter et de monter en culture ensemble\u2026", "description": "Espace de Cowork:\r\nUn espace de travail partag\u00e9, avec du wifi, du caf\u00e9 mais aussi un r\u00e9seau de travailleurs encourageant l'\u00e9change et l'ouverture.\r\n\r\nFab LAB:\r\nUn laboratoire de fabrication num\u00e9rique pour la conception et la r\u00e9alisation d'objets et prototypes.\r\n\r\nLiving LAB:\r\nUn regroupement d'acteurs publics et priv\u00e9s, d'entreprises, d'associations et de citoyens, pour inventer et tester des services, des outils ou des usages nouveaux.", "geometry": {"type": "Point", "coordinates": [5.34531, 45.9545285]}, "country": "France"}, -{"city": "Havre (Le)", "kind_name": "fab_lab", "links": ["http://www.lh-fab-lab.e-monsite.com"], "url": "https://www.fablabs.io/labs/LH3Dfablab", "name": "LH3D fab lab", "email": "lh3d.fablab@gmail.com", "longitude": 0.124160899999993, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/52/00/b920edf7-e9e9-49b6-b55d-73eb93c165e4/LH3D fab lab.jpg", "county": "Haute-Normandie", "phone": "0602360075", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/28/ab516a02-357b-42c0-a5b3-672d747ac342/LH3D fab lab.jpg", "postal_code": "76600", "capabilities": "three_d_printing;cnc_milling", "country_code": "fr", "latitude": 49.4974627, "address_1": "1 Rue Dum\u00e9 d'Aplemont", "coordinates": [49.4974627, 0.1241609], "address_2": "1 rue dum\u00e9 d'aplemont", "blurb": "1er fab lab de Haute Normandie, situ\u00e9 au lyc\u00e9e Jules Siegfried. Nous sommes sp\u00e9cialis\u00e9 dans l'impression 3D.", "description": "Nous sommes le 1er fab lab de Haute Normandie, nous disposons de 5 imprimantes 3D plus 2 en constructions, et de tout le mat\u00e9riel n\u00e9cessaire \u00e0 la r\u00e9alisation de projets.\r\nNous vous invitons \u00e0 visiter notre site web pour plus d'infos.\r\nwww.lh-fab-lab.e-monsite.com", "geometry": {"type": "Point", "coordinates": [0.1241609, 49.4974627]}, "country": "France"}, -{"city": "Pau", "kind_name": "fab_lab", "links": ["http://www.fablab-pau.org"], "url": "https://www.fablabs.io/labs/fablabpau", "name": "FabLab Pau", "longitude": -0.367391399999974, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/52/08/befe8426-1b9a-4331-95cf-a96783d05282/FabLab Pau.jpg", "postal_code": "64000", "coordinates": [43.2955172, -0.3673914], "country_code": "fr", "latitude": 43.2955172, "address_1": "18 Rue Latapie", "capabilities": "three_d_printing;cnc_milling", "email": "contact@fablab-pau.org", "blurb": "FabLab Pau est un \"LABoratoire de FABrication\" associatif. Ouvert \u00e0 tous, c'est un espace de travail, un lieu d'\u00e9change et de partage des connaissances.", "description": "FabLab Pau est un \"LABoratoire de FABrication\" associatif. Ouvert \u00e0 tous, c'est un espace de travail, un lieu d'\u00e9change et de partage des connaissances en vue de la r\u00e9alisation de projets coop\u00e9ratifs ayant une composante culturelle, humanitaire, scientifique, artistique, technique, etc.\r\n\r\nD\u00e9sireux d'engager des actions susceptibles d'accro\u00eetre la libert\u00e9 de chacun d'utiliser, de cr\u00e9er, d'analyser, de modifier tout objet ou bien, quelque soit son niveau de connaissances, et d'agir pour la promotion des sciences et techniques, FabLab Pau s'efforce de respecter la charte actuelle des fablabs. Acc\u00e8s \u00e0 tous, co-construction du savoir et apprentissage par les pairs, responsabilisation des usagers, partage des concepts et processus sans faire obstacle \u00e0 la propri\u00e9t\u00e9 intellectuelle, telles sont les bases du projet FabLab Pau.", "geometry": {"type": "Point", "coordinates": [-0.3673914, 43.2955172]}, "country": "France"}, -{"capabilities": "three_d_printing;laser", "city": "Rennes", "kind_name": "fab_lab", "links": ["http://myhumankit.org"], "parent_id": 18, "url": "https://www.fablabs.io/labs/humanlab", "name": "Humanlab", "coordinates": [48.1258548, -1.7012871], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/55/21/cdaa3f77-6a54-4585-988f-9cc635c04a55/Humanlab.jpg", "phone": "+33(0)728328321", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/34/27/c516866d-a6fa-4e2e-9bc1-4c7a8404cbc9/Humanlab.jpg", "postal_code": "35000", "longitude": -1.70128709999994, "country_code": "fr", "latitude": 48.1258548, "address_1": "2 Avenue du Bois Labb\u00e9", "address_notes": "Fully accessible to wheelchairs.", "email": "contact@myhumankit.org", "blurb": "The humanlab will allow self-repairing of humans the ones with the others, both disabled or not, provinding new open-source solutions.", "description": "The aim is then to spread this model to all volunteer places on the planet, including fablabs, to create a network of mutual assistance and international protopying. The humanlab project is a google impact challenge winner, a hackathon organizer, and a game changer.", "geometry": {"type": "Point", "coordinates": [-1.7012871, 48.1258548]}, "country": "France"}, -{"city": "Ch\u00e2teau-Thierry", "coordinates": [49.0374892, 3.3981522], "kind_name": "fab_lab", "links": ["https://www.facebook.com/fablab.chateau.thierry/", "http://www.fablab02.org"], "url": "https://www.fablabs.io/labs/fablabchateauthierry", "name": "FAB LAB Ch\u00e2teau Thierry", "longitude": 3.39815220000003, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/50/25/015373c3-5ebe-49f3-a9da-ea8f8d7e78eb/FAB LAB Ch\u00e2teau Thierry.jpg", "county": "Picardie", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/14/03/68cda855-83f8-437e-8885-3ec7f78e655d/FAB LAB Ch\u00e2teau Thierry.jpg", "postal_code": "02400", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 49.0374892, "address_1": "7 avenue de l'Europe", "address_notes": "Office : 7, avenue de l'Europe 02400 Chateau Thierry", "email": "fablabct02@gmail.com", "blurb": "Nouveau site internet ! New website !", "description": "Our purpose is to show the young people how fun making is ; this country fab lab is also a place to meet industrials who search for new employees or students.\r\nWe are FAB LAB SOLIDAIRE granted by Fondation Orange\r\n\r\n Tuesday : 13.30 - 20.00\r\n Wednesday : 13.30 - 20.00\r\n Thursday : 13.30 -20.00\r\n Friday : 13.30 - 17.30 OPEN LAB 17.30 - 20.30\r\n Saturday : 10.00 - 12.00 ; 13.30 - 20.00\r\n\r\n\r\nNew machine ! Digital Sewing, new CNC Rooter, 7 CAD stations, sublimation hot transfert machine, Form 2", "geometry": {"type": "Point", "coordinates": [3.3981522, 49.0374892]}, "country": "France"}, -{"city": "Les Ulis", "address_notes": "Dans l'Espace Num\u00e9rique", "kind_name": "mini_fab_lab", "links": ["http://fabriquesnumeriques.tumblr.com/"], "url": "https://www.fablabs.io/labs/fablabmobilecaps", "capabilities": "three_d_printing;cnc_milling;vinyl_cutting", "name": "Fab Lab Mobile", "phone": "01 69 29 34 40", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/21/34/f4d14f44-725e-4888-b2a7-ddee7eb200d4/Fab Lab Mobile.jpg", "postal_code": "91940", "longitude": 2.17101950000006, "address_2": "M\u00e9diath\u00e8que Fran\u00e7ois Mitterrand", "latitude": 48.6812779, "address_1": "Rue du Forez", "country_code": "fr", "coordinates": [48.6812779, 2.1710195], "email": "fabriquesnumeriques@caps.fr", "description": "L\u2019espace Num\u00e9rique est dot\u00e9 d\u2019un FabLab Mobile comprenant deux imprimantes 3D Foldarap, des fraiseuses num\u00e9riques, une d\u00e9coupeuse vinyle et des cartes de programmation (Arduino, MakeyMakey).\r\n\r\nLe FabLab est amen\u00e9 \u00e0 circuler sur la ville des Ulis et sur le r\u00e9seau de la CAPS.", "geometry": {"type": "Point", "coordinates": [2.1710195, 48.6812779]}, "country": "France"}, -{"city": "Paris", "description": "As a place open on the city, the New Factory will share its experience with the largest audience through various activities in partnership with the Municipality, the Regional Council and other partners (schools / art centres / institutions for the promotion of design, etc\u2026). It will sensitize its various audiences to digital technologies by developing partnerships with local primary and secondary schools, by proposing activities for young people outside of the school terms, workshops for adults amateurs, and master class for professionals wishing to be trained in digital design (computer-aided-design, computer-aided-manufacturing, programming).", "links": ["http://www.nouvellefabrique.fr"], "parent_id": 15, "url": "https://www.fablabs.io/labs/nouvellefabrique", "longitude": 2.36995479999996, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/52/21/767841ab-4736-4298-8b67-174ddb438419/Nouvelle Fabrique.jpg", "county": "France", "phone": "+ 33 6 62 61 52 45", "kind_name": "fab_lab", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/47/8cd466ae-e18f-4fe8-aa35-f994e9063415/Nouvelle Fabrique.jpg", "postal_code": "75019", "capabilities": "three_d_printing;cnc_milling;precision_milling", "country_code": "fr", "latitude": 48.8901712, "address_1": "104 Rue d'Aubervilliers", "coordinates": [48.8901712, 2.3699548], "email": "contact@nouvellefabrique.fr", "blurb": "The New Factory is a place for production, sharing and reflection. It consists in a pool of digital machines in the heart of Paris", "name": "Nouvelle Fabrique", "geometry": {"type": "Point", "coordinates": [2.3699548, 48.8901712]}, "country": "France"}, -{"city": "Crest", "kind_name": "fab_lab", "links": ["http://www.8fablab.fr"], "url": "https://www.fablabs.io/labs/8fablabdrome", "coordinates": [44.7278841717, 5.02208319364], "name": "8 FabLab Dr\u00f4me", "county": "Dr\u00f4me", "phone": "0475551478", "postal_code": "26400", "longitude": 5.02208319364013, "country_code": "fr", "latitude": 44.7278841717018, "capabilities": "three_d_printing;cnc_milling;laser;precision_milling;vinyl_cutting", "email": "contact@8fablab.fr", "address_1": "8 rue courre-comm\u00e8re", "geometry": {"type": "Point", "coordinates": [5.02208319364, 44.7278841717]}, "country": "France"}, -{"city": "Barbizon", "kind_name": "mini_fab_lab", "links": ["http://www.fablab-moebius.org"], "url": "https://www.fablabs.io/labs/fablabmoebius", "name": "Fablab Moebius", "longitude": 2.60536930000001, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/54/23/b8c68e85-a134-45e1-9854-5937e702a876/Fablab Moebius.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/17/44/bdb3d40a-bc78-44b2-ab5a-5aa63ce6626d/Fablab Moebius.jpg", "postal_code": "77630", "coordinates": [48.4442477, 2.6053693], "country_code": "fr", "latitude": 48.4442477, "address_1": "8 rue Th\u00e9odore Rousseau", "capabilities": "three_d_printing", "email": "contact@fablab-moebius.org", "blurb": "Small Fablab in Seine & Marne (France). We specialised in 3D printing, small robots & micro-controllers programming.", "description": "We are a small Fablab in France at Barbizon (77). We have sessions every saturday afternoon for the moment. We welcome everyone from 8 years.\r\n\r\nWe have material to work with wood, 3D printers (2), CNC, Basic electronic (solder iron, mats, clamp), Micro-controllers, Lego we do, Rasberry Pi...\r\n\r\nWe welcome everyone with knowledge of some sort that would like to share and experiment with others in making things.\r\n\r\nTo know more : http://www.fablab-moebius.org", "geometry": {"type": "Point", "coordinates": [2.6053693, 48.4442477]}, "country": "France"}, -{"city": "Plouzane", "coordinates": [48.3579997, -4.5701172], "kind_name": "fab_lab", "links": ["http://telefab.fr"], "url": "https://www.fablabs.io/labs/telefab", "name": "Telefab", "longitude": -4.57011720000003, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/56/25/1156039b-e747-4adf-9f93-6dbed6f12ecf/Telefab.jpg", "county": "Bretagne", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/19/42/c7452aac-1010-463b-8f4d-4395aa644d9e/Telefab.jpg", "postal_code": "29280", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;vinyl_cutting", "country_code": "fr", "latitude": 48.3579997, "address_notes": "Pour des raisons de s\u00e9curit\u00e9 (plan Vigipirate), l'acc\u00e8s au site de T\u00e9l\u00e9com Bretagne est maintenant sous contr\u00f4le d'acc\u00e8s. Merci de pr\u00e9venir contact@telefab.fr si vous souhaitez venir au fablab.", "email": "contact@telefab.fr", "blurb": "It is a lab inside a graduate engineering schools (\"Grandes Ecoles\") about Telecommunication in Brest, in the far west of France but our lab is opened to everyone.", "address_1": "655 avenue du technopole", "geometry": {"type": "Point", "coordinates": [-4.5701172, 48.3579997]}, "country": "France"}, -{"city": "Sarreguemines", "kind_name": "fab_lab", "links": ["https://sketchfab.com/fabulis", "https://twitter.com/FABULIShn", "https://www.thingiverse.com/Fabulis/about", "http://www.fabulis.org"], "url": "https://www.fablabs.io/labs/fabulis", "name": "FABULIS", "email": "alexandre.benassar@wanadoo.fr", "longitude": 7.07086200000003, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/56/54/da40a877-c973-4d1d-a16b-49df5b8e9a07/FABULIS .jpg", "county": "Lorraine/Moselle", "phone": "0387953132", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/20/13/b4614507-aa3f-4834-8b55-e955b48ff937/FABULIS .jpg", "postal_code": "57215", "capabilities": "three_d_printing;cnc_milling;precision_milling", "country_code": "fr", "latitude": 49.106031, "address_1": "Sarreguemines", "coordinates": [49.106031, 7.070862], "address_2": "60 rue du Mar\u00e9chal Foch", "blurb": "Share, Learn, Create. We can locate Fabulis at the confluence of project-based learning and learning based on the design.", "description": "INCLUSION DES \u00c9L\u00c8VES DE L'ULIS PRO\r\n\r\nFABULIS est un espace de travail colaboratif et cr\u00e9atif entre toutes les personnes fr\u00e9quentant le lyc\u00e9e Henri Nomin\u00e9. Les \u00e9l\u00e8ves de l'ULIS pro sont au coeur de cette structure, ce qui facilite \u00e9norm\u00e9ment leur inclusion. \r\n\r\n \r\n\r\nSUSCITER LA CURIOSIT\u00c9 ET LA CR\u00c9ATIVIT\u00c9\r\n\r\nUne des activit\u00e9s de FABULIS est d'associer les enfants le plus rapidement possible \u00e0 de vrais projets, cr\u00e9er un contexte d'apprentissage authentique. Ce processus alimente la curiosit\u00e9 des \u00e9l\u00e8ves.\r\n\r\nFABULIS permet aux enfants d'exp\u00e9rimenter, prendre des risques, d'apprendre avec leurs propres id\u00e9es et le droit \u00e0 l'erreur leur permet de prendre plus facilement confiance en eux.\r\n\r\n \r\n\r\nAIDE A LA MISE EN PLACE DE FABLAB DANS LES LYC\u00c9ES ET COLL\u00c8GES.\r\n\r\nLes nouvelles technologies int\u00e9ressent de nombreux enseignants qui n'ont pas forc\u00e9ment la possibilit\u00e9 de se former et de s'\u00e9quiper correctement.\r\n\r\nFABULIS a pour objectif de diffuser des outils open-source (Imprimante 3D, Scanner 3D...) permettant d'\u00e9quiper les coll\u00e8ges et lyc\u00e9es de Lorraine. Ainsi les enseignants pourront se former et acqu\u00e9rir plus facilement du mat\u00e9riel en relation avec les nouvelles technologies.\r\n\r\n \r\n\r\nFABULIS TEND A SE SPECIALISER DANS LE PROTOTIPAGE D'OBJETS ET LA FABRICATION ADDITIVE.\r\n\r\nLes Fablab sont confront\u00e9s \u00e0 une forte demande de r\u00e9alisation d'objets imprim\u00e9s en 3D. Nous souhaitons acqu\u00e9rir un parc machine permettant de r\u00e9pondre \u00e0 ces sollicitations.\r\n\r\nLes \u00e9l\u00e8ves vont ainsi travailler au contact des associations, collectivit\u00e9s, start-up et PME. Toutes ces collaborations permettent de valoriser et d'am\u00e9liorer leurs comp\u00e9tences professionnelles.\r\n\r\nLes \u00e9changes amen\u00e9s par ces collaborations permettent aux \u00e9l\u00e8ves de ma\u00eetriser les outils multim\u00e9dia et de l'internet qui sont indispensables dans une carri\u00e8re professionnelle.", "geometry": {"type": "Point", "coordinates": [7.070862, 49.106031]}, "country": "France"}, -{"capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "city": "Nancy", "kind_name": "fab_lab", "links": ["http://www.lf2l.fr"], "parent_id": 332, "url": "https://www.fablabs.io/labs/lf2l", "coordinates": [48.6936291, 6.1991858], "name": "Lorraine Fab Living Lab", "phone": "+33 (0)3 54 50 47 91", "postal_code": "54000", "longitude": 6.19918580000001, "country_code": "fr", "latitude": 48.6936291, "address_1": "49 Boulevard d'Austrasie", "address_notes": "Get to the parking next to the concert hall, we are behind the \"grande halle\" the big glass building. It's the first door!", "email": "cedric.bleimling@univ-lorraine.fr", "blurb": "Nancy's University's FabLab. It was made possible by the ERPI lab and the ENSGSI School.", "description": "The Lorraine Fab Living Lab is the FabLab of the University. It Specializes in the students projects as well as companies who would need our services. We have an openLab session in association with Nybi.cc.", "geometry": {"type": "Point", "coordinates": [6.1991858, 48.6936291]}, "country": "France"}, -{"city": "Villeurbanne", "kind_name": "fab_lab", "links": ["http://www.youfactory.co"], "url": "https://www.fablabs.io/labs/youfactory", "name": "YOUFACTORY", "longitude": 4.89816339999993, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/58/26/03eb9034-bbea-42f2-9e29-24f879d8318d/YOUFACTORY.jpg", "phone": "0426687419", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/22/06/3350e97d-af8f-4896-9f3e-719f30edfa82/YOUFACTORY.jpg", "postal_code": "69100", "capabilities": "three_d_printing;cnc_milling;laser;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 45.7582828, "address_1": "50 Rue Antoine Primat", "coordinates": [45.7582828, 4.8981634], "address_2": "P\u00f4le PIXEL", "blurb": "Usine Collaborative", "description": "YOUFACTORY, premi\u00e8re usine collaborative en Rh\u00f4ne-Alpes, offre l'acc\u00e8s aux nouvelles technologies d'usinage et de prototypage rapide, \u00e0 des machines conventionnelles, et propose un espace de coworking atelier et des conseils de professionnels.\r\n\r\nUtilisation d'un atelier en fonction de vos besoins, mutualisation des moyens de production, \u00e9change et partage de connaissances : d\u00e9couvrez YOUFACTORY, la nouvelle usine collaborative pour les entrepreneurs de la cr\u00e9ation et de l\u2019innovation.", "geometry": {"type": "Point", "coordinates": [4.8981634, 45.7582828]}, "country": "France"}, -{"city": "Rodez", "description": "Inauguration le mercredi 10 juin 2015.", "links": ["http://mjcrodez.fr/clubs/espace-num%C3%A9rique/142-fablab-mjc-rodez.html", "https://www.facebook.com/pages/FabLabMjcRodez/882000348479514"], "url": "https://www.fablabs.io/labs/rutech", "coordinates": [44.3532319, 2.5782843], "name": "Rutech", "county": "Midi-Pyr\u00e9n\u00e9es", "phone": "+33565670113", "postal_code": "12000", "longitude": 2.57828429999995, "country_code": "fr", "latitude": 44.3532319, "address_1": "1 Rue Saint-Cyrice", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;vinyl_cutting", "email": "fablab@mjcrodez.com", "blurb": "Lieu ouvert d'exp\u00e9rimentations pour et avec les jeunes, port\u00e9 par les animateurs de la Cyber-base et les b\u00e9n\u00e9voles du club bidouille num\u00e9rique, au sein de la MJC de Rodez.", "kind_name": "supernode", "geometry": {"type": "Point", "coordinates": [2.5782843, 44.3532319]}, "country": "France"}, -{"city": "Romorantin-Lanthenay", "kind_name": "mini_fab_lab", "links": ["https://plus.google.com/110874629293812760062", "https://twitter.com/AtelierNumRomo", "https://www.facebook.com/Atelier-Num%C3%A9rique-Romorantin-1122174277856730/", "http://anr.adeti.org"], "url": "https://www.fablabs.io/labs/ateliernumeriqueromorantin", "name": "Atelier Num\u00e9rique Romorantin", "longitude": 1.75227250674584, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/51/39/38f839b5-9e47-4170-afda-418f529f926c/Atelier Num\u00e9rique Romorantin.jpg", "county": "Centre Val de Loire", "email": "contact@anr.adeti.org", "postal_code": "41200", "capabilities": "three_d_printing;vinyl_cutting", "country_code": "fr", "latitude": 47.3552132877622, "coordinates": [47.3552132878, 1.75227250675], "address_2": "3 Rue Jean Monnet", "blurb": "Espace partag\u00e9 et collaboratif du Romorantinais et du Monestois pour se former et d\u00e9velopper les projets des citoyens et entrepreneurs avec les nouveaux outils et usages du num\u00e9rique.", "address_1": "Bat. l'Atelier", "geometry": {"type": "Point", "coordinates": [1.75227250675, 47.3552132878]}, "country": "France"}, -{"city": "Arras", "description": "Le fablab de la ville d'Arras", "links": ["http://fablabarras.fr/"], "url": "https://www.fablabs.io/labs/fablabarras", "capabilities": "three_d_printing", "county": "Hauts de France", "kind_name": "mini_fab_lab", "postal_code": "62000", "address_1": "2 Rue Gustave Eiffel", "country_code": "fr", "address_notes": "AFP2I", "email": "perspectives@3d-nord.fr", "blurb": "Le fablab de la ville d'Arras", "name": "Fablab Arras", "geometry": {}, "country": "France"}, -{"city": "Bourges", "kind_name": "fab_lab", "links": ["http://www.bourgeslab.fr"], "capabilities": "three_d_printing;cnc_milling;circuit_production;precision_milling;vinyl_cutting", "url": "https://www.fablabs.io/labs/bourgeslab", "coordinates": [47.081012, 2.398782], "name": "Bourges Lab", "county": "france", "parent_id": 1123, "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/33/42/8f3c74ed-9d59-4659-9119-f007d7aa0d3a/Bourges Lab.jpg", "postal_code": "18000", "longitude": 2.39878199999998, "address_2": "CCI du Cher - Esplanade de l'a\u00e9roport", "latitude": 47.081012, "address_1": "Bourges", "country_code": "fr", "address_notes": "Esplanade de l'A\u00e9roport, route d'issoudun, 18000 Bourges", "email": "contact@bourgeslab.fr", "blurb": "FabLab de Bourges", "description": "Le Bourges Lab est un FabLab - Laboratoire de Fabrication - o\u00f9 il est mis \u00e0 disposition toutes sortes d'outils de fabrication num\u00e9rique notamment des imprimantes 3D, des machines-outils pilot\u00e9es par ordinateur, pour la conception et la r\u00e9alisation de projets et d'objets.", "geometry": {"type": "Point", "coordinates": [2.398782, 47.081012]}, "country": "France"}, -{"city": "Vannes", "kind_name": "fab_lab", "links": ["http://www.makerspace56.org"], "url": "https://www.fablabs.io/labs/makerspace56", "coordinates": [47.6423116, -2.7541692], "name": "Makerspace 56", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/12/51/34fe3ec9-e5f4-4c31-bd25-e9fafdc91795/Makerspace 56.jpg", "postal_code": "56000", "longitude": -2.75416919999998, "country_code": "fr", "latitude": 47.6423116, "capabilities": "three_d_printing", "address_1": "Place Albert Einstein", "geometry": {"type": "Point", "coordinates": [-2.7541692, 47.6423116]}, "country": "France"}, -{"city": "Paris", "kind_name": "fab_lab", "links": ["https://github.com/LPFP", "http://lepetitfablabdeparis.fr"], "url": "https://www.fablabs.io/labs/lepetitfablabdeparis", "name": "Le Petit FabLab de Paris", "longitude": 2.39198629999999, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/48/38/7fd8a398-22b8-4112-a338-e921a4b9e52b/Le Petit FabLab de Paris.jpg", "phone": "+33 9 51 58 75 50", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/12/34/e1a85508-5ed8-41df-a6fb-cdb84872e2a2/Le Petit FabLab de Paris.jpg", "postal_code": "75011", "coordinates": [48.8551859, 2.3919863], "country_code": "fr", "latitude": 48.8551859, "capabilities": "three_d_printing;laser;vinyl_cutting", "email": "bonjour@lepetitfablabdeparis.fr", "blurb": "Open on Week-Ends, Le Petit FabLab de Paris is a small space in the center of Paris with good equipment. It hosts collaborative projects.", "address_1": "86 Avenue Philippe Auguste", "geometry": {"type": "Point", "coordinates": [2.3919863, 48.8551859]}, "country": "France"}, -{"city": "Caen", "kind_name": "fab_lab", "links": ["https://fablab.relais-sciences.org"], "url": "https://www.fablabs.io/labs/fablabcaen", "name": "FabLab caen", "longitude": -0.347538999999983, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/51/58/b7fee3a0-7c90-40a2-83a2-f758cec13f6a/FabLab caen.jpg", "phone": "0231066050", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/27/3ea92fb3-1422-4eb7-8da1-ce587d9097c8/FabLab caen.jpg", "postal_code": "14000", "coordinates": [49.181016, -0.347539], "country_code": "fr", "latitude": 49.181016, "address_1": "Esplanade St\u00e9phane Hessel", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "email": "jlefebvre@relais-sciences.org", "blurb": "Equipement : imprimantes 3D, fraiseuse 3 & 4 axes, D\u00e9coupe vinyle, Laser, Machines \u00e0 coudre et \u00e0 broder, banc \u00e9lectronique... Il est ouvert \u00e0 tous.", "description": "Port\u00e9 par Relais d'sciences dans le cadre du programme 'Inm\u00e9diats', le FabLab de Caen est ouvert aux particuliers comme aux professionnels. Il est implant\u00e9 dans un espace de 400m2 au sein de la Maison de la Recherche et de l'Imagination (MRI) \u00e0 Caen.", "geometry": {"type": "Point", "coordinates": [-0.347539, 49.181016]}, "country": "France"}, -{"city": "Compi\u00e8gne", "kind_name": "fab_lab", "links": ["http://assos.utc.fr/fablab/index.html"], "capabilities": "three_d_printing;cnc_milling;laser", "url": "https://www.fablabs.io/labs/fablabutcompiegne", "coordinates": [49.4019280494, 2.7955892749], "name": "FabLab UTCompi\u00e8gne", "county": "France/Picardie/Oise", "phone": "0662881376", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/23/50/f2c5caef-6d30-4543-ad03-9dd2271ad359/FabLab UTCompi\u00e8gne.jpg", "postal_code": "60200", "longitude": 2.79558927490234, "address_2": "Avenue de Landshut", "latitude": 49.4019280493973, "address_1": "Compi\u00e8gne", "country_code": "fr", "address_notes": "Avenue de Landshut", "email": "fablabutc@assos.utc.fr", "blurb": "Le FabLab de l'Universit\u00e9 de Technologie de Compi\u00e8gne !", "description": "Nous mettons \u00e0 disposition des \u00e9tudiants, des professeurs et des chercheurs, les outils n\u00e9cessaires pour r\u00e9aliser des pi\u00e8ces, aussi bien par des machines \u00e0 commandes num\u00e9riques que par de simples outils de la vie de tous les jours !", "geometry": {"type": "Point", "coordinates": [2.7955892749, 49.4019280494]}, "country": "France"}, -{"city": "Dax", "description": "Art3fact lab vise \u00e0 \u00eatre, dans le tissu \u00e9conomique du Grand Dax, un op\u00e9rateur agile, dont l\u2019objectif est de favoriser l\u2019\u00e9mergence de savoirs, de technologies et de produits innovants.\r\nPar son fonctionnement : Art3fact lab est issu du monde de l\u2019open source ou de l\u2019open hardware. Art3fact lab est un atelier de futurs possibles par le fait qu\u2019il s\u2019oganise de mani\u00e8re collaborative\u2026 Tags en correlation : M\u00e9thodologies agiles, \u201ccare\u201d, collaboratif, ouverture, open research, durabilit\u00e9, humain.", "links": ["https://www.facebook.com/art3factlab?fref=ts"], "parent_id": 338, "url": "https://www.fablabs.io/labs/art3factlab", "longitude": -1.05203860793461, "name": "Art3fact lab", "county": "Aquitaine", "phone": "0683660277", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/16/01/b1bc8ae2-1432-4f51-8035-59aa3056e61c/Art3fact lab.jpg", "postal_code": "40100", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 43.7201097202689, "address_1": "1, avenue de la gare", "coordinates": [43.7201097203, -1.05203860793], "email": "art3factdax@gmail.com", "blurb": "TIC, Electronique embarqu\u00e9e, dr\u00f4nes, fabrication num\u00e9rique, prototypage rapide", "kind_name": "supernode", "geometry": {"type": "Point", "coordinates": [-1.05203860793, 43.7201097203]}, "country": "France"}, -{"city": "Aix-en-Provence", "kind_name": "fab_lab", "links": ["https://github.com/LabAixBidouille/", "https://www.facebook.com/LabAixBidouille", "https://twitter.com/LabAixBidouille", "http://www.meetup.com/Labaixbidouille", "http://www.labaixbidouille.com"], "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "url": "https://www.fablabs.io/labs/labaixbidouille", "name": "Laboratoire d'Aix-p\u00e9rimentation et de Bidouille", "email": "contact@labaixbidouille.com", "coordinates": [43.5143078, 5.451476], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/49/12/3fdb98cd-d9ad-43a0-9db4-a494f88cf19b/Laboratoire d'Aix-p\u00e9rimentation et de Bidouille.jpg", "phone": "+33 6 98 47 35 67", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/06/ed69da8f-9a55-464a-8f0c-9e6c29f127b1/Laboratoire d'Aix-p\u00e9rimentation et de Bidouille.jpg", "postal_code": "13100", "longitude": 5.45147599999996, "country_code": "fr", "latitude": 43.5143078, "address_1": "IUT d'Aix-en-Provence", "address_notes": "Le L.A.B est situ\u00e9 au niveau de l'entr\u00e9e principale de l'IUT. \u00c0 30m \u00e0 gauche de l'escalier principal.", "address_2": "413 Avenue Gaston Berger", "blurb": "Le Laboratoire d'Aix-p\u00e9rimentation et de Bidouille (L.A.B) est un espace de fabrication partag\u00e9 situ\u00e9 \u00e0 Aix-en-Provence.", "description": "Le L.A.B (Laboratoire d\u2019Aix-p\u00e9rimentation et de Bidouille) a pour objectif de favoriser l\u2019\u00e9mergence d'espaces collaboratif et communautaire d\u2019\u00e9change technologique \u00e0 Aix-en-Provence. Collaboration, Exp\u00e9rimentation, Fabrication, D\u00e9veloppement et Programmation, Formation et \u00c9changes sont autant de mots qui d\u00e9finissent notre projet \u00e0 haute teneur humaine et technologique. La vocation de cet espace est de devenir le premier Fab Lab en pays d\u2019Aix, ouvert au public et respectant la charte du MIT.\r\n\r\nLe Fab Lab d\u2019Aix est actuellement h\u00e9berg\u00e9 par l\u2019IUT d\u2019Aix-Marseille. Dans le cadre du projet Fab Lab provence, le CEEI Provence, acteur de l\u2019innovation en Paca, s\u2019associe au L.A.B, \u00e0 Design the Future Now et \u00e0 la Communaut\u00e9 du Pays d\u2019Aix pour initier la cr\u00e9ation de Fab Labs rayonnant sur la Provence dont l\u2019ancrage principal sera situ\u00e9 au centre-ville d\u2019Aix-en-Provence.\r\n\r\nVous pouvez nous retrouver tous les mardi et mercredi de 17h \u00e0 20h pour nos permanences.", "geometry": {"type": "Point", "coordinates": [5.451476, 43.5143078]}, "country": "France"}, -{"city": "Paris", "kind_name": "fab_lab", "links": ["http://www.woma.fr"], "url": "https://www.fablabs.io/labs/woma", "name": "WoMa", "longitude": 2.38263892337034, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/56/36/e8dd8f57-d9fe-436f-939d-cb2b208ec06d/WoMa.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/19/51/b640f7fa-420c-45c6-a9fa-56a55b999ead/WoMa.jpg", "postal_code": "75019", "coordinates": [48.8878877817, 2.38263892337], "country_code": "fr", "latitude": 48.8878877816678, "address_1": "15 bis Rue L\u00e9on Giraud", "capabilities": "three_d_printing;cnc_milling;laser;precision_milling", "email": "woma@wa-office.com", "blurb": "Notre souhait : relier l\u2019id\u00e9e et la mati\u00e8re de mani\u00e8re collaborative. Ouste les vieux clich\u00e9s, cols blancs / cerveau d\u2019un c\u00f4t\u00e9, gris / mains de l\u2019autre !", "description": "Ajourd'hui\r\nNous sommes un crew de 7 personnes aux comp\u00e9tences vari\u00e9es (architectes, designers, sociologues et communicants, ...). R\u00e9unies gr\u00e2ce et autour d\u2019un lieu : WoMa, et d\u2019une envie : impulser des pratiques collaboratives en milieu urbain.\r\nDemain\r\nEn tant que projet collaboratif, WoMa reste ouvert. Vous qui souhaitez insuffler temps, comp\u00e9tences, id\u00e9es et \u00e9nergies faites d\u00e9j\u00e0 (presque) partie de l\u2019\u00e9quipe.", "geometry": {"type": "Point", "coordinates": [2.38263892337, 48.8878877817]}, "country": "France"}, -{"city": "Saint-L\u00f4", "coordinates": [49.1132251191, -1.04935674285], "kind_name": "mini_fab_lab", "links": ["http://www.manchenumerique.fr"], "url": "https://www.fablabs.io/labs/manchelab", "name": "Manche Lab", "longitude": -1.04935674285275, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/56/47/8992837b-cccd-45b1-b701-49bfe0ef5686/Manche Lab.jpg", "phone": "0233778360", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/20/01/ad62cc8b-d516-4acf-8dd1-7bd22ea5e893/Manche Lab.jpg", "postal_code": "50000", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "country_code": "fr", "latitude": 49.1132251190982, "address_1": "205 Rue Joseph Cugnot", "address_notes": "Le Manche Lab est bas\u00e9 \u00e0 Saint-L\u00f4 mais se d\u00e9place partout dans le d\u00e9partement !", "email": "manchelab@manchenumerique.fr", "blurb": "Le Manche Lab est un Fab Lab mobile, mutualis\u00e9 sur tout le d\u00e9partement de la Manche.", "description": "La caravane du Manche Lab est \u00e0 la fois moyen de stockage et de d\u00e9placement des machines et \u00e9quipements, et lieu d'accueil du public. Le rayon d'action est \u00e9tendu \u00e0 l'ensemble de la Manche, par le biais de nombreux partenariats, pour des actions de sensibilisation \u00e0 la fabrication num\u00e9rique, \u00e0 destination de tous publics (grand public, professionnels, artisans, scolaires, \u00e9tudiants, artistes...).", "geometry": {"type": "Point", "coordinates": [-1.04935674285, 49.1132251191]}, "country": "France"}, -{"city": "Gr\u00e9asque", "coordinates": [43.4337271, 5.5404486], "kind_name": "mini_fab_lab", "links": ["http://fuvlab.org/wordpress/creations/", "http://www.fuvlab.org", "http://fuvlab.association-club.mygaloo.fr/PageAssociation-ListeEvenements/"], "url": "https://www.fablabs.io/labs/fuvlab", "name": "FuvLab", "longitude": 5.54044859999999, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/47/35/efbfbbd0-76f5-43ff-baa8-897956c5197f/FuvLab.jpg", "phone": "+33 6 13 81 56 62", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/28/00/bb069ec6-b54c-4eea-84b8-af56c7f53124/FuvLab.jpg", "postal_code": "13850", "capabilities": "three_d_printing;cnc_milling;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 43.4337271, "address_1": "62 Avenue Ferdinand Arnaud", "address_notes": "Inside \"La Nouvelle Mine\" building", "email": "contact@fuvlab.org", "blurb": "Makerspace launched in october 2014, 80m2 in the french city of Greasque near Marseille and Aix en Provence.", "description": "open access every monday and thursday 6:30 PM to 9 PM for adults and teens, every wednesday afternoon for child. Other open hours depending on scheduled workshops on http://fuvlab.association-club.mygaloo.fr/PageAssociation-ListeEvenements/", "geometry": {"type": "Point", "coordinates": [5.5404486, 43.4337271]}, "country": "France"}, -{"city": "Saint-Jacques-de-la-Lande", "kind_name": "fab_lab", "links": ["https://www.facebook.com/RDTECHFRANCE-707872772634493/?ref=settings", "http://www.concretease.com/", "http://www.retdtechfrance.fr/"], "url": "https://www.fablabs.io/labs/rdtechfrance", "coordinates": [48.0655016, -1.7045483], "name": "R&DTECHFRANCE", "phone": "02-23-45-32-78", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/29/17/e2a1f2da-e4ed-4d94-a74e-f270e9d0816b/R&DTECHFRANCE.jpg", "postal_code": "35136", "longitude": -1.70454829999994, "country_code": "fr", "latitude": 48.0655016, "address_1": "7 Rue Emile Souvestre", "capabilities": "three_d_printing;laser", "email": "philippe.michel@retdtechfrance.com", "description": "R&DTECHFRANCE est un bureau d'\u00e9tude m\u00e9catronique sp\u00e9cialis\u00e9 dans la conception et la r\u00e9alisation de drone maritime, terrestre et a\u00e9rien.\r\nNous avons des moyens de prototypage rapide (impression num\u00e9rique 3D, d\u00e9coupe laser, thermoformeuse)\r\nGr\u00e2ce \u00e0 notre service design, nous pouvons concevoir tous vos fichiers en 3D.", "geometry": {"type": "Point", "coordinates": [-1.7045483, 48.0655016]}, "country": "France"}, -{"city": "Autun", "description": "Le FabLab de Bellevue, ou laboratoire de fabrication num\u00e9rique est un espace ouvert \u00e0 tous pour la cr\u00e9ation, la conception et la collaboration d'id\u00e9es et projets au travers d'\u00e9changes de connaissances et de comp\u00e9tences.\r\n\r\n \r\nUn lieu consacr\u00e9 au d\u00e9veloppement d'activit\u00e9s num\u00e9riques\r\n\r\n \r\nSitu\u00e9 \u00e0 la p\u00e9pini\u00e8re num\u00e9rique de Bellevue, le FabLab est ouvert \u00e0 tous: professionnels, particuliers, porteurs de projets, \u00e9tudiants, bricoleurs, artistes, etc.\r\n\r\nSi vous souhaitez en savoir plus, rencontrer, \u00e9changer, apprendre, utiliser du mat\u00e9riel gratuitement, transmettre des savoirs techniques, innover, cr\u00e9er, r\u00e9parer ... le FabLab est fait pour vous !\r\nHoraires d'ouverture\r\n\r\n\r\nLundi : sur rdv (pros / \u00e9coles)\r\nMardi : sur rdv (pros / \u00e9coles)\u200b\r\nMercredi : 10h-12h/13h30-17h\r\nJeudi : 10h-12h/14h-20h\r\nVendredi : 10h-16h\r\nChaque dernier samedi du mois : 13h30-17h", "links": ["http://www.grandautunoismorvan.fr/fablab-de-bellevue"], "parent_id": 216, "url": "https://www.fablabs.io/labs/fablabdebellevue", "email": "raphael.mathieu@grandautunoismorvan.fr", "longitude": 4.27093492327879, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/52/15/b301cb75-a1c4-4165-b852-4bcad5f31f70/Fablab de Bellevue.jpg", "phone": "0385865116", "kind_name": "fab_lab", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/31/45/f7e71ea9-0e2d-493d-b723-ba04c0f36c0d/Fablab de Bellevue.jpg", "postal_code": "71400", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "country_code": "fr", "latitude": 46.9702315870116, "address_1": "Autun", "coordinates": [46.970231587, 4.27093492328], "address_2": "La p\u00e9pini\u00e8re de bellevue rue du maquis de l'Autunois Morvan", "blurb": "Fablab de la communaut\u00e9 de communes du Grand Autunois Morvan", "name": "Fablab de Bellevue", "geometry": {"type": "Point", "coordinates": [4.27093492328, 46.970231587]}, "country": "France"}, -{"city": "Montreuil", "description": "Bien ancr\u00e9 dans le territoire de Montreuil depuis plus de 50 ans, la Maison Populaire propose \u00e0 quelques 2 500 adh\u00e9rents annuels un acc\u00e8s \u00e0 tous les domaines de la culture, \u00e0 travers diff\u00e9rentes formes, des ateliers annuels aux workshops mensuels.\r\n\r\nDepuis 2016, la Maison Populaire accueille un fablab, baptis\u00e9 pour l'occasion Pop [lab].\r\n\r\nLe Pop [lab] propose une vari\u00e9t\u00e9 d'atliers \u00e0 l'ann\u00e9e, notamment pour les enfants de 8 \u00e0 13 ans, et quelques rendez-vous mensuels ouvert \u00e0 tous, dans un esprit DIY.\r\n\r\nLe Pop [lab] est aussi ouvert gratuitement en acc\u00e8s libre \u00e0 tout adh\u00e9rent de la Maison Populaire. Les horaires d'acc\u00e8s libre sont flexibles dans le temps pour s'adapter \u00e0 la demande et la fr\u00e9quentation. Les horaires \u00e0 jours sont toujours disponibles sur le wiki du Pop [lab]", "links": ["http://poplab.maisonpop.fr"], "parent_id": 328, "url": "https://www.fablabs.io/labs/poplabmontreuil", "coordinates": [48.8642236, 2.449594], "name": "Pop [lab]", "phone": "01 42 87 08 68", "postal_code": "93100", "longitude": 2.44959399999993, "address_2": "9bis Rue Dombasle", "latitude": 48.8642236, "address_1": "Maison Populaire", "country_code": "fr", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "email": "poplab@maisonpop.fr", "blurb": "Le fablab de la Maison Populaire, ouvert \u00e0 tous les adh\u00e9rents, avec la volont\u00e9 de partager l'acc\u00e8s aux outils et machines num\u00e9riques pour promouvoir les inventions et les expressions personnelles.", "kind_name": "fab_lab", "geometry": {"type": "Point", "coordinates": [2.449594, 48.8642236]}, "country": "France"}, -{"city": "Toulouse", "coordinates": [43.5941447, 1.4214297], "kind_name": "supernode", "links": ["http://www.fablabfestival.fr", "http://www.artilect.fr", "http://vimeo.com/user4871340", "http://www.youtube.com/user/fabLabArtilect", "http://twitter.com/FabLab_Toulouse", "http://www.facebook.com/pages/Artilect-FabLab-Toulouse"], "url": "https://www.fablabs.io/labs/artilectfablab", "name": "Artilect FabLab Toulouse", "longitude": 1.42142969999998, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/46/03/054eac98-3090-48ed-8754-65f6d10fd8ef/Artilect FabLab Toulouse.jpg", "county": "Midi-Pyr\u00e9n\u00e9es", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/10/29/167a630b-194d-462d-b8e7-47204b976cf8/Artilect FabLab Toulouse.jpg", "postal_code": "31300", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 43.5941447, "address_notes": "M\u00e9tro Patte d\u2019Oie (Ligne A) | M\u00e9tro Ar\u00e8ne (Ligne A-C) | Tram (T1) | Parking)", "email": "contact@artilect.fr", "blurb": "Le FabLab Toulouse a \u00e9t\u00e9 cr\u00e9e en 2009 par l\u2019association Artilect. C\u2019est le premier FabLab cr\u00e9er en France et le premier \u00e0 avoir \u00e9t\u00e9 lab\u00e9lis\u00e9 FabLab MIT en 2010.", "address_1": "27bis All\u00e9es Maurice Sarraut", "geometry": {"type": "Point", "coordinates": [1.4214297, 43.5941447]}, "country": "France"}, -{"city": "Orl\u00e9ans", "kind_name": "fab_lab", "links": ["http://www.fablab-orleanais.fr"], "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "url": "https://www.fablabs.io/labs/fablaborlanais", "coordinates": [47.8442611, 1.9389593], "name": "FabLab Orl\u00e9anais", "phone": "33659958827", "postal_code": "45100", "longitude": 1.93895929999996, "country_code": "fr", "latitude": 47.8442611, "address_notes": "16, rue l\u00e9onard de Vinci", "email": "filipe.franco@fablab-orleanais.fr", "address_1": "8 Rue L\u00e9onard de Vinci", "geometry": {"type": "Point", "coordinates": [1.9389593, 47.8442611]}, "country": "France"}, -{"city": "Montpellier", "kind_name": "fab_lab", "links": ["http://listes.labsud.org", "http://wiki.labsud.org", "https://facebook.com/labsud", "https://twitter.com/labsud", "http://membres.labsud.org", "http://forum.labsud.org"], "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "url": "https://www.fablabs.io/labs/labsudmontpellier", "name": "LABSud Montpellier", "email": "contact@labsud.org", "coordinates": [43.6150758, 3.9104845], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/48/46/3e725940-c6b6-4601-86ca-d7431199ae87/LABSud Montpellier.jpg", "phone": "+33(0)9 84 31 82 08", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/12/39/3d0dbe9e-1fcf-4d92-bbf4-51bb28b4e427/LABSud Montpellier.jpg", "postal_code": "34000", "longitude": 3.91048449999994, "country_code": "fr", "latitude": 43.6150758, "address_1": "Hotel d'Entreprise de l'Agglomeration de Montpelllier", "address_notes": "Big parking. After 7pm or during the week end gate is closed for security reasons. Just have to phone to make it open or use the Mobile App developped form Membrers.", "address_2": "120 All\u00e9e John Napier", "blurb": "Labsud le Fablab de Montpellier", "description": "Situ\u00e9 au c\u0153ur de la zone d'activit\u00e9 \u00e9conomique du Mill\u00e9naire \u00e0 Montpellier, le Fablab Labsud offre dans un espace de 270 m\u00e8tres carr\u00e9s un ensemble de moyens techniques (fraiseuses CNC, d\u00e9coupe laser, atelier d'\u00e9lectronique) accessible \u00e0 tous (entreprises, \u00e9ducation, particuliers). \r\n\r\nOrganis\u00e9 en plusieurs espaces, il offre notamment :\r\n* Un espace de projection / formation de 40 m2\r\n* Un espace d\u00e9di\u00e9e \u00e0 l'\u00e9lectronique de 30 M2\r\n* Un espace d\u00e9tente de 20 m2\r\n* Un espace d\u00e9di\u00e9 \u00e0 l'impression 3D de 20 m2\r\n* Une salle insonoris\u00e9e de 80m2 avec les machines d'usinages \u00e0 commande num\u00e9rique\r\n* Une salle avec d\u00e9coupe laser de 15 m2.\r\n\r\nHoraires d'ouverture : \r\n* Tous les apr\u00e8s midi de 14h00 \u00e0 17h00 pour tous les publics (pro, scolaires, particuliers)\r\n* Du Mardi au Samedi, de 10h000 \u00e012h30 sur rendez vous pour les pros\r\n* Les mardis et vendredis soir de 19H00 \u00e0 1h00 du matin pour tous les publics\r\n\r\nLe Fablab Labsud est accompagn\u00e9 par Montpellier M\u00e9tropole (Montpellier 3M) et soutient la French Tech Montpellier.", "geometry": {"type": "Point", "coordinates": [3.9104845, 43.6150758]}, "country": "France"}, -{"city": "Vaulx-en-Velin", "kind_name": "fab_lab", "links": ["https://vimeo.com/187997011", "http://www.lyon.archi.fr/fr/acklab"], "url": "https://www.fablabs.io/labs/acklab", "coordinates": [45.7798574, 4.9236727], "name": "'AckLab - ateliers d'innovation architecturale - architectural innovation studio", "county": "Auvergne-Rh\u00f4ne/Alpes", "phone": "0478795072", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/30/14/de7aeb22-0114-4509-8ace-78d0c1b19ad2/'AckLab - ateliers d'innovation architecturale - architectural innovation studio.jpg", "postal_code": "69120", "longitude": 4.9236727, "country_code": "fr", "latitude": 45.7798574, "address_1": "3 rue Maurice Audin", "capabilities": "three_d_printing;cnc_milling;laser;precision_milling;vinyl_cutting", "email": "acklab@lyon.archi.fr", "blurb": "\u2019AckLab, est un fablab tourn\u00e9 vers l\u2019architecture, comprenant trois espaces de travail mobiles. \u2018AckLab, is a fablab specialized in architecture, which includes three mobile workspaces.", "description": "\\ L\u2019Ecole Nationale Sup\u00e9rieure d\u2019Architecture de Lyon\r\n\u201cL\u2019ENSAL forme les architectes dipl\u00f4m\u00e9s d\u2019\u00c9tat, appel\u00e9s \u00e0 porter le titre d\u2019Architecte et \u00e0 exercer la responsabilit\u00e9 du projet architectural dans les conditions pr\u00e9vues en France par la loi.\u201d Nathalie Mezureux, Directrice de l\u2019ENSAL\r\nLes \u00e9tudes d\u2019architecture se d\u00e9roulent en trois cycles : licence, master, doctorat. L\u2019ENSAL propose \u00e9galement l\u2019HMONP (Habilitation \u00e0 la Maitrise d\u2019\u0152uvre en son Nom Propre) et pr\u00e9pare aux concours d\u2019AUE (Architecte Urbaniste de l\u2019Etat). \r\nLa conception du projet architectural et urbain, l\u2019exp\u00e9rimentation par la maquette et le prototypage sont des \u00e9l\u00e9ments importants de ces formations. \r\nLe laboratoire de recherche MAP-ARIA (UMR CNRS-MCC 3495), qui travaille depuis plus de 10 ans sur le continuum conception-fabrication, apporte son exp\u00e9rience en mati\u00e8re de conception num\u00e9rique. Les Ateliers d\u2019Innovation Architecturale compl\u00e8tent les Grands Ateliers de l\u2019Isle d\u2019Abeau, ateliers commun des \u00e9coles d\u2019architecture sp\u00e9cialis\u00e9s dans l\u2019exp\u00e9rimentation constructive \u00e0 \u00e9chelle 1.\r\n\\ The Lyon National Graduate School of Architecture (ENSAL)\r\n\u201cThe ENSAL trains state-certified architects, meant to use the professional title of Architect and be responsible for the implementation of the architectural project as describe in the French law.\u201d Nathalie Mezureux, the head of ENSAL.\r\n\tStudies of architecture are organized in three cycles: bachelor, master, doctorate. The ENSAL also provides the \u201cHMONP\u201d (professional qualification) and forms for the AUE (state urbanist certificate) competition.\r\nArchitectural and urban projects designing, experimentation with models and prototypes are important parts of the program.\r\nThe MAP-ARIA research laboratory (CNRS-UMR 3495 MCC), which has been working in the designing-manufacturing continuum for more than 10 years, brings its experience in digital conception. The Architectural Innovation Studio is the complement of GAIA (Grands Ateliers de l\u2019Isle d'Abeau), collective workshops for Schools of Architecture specialized in 1:1 scale construction experiment.\r\n\r\n\r\n\\ Un FabLab pour les \u00e9tudiants\r\n\u2019AckLab, Ateliers d\u2019Innovation Architecturale, est un fablab tourn\u00e9 vers l\u2019architecture, comprenant trois espaces de travail mobiles, au sein de l\u2019ENSAL.\r\nLes nouveaux locaux de 230m\u00b2, inaugur\u00e9s en Octobre 2016, sont \u00e9quip\u00e9s d\u2019outils num\u00e9riques : imprimantes 3D, d\u00e9coupes laser, CNC, d\u00e9coupe vinyle, thermoformeuse et d\u2019outils traditionnels. Ces nouveaux outils permettent aux \u00e9tudiants de concevoir et r\u00e9aliser des maquettes et prototypes pour leurs projets personnels ou p\u00e9dagogiques.\r\n\\ A FabLab for students\r\n'AckLab, Architectural Innovation Studio is a fablab specialized in architecture, which includes three mobile workspaces, in the Lyon National School of Architecture.\r\nThe new 230m\u00b2 premises, inaugurated in October 2016, are equipped with digital tools: 3D printers, lasers cutters, CNC milling machine, vinyl plotter, thermoforming machine and traditional tools. These different tools enable students to create and conceive models and prototypes for both personal and school projects. \r\n \r\n\r\n\\ Un FabLab pour tous\r\nDans un premier temps, \u2019AckLab est ouvert aux \u00e9tudiants et personnels de l\u2019ENSAL, puis aux diff\u00e9rents partenaires de l\u2019\u00e9cole : \u00e9coles, universit\u00e9s, entreprises, associations et enfin au grand public. \u2019AckLab a vocation \u00e0 s\u2019ouvrir progressivement \u00e0 tous.\r\n\\ A FabLab for all\r\nAs a first step, 'AckLab is open to ENSAL\u2019s students and staff. Then, all different school\u2019s partners will be able to get in: schools, universities, companies, associations and finally the general public. 'AckLab aims to gradually widen its audience.\r\n\r\n \r\n\\ Objectifs du FabLab\r\n-Cr\u00e9er un espace o\u00f9 les \u00e9tudiants et chercheurs peuvent penser, fabriquer et partager l\u2019innovation et la culture architecturale.\r\n-Doter l\u2019\u00e9cole d\u2019un espace et d\u2019outils pour accompagner les enseignants dans leurs processus p\u00e9dagogiques.\r\n-Encourager la production de nouveaux concepts et le partage des connaissances.\r\n-Cr\u00e9er un espace de r\u00e9flexion et d'apprentissage par des r\u00e9alisations concr\u00e8tes.\r\n-Promouvoir, diffuser la culture et l\u2019innovation architecturale sur le territoire.\r\n\\ Goals of the FabLab\r\n-Offer students and researchers a space where they can think, make and share innovation and architectural culture.\r\n-Provide tools and spaces at the school to support teachers in their educational processes.\r\n-Encourage the production of new concepts and knowledge sharing.\r\n-Create a space for thinking and learning by making.\r\n-Promote and spread the architectural culture and innovation on the territory.", "geometry": {"type": "Point", "coordinates": [4.9236727, 45.7798574]}, "country": "France"}, -{"city": "Saint-Cyr-de-Favi\u00e8res", "kind_name": "mini_fab_lab", "links": ["http://www.chantierlibre.org"], "url": "https://www.fablabs.io/labs/chantierlibre", "name": "Chantier Libre", "email": "contact@chantierlibre.org", "longitude": 4.13172464816284, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/51/50/168d65fc-ee95-4777-ab13-373e31155ad5/Chantier Libre.jpg", "phone": "06 74 98 91 09", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/11/ef612202-05a2-4e0d-ace5-f55ba04ce7c6/Chantier Libre.jpg", "postal_code": "42123", "capabilities": "three_d_printing;cnc_milling", "country_code": "fr", "latitude": 45.9741943624046, "address_1": "Place de la Gare", "coordinates": [45.9741943624, 4.13172464816], "address_2": "L'H\u00f4pital sur Rhins", "blurb": "Un fablab associatif mobile pour le nord de la Loire et les environs de Roanne, pour permettre au plus grand nombre de d\u00e9couvrir, exp\u00e9rimenter, se r\u00e9approprier la technologie \u00e0 l'aide d'outils libres.", "description": "Un FabLab (Fabrication Laboratory) est un lieu regroupant des machines num\u00e9riques, des outils et des comp\u00e9tences pour permettre \u00e0 tout le monde de cr\u00e9er et fabriquer toutes sortes de choses. Nous utilisons du mat\u00e9riel et des logiciels libres.\r\n\r\nNous croyons que les outils num\u00e9riques sont de formidables vecteurs de cr\u00e9ation, de partage et d\u2019intelligence si ils sont bien utilis\u00e9s. Nous ne parlons pas ici d\u2019accumuler les derniers gadgets superflus \u00e0 la mode, pour les remplacer quelques mois apr\u00e8s. Nous souhaitons que le plus de gens possible comprennent le r\u00f4le que jouent d\u00e9j\u00e0 les technologies de l\u2019information et de la communication dans leur vie, et donc les enjeux qui y sont li\u00e9s. Nous esp\u00e9rons aider les utilisateurs \u00e0 comprendre comment fonctionne l\u2019informatique, comment l\u2019utiliser au mieux et ainsi comment la contr\u00f4ler. C\u2019est pour cela que nous faisons la promotion du Logiciel Libre, mais aussi au del\u00e0 de la culture libre et du mat\u00e9riel libre.\r\n\r\nChantier Libre est ouvert \u00e0 tous ceux qui souhaitent utiliser ces outils libres, les d\u00e9couvrir, se faire aider, partager\u2026 Bas\u00e9 sur le nord du d\u00e9partement de la Loire, nous visons \u00e0 \u00eatre accessibles pour les habitants de l'agglom\u00e9ration de Roanne et de tout le nord du d\u00e9partement.", "geometry": {"type": "Point", "coordinates": [4.13172464816, 45.9741943624]}, "country": "France"}, -{"city": "Lourmarin", "kind_name": "fab_lab", "links": ["http://www.fablab-lourmarin.com/"], "url": "https://www.fablabs.io/labs/fablablourmarin", "name": "EPN Fablab Lourmarin", "email": "epn@lourmarin.com", "longitude": 5.36195900000007, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/55/53/0816cd1a-be88-4513-b008-91e433754973/EPN Fablab Lourmarin.jpg", "county": "Vaucluse", "phone": "0966938431", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/19/15/16695692-76fa-4bbe-8cda-e6a1bdb6a7cf/EPN Fablab Lourmarin.jpg", "postal_code": "84160", "capabilities": "three_d_printing;cnc_milling;laser;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 43.7661662, "address_1": "Cour Savornin", "coordinates": [43.7661662, 5.361959], "address_2": "Avenue du Rayol", "blurb": "Lieu de cr\u00e9ation o\u00f9 l'on peut donner libre cours \u00e0 son imagination et mat\u00e9rialiser ses conceptions, ses prototypes.", "description": "Un espace qui permet de mettre l'innovation \u00e0 port\u00e9e de tous et de tisser des relations \u00e0 travers la cr\u00e9ativit\u00e9, le partage et la cr\u00e9ation collaborative.", "geometry": {"type": "Point", "coordinates": [5.361959, 43.7661662]}, "country": "France"}, -{"city": "Saint-Di\u00e9-des-Vosges", "coordinates": [48.2809696389, 6.94815979577], "kind_name": "fab_lab", "links": ["https://www.facebook.com/fablabvosges"], "url": "https://www.fablabs.io/labs/fablabvosges", "name": "FabLab Vosges", "longitude": 6.94815979577027, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/56/02/76930dd8-ef8a-4e93-b03e-e5871a4314c0/FabLab Vosges.jpg", "phone": "0674590344", "postal_code": "88100", "capabilities": "three_d_printing;cnc_milling;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 48.280969638864, "address_1": "15 Rue du Petit Saint-Di\u00e9", "address_notes": "1er \u00e9tage chez Reversale Developpement", "email": "contact@fablab-vosges.fr", "blurb": "FabLab classique avec une sp\u00e9cialit\u00e9 dans l'internet des objets et du monde connect\u00e9.", "description": "FabLab permettant de b\u00e9n\u00e9ficier des comp\u00e9tences d'un bureau d'\u00e9tude d'\u00e9lectronique (Reversale Developpement) sp\u00e9cialis\u00e9 dans les produits connect\u00e9s et dans l'internet des objets.\r\n\r\nNous sommes actuellement h\u00e9berg\u00e9 par la P\u00e9pini\u00e8re de Saint-Di\u00e9 des Vosges.\r\n\r\nMat\u00e9riel actuellement disponible :\r\nImprimante 3D FlashForge Creator\r\n2 x Fraiseuses \u00e0 commande num\u00e9rique : 100x50cm et 150x100cm\r\nMachine \u00e0 coudre\r\nStation de montage vid\u00e9o\r\nAppareil de prise de vue professionnel\r\n\r\n\u00catres humains actuellement disponibles : \r\nPlusieurs cr\u00e9atifs (\u00e2g\u00e9s de 24 \u00e0 54 ans), passionn\u00e9s d'impression 3D, de cr\u00e9ation num\u00e9rique (photo,vid\u00e9o...), d'innovation, d'usinage, d'\u00e9lectronique, de d\u00e9veloppement d'objets connect\u00e9s...\r\n \r\nNous sommes actuellement en train de d\u00e9velopper notre communaut\u00e9.\r\n\r\nNicolas", "geometry": {"type": "Point", "coordinates": [6.94815979577, 48.2809696389]}, "country": "France"}, -{"city": "Bordeaux", "kind_name": "fab_lab", "links": ["http://127.cap-sciences.net/#!/"], "url": "https://www.fablabs.io/labs/fablabdu127degres", "name": "Fab Lab du 127\u00b0", "longitude": -0.560466899999938, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/57/15/f031b2e1-26ba-48d3-b0eb-f9a493e08ff1/ Fab Lab du 127\u00b0 .jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/20/31/4666f1c5-4e6f-4899-aa90-f881e03f0c2c/ Fab Lab du 127\u00b0 .jpg", "postal_code": "33300", "coordinates": [44.8577845, -0.5604669], "country_code": "fr", "latitude": 44.8577845, "address_1": "Cap Sciences, 20 Quai de Bacalan", "capabilities": "three_d_printing;cnc_milling;laser;precision_milling;vinyl_cutting", "email": "fabmanager@cap-sciences.net", "blurb": "Port\u00e9 par Cap Sciences dans le cadre du programme 'Inm\u00e9diats', en tant que centre de sciences, nous montrons au grand public ce que sont les laboratoires fabrication num\u00e9rique!", "description": "Le 127\u00b0 comporte un Fab Lab, c'est \u00e0 dire un atelier de fabrication num\u00e9rique o\u00f9 vous pouvez utiliser toutes sortes de machines (d\u00e9coupe laser, imprimantes 3D\u2026) permettant de travailler sur des mat\u00e9riaux vari\u00e9s (plastique, bois, carton, vinyle\u2026) afin de fabriquer (presque) n'importe quoi ! Il s'agit aussi d'un lieu d\u2019\u00e9changes et de rencontres, g\u00e9n\u00e9rateur d'id\u00e9es innovantes.\r\n\r\nLe 127\u00b0 est un espace permanent : ouvert \u00e0 tous, il offre la possibilit\u00e9 de r\u00e9aliser des objets soi-m\u00eame, de partager ses comp\u00e9tences et d\u2019apprendre au contact des m\u00e9diateurs et des autres usagers.", "geometry": {"type": "Point", "coordinates": [-0.5604669, 44.8577845]}, "country": "France"}, -{"city": "Lormes", "description": "Le FabLab du Morvan propose : imprimantes 3D, fraiseuse CNC, D\u00e9coupeuse vinyl, D\u00e9coupeuse \u00e0 fil chaud, \u00e9lectronique, dr\u00f4ne vid\u00e9o, mini studio photo, scanner 3D", "links": ["http://www.nivernaismorvan.net"], "parent_id": 874, "url": "https://www.fablabs.io/labs/fablabdumorvan", "coordinates": [47.2985221, 3.8243096], "name": "FabLab du Morvan", "phone": "+33.3.86.22.51.42", "postal_code": "58140", "longitude": 3.82430959999999, "country_code": "fr", "latitude": 47.2985221, "address_1": "114 Route d'Avallon", "capabilities": "three_d_printing;cnc_milling;circuit_production;vinyl_cutting", "email": "fablabdumorvan@gmail.com", "blurb": "Le FabLab du Morvan est ouvert \u00e0 tous.", "kind_name": "fab_lab", "geometry": {"type": "Point", "coordinates": [3.8243096, 47.2985221]}, "country": "France"}, -{"links": ["http://sigmake.jimdo.com/"], "county": "Auvergne", "postal_code": "63178", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "country_code": "fr", "kind_name": "fab_lab", "city": "Aubiere", "coordinates": [45.7576618, 3.1129966], "parent_id": 357, "latitude": 45.7576618, "email": "sigmake@sigma-clermont.fr", "blurb": "SIGMAke was created in a french engineering school specialized in chemistry and mechanics. Its specialization is therefore more directed towards mechanics, design and mecatronics.", "description": "SIGMAke is a place where we share knowledge and creation in mechnical design, construction and mecatronics (bring our objects to life...). We have, then, milling and turning machines, added to 3D printers, and scanners, laser cutting, electronics bench... The Lab brings the opportunity to work with all material types from steel to cardboard, including plastics, wood... and mix tem to build complex animated mechanisms, or simple everyday things.", "phone": "+33 473 28 80 92", "name": "SIGMAke", "url": "https://www.fablabs.io/labs/sigmake", "longitude": 3.11299659999997, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/04/18/4a679396-748c-499d-bcd2-ab758fdb3b16/SIGMAke.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/27/36/069ab0c6-a03f-4010-9a4f-ebfd2e423363/SIGMAke.jpg", "address_1": "SIGMAke - SIGMA Clermont", "address_2": "Campus des Cezeaux", "address_notes": "SIGMAke is inside the same building as the Center for Technology Center", "geometry": {"type": "Point", "coordinates": [3.1129966, 45.7576618]}, "country": "France"}, -{"city": "Rodez", "description": "Since the beginning of the adventure, the MJC de Rodez develops access to digital tools and combines all young people interested. Clubs (columnists radio, hack digital) passing through the workshops, the project offers multiple points of entry.\r\n Thanks to funding from the EYF, only the MJC card is requested to participate in the workshops. Be creative, enter the participatory universe !", "links": ["https://twitter.com/fablabrutech", "https://www.facebook.com/FabLabRuTech/", "http://rutech.fr/"], "parent_id": 21, "url": "https://www.fablabs.io/labs/fablabrodez", "longitude": 2.57828429999995, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/47/44/0d983ec1-0e9c-4c7b-941e-157a4b44e269/RuTech FabLab MJC Rodez.jpg", "phone": "05.65.67.01.13", "kind_name": "fab_lab", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/28/09/bdd45ad0-1b08-4212-b691-466cef542cb3/RuTech FabLab MJC Rodez.jpg", "postal_code": "12000", "coordinates": [44.3532319, 2.5782843], "country_code": "fr", "latitude": 44.3532319, "address_1": "1 rue Saint-Cyrice.", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;vinyl_cutting", "email": "info@rutech.fr", "name": "RuTech FabLab MJC Rodez", "geometry": {"type": "Point", "coordinates": [2.5782843, 44.3532319]}, "country": "France"}, -{"city": "Saint-\u00c9tienne", "coordinates": [45.450766, 4.386989], "kind_name": "fab_lab", "links": ["http://movilab.org/index.php?title=OpenFactory", "http://www.openfactory42.org"], "url": "https://www.fablabs.io/labs/openfactory42", "name": "OpenFactory", "longitude": 4.38698899999997, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/00/20/73c43508-f733-4089-b12e-428aef637bf7/OpenFactory.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/23/51/f49c5a6d-3449-4f9c-a7ea-cb205d6c7a6f/OpenFactory.jpg", "postal_code": "42000", "capabilities": "three_d_printing;circuit_production;vinyl_cutting", "country_code": "fr", "latitude": 45.450766, "address_1": "5 Rue Javelin Pagnon", "address_notes": "In historical buildings, in front of the observatory tower.", "email": "fabmanager@openfactory42.org", "blurb": "An independent associative FabLab in Saint-Etienne.", "description": "OpenFactorySaint\u00e9 est une Usine Ouverte (Open Factory) \u00e0 mi-chemin entre un FabLab, un TechShop et un HackerSpace, un projet d\u2019ambition pour le territoire st\u00e9phanois et le quartier cr\u00e9atif Manufacture-Plaine Achille.\r\n\r\n\u00c9galement appel\u00e9 le \u00ab FabLab du Mixeur \u00bb, OpenFactorySaint\u00e9 constitue un lieu ouvert \u00e0 tous et toutes, permettant d\u2019acc\u00e9der \u00e0 des outils de conception, de simulation et de maquettage num\u00e9rique. Il assure par ailleurs l\u2019acc\u00e8s en temps partag\u00e9 \u00e0 des outils de fabrication num\u00e9rique, incluant un atelier \u00e9lectronique ou des machines-outils : imprimante 3D, machines \u00e0 coudre, d\u00e9coupeuse \u00e0 vinyle.\r\n\r\nOpenFactorySaint\u00e9 est le fruit de la conjugaison des comp\u00e9tences de plusieurs acteurs sp\u00e9cialis\u00e9s dans l\u2019activit\u00e9 de la fabrication num\u00e9rique, du design et du conseil afin de se doter d\u2019un processus unique d\u00e9di\u00e9 \u00e0 l\u2019innovation et au service du d\u00e9veloppement de l\u2019\u00e9conomie lig\u00e9rienne.", "geometry": {"type": "Point", "coordinates": [4.386989, 45.450766]}, "country": "France"}, -{"city": "Metz", "coordinates": [49.1262692, 6.182086], "kind_name": "fab_lab", "links": ["http://metzfablab.fr", "http://ecofablab.fr", "http://mdesign.fr "], "url": "https://www.fablabs.io/labs/MDesign", "name": "Eco FabLab MDesign", "longitude": 6.18208600000003, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/01/25/918fb1d1-88d3-40ab-9a75-756482a722d9/Eco FabLab MDesign.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/24/58/44d47209-7d22-408e-956e-d9bb2fd090dd/Eco FabLab MDesign.jpg", "postal_code": "57000", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 49.1262692, "address_1": "7 Avenue de Blida", "address_notes": "Ring the \"administration\" bell :)\r\nSonnez chez administration :)", "email": "mail@mdesign.fr", "blurb": "MDesign is an Eco Fablab based in Metz, France. Our goal is to empower people with the new tools of makers, as well as transmitting the knowledge of classic manufacturing techniques.", "description": "En : Various profiles of members bring creativity and a happy way to make things together in our lab.\r\nshare your experience and your projects and be part of our Eco Fablab MDesign !\r\n\r\nFr : Au FabLab MDesign, vous trouverez tous les outils pour cr\u00e9er, inventer et partager autour de projets partag\u00e9s!\r\nvous pourrez apprendre l'utilisation de l'Impression 3D, la gravure lazer, le fraisage \u00e0 commande num\u00e9rique, ainsi que les outils classiques du tourne vis \u00e0 la perceuse :)", "geometry": {"type": "Point", "coordinates": [6.182086, 49.1262692]}, "country": "France"}, -{"city": "Bras-sur-Meuse", "coordinates": [49.2100269, 5.377166], "kind_name": "fab_lab", "links": ["http://numerifab.jimdo.com/"], "url": "https://www.fablabs.io/labs/numrifab", "name": "Num\u00e9rifab", "longitude": 5.37716599999999, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/01/53/dfc56f57-58b5-4c79-b202-3abfb38413f3/Num\u00e9rifab.jpg", "phone": "00 33 3 29 85 67 15", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/25/27/a91f2485-07ac-4f6f-a371-1978c605d1f3/Num\u00e9rifab.jpg", "postal_code": "55100", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "country_code": "fr", "latitude": 49.2100269, "address_1": "3 Place de la Mairie", "address_notes": "Dans les locaux de la Mairie, acc\u00e8s par l'arri\u00e8re.", "email": "lenumerifab@gmail.fr", "blurb": "Fablab en Meuse", "description": "Laboratoire de fabrication num\u00e9rique \u00e0 Bras/Meuse", "geometry": {"type": "Point", "coordinates": [5.377166, 49.2100269]}, "country": "France"}, -{"city": "Paris", "kind_name": "fab_lab", "links": ["http://www.ece.fr/event/innovawards/", "https://www.facebook.com/projetsece/?notif_t=page_user_activity", "http://projects.ece.fr/", "http://www.ece.fr/ecole-ingenieur/cursus/projets-etudiants/fablab-ece-makers/"], "url": "https://www.fablabs.io/labs/ECEmakers", "name": "ECEmakers", "email": "buruian@ece.fr", "longitude": 2.28609949999998, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/03/31/2f76f3d1-b7a5-4e8a-bbdc-593ccfc93301/ECEmakers.jpg", "phone": "+33144390600", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/26/52/dac603a6-51b7-41e8-8552-95b62c9ec65a/ECEmakers.jpg", "postal_code": "75015", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "country_code": "fr", "latitude": 48.852152, "address_1": "37 Quai de Grenelle", "coordinates": [48.852152, 2.2860995], "address_2": "Immeuble Pollux", "blurb": "ECEmakers is a prototyping area primarily for students but open to business partners, startups and others. It is located at ECE Paris, a general and high-tech engineering school .", "description": "ECEmakers is defined as a fabrication laboratory adapted to the specificity of ECE\u2019s project-based teaching. Every year we have more than 300 projects and an important number have needs in making a proof of concept and sometimes even a prototype.\r\nWe also have partnerships with a lot of companies which propose subjects of new projects and work together with our students and staff.\r\nAnd because we love to share our experience with anyone is interested we organise workshops and guided visites.\r\nOur laboratory is used by all the members of our incubator ECECube in the development of their prototypes. Some successful example: Prizm (www.meetprizm.com) , Enovap (www.enovap.com/en) or Kuantom (http://www.kuantom.com) to mention just three of them.", "geometry": {"type": "Point", "coordinates": [2.2860995, 48.852152]}, "country": "France"}, -{"city": "Mont-Saint-Aignan", "kind_name": "mini_fab_lab", "links": ["http://corporate.cesi.fr/centre-rouen-mont-st-aignan.asp"], "url": "https://www.fablabs.io/labs/fablabducesirouen", "name": "FabLab du CESI Rouen", "email": "ctsafack@cesi.fr", "longitude": 1.0912687, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/14/03/44/3ad1664a-3bb9-4cfd-b025-b3e39aecc84f/FabLab du CESI Rouen.jpg", "phone": "+33 235 595 081", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/27/01/49b8d3f5-4693-47eb-bc34-3ae276d3db01/FabLab du CESI Rouen.jpg", "postal_code": "76130", "capabilities": "three_d_printing;cnc_milling;laser", "country_code": "fr", "latitude": 49.4752409, "coordinates": [49.4752409, 1.0912687], "address_2": "Parc de La Vatine", "blurb": "Ce FabLab appuy\u00e9 \u00e0 l'\u00e9cole d'ing\u00e9nieur du CESI de Rouen, propose ses moyens aux particuliers et professionnels", "address_1": "9 rue Andre\u00ef Sakharov", "geometry": {"type": "Point", "coordinates": [1.0912687, 49.4752409]}, "country": "France"}, -{"city": "ST GEORGES DE MONTAIGU", "description": "Our Lab works quite like a gym. We offer month subscriptions, personnal coaching, workshops for 4-6 people, OpenLabs and some free moments to discover our way to create, learn, share and make differently with collective intelligence. You're more than welcome to come over here !\r\n\r\nIn here, you'll find first a great community and some machines too :\r\n\r\nSEWING\r\n1 Serger : PFAFF 1230OL\r\n1 Digital embroiderer : HUSQVARNA VIKING TOPAZ 50\r\n1 Sewing Machine Singer\r\n\r\nWOOD/WORKSHOP\r\n1 Dremel \r\n1 Drill Press\r\n1 ShopBot\r\nA few useful tools (saw, hammer, spanners, screw drivers...)\r\n\r\n3D PRINTING\r\n1 Ultimaker 3\r\n2 Ultimaker 2\r\n1 Up! Mini\r\n1 Dagoma Discovery 200\r\n1 FormLabs 1+\r\n\r\n3D SCANNERS\r\n1 DAVID SLS-2\r\n2 3D Sense\r\n\r\nVINYL CUTTING\r\n1 Silhouette Cameo\r\n\r\nLASER\r\n1 Speedy Trotec 300 60W\r\n1 Epilog Helix 60W\r\n\r\nCOMPUTERS and CO\r\nA few Laptops (Core i3/i5)\r\nA few tablets Nexus 7 2013\r\nA few 24/27 screens\r\n1 Videoprojector\r\n2 TV Screen 105\"\r\n1 Audio 5.1 System and 2 microphones\r\n\r\nCOWORKING\r\nTennis Table as modular desktop :)\r\n\r\nELECTRONIC\r\nArduino Unos\r\nStarter kits\r\nPhotons\r\nLittleBits\r\nA few kits \r\nElectronic components\r\n\r\nROBOTIC\r\n1 Thymio \r\n3 Dash and Dots\r\n\r\n+ A Great Coffee machine, a kitchen, a library, big and small meeting rooms, a shower, 3 toilets, a giant 400m2 space and enough place to gather and have nice chats !", "links": ["http://www.zbis.fr"], "parent_id": 15, "url": "https://www.fablabs.io/labs/zbis", "email": "contact@zbis.fr", "longitude": -1.30090429999996, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/02/07/23/04/17/2b6db249-95a9-4ad5-9627-a675593bf733/IMG_20161124_203422.jpg", "county": "Vend\u00e9e", "phone": "+33980519597", "kind_name": "fab_lab", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/02/07/23/04/17/e1470bdb-065b-40bf-a868-55a846d46dc1/logo-zBis-couleurs-carre.jpg", "postal_code": "85600", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "country_code": "fr", "latitude": 46.9596866, "address_1": "ZI de Chassereau", "coordinates": [46.9596866, -1.3009043], "address_2": "Rue Pasteur", "blurb": "zBis is a Micro-factory local and shared. In the heart of western France, close to the coast, we try do democratize digital fabrication and creation for kids, youngs, adults, simple citizen and pros", "name": "zBis", "geometry": {"type": "Point", "coordinates": [-1.3009043, 46.9596866]}, "country": "France"}, -{"city": "Clermont-Ferrand", "kind_name": "mini_fab_lab", "links": ["http://acolab.fr"], "capabilities": "three_d_printing;circuit_production;vinyl_cutting", "url": "https://www.fablabs.io/labs/acolab", "coordinates": [45.7941993299, 3.07563051059], "name": "ACoLab", "phone": "+33(0)651800518", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/48/fd42c5cd-21ac-4abf-9a20-8f9bb602c7b1/ACoLab.jpg", "postal_code": "63000", "longitude": 3.07563051058958, "country_code": "fr", "latitude": 45.7941993298608, "address_1": "2 bis rue du Clos Perret", "address_notes": "Au quatri\u00e8me \u00e9tage du b\u00e2timent, entr\u00e9e par le 2bis rue du Clos Perret\r\n\r\nAdresse 'historique' (2013/Mai2015), chez les Petits D\u00e9brouillards d'Auvergne : 32 Rue du Pont Naturel, 63000 Clermont-Ferrand\r\nIl faut traverser la petite place entre les immeubles et descendre quelques marches.", "email": "contact@acolab.fr", "blurb": "Atelier Collaboratif - Ouvert les lundi et mercredi soir", "description": "FabLab associatif cr\u00e9e en 2013\r\n\u00c9quip\u00e9 d'une d\u00e9coupeuse vinyle, d'une imprimante 3D type Mendel Max, d'un petit tour \u00e0 m\u00e9taux, utilisation d'Arduino, de Raspberry Pi...\r\n\r\nBeaucoup de r\u00e9cup\u00e9ration et de bidouillages vari\u00e9s dans la bonne humeur et le partage.", "geometry": {"type": "Point", "coordinates": [3.07563051059, 45.7941993299]}, "country": "France"}, -{"city": "brest", "kind_name": "fab_lab", "links": ["http://wiki.lesfabriquesduponant.net", "http://www.lesfabriquesduponant.net"], "url": "https://www.fablabs.io/labs/lesfabriquesduponant", "name": "Les Fabriques du Ponant", "longitude": -4.47982980000006, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/48/32/6d2e62f0-0f08-424a-883e-b9a15e90ee8a/Les Fabriques du Ponant.jpg", "phone": "+33.685176295", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/02/08/10/11/15/20e09c48-5ac6-40fc-8462-bce909c24de0/531px-Logofabdupo.png", "postal_code": "29200", "coordinates": [48.4086189, -4.4798298], "country_code": "fr", "latitude": 48.4086189, "address_1": "40, rue Jules Lesven", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;vinyl_cutting", "email": "contact@lesfabriquesduponant.net", "blurb": "\"Les Fabrique du Ponant\" is run by \"T\u00e9l\u00e9com Bretagne\" and \"Les petits d\u00e9brouillards\". Its main goal is to propose digital manufacturing services, organise digital cultural events and digital education", "description": "Installed in high school Vauban in Brest, \"Les Fabrique du Ponant\" (which can be translate in \"Factories Ponant\") offer a coworking space, a fully equipped fablab, a webTV studio, a training room. \"Les Fabrique du Ponant\" organize demonstrations (initiation days and discovery), cultural events on digital as the \"Open Bidouille Camp\" or \"Science Hack Day\", trainings, educational activities.", "geometry": {"type": "Point", "coordinates": [-4.4798298, 48.4086189]}, "country": "France"}, -{"city": "Tours", "coordinates": [47.3932037, 0.6687421], "kind_name": "mini_fab_lab", "links": ["http://funlab.fr"], "url": "https://www.fablabs.io/labs/funlab", "name": "FunLab Tours", "longitude": 0.668742100000031, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/50/06/7863f4ba-28b3-4018-9351-c1d8c70a5b69/FunLab Tours.jpg", "phone": "+33603951216", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/52/7d30f2aa-d5b7-482a-8334-a72d17e0a6fe/FunLab Tours.jpg", "postal_code": "37000", "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "country_code": "fr", "latitude": 47.3932037, "address_1": "49, boulevard Preuilly", "address_notes": "Nous sommes occupants de site MAME \"cit\u00e9 de la cr\u00e9ation et du num\u00e9rique\"", "email": "contact@funlab.fr", "blurb": "Fabrique d'Usages Num\u00e9riques", "description": "La communaut\u00e9 existe, des rencontres toutes les semaines. 49, Boulevard Preuilly, 37000 Tours.", "geometry": {"type": "Point", "coordinates": [0.6687421, 47.3932037]}, "country": "France"}, -{"city": "Bron", "kind_name": "fab_lab", "links": ["http://fablab-lyon.fr"], "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "url": "https://www.fablabs.io/labs/fabriquedobjetslibres", "name": "Fabrique d'Objets Libres", "email": "contact@fabriquedobjetslibres.fr", "coordinates": [45.7429334, 4.9082135], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/50/01/0190e790-aaec-4f2f-9985-11156655145d/Fabrique d'Objets Libres.jpg", "county": "Rh\u00f4ne", "phone": "+33 7 68 01 40 26 (Tue-Sat 2pm-6pm)", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/49/73ea9f2d-0216-4f52-a6bf-2ff97ee474b2/Fabrique d'Objets Libres.jpg", "postal_code": "69500", "longitude": 4.90821349999999, "country_code": "fr", "latitude": 45.7429334, "address_1": "All\u00e9e Gaillard Romanet", "address_notes": "Au sous-sol de la MJC. Downstairs inside the MJC.", "address_2": "MJC Louis Aragon", "blurb": "Le fablab lyonnais, install\u00e9 \u00e0 la MJC Louis Aragon de Bron, ouvert tous les mercredis et formation hebdomadaire de fabrication num\u00e9rique. Projets autour du handicap, des arts et du recyclage.", "description": "La Fabrique d'Objets Libres est un fablab associatif sur Lyon et sa r\u00e9gion. Install\u00e9 \u00e0 la MJC Louis Aragon de Bron depuis janvier 2013, c'est un espace de cr\u00e9ation et de fabrication num\u00e9rique ouvert \u00e0 tous, qui permet \u00e0 chacun de d\u00e9couvrir, d'inventer et de fabriquer tout type d'objet.\r\n \r\nV\u00e9ritable laboratoire citoyen de fabrication, la Fabrique d\u2019Objets Libres met \u00e0 disposition de ses adh\u00e9rents des outils \u00e0 commande num\u00e9rique et des mati\u00e8res premi\u00e8res secondaires permettant de concevoir et de fabriquer localement des objets libres.\r\nC\u2019est une plate-forme pluridisciplinaire collaborative qui m\u00eale les profils (techniciens, informaticiens, ing\u00e9nieurs, scientifiques, bricoleurs, cr\u00e9ateurs...) et les g\u00e9n\u00e9rations afin de r\u00e9unir tous types de comp\u00e9tences.\r\n\r\nLe fablab est ouvert tous les mercredis pour les \"temps libres\", durant lesquels les adh\u00e9rents utilisent les machines librement. Par ailleurs, il propose un atelier hebdomadaire aux adh\u00e9rents de la MJC, \"De l'id\u00e9e \u00e0 l'objet\": en une dizaine de s\u00e9ances sur un trimestre, les participants apprennent \u00e0 utiliser toutes les machines du fablab pour r\u00e9aliser leurs objets, et r\u00e9fl\u00e9chissent autour d'une th\u00e9matique sociale comme le handicap, la musique, ou la ville.\r\n\r\nL'association organise \u00e9galement des \u00e9v\u00e9nements et ateliers th\u00e9matiques utilisant la fabrication num\u00e9rique autour de sujet plus vastes, comme l'art, avec les machines \u00e0 dessiner, ou le handicap, dans le cadre du projet Handilab, ou encore la fin de vie des objets, avec le Laboratoire de l'Obsolescence D\u00e9programm\u00e9e. Enfin, le fablab s'associe \u00e0 d'autres associations et des entreprises pour des projets communs.", "geometry": {"type": "Point", "coordinates": [4.9082135, 45.7429334]}, "country": "France"}, -{"city": "N\u00e9ons-sur-Creuse", "kind_name": "fab_lab", "links": ["http://www.rurallab.org"], "url": "https://www.fablabs.io/labs/rurallab", "coordinates": [46.744746, 0.931698], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/49/00/95c7b9f2-a034-4b2b-931d-43ced33ddfb1/RuralLab.jpg", "phone": "+33603318810", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/12/49/ec5f7c54-e6ce-40fd-b5c5-c4142d208e6b/RuralLab.jpg", "postal_code": "36220", "longitude": 0.931697999999983, "country_code": "fr", "latitude": 46.744746, "address_1": "Rue de l'\u00c9cole", "email": "rurallab36@gmail.com", "blurb": "A FabLab in the countryside in Neons sur Creuse, France", "name": "RuralLab", "geometry": {"type": "Point", "coordinates": [0.931698, 46.744746]}, "country": "France"}, -{"city": "Gif-sur-Yvette", "kind_name": "supernode", "links": ["http://fablab.digiscope.fr/#!/", "http://fablabdigiscope.wordpress.com"], "url": "https://www.fablabs.io/labs/fablabdigiscope", "name": "(Fab)Lab Digiscope", "longitude": 2.16830979999997, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/52/18/8d63351d-c2fb-4a90-8e58-bb45422202a6/(Fab)Lab Digiscope.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/46/51553da4-b295-426c-837f-934c311933ba/(Fab)Lab Digiscope.jpg", "postal_code": "91190", "coordinates": [48.7117632, 2.1683098], "country_code": "fr", "latitude": 48.7117632, "address_1": "660 Rue Noetzlin", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "email": "fablabdigiscope@gmail.com", "blurb": "(FAB)LAB DIGISCOPE is a fabrication laboratory dedicated to research in sciences | design | education | art | engineering and what ever field of research you come from. Open to Everyone. Book now!", "description": "(FAB)LAB DIGISCOPE is a fabrication laboratory dedicated to research in sciences | design | education | arts | engineering and what ever field of research you come from. We host Fab Academy and Bio Academy. We host Digital Fabrication Classes for EITC Master. Open to Everyone since the beginning.\r\n\r\nFablab Digiscope started in 2013 when Aviz-INRIA research team director Jean-Daniel Fekete and colleague researcher Pierre Dragicevic hired Romain Di Vozzo as a R&D Engineer to be the fablab manager of what would later become an attractive place on the new Campus Paris-Saclay. Fablab Digiscope is part of the Digiscope Project, a network of 10 high-performance platforms for interactive visualization of large datasets and complex computation for which Michel Beaudouin-Lafon is the scientific Director. Fablab Digiscope is mutualised between 10 institutions involved in research and education.\r\n\r\nRomain Di Vozzo runs and develops Fablab Digiscope everyday, trains publics, designs objects, shares creative thoughts, gives advices on designs, etc. Romain also actively collaborates to the globally distributed fablab network and with the Fab Foundation by operating as Fab Academy SuperNode, as Instructor for Fab Academy and Bio Academy, by giving conferences and workshops in France and abroad and by performing very small tasks that make the fablab network grow.", "geometry": {"type": "Point", "coordinates": [2.1683098, 48.7117632]}, "country": "France"}, -{"city": "Metz", "kind_name": "fab_lab", "links": ["http://graoulab.org/wiki", "http://graoulab.org"], "url": "https://www.fablabs.io/labs/graoulab", "coordinates": [49.1262692, 6.182086], "name": "GraouLab", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/18/24/af4709d8-1f60-48a7-ba35-4c42ef40a195/GraouLab.jpg", "postal_code": "57000", "longitude": 6.18208600000003, "country_code": "fr", "latitude": 49.1262692, "capabilities": "three_d_printing;laser", "email": "contact@graoulab.org", "blurb": "The FabLab of Metz. A place for folks innovation.", "address_1": "7 Avenue de Blida", "geometry": {"type": "Point", "coordinates": [6.182086, 49.1262692]}, "country": "France"}] \ No newline at end of file diff --git a/bonobo/examples/datasets/services.py b/bonobo/examples/datasets/services.py new file mode 100644 index 0000000..eb9e8e8 --- /dev/null +++ b/bonobo/examples/datasets/services.py @@ -0,0 +1,7 @@ +import bonobo + + +def get_services(): + return { + 'fs': bonobo.open_fs(bonobo.get_examples_path('datasets')) + } \ No newline at end of file diff --git a/bonobo/execution/strategies/executor.py b/bonobo/execution/strategies/executor.py index 44a2160..1e2d45f 100644 --- a/bonobo/execution/strategies/executor.py +++ b/bonobo/execution/strategies/executor.py @@ -54,7 +54,9 @@ class ExecutorStrategy(Strategy): with node: node.loop() except: - logging.getLogger(__name__).critical('Critical error in threadpool node starter.', exc_info=sys.exc_info()) + logging.getLogger(__name__).critical( + 'Critical error in threadpool node starter.', exc_info=sys.exc_info() + ) try: futures.append(executor.submit(_runner)) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index a1b1076..9708996 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -16,6 +16,7 @@ __all__ = [ 'FixedWindow', 'Format', 'Limit', + 'OrderFields', 'PrettyPrinter', 'Rename', 'SetFields', @@ -196,8 +197,39 @@ class FixedWindow(Configurable): buffer.set([]) +@transformation_factory +def OrderFields(fields): + """ + Transformation factory to reorder fields in a data stream. + + :param fields: + :return: callable + """ + fields = list(fields) + + @use_context + @use_raw_input + def _OrderFields(context, row): + nonlocal fields + context.setdefault('remaining', None) + if not context.output_type: + context.remaining = list(sorted(set(context.get_input_fields()) - set(fields))) + context.set_output_fields(fields + context.remaining) + + yield tuple(row.get(field) for field in context.get_output_fields()) + + return _OrderFields + + @transformation_factory def SetFields(fields): + """ + Transformation factory that sets the field names on first iteration, without touching the values. + + :param fields: + :return: callable + """ + @use_context @use_no_input def _SetFields(context): From a8ff1b4df1cc13f02b2755c71d4cd8331c12a6b5 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 2 Dec 2017 14:51:33 +0100 Subject: [PATCH 124/145] [tests] adding a spec to magicmock of nodes to avoid it being seen as partially configured nodes --- tests/execution/contexts/test_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/execution/contexts/test_node.py b/tests/execution/contexts/test_node.py index c9c8f1f..5af665d 100644 --- a/tests/execution/contexts/test_node.py +++ b/tests/execution/contexts/test_node.py @@ -186,7 +186,7 @@ def test_node_tuple_dict(): def test_node_lifecycle_natural(): - func = MagicMock() + func = MagicMock(spec=object()) ctx = NodeExecutionContext(func) assert not any((ctx.started, ctx.stopped, ctx.killed, ctx.alive)) @@ -205,7 +205,7 @@ def test_node_lifecycle_natural(): def test_node_lifecycle_with_kill(): - func = MagicMock() + func = MagicMock(spec=object()) ctx = NodeExecutionContext(func) assert not any((ctx.started, ctx.stopped, ctx.killed, ctx.alive)) From 893a61aba2e2d210ff1a73e4311d834d703866fd Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 2 Dec 2017 15:03:13 +0100 Subject: [PATCH 125/145] For some obscure reason, coverage is broken under python 3.7 making the test suite fail, disabled python3.7 in travis waiting for it to be fixed. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 27eebb7..c27e1a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ python: - 3.5-dev - 3.6 - 3.6-dev - - 3.7-dev - - nightly +# - 3.7-dev +# - nightly install: - make install-dev - pip install coveralls From 7a25774b0f8d06f5035a14855d683c318d82f0fc Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 2 Dec 2017 15:57:14 +0100 Subject: [PATCH 126/145] Removing datasets from the repository. --- bonobo/examples/datasets/__main__.py | 35 +++++++++++++++++- bonobo/examples/datasets/coffeeshops.py | 5 ++- bonobo/examples/datasets/fablabs.py | 5 ++- bonobo/examples/datasets/services.py | 15 ++++++-- .../examples/datasets/{ => static}/Makefile | 0 .../examples/datasets/{ => static}/passwd.txt | 0 .../examples/datasets/{ => static}/spam.tgz | Bin .../datasets/{ => static}/theaters.json | 0 8 files changed, 54 insertions(+), 6 deletions(-) rename bonobo/examples/datasets/{ => static}/Makefile (100%) rename bonobo/examples/datasets/{ => static}/passwd.txt (100%) rename bonobo/examples/datasets/{ => static}/spam.tgz (100%) rename bonobo/examples/datasets/{ => static}/theaters.json (100%) diff --git a/bonobo/examples/datasets/__main__.py b/bonobo/examples/datasets/__main__.py index a066e07..8dd4c49 100644 --- a/bonobo/examples/datasets/__main__.py +++ b/bonobo/examples/datasets/__main__.py @@ -1,8 +1,10 @@ +import os + import bonobo from bonobo import examples from bonobo.examples.datasets.coffeeshops import get_graph as get_coffeeshops_graph from bonobo.examples.datasets.fablabs import get_graph as get_fablabs_graph -from bonobo.examples.datasets.services import get_services +from bonobo.examples.datasets.services import get_services, get_datasets_dir graph_factories = { 'coffeeshops': get_coffeeshops_graph, @@ -14,6 +16,7 @@ if __name__ == '__main__': parser.add_argument( '--target', '-t', choices=graph_factories.keys(), nargs='+' ) + parser.add_argument('--sync', action='store_true', default=False) with bonobo.parse_args(parser) as options: graph_options = examples.get_graph_options(options) @@ -22,8 +25,38 @@ if __name__ == '__main__': if options['target'] else sorted(graph_factories.keys()) ) + # Create a graph with all requested subgraphs graph = bonobo.Graph() for name in graph_names: graph = graph_factories[name](graph, **graph_options) bonobo.run(graph, services=get_services()) + + if options['sync']: + # XXX/TODO: when parallel option for node will be implemented, need to be rewriten to use a graph. + import boto3 + + s3 = boto3.client('s3') + + local_dir = get_datasets_dir() + for root, dirs, files in os.walk(local_dir): + for filename in files: + local_path = os.path.join(root, filename) + relative_path = os.path.relpath(local_path, local_dir) + s3_path = os.path.join( + bonobo.__version__, relative_path + ) + + try: + s3.head_object( + Bucket='bonobo-examples', Key=s3_path + ) + except: + s3.upload_file( + local_path, + 'bonobo-examples', + s3_path, + ExtraArgs={ + 'ACL': 'public-read' + } + ) diff --git a/bonobo/examples/datasets/coffeeshops.py b/bonobo/examples/datasets/coffeeshops.py index 85883d7..93aa0d5 100644 --- a/bonobo/examples/datasets/coffeeshops.py +++ b/bonobo/examples/datasets/coffeeshops.py @@ -58,4 +58,7 @@ if __name__ == '__main__': parser = examples.get_argument_parser() with bonobo.parse_args(parser) as options: - bonobo.run(get_graph(**examples.get_graph_options(options)), services=get_services()) + bonobo.run( + get_graph(**examples.get_graph_options(options)), + services=get_services() + ) diff --git a/bonobo/examples/datasets/fablabs.py b/bonobo/examples/datasets/fablabs.py index 086bdfc..0a6e188 100644 --- a/bonobo/examples/datasets/fablabs.py +++ b/bonobo/examples/datasets/fablabs.py @@ -62,4 +62,7 @@ if __name__ == '__main__': parser = examples.get_argument_parser() with bonobo.parse_args(parser) as options: - bonobo.run(get_graph(**examples.get_graph_options(options)), services=get_services()) + bonobo.run( + get_graph(**examples.get_graph_options(options)), + services=get_services() + ) diff --git a/bonobo/examples/datasets/services.py b/bonobo/examples/datasets/services.py index eb9e8e8..9c8f2ac 100644 --- a/bonobo/examples/datasets/services.py +++ b/bonobo/examples/datasets/services.py @@ -1,7 +1,16 @@ +import os + import bonobo +def get_datasets_dir(*dirs): + home_dir = os.path.expanduser('~') + target_dir = os.path.join( + home_dir, '.cache/bonobo', bonobo.__version__, *dirs + ) + os.makedirs(target_dir, exist_ok=True) + return target_dir + + def get_services(): - return { - 'fs': bonobo.open_fs(bonobo.get_examples_path('datasets')) - } \ No newline at end of file + return {'fs': bonobo.open_fs(get_datasets_dir('datasets'))} diff --git a/bonobo/examples/datasets/Makefile b/bonobo/examples/datasets/static/Makefile similarity index 100% rename from bonobo/examples/datasets/Makefile rename to bonobo/examples/datasets/static/Makefile diff --git a/bonobo/examples/datasets/passwd.txt b/bonobo/examples/datasets/static/passwd.txt similarity index 100% rename from bonobo/examples/datasets/passwd.txt rename to bonobo/examples/datasets/static/passwd.txt diff --git a/bonobo/examples/datasets/spam.tgz b/bonobo/examples/datasets/static/spam.tgz similarity index 100% rename from bonobo/examples/datasets/spam.tgz rename to bonobo/examples/datasets/static/spam.tgz diff --git a/bonobo/examples/datasets/theaters.json b/bonobo/examples/datasets/static/theaters.json similarity index 100% rename from bonobo/examples/datasets/theaters.json rename to bonobo/examples/datasets/static/theaters.json From 47f389784764bbec27d92ff21a688246c7b7ede2 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 2 Dec 2017 16:19:27 +0100 Subject: [PATCH 127/145] Example datasets are now stored by bonobo minor version. --- bonobo/examples/datasets/__main__.py | 6 +++--- bonobo/examples/datasets/services.py | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bonobo/examples/datasets/__main__.py b/bonobo/examples/datasets/__main__.py index 8dd4c49..b5b3b4f 100644 --- a/bonobo/examples/datasets/__main__.py +++ b/bonobo/examples/datasets/__main__.py @@ -4,7 +4,7 @@ import bonobo from bonobo import examples from bonobo.examples.datasets.coffeeshops import get_graph as get_coffeeshops_graph from bonobo.examples.datasets.fablabs import get_graph as get_fablabs_graph -from bonobo.examples.datasets.services import get_services, get_datasets_dir +from bonobo.examples.datasets.services import get_services, get_datasets_dir, get_minor_version graph_factories = { 'coffeeshops': get_coffeeshops_graph, @@ -33,7 +33,7 @@ if __name__ == '__main__': bonobo.run(graph, services=get_services()) if options['sync']: - # XXX/TODO: when parallel option for node will be implemented, need to be rewriten to use a graph. + # TODO: when parallel option for node will be implemented, need to be rewriten to use a graph. import boto3 s3 = boto3.client('s3') @@ -44,7 +44,7 @@ if __name__ == '__main__': local_path = os.path.join(root, filename) relative_path = os.path.relpath(local_path, local_dir) s3_path = os.path.join( - bonobo.__version__, relative_path + get_minor_version(), relative_path ) try: diff --git a/bonobo/examples/datasets/services.py b/bonobo/examples/datasets/services.py index 9c8f2ac..6412156 100644 --- a/bonobo/examples/datasets/services.py +++ b/bonobo/examples/datasets/services.py @@ -3,10 +3,14 @@ import os import bonobo +def get_minor_version(): + return '.'.join(bonobo.__version__.split('.')[:2]) + + def get_datasets_dir(*dirs): home_dir = os.path.expanduser('~') target_dir = os.path.join( - home_dir, '.cache/bonobo', bonobo.__version__, *dirs + home_dir, '.cache/bonobo', get_minor_version(), *dirs ) os.makedirs(target_dir, exist_ok=True) return target_dir From 02d6f4923e62dcf2853f72455f19431424f04780 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 3 Dec 2017 06:54:54 +0100 Subject: [PATCH 128/145] Removes dead snippet. --- bonobo/nodes/io/csv.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index ed0a738..e3b1813 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -93,16 +93,6 @@ class CsvReader(FileReader, CsvHandler): __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): @Method( From 23374e7d58d3c63e2a52d8fb4e4ef773c4a7b183 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 3 Dec 2017 08:14:19 +0100 Subject: [PATCH 129/145] Release: 0.6.0a0 --- Makefile | 2 +- docs/changelog-0.6.rst | 131 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 docs/changelog-0.6.rst diff --git a/Makefile b/Makefile index 53582a5..fe11dae 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4.2 on 2017-12-02. +# Generated by Medikit 0.4.2 on 2017-12-03. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/docs/changelog-0.6.rst b/docs/changelog-0.6.rst new file mode 100644 index 0000000..a8014c4 --- /dev/null +++ b/docs/changelog-0.6.rst @@ -0,0 +1,131 @@ +Bonobo 0.6.0 +:::::::::::: + +* Removes dead snippet. (Romain Dorgueil) +* Example datasets are now stored by bonobo minor version. (Romain Dorgueil) +* Removing datasets from the repository. (Romain Dorgueil) +* For some obscure reason, coverage is broken under python 3.7 making the test suite fail, disabled python3.7 in travis waiting for it to be fixed. (Romain Dorgueil) +* [tests] adding a spec to magicmock of nodes to avoid it being seen as partially configured nodes (Romain Dorgueil) +* Adds an OrderFields transformation factory, update examples. (Romain Dorgueil) +* Check partially configured transformations that are function based (aka transformation factories) on execution context setup. (Romain Dorgueil) +* Fix PrettyPrinter, output verbosity is now slightly more discreete. (Romain Dorgueil) +* Inheritance of bags and better jupyter output for pretty printer. (Romain Dorgueil) +* Documentation cosmetics. (Romain Dorgueil) +* Simple "examples" command that just show examples for now. (Romain Dorgueil) +* Rewritting Bags from scratch using a namedtuple approach, along with other (less major) updates. (Romain Dorgueil) +* Adding services to naive execution (Kenneth Koski) +* Fix another typo in `run` (Daniel Jilg) +* Fix two typos in the ContextProcessor documentation (Daniel Jilg) +* Core: refactoring contexts with more logical responsibilities, stopping to rely on kargs ordering for compat with python3.5 (Romain Dorgueil) +* Simplification of node execution context, handle_result is now in step() as it is the only logical place where this will actually be called. (Romain Dorgueil) +* Less strict CSV processing, to allow dirty input. (Romain Dorgueil) +* [stdlib] Adds Update(...) and FixedWindow(...) the the standard nodes provided with bonobo. (Romain Dorgueil) +* Adds a benchmarks directory with small scripts to test performances of things. (Romain Dorgueil) +* Moves jupyter extension to both bonobo.contrib.jupyter (for the jupyter widget) and to bonobo.plugins (for the executor-side plugin). (Romain Dorgueil) +* Fix examples with new module paths. (Romain Dorgueil) +* IOFormats: if no kwargs, then try with one positional argument. (Romain Dorgueil) +* Adds a __getattr__ dunder to ValueHolder to enable getting attributes, and especially method calls, on contained objects. (Romain Dorgueil) +* Moves ODS extension to contrib module. (Romain Dorgueil) +* Moves google extension to contrib module. (Romain Dorgueil) +* Moves django extension to contrib module. (Romain Dorgueil) +* Update graphs.rst (CW Andrews) +* Adds argument parser support to django extension. (Romain Dorgueil) +* Trying to understand conda... (Romain Dorgueil) +* Trying to understand conda... (Romain Dorgueil) +* Trying to understand conda... (Romain Dorgueil) +* Update conda conf so readthedocs can maybe build. (Romain Dorgueil) +* Working on the new version of the tutorial. Only Step1 implemented. (Romain Dorgueil) +* Adds a "bare" template, containing the very minimum you want to have in 90% of cases. (Romain Dorgueil) +* Fix default logging level, adds options to default template. (Romain Dorgueil) +* Skip failing order test for python 3.5 (temporary). (Romain Dorgueil) +* Switch to stable mondrian. (Romain Dorgueil) +* Moves timer to statistics utilities. (Romain Dorgueil) +* Adds basic test for convert command. (Romain Dorgueil) +* [tests] adds node context lifecycle test.( (Romain Dorgueil) +* Small changes in events, and associated tests. (Romain Dorgueil) +* [core] Moves bonobo.execution context related package to new bonobo.execution.contexts package, also moves bonobo.strategies to new bonobo.execution.strategies package, so everything related to execution is now contained under the bonobo.execution package. (Romain Dorgueil) +* Remove the sleep() in tick() that causes a minimum execution time of 2*PERIOD, more explicit status display and a small test case for console plugin. (Romain Dorgueil) +* [tests] Fix path usage for python 3.5 (Romain Dorgueil) +* Adds a test for default file init command. (Romain Dorgueil) +* Adds 3.7-dev target to travis runner. (Romain Dorgueil) +* Update requirements with first whistle stable. (Romain Dorgueil) +* [core] Refactoring to use an event dispatcher in the main thread. (Romain Dorgueil) +* Update to mondrian 0.4a0. (Romain Dorgueil) +* Fix imports. (Romain Dorgueil) +* Removing old error handler. (Romain Dorgueil) +* [errors] Move error handling in transformations to use mondrian. (Romain Dorgueil) +* [logging] Switching to mondrian, who got all our formating code. (Romain Dorgueil) +* Adds argument parser support in default template. (Romain Dorgueil) +* Adds the ability to initialize a package from bonobo init. (Romain Dorgueil) +* Still cleaning up. (Romain Dorgueil) +* [examples] comments. (Romain Dorgueil) +* Update dependencies, remove python-dotenv. (Romain Dorgueil) +* Remove unused argument. (Romain Dorgueil) +* Remove files in examples that are not used anymore. (Romain Dorgueil) +* Refactoring the runner to go more towards standard python, also adds the ability to use bonobo argument parser from standard python execution. (Romain Dorgueil) +* Removes cookiecutter. (Romain Dorgueil) +* Switch logger setup to mondrian (deps). (Romain Dorgueil) +* Module registry reimported as it is needed for "bonobo convert". (Romain Dorgueil) +* [core] Simplification: as truthfully stated by Maik at Pycon.DE sprint «lets try not to turn python into javascript». (Romain Dorgueil) +* [core] still refactoring env-related stuff towards using __main__ blocks (but with argparser, if needed). (Romain Dorgueil) +* [core] Refactoring of commands to move towards a more pythonic way of running the jobs. Commands are now classes, and bonobo "graph" related commands now hooks into bonobo.run() calls so it will use what you actually put in your __main__ block. (Romain Dorgueil) +* Minor test change. (Romain Dorgueil) +* [core] Change the token parsing part in prevision of different flags. (Romain Dorgueil) +* Support line-delimited JSON (Michael Penkov) +* Update Makefile/setup. (Romain Dorgueil) +* [tests] simplify assertion (Romain Dorgueil) +* Issue #134: use requests.get as a context manager (Michael Penkov) +* Issue #134: use requests instead of urllib (Michael Penkov) +* update Projectfile with download entry point (Michael Penkov) +* Issue #134: update documentation (Michael Penkov) +* Issue #134: add a `bonobo download url` command (Michael Penkov) +* commands.run: Enable relative imports in main.py (Stefan Zimmermann) +* adapt tutorial "Working with files" to the latest develop version (Peter Uebele) +* Add a note about the graph variable (Michael Penkov) +* [tests] trying to speed up the init test. (Romain Dorgueil) +* [tests] bonobo.util.objects (Romain Dorgueil) +* [nodes] Removing draft quality factory from bonobo main package, will live in separate personnal package until it is good enough to live here. (Romain Dorgueil) +* [tests] rename factory test and move bag detecting so any bag is returned as is as an output. (Romain Dorgueil) +* [core] Still refactoring the core behaviour of bags, starting to be much simpler. (Romain Dorgueil) +* Fix python 3.5 os.chdir not accepting LocalPath (arimbr) +* Remove unused shutil import (arimbr) +* Use pytest tmpdir fixture and add more init tests (arimbr) +* Check if target directory is empty instead of current directory and remove overwrite_if_exists argument (arimbr) +* Remove dispatcher as it is not a dependency, for now, and as such breaks the continuous integration (yes, again.). (Romain Dorgueil) +* Remove dispatcher as it is not a dependency, for now, and as such breaks the continuous integration. (Romain Dorgueil) +* Code formating. (Romain Dorgueil) +* [core] Testing and fixing new args/kwargs behaviour. (Romain Dorgueil) +* [core] simplification of result interpretation. (Romain Dorgueil) +* [tests] fix uncaptured output in test_commands (Romain Dorgueil) +* Documentation for new behaviour. (Romain Dorgueil) +* [django, misc] adds create_or_update to djangos ETLCommand class, adds getitem/setitem/contains dunders to ValueHolder. (Romain Dorgueil) +* [core] (..., dict) means Bag(..., **dict) (Romain Dorgueil) +* [django, google] Implements basic extensions for django and google oauth systems. (Romain Dorgueil) +* Test tweak to work for Windows CI. (cwandrews) +* Updated requirements files using edgy-project. (cwandrews) +* Updated Projectfile to include python-dotenv dependency. (cwandrews) +* Add tests for bonobo init new directory and init within empty directory (arimbr) +* Update environment.rst (CW Andrews) +* Update environment.rst (CW Andrews) +* Cast env_dir to string before passing to load_dotenv as passing a PosixPath to load_dotenv raises an exception in 3.5. (cwandrews) +* Updated environment documentation in guides to account for env files. (cwandrews) +* Added more tests and moved all env and env file testing to classes (it might make more sense to just move them to separate files?). (cwandrews) +* Moved env vars tests to class. (cwandrews) +* Updated .env >>> .env_one to include in repo (.env ignored). (cwandrews) +* [core] Refactoring IOFormats so there is one and only obvious way to send it. (Romain Dorgueil) +* Set cookiecutter overwrite_if_exists parameter to True if current directory is empty (arimbr) +* [cli/util] fix requires to use the right stack frame, remove --print as "-" does the job (Romain Dorgueil) +* [cli] Adds a --filter option to "convert" command, allowing to use arbitrary filters to a command line conversion. Also adds --print and "-" output to pretty print to terminal instead of file output. (Romain Dorgueil) +* [cli] convert, remove useless import. (Romain Dorgueil) +* [config] adds a __doc__ constructor kwarg to set option documentation inline. (Romain Dorgueil) +* [doc] formating (Romain Dorgueil) +* [cli] adds ability to override reader/writer options from cli convert. (Romain Dorgueil) +* comparison to None|True|False should be 'if cond is None:' (mouadhkaabachi) +* Fixed bug involved in finding env when running module. (cwandrews) +* Moved default-env-file tests to class. (cwandrews) +* Small adjustment to test parameters. (cwandrews) +* Added tests for running file with combinations of multiple default env files, env files, and env vars. Also reorganized environment directory in examples. (cwandrews) +* Updated requirements.txt and requirements-dev.txt to include python-dotenv and dependencies. (cwandrews) +* default-env-file, default-env, and env-file now in place alongside env. default-env-file and default-env both use os.environ.setdefault so as not to overwrite existing variables (system environment) while env-file and env will overwrite existing variables. All four allow for multiple values (***How might this affect multiple default-env and default-env-file values, I expect that unlike env-file and env the first passed variables would win). (cwandrews) +* Further Refactored the setting of env vars passed via the env flag. (cwandrews) +* Refactored setting of env vars passed via the env flag. (cwandrews) From e39ab0e62e8c4c15f9fe25e61b745a1ba9278f20 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 3 Dec 2017 09:57:56 +0100 Subject: [PATCH 130/145] Fixes packaging: *.py-tpl are now included using recursive-include in MANIFEST.in --- MANIFEST.in | 2 +- docs/_static/custom.css | 18 ------------------ docs/_templates/sidebarintro.html | 5 +---- docs/install.rst | 24 +++++++++++++++++------- docs/tutorial/index.rst | 12 ++++++++---- tests/commands/test_convert.py | 3 --- 6 files changed, 27 insertions(+), 37 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 4c2c662..f0e3359 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt -include *.py-tpl +recursive-include bonobo *.py-tpl diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 231ec92..3de53da 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -19,28 +19,10 @@ div.related { font-size: 0.9em; } -div.sphinxsidebar h1, -div.sphinxsidebar h2, -div.sphinxsidebar h3, -div.sphinxsidebar h4, -div.sphinxsidebar h5, -div.sphinxsidebar h6 { - font-family: 'Ubuntu', 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; -} - div.sphinxsidebar h3 { margin: 30px 0 10px 0; } -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Ubuntu', 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; -} - div.admonition p.admonition-title { font-family: 'Ubuntu', 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; } diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index d4abd00..05a2198 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -1,9 +1,6 @@

About Bonobo

- Bonobo is a data-processing toolkit for python 3.5+. -

-

- It's a swiss-army knife for everyday's data. + Bonobo is a data-processing toolkit for python 3.5+, your swiss-army knife for everyday's data.

Other Formats

diff --git a/docs/install.rst b/docs/install.rst index 615e4bc..e220ab0 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -35,13 +35,6 @@ Now, you can head to :doc:`tutorial/index`. along with pointers on how to move this first file into an existing fully featured python package. - You can also avoid all the comments boilerplate by using `--bare` option - (which is a shorthand for `--template bare`): - - .. code-block:: shell-session - - $ bonobo init --bare my-bare-etl-job.py - Other installation options :::::::::::::::::::::::::: @@ -55,6 +48,12 @@ You can install it directly from the `Python Package Index ` -* :doc:`Extensions <../extension/index>` + +Once you're familiar with all the base concepts, you can... + +* Read the :doc:`Guides ` to have a deep dive in each concept. +* Explore the :doc:`Extensions ` to widen the possibilities. +* Open the :doc:`References ` and start hacking like crazy. -We're there! -:::::::::::: +You're not alone! +::::::::::::::::: Good documentation is not easy to write. diff --git a/tests/commands/test_convert.py b/tests/commands/test_convert.py index 8f1aea8..d61d8c5 100644 --- a/tests/commands/test_convert.py +++ b/tests/commands/test_convert.py @@ -6,9 +6,6 @@ from bonobo.util.environ import change_working_directory from bonobo.util.testing import all_runners -@pytest.mark.skipif( - sys.version_info < (3, 6), reason="python 3.5 does not preserve kwargs order and this cant pass for now" -) @all_runners def test_convert(runner, tmpdir): csv_content = 'id;name\n1;Romain' From a1f883e3c68a2232e0dbab985291c1dda15818b4 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 3 Dec 2017 10:00:47 +0100 Subject: [PATCH 131/145] Release: 0.6.0a1 --- bonobo/_version.py | 2 +- requirements-docker.txt | 7 +++++++ requirements-sqlalchemy.txt | 7 +++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/bonobo/_version.py b/bonobo/_version.py index 1986ae4..351b942 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.6.0a0' +__version__ = '0.6.0a1' diff --git a/requirements-docker.txt b/requirements-docker.txt index 880bb57..c7f5d8a 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -7,14 +7,21 @@ colorama==0.3.9 docker-pycreds==0.2.1 docker==2.3.0 fs==2.0.17 +graphviz==0.8.1 idna==2.6 +jinja2==2.10 +markupsafe==1.0 +mondrian==0.6.0 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 websocket-client==0.44.0 +whistle==1.0.0 diff --git a/requirements-sqlalchemy.txt b/requirements-sqlalchemy.txt index 7d97881..af7503e 100644 --- a/requirements-sqlalchemy.txt +++ b/requirements-sqlalchemy.txt @@ -5,14 +5,21 @@ certifi==2017.11.5 chardet==3.0.4 colorama==0.3.9 fs==2.0.17 +graphviz==0.8.1 idna==2.6 +jinja2==2.10 +markupsafe==1.0 +mondrian==0.6.0 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 sqlalchemy==1.1.15 stevedore==1.27.1 +unidecode==0.4.21 urllib3==1.22 +whistle==1.0.0 From 99c4745b4e097aba36f8893548851c60acc2de84 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 4 Dec 2017 08:31:24 +0100 Subject: [PATCH 132/145] Work in progress on documentation for 0.6 --- bonobo/config/configurables.py | 7 +----- bonobo/errors.py | 41 +++++++++++----------------------- docs/tutorial/1-init.rst | 6 ++--- docs/tutorial/2-jobs.rst | 32 ++++++++++++++++++++++++++ docs/tutorial/3-files.rst | 10 +++++++++ docs/tutorial/5-packaging.rst | 4 ++++ 6 files changed, 63 insertions(+), 37 deletions(-) diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index 3e6f154..7428bf8 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -3,7 +3,6 @@ from bonobo.util import isoption, iscontextprocessor, sortedlist __all__ = [ 'Configurable', - 'Option', ] get_creation_counter = lambda v: v._creation_counter @@ -192,11 +191,7 @@ class Configurable(metaclass=ConfigurableMeta): position += 1 def __call__(self, *args, **kwargs): - raise AbstractError( - 'You must implement the __call__ method in your configurable class {} to actually use it.'.format( - type(self).__name__ - ) - ) + raise AbstractError(self.__call__) @property def __options__(self): diff --git a/bonobo/errors.py b/bonobo/errors.py index 53d0a5d..173ce40 100644 --- a/bonobo/errors.py +++ b/bonobo/errors.py @@ -1,31 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2012-2014 Romain Dorgueil -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - - -class AbstractError(NotImplementedError): - """Abstract error is a convenient error to declare a method as "being left as an exercise for the reader".""" - - def __init__(self, method): - super().__init__( - 'Call to abstract method {class_name}.{method_name}(...): missing implementation.'.format( - class_name=method.__self__.__name__, - method_name=method.__name__, - ) - ) +from bonobo.util import get_name class InactiveIOError(IOError): @@ -63,6 +36,18 @@ class UnrecoverableError(Exception): because you know that your transformation has no point continuing runnning after a bad event.""" +class AbstractError(UnrecoverableError, NotImplementedError): + """Abstract error is a convenient error to declare a method as "being left as an exercise for the reader".""" + + def __init__(self, method): + super().__init__( + 'Call to abstract method {class_name}.{method_name}(...): missing implementation.'.format( + class_name=get_name(method.__self__), + method_name=get_name(method), + ) + ) + + class UnrecoverableTypeError(UnrecoverableError, TypeError): pass diff --git a/docs/tutorial/1-init.rst b/docs/tutorial/1-init.rst index c4f6b65..5a1bbd9 100644 --- a/docs/tutorial/1-init.rst +++ b/docs/tutorial/1-init.rst @@ -150,8 +150,8 @@ Transformations that take input and yields nothing are also called **loaders**. different types, to work with various external systems. Please note that as a convenience mean and because the cost is marginal, most builtin `loaders` will send their -inputs to their output, so you can easily chain more than one loader, or apply more transformations after a given -loader was applied. +inputs to their output unmodified, so you can easily chain more than one loader, or apply more transformations after a +given loader. Graph Factory @@ -255,4 +255,4 @@ You now know: * How to execute a job file. * How to read the console output. -**Next: :doc:`2-jobs`** +**Jump to** :doc:`2-jobs` diff --git a/docs/tutorial/2-jobs.rst b/docs/tutorial/2-jobs.rst index c3a6c8b..dd7e183 100644 --- a/docs/tutorial/2-jobs.rst +++ b/docs/tutorial/2-jobs.rst @@ -1,6 +1,38 @@ Part 2: Writing ETL Jobs ======================== +What's an ETL job ? +::::::::::::::::::: + +- data flow, stream processing +- each node, first in first out +- parallelism + +Each node has input rows, each row is one call, and each call has the input row passed as *args. + +Each call can have outputs, sent either using return, or yield. + +Each output row is stored internally as a tuple (or a namedtuple-like structure), and each output row must have the same structure (same number of fields, same len for tuple). + +If you yield something which is not a tuple, bonobo will create a tuple of one element. + +By default, exceptions are not fatal in bonobo. If a call raise an error, then bonobo will display the stack trace, increment the "err" counter for this node and move to the next input row. + +Some errors are fatal, though. For example, if you pass a 2 elements tuple to a node that takes 3 args, bonobo will raise an UnrecoverableTypeError, and exit the current execution. + +Let's write one +::::::::::::::: + +We'll create a job to do the following + +* Extract all the FabLabs from an open data API +* Apply a bit of formating +* Geocode the address and normalize it, if we can +* Display it (in the next step, we'll learn about writing the result to a file. + + + + Moving forward :::::::::::::: diff --git a/docs/tutorial/3-files.rst b/docs/tutorial/3-files.rst index adcc334..5430f69 100644 --- a/docs/tutorial/3-files.rst +++ b/docs/tutorial/3-files.rst @@ -1,6 +1,16 @@ Part 3: Working with Files ========================== +* Filesystems + +* Reading files + +* Writing files + +* Writing files to S3 + +* Atomic writes ??? + Moving forward :::::::::::::: diff --git a/docs/tutorial/5-packaging.rst b/docs/tutorial/5-packaging.rst index bf4537b..7362311 100644 --- a/docs/tutorial/5-packaging.rst +++ b/docs/tutorial/5-packaging.rst @@ -1,6 +1,10 @@ Part 5: Projects and Packaging ============================== +Until then, we worked with one file managing a job. But real life is about set of jobs working together within a project. + +Let's see how to move from the current status to a package. + Moving forward :::::::::::::: From 5363a5b0040383e7b93f783a2bb2dad175fa8f1c Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 13 Dec 2017 09:27:45 +0100 Subject: [PATCH 133/145] Django: fix create_or_update with no defaults. --- bonobo/contrib/django/utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bonobo/contrib/django/utils.py b/bonobo/contrib/django/utils.py index 56f1201..8adde6a 100644 --- a/bonobo/contrib/django/utils.py +++ b/bonobo/contrib/django/utils.py @@ -12,10 +12,11 @@ def create_or_update(model, *, defaults=None, save=True, **kwargs): updated = False if not created: - for k, v in defaults.items(): - if getattr(obj, k) != v: - setattr(obj, k, v) - updated = True + if defaults: + for k, v in defaults.items(): + if getattr(obj, k) != v: + setattr(obj, k, v) + updated = True if updated and save: obj.save() From 281cc6c8edf0497d614ec5f92729c38aef715c9e Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 13 Dec 2017 09:28:36 +0100 Subject: [PATCH 134/145] Update dependencies. --- Makefile | 2 +- requirements-dev.txt | 2 +- requirements-docker.txt | 4 ++-- requirements-jupyter.txt | 10 +++++----- requirements-sqlalchemy.txt | 4 ++-- requirements.txt | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index fe11dae..29e4d63 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4.2 on 2017-12-03. +# Generated by Medikit 0.4.5 on 2017-12-13. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/requirements-dev.txt b/requirements-dev.txt index 8d4d4e7..a86c724 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -16,7 +16,7 @@ pygments==2.2.0 pytest-cov==2.5.1 pytest-sugar==0.9.0 pytest-timeout==1.2.1 -pytest==3.3.0 +pytest==3.3.1 pytz==2017.3 requests==2.18.4 six==1.11.0 diff --git a/requirements-docker.txt b/requirements-docker.txt index c7f5d8a..687cfc5 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -11,10 +11,10 @@ graphviz==0.8.1 idna==2.6 jinja2==2.10 markupsafe==1.0 -mondrian==0.6.0 +mondrian==0.6.1 packaging==16.8 pbr==3.1.1 -psutil==5.4.1 +psutil==5.4.2 pyparsing==2.2.0 python-slugify==1.2.4 pytz==2017.3 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 0b3b5ac..2689ba3 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -1,10 +1,10 @@ -e .[jupyter] appnope==0.1.0 -bleach==2.1.1 +bleach==2.1.2 decorator==4.1.2 entrypoints==0.2.3 -html5lib==1.0b10 -ipykernel==4.6.1 +html5lib==1.0.1 +ipykernel==4.7.0 ipython-genutils==0.2.0 ipython==6.2.1 ipywidgets==6.0.1 @@ -16,13 +16,13 @@ jupyter-console==5.2.0 jupyter-core==4.4.0 jupyter==1.0.0 markupsafe==1.0 -mistune==0.8.1 +mistune==0.8.3 nbconvert==5.3.1 nbformat==4.4.0 notebook==5.2.2 pandocfilters==1.4.2 parso==0.1.0 -pexpect==4.3.0 +pexpect==4.3.1 pickleshare==0.7.4 prompt-toolkit==1.0.15 ptyprocess==0.5.2 diff --git a/requirements-sqlalchemy.txt b/requirements-sqlalchemy.txt index af7503e..e89781a 100644 --- a/requirements-sqlalchemy.txt +++ b/requirements-sqlalchemy.txt @@ -9,10 +9,10 @@ graphviz==0.8.1 idna==2.6 jinja2==2.10 markupsafe==1.0 -mondrian==0.6.0 +mondrian==0.6.1 packaging==16.8 pbr==3.1.1 -psutil==5.4.1 +psutil==5.4.2 pyparsing==2.2.0 python-slugify==1.2.4 pytz==2017.3 diff --git a/requirements.txt b/requirements.txt index f7a1d5e..103015e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,10 +8,10 @@ graphviz==0.8.1 idna==2.6 jinja2==2.10 markupsafe==1.0 -mondrian==0.6.0 +mondrian==0.6.1 packaging==16.8 pbr==3.1.1 -psutil==5.4.1 +psutil==5.4.2 pyparsing==2.2.0 python-slugify==1.2.4 pytz==2017.3 From 603d917712bf59f5f563b45f3a6c85166379869c Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 13 Dec 2017 09:29:33 +0100 Subject: [PATCH 135/145] Release: 0.6.0a2 --- bonobo/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonobo/_version.py b/bonobo/_version.py index 351b942..dfa0ce4 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.6.0a1' +__version__ = '0.6.0a2' From 018289dad6a488fde53ad5293d5961010846dda2 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 13 Dec 2017 09:32:33 +0100 Subject: [PATCH 136/145] Loosen stevedore requirement to avoid conflicts. --- Projectfile | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Projectfile b/Projectfile index 1beef5d..6016415 100644 --- a/Projectfile +++ b/Projectfile @@ -51,7 +51,7 @@ python.add_requirements( 'psutil >=5.4,<6', 'python-slugify >=1.2,<1.3', 'requests >=2,<3', - 'stevedore >=1.27,<1.28', + 'stevedore ~=1.27', 'whistle >=1.0,<1.1', dev=[ 'pytest-sugar >=0.9,<0.10', diff --git a/requirements.txt b/requirements.txt index 103015e..24321ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ python-slugify==1.2.4 pytz==2017.3 requests==2.18.4 six==1.11.0 -stevedore==1.27.1 +stevedore==1.28.0 unidecode==0.4.21 urllib3==1.22 whistle==1.0.0 diff --git a/setup.py b/setup.py index b430f73..741883e 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ setup( install_requires=[ 'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'graphviz (>= 0.8, < 0.9)', 'jinja2 (>= 2.9, < 3)', 'mondrian (>= 0.6, < 0.7)', '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)' + 'requests (>= 2, < 3)', 'stevedore (~= 1.27)', 'whistle (>= 1.0, < 1.1)' ], extras_require={ 'dev': [ From 46a8fd192ebf327fe89a6a27b0dc1c0d229919d1 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 27 Dec 2017 11:32:47 +0100 Subject: [PATCH 137/145] Refactoring API, writing docs. --- Makefile | 2 +- Projectfile | 32 ++++++++++----------- bonobo/_api.py | 52 ++++++++--------------------------- bonobo/util/api.py | 35 +++++++++++++++++++++++ docs/tutorial/2-jobs.rst | 3 -- docs/tutorial/4-services.rst | 3 -- docs/tutorial/5-packaging.rst | 15 +++++++++- docs/tutorial/python.rst | 11 -------- requirements-docker.txt | 4 +-- requirements-jupyter.txt | 10 +++++-- requirements-sqlalchemy.txt | 2 +- setup.py | 13 ++++----- 12 files changed, 91 insertions(+), 91 deletions(-) create mode 100644 bonobo/util/api.py delete mode 100644 docs/tutorial/python.rst diff --git a/Makefile b/Makefile index 29e4d63..bc29a5b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4.5 on 2017-12-13. +# Generated by Medikit 0.4.5 on 2017-12-27. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index 6016415..5c880db 100644 --- a/Projectfile +++ b/Projectfile @@ -7,7 +7,7 @@ python = require('python') sphinx = require('sphinx') yapf = require('yapf') -# python.set_versions('3.5', '3.6', '3.7') --> not yet implemented +# python.set_versions('3.5', '3.6', '3.7') --> not yet implemented in medikit python.setup( name='bonobo', @@ -43,34 +43,30 @@ python.setup( ) python.add_requirements( - 'fs >=2.0,<2.1', + 'fs ~=2.0', 'graphviz >=0.8,<0.9', - 'jinja2 >=2.9,<3', - 'mondrian >=0.6,<0.7', - 'packaging >=16,<17', - 'psutil >=5.4,<6', - 'python-slugify >=1.2,<1.3', - 'requests >=2,<3', + 'jinja2 ~=2.9', + 'mondrian ~=0.6', + 'packaging ~=16.0', + 'psutil ~=5.4', + 'python-slugify ~=1.2.0', + 'requests ~=2.0', 'stevedore ~=1.27', - 'whistle >=1.0,<1.1', + 'whistle ~=1.0', dev=[ 'pytest-sugar >=0.9,<0.10', - 'pytest-timeout >=1,<2', + 'pytest-timeout ~=1.0', ], docker=[ - 'bonobo-docker >=0.5.0', + 'bonobo-docker', ], jupyter=[ - 'ipywidgets >=6.0.0,<7', - 'jupyter >=1.0,<1.1', + 'ipywidgets ~=6.0', + 'jupyter ~=1.0', ], sqlalchemy=[ - 'bonobo-sqlalchemy >=0.5.1', + 'bonobo-sqlalchemy', ], ) -# Following requirements are not enforced, because some dependencies enforce them so we don't want to break -# the packaging in case it changes in dep. -python.add_requirements('colorama >=0.3') - # vim: ft=python: diff --git a/bonobo/_api.py b/bonobo/_api.py index 58aab3d..df38004 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -3,45 +3,15 @@ from bonobo.nodes import __all__ as _all_nodes from bonobo.nodes import * from bonobo.structs import Graph from bonobo.util import get_name +from bonobo.util.api import ApiHelper from bonobo.util.environ import parse_args, get_argument_parser __all__ = [] - -def register_api(x, __all__=__all__): - """Register a function as being part of Bonobo's API, then returns the original function.""" - __all__.append(get_name(x)) - return x +api = ApiHelper(__all__) -def register_graph_api(x, __all__=__all__): - """ - Register a function as being part of Bonobo's API, after checking that its signature contains the right parameters - to work correctly, then returns the original function. - """ - from inspect import signature - parameters = list(signature(x).parameters) - required_parameters = {'plugins', 'services', 'strategy'} - assert parameters[0] == 'graph', 'First parameter of a graph api function must be "graph".' - assert required_parameters.intersection( - parameters - ) == required_parameters, 'Graph api functions must define the following parameters: ' + ', '.join( - sorted(required_parameters) - ) - - return register_api(x, __all__=__all__) - - -def register_api_group(*args, check=None): - check = set(check) if check else None - for attr in args: - register_api(attr) - if check: - check.remove(get_name(attr)) - assert not (check and len(check)) - - -@register_graph_api +@api.register_graph def run(graph, *, plugins=None, services=None, strategy=None): """ Main entry point of bonobo. It takes a graph and creates all the necessary plumbing around to execute it. @@ -104,7 +74,7 @@ def _inspect_as_graph(graph): _inspect_formats = {'graph': _inspect_as_graph} -@register_graph_api +@api.register_graph def inspect(graph, *, plugins=None, services=None, strategy=None, format): if not format in _inspect_formats: raise NotImplementedError( @@ -116,14 +86,14 @@ def inspect(graph, *, plugins=None, services=None, strategy=None, format): # data structures -register_api_group(Graph) +api.register_group(Graph) # execution strategies -register_api(create_strategy) +api.register_group(create_strategy) # Shortcut to filesystem2's open_fs, that we make available there for convenience. -@register_api +@api.register def open_fs(fs_url=None, *args, **kwargs): """ Wraps :func:`fs.open_fs` function with a few candies. @@ -148,7 +118,7 @@ def open_fs(fs_url=None, *args, **kwargs): # standard transformations -register_api_group( +api.register_group( CsvReader, CsvWriter, FileReader, @@ -189,16 +159,16 @@ def _is_jupyter_notebook(): return False -@register_api +@api.register def get_examples_path(*pathsegments): import os import pathlib return str(pathlib.Path(os.path.dirname(__file__), 'examples', *pathsegments)) -@register_api +@api.register def open_examples_fs(*pathsegments): return open_fs(get_examples_path(*pathsegments)) -register_api_group(get_argument_parser, parse_args) +api.register_group(get_argument_parser, parse_args) diff --git a/bonobo/util/api.py b/bonobo/util/api.py new file mode 100644 index 0000000..1acf5c8 --- /dev/null +++ b/bonobo/util/api.py @@ -0,0 +1,35 @@ +from bonobo.util import get_name + + +class ApiHelper: + def __init__(self, __all__): + self.__all__ = __all__ + + def register(self, x, graph=False): + """Register a function as being part of an API, then returns the original function.""" + + if graph: + # This function must comply to the "graph" API interface, meaning it can bahave like bonobo.run. + from inspect import signature + parameters = list(signature(x).parameters) + required_parameters = {'plugins', 'services', 'strategy'} + assert parameters[0] == 'graph', 'First parameter of a graph api function must be "graph".' + assert required_parameters.intersection( + parameters + ) == required_parameters, 'Graph api functions must define the following parameters: ' + ', '.join( + sorted(required_parameters) + ) + + self.__all__.append(get_name(x)) + return x + + def register_graph(self, x): + return self.register(x, graph=True) + + def register_group(self, *args, check=None): + check = set(check) if check else None + for attr in args: + self.register(attr) + if check: + check.remove(get_name(attr)) + assert not (check and len(check)) diff --git a/docs/tutorial/2-jobs.rst b/docs/tutorial/2-jobs.rst index dd7e183..d2bbfe5 100644 --- a/docs/tutorial/2-jobs.rst +++ b/docs/tutorial/2-jobs.rst @@ -31,9 +31,6 @@ We'll create a job to do the following * Display it (in the next step, we'll learn about writing the result to a file. - - - Moving forward :::::::::::::: diff --git a/docs/tutorial/4-services.rst b/docs/tutorial/4-services.rst index f2ada53..097d0f6 100644 --- a/docs/tutorial/4-services.rst +++ b/docs/tutorial/4-services.rst @@ -1,9 +1,6 @@ Part 4: Services and Configurables ================================== -.. note:: - - This section lacks completeness, sorry for that (but you can still read it!). In the last section, we used a few new tools. diff --git a/docs/tutorial/5-packaging.rst b/docs/tutorial/5-packaging.rst index 7362311..d0e29d5 100644 --- a/docs/tutorial/5-packaging.rst +++ b/docs/tutorial/5-packaging.rst @@ -1,7 +1,20 @@ Part 5: Projects and Packaging ============================== -Until then, we worked with one file managing a job. But real life is about set of jobs working together within a project. +Until then, we worked with one file managing a job. + +Real life often involves more complicated setups, with relations and imports between different files. + +This section will describe the options available to move this file into a package, either a new one or something +that already exists in your own project. + +Data processing is something a wide variety of tools may want to include, and thus |bonobo| does not enforce any +kind of project structure, as the targert structure will be dicated by the hosting project. For example, a `pipelines` +sub-package would perfectly fit a django or flask project, or even a regular package, but it's up to you to chose the +structure of your project. + +about using |bonobo| in a pyt + is about set of jobs working together within a project. Let's see how to move from the current status to a package. diff --git a/docs/tutorial/python.rst b/docs/tutorial/python.rst deleted file mode 100644 index d045031..0000000 --- a/docs/tutorial/python.rst +++ /dev/null @@ -1,11 +0,0 @@ -Just enough Python for Bonobo -============================= - -.. todo:: - - This is a work in progress and it is not yet available. Please come back later or even better, help us write this - guide! - - This guide is intended to help programmers or enthusiasts to grasp the python basics necessary to use Bonobo. It - should definately not be considered as a general python introduction, neither a deep dive into details. - diff --git a/requirements-docker.txt b/requirements-docker.txt index 687cfc5..6fbec83 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -20,8 +20,8 @@ python-slugify==1.2.4 pytz==2017.3 requests==2.18.4 six==1.11.0 -stevedore==1.27.1 +stevedore==1.28.0 unidecode==0.4.21 urllib3==1.22 -websocket-client==0.44.0 +websocket-client==0.45.0 whistle==1.0.0 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 2689ba3..2f5b6fe 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -1,5 +1,6 @@ -e .[jupyter] appnope==0.1.0 +attrs==17.3.0 bleach==2.1.2 decorator==4.1.2 entrypoints==0.2.3 @@ -8,10 +9,10 @@ ipykernel==4.7.0 ipython-genutils==0.2.0 ipython==6.2.1 ipywidgets==6.0.1 -jedi==0.11.0 +jedi==0.11.1 jinja2==2.10 jsonschema==2.6.0 -jupyter-client==5.1.0 +jupyter-client==5.2.0 jupyter-console==5.2.0 jupyter-core==4.4.0 jupyter==1.0.0 @@ -21,12 +22,15 @@ nbconvert==5.3.1 nbformat==4.4.0 notebook==5.2.2 pandocfilters==1.4.2 -parso==0.1.0 +parso==0.1.1 pexpect==4.3.1 pickleshare==0.7.4 +pluggy==0.6.0 prompt-toolkit==1.0.15 ptyprocess==0.5.2 +py==1.5.2 pygments==2.2.0 +pytest==3.3.1 python-dateutil==2.6.1 pyzmq==17.0.0b3 qtconsole==4.3.1 diff --git a/requirements-sqlalchemy.txt b/requirements-sqlalchemy.txt index e89781a..d46e7aa 100644 --- a/requirements-sqlalchemy.txt +++ b/requirements-sqlalchemy.txt @@ -19,7 +19,7 @@ pytz==2017.3 requests==2.18.4 six==1.11.0 sqlalchemy==1.1.15 -stevedore==1.27.1 +stevedore==1.28.0 unidecode==0.4.21 urllib3==1.22 whistle==1.0.0 diff --git a/setup.py b/setup.py index 741883e..5d89c9a 100644 --- a/setup.py +++ b/setup.py @@ -59,18 +59,17 @@ setup( packages=find_packages(exclude=['ez_setup', 'example', 'test']), 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.6, < 0.7)', 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6)', 'python-slugify (>= 1.2, < 1.3)', - 'requests (>= 2, < 3)', 'stevedore (~= 1.27)', 'whistle (>= 1.0, < 1.1)' + 'fs (~= 2.0)', 'graphviz (>= 0.8, < 0.9)', 'jinja2 (~= 2.9)', 'mondrian (~= 0.6)', 'packaging (~= 16.0)', + 'psutil (~= 5.4)', 'python-slugify (~= 1.2.0)', 'requests (~= 2.0)', 'stevedore (~= 1.27)', 'whistle (~= 1.0)' ], extras_require={ 'dev': [ 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', 'pytest-cov (>= 2.5, < 3.0)', - 'pytest-sugar (>= 0.9, < 0.10)', 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)', 'yapf' + 'pytest-sugar (>= 0.9, < 0.10)', 'pytest-timeout (~= 1.0)', 'sphinx (>= 1.6, < 2.0)', 'yapf' ], - 'docker': ['bonobo-docker (>= 0.5.0)'], - 'jupyter': ['ipywidgets (>= 6.0.0, < 7)', 'jupyter (>= 1.0, < 1.1)'], - 'sqlalchemy': ['bonobo-sqlalchemy (>= 0.5.1)'] + 'docker': ['bonobo-docker'], + 'jupyter': ['ipywidgets (~= 6.0)', 'jupyter (~= 1.0)'], + 'sqlalchemy': ['bonobo-sqlalchemy'] }, entry_points={ 'bonobo.commands': [ From f64e6e3c76441b9a4676e388e9fea7affcf141c6 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 27 Dec 2017 11:33:31 +0100 Subject: [PATCH 138/145] Release: 0.6.0a3 --- bonobo/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonobo/_version.py b/bonobo/_version.py index dfa0ce4..26618ea 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.6.0a2' +__version__ = '0.6.0a3' From 464d08a4a2053c610f70fc883bf962eae1319e5f Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 27 Dec 2017 14:21:50 +0100 Subject: [PATCH 139/145] Generalize tuplize decorator to cast(...) decorator. --- bonobo/util/__init__.py | 3 ++- bonobo/util/api.py | 4 ++- bonobo/util/collections.py | 47 +++++++++++++++++++--------------- bonobo/util/resolvers.py | 4 +-- tests/util/test_collections.py | 11 +++++--- 5 files changed, 40 insertions(+), 29 deletions(-) diff --git a/bonobo/util/__init__.py b/bonobo/util/__init__.py index d07b0b4..b03c0a9 100644 --- a/bonobo/util/__init__.py +++ b/bonobo/util/__init__.py @@ -1,4 +1,4 @@ -from bonobo.util.collections import ensure_tuple, sortedlist, tuplize +from bonobo.util.collections import cast, ensure_tuple, sortedlist, tuplize from bonobo.util.compat import deprecated, deprecated_alias from bonobo.util.inspect import ( inspect_node, @@ -16,6 +16,7 @@ from bonobo.util.objects import (get_name, get_attribute_or_create, ValueHolder) # Bonobo's util API __all__ = [ 'ValueHolder', + 'cast', 'deprecated', 'deprecated_alias', 'ensure_tuple', diff --git a/bonobo/util/api.py b/bonobo/util/api.py index 1acf5c8..1561207 100644 --- a/bonobo/util/api.py +++ b/bonobo/util/api.py @@ -2,6 +2,7 @@ from bonobo.util import get_name class ApiHelper: + # TODO __all__ kwarg only def __init__(self, __all__): self.__all__ = __all__ @@ -13,7 +14,8 @@ class ApiHelper: from inspect import signature parameters = list(signature(x).parameters) required_parameters = {'plugins', 'services', 'strategy'} - assert parameters[0] == 'graph', 'First parameter of a graph api function must be "graph".' + assert len(parameters) > 0 and parameters[ + 0] == 'graph', 'First parameter of a graph api function must be "graph".' assert required_parameters.intersection( parameters ) == required_parameters, 'Graph api functions must define the following parameters: ' + ', '.join( diff --git a/bonobo/util/collections.py b/bonobo/util/collections.py index 745c3b9..de706f4 100644 --- a/bonobo/util/collections.py +++ b/bonobo/util/collections.py @@ -26,31 +26,36 @@ def ensure_tuple(tuple_or_mixed, *, cls=tuple): if isinstance(tuple_or_mixed, tuple): return tuple.__new__(cls, tuple_or_mixed) - return tuple.__new__(cls, (tuple_or_mixed, )) + return tuple.__new__(cls, (tuple_or_mixed,)) -def tuplize(generator): - """ - Decorates a generator and make it a tuple-returning function. As a side effect, it can also decorate any - iterator-returning function to force return value to be a tuple. +def cast(type_): + def _wrap_cast(f): + @functools.wraps(f) + def _wrapped_cast(*args, **kwargs): + nonlocal f, type_ + return type_(f(*args, **kwargs)) - >>> tuplized_lambda = tuplize(lambda: [1, 2, 3]) - >>> tuplized_lambda() - (1, 2, 3) + return _wrapped_cast - >>> @tuplize - ... def my_generator(): - ... yield 1 - ... yield 2 - ... yield 3 - ... - >>> my_generator() - (1, 2, 3) + return _wrap_cast - """ - @functools.wraps(generator) - def tuplized(*args, **kwargs): - return tuple(generator(*args, **kwargs)) +tuplize = cast(tuple) +tuplize.__doc__ = """ +Decorates a generator and make it a tuple-returning function. As a side effect, it can also decorate any +iterator-returning function to force return value to be a tuple. - return tuplized +>>> tuplized_lambda = tuplize(lambda: [1, 2, 3]) +>>> tuplized_lambda() +(1, 2, 3) + +>>> @tuplize +... def my_generator(): +... yield 1 +... yield 2 +... yield 3 +... +>>> my_generator() +(1, 2, 3) +""" diff --git a/bonobo/util/resolvers.py b/bonobo/util/resolvers.py index 60934d8..5cf2738 100644 --- a/bonobo/util/resolvers.py +++ b/bonobo/util/resolvers.py @@ -8,7 +8,7 @@ import os import runpy import bonobo -from bonobo.util.collections import tuplize +from bonobo.util import cast class _RequiredModule: @@ -61,7 +61,7 @@ def _resolve_options(options=None): return dict() -@tuplize +@cast(tuple) def _resolve_transformations(transformations): """ Resolve a collection of strings into the matching python objects, defaulting to bonobo namespace if no package is provided. diff --git a/tests/util/test_collections.py b/tests/util/test_collections.py index 3a1e517..f502afd 100644 --- a/tests/util/test_collections.py +++ b/tests/util/test_collections.py @@ -1,5 +1,7 @@ +import pytest + from bonobo.util import sortedlist, ensure_tuple -from bonobo.util.collections import tuplize +from bonobo.util.collections import tuplize, cast def test_sortedlist(): @@ -12,12 +14,13 @@ def test_sortedlist(): def test_ensure_tuple(): - assert ensure_tuple('a') == ('a', ) - assert ensure_tuple(('a', )) == ('a', ) + assert ensure_tuple('a') == ('a',) + assert ensure_tuple(('a',)) == ('a',) assert ensure_tuple(()) is () -def test_tuplize(): +@pytest.mark.parametrize('tuplize', [tuplize, cast(tuple)]) +def test_tuplize(tuplize): tuplized_lambda = tuplize(lambda: [1, 2, 3]) assert tuplized_lambda() == (1, 2, 3) From 99a9efa0a66396690009e616e572cca59ff98bb6 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 27 Dec 2017 14:22:47 +0100 Subject: [PATCH 140/145] Release: 0.6.0a4 --- bonobo/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonobo/_version.py b/bonobo/_version.py index 26618ea..63f9f1e 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.6.0a3' +__version__ = '0.6.0a4' From 229117fccae955005b5eee08551cdc651cb26479 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 27 Dec 2017 17:36:13 +0100 Subject: [PATCH 141/145] .gitignore update --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ae199da..977da0d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ .ipynb_checkpoints .python-version /.idea +/.medikit-pipeline /.release /bonobo/contrib/jupyter/js/node_modules/ /bonobo/examples/work_in_progress/ From 9d52d9971a5ae787f6128df35566db9124e346bc Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 30 Dec 2017 15:18:39 +0100 Subject: [PATCH 142/145] Update requirements for beta. --- Makefile | 2 +- Projectfile | 4 ++-- requirements-dev.txt | 2 +- requirements-docker.txt | 9 +++++---- requirements-jupyter.txt | 4 ++-- requirements-sqlalchemy.txt | 6 +++--- requirements.txt | 2 +- setup.py | 4 ++-- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index bc29a5b..2a58251 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4.5 on 2017-12-27. +# Generated by Medikit 0.4.5 on 2017-12-30. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/Projectfile b/Projectfile index 5c880db..2faba69 100644 --- a/Projectfile +++ b/Projectfile @@ -58,14 +58,14 @@ python.add_requirements( 'pytest-timeout ~=1.0', ], docker=[ - 'bonobo-docker', + 'bonobo-docker ~=0.6.0a1', ], jupyter=[ 'ipywidgets ~=6.0', 'jupyter ~=1.0', ], sqlalchemy=[ - 'bonobo-sqlalchemy', + 'bonobo-sqlalchemy ~=0.6.0a1', ], ) diff --git a/requirements-dev.txt b/requirements-dev.txt index a86c724..aa5e833 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ -e .[dev] alabaster==0.7.10 -attrs==17.3.0 +attrs==17.4.0 babel==2.5.1 certifi==2017.11.5 chardet==3.0.4 diff --git a/requirements-docker.txt b/requirements-docker.txt index 6fbec83..b81c208 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,13 +1,13 @@ -e .[docker] appdirs==1.4.3 -bonobo-docker==0.5.0 +bonobo-docker==0.6.0a1 certifi==2017.11.5 chardet==3.0.4 colorama==0.3.9 docker-pycreds==0.2.1 -docker==2.3.0 +docker==2.7.0 fs==2.0.17 -graphviz==0.8.1 +graphviz==0.8.2 idna==2.6 jinja2==2.10 markupsafe==1.0 @@ -19,9 +19,10 @@ pyparsing==2.2.0 python-slugify==1.2.4 pytz==2017.3 requests==2.18.4 +semantic-version==2.6.0 six==1.11.0 stevedore==1.28.0 unidecode==0.4.21 urllib3==1.22 -websocket-client==0.45.0 +websocket-client==0.46.0 whistle==1.0.0 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 2f5b6fe..832cbde 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -1,6 +1,6 @@ -e .[jupyter] appnope==0.1.0 -attrs==17.3.0 +attrs==17.4.0 bleach==2.1.2 decorator==4.1.2 entrypoints==0.2.3 @@ -38,7 +38,7 @@ simplegeneric==0.8.1 six==1.11.0 terminado==0.8.1 testpath==0.3.1 -tornado==4.5.2 +tornado==5.0a1 traitlets==4.3.2 wcwidth==0.1.7 webencodings==0.5.1 diff --git a/requirements-sqlalchemy.txt b/requirements-sqlalchemy.txt index d46e7aa..46888e2 100644 --- a/requirements-sqlalchemy.txt +++ b/requirements-sqlalchemy.txt @@ -1,11 +1,11 @@ -e .[sqlalchemy] appdirs==1.4.3 -bonobo-sqlalchemy==0.5.1 +bonobo-sqlalchemy==0.6.0a1 certifi==2017.11.5 chardet==3.0.4 colorama==0.3.9 fs==2.0.17 -graphviz==0.8.1 +graphviz==0.8.2 idna==2.6 jinja2==2.10 markupsafe==1.0 @@ -18,7 +18,7 @@ python-slugify==1.2.4 pytz==2017.3 requests==2.18.4 six==1.11.0 -sqlalchemy==1.1.15 +sqlalchemy==1.2.0 stevedore==1.28.0 unidecode==0.4.21 urllib3==1.22 diff --git a/requirements.txt b/requirements.txt index 24321ea..ef36dda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ certifi==2017.11.5 chardet==3.0.4 colorama==0.3.9 fs==2.0.17 -graphviz==0.8.1 +graphviz==0.8.2 idna==2.6 jinja2==2.10 markupsafe==1.0 diff --git a/setup.py b/setup.py index 5d89c9a..716967d 100644 --- a/setup.py +++ b/setup.py @@ -67,9 +67,9 @@ setup( 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', 'pytest-cov (>= 2.5, < 3.0)', 'pytest-sugar (>= 0.9, < 0.10)', 'pytest-timeout (~= 1.0)', 'sphinx (>= 1.6, < 2.0)', 'yapf' ], - 'docker': ['bonobo-docker'], + 'docker': ['bonobo-docker (~= 0.6.0a1)'], 'jupyter': ['ipywidgets (~= 6.0)', 'jupyter (~= 1.0)'], - 'sqlalchemy': ['bonobo-sqlalchemy'] + 'sqlalchemy': ['bonobo-sqlalchemy (~= 0.6.0a1)'] }, entry_points={ 'bonobo.commands': [ From 7d4fb1dff09b61f8ec424b3a21e67277dfa5f8a0 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 30 Dec 2017 15:20:56 +0100 Subject: [PATCH 143/145] Release: 0.6.0b1 --- bonobo/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonobo/_version.py b/bonobo/_version.py index 63f9f1e..0520fdb 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.6.0a4' +__version__ = '0.6.0b1' From f640e358b42d874c1cda50a2eff16001ea10b5ae Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 1 Jan 2018 22:18:21 +0100 Subject: [PATCH 144/145] Doc update --- docs/tutorial/1-init.rst | 6 ++--- docs/tutorial/2-jobs.rst | 57 +++++++++++++++++++++++++++++----------- docs/tutorial/index.rst | 18 ++++--------- 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/docs/tutorial/1-init.rst b/docs/tutorial/1-init.rst index 5a1bbd9..9fc92f5 100644 --- a/docs/tutorial/1-init.rst +++ b/docs/tutorial/1-init.rst @@ -249,10 +249,10 @@ That's all for this first step. You now know: -* How to create a new job file. -* How to inspect the content of a job file. +* How to create a new job (using a single file). +* How to inspect the content of a job. * What should go in a job file. * How to execute a job file. * How to read the console output. -**Jump to** :doc:`2-jobs` +It's now time to jump to :doc:`2-jobs`. diff --git a/docs/tutorial/2-jobs.rst b/docs/tutorial/2-jobs.rst index d2bbfe5..e7d4baf 100644 --- a/docs/tutorial/2-jobs.rst +++ b/docs/tutorial/2-jobs.rst @@ -4,31 +4,56 @@ Part 2: Writing ETL Jobs What's an ETL job ? ::::::::::::::::::: -- data flow, stream processing -- each node, first in first out -- parallelism +In |bonobo|, an ETL job is a formal definition of an executable graph. -Each node has input rows, each row is one call, and each call has the input row passed as *args. +Each node of a graph will be executed in isolation from the other nodes, and the data is passed from one node to the +next using FIFO queues, managed by the framework. It's transparent to the end-user, though, and you'll only use +function arguments (for inputs) and return/yield values (for outputs). -Each call can have outputs, sent either using return, or yield. +Each input row of a node will cause one call to this node's callable. Each output is cast internally as a tuple-like +data structure (or more precisely, a namedtuple-like data structure), and for one given node, each output row must +have the same structure. -Each output row is stored internally as a tuple (or a namedtuple-like structure), and each output row must have the same structure (same number of fields, same len for tuple). +If you return/yield something which is not a tuple, bonobo will create a tuple of one element. -If you yield something which is not a tuple, bonobo will create a tuple of one element. +Properties +---------- -By default, exceptions are not fatal in bonobo. If a call raise an error, then bonobo will display the stack trace, increment the "err" counter for this node and move to the next input row. +|bonobo| assists you with defining the data-flow of your data engineering process, and then streams data through your +callable graphs. -Some errors are fatal, though. For example, if you pass a 2 elements tuple to a node that takes 3 args, bonobo will raise an UnrecoverableTypeError, and exit the current execution. +* Each node call will process one row of data. +* Queues that flows the data between node are first-in, first-out (FIFO) standard python :class:`queue.Queue`. +* Each node will run in parallel +* Default execution strategy use threading, and each node will run in a separate thread. + +Fault tolerance +--------------- + +Node execution is fault tolerant. + +If an exception is raised from a node call, then this node call will be aborted but bonobo will continue the execution +with the next row (after outputing the stack trace and incrementing the "err" counter for the node context). + +It allows to have ETL jobs that ignore faulty data and try their best to process the valid rows of a dataset. + +Some errors are fatal, though. + +If you pass a 2 elements tuple to a node that takes 3 args, |bonobo| will raise an :class:`bonobo.errors.UnrecoverableTypeError`, and exit the +current graph execution as fast as it can (finishing the other node executions that are in progress first, but not +starting new ones if there are remaining input rows). + + +Let's write a sample data integration job +::::::::::::::::::::::::::::::::::::::::: + +Let's create a sample application. + +The goal of this application will be to extract all the fablabs in the world using an open-data API, normalize this +data and, for now, display it. We'll then build on this foundation in the next steps to write to files, databases, etc. -Let's write one -::::::::::::::: -We'll create a job to do the following -* Extract all the FabLabs from an open data API -* Apply a bit of formating -* Geocode the address and normalize it, if we can -* Display it (in the next step, we'll learn about writing the result to a file. Moving forward diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 438d9d8..6f57dc1 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -1,9 +1,6 @@ First steps =========== -What is Bonobo? -::::::::::::::: - Bonobo is an ETL (Extract-Transform-Load) framework for python 3.5. The goal is to define data-transformations, with python code in charge of handling similar shaped independent lines of data. @@ -14,8 +11,7 @@ Bonobo is a lean manufacturing assembly line for data that let you focus on the Bonobo uses simple python and should be quick and easy to learn. -Tutorial -:::::::: +**Tutorials** .. toctree:: :maxdepth: 1 @@ -26,8 +22,8 @@ Tutorial 4-services 5-packaging -More -:::: + +**Integrations** .. toctree:: :maxdepth: 1 @@ -36,9 +32,7 @@ More notebooks sqlalchemy -What's next? -:::::::::::: - +**What's next?** Once you're familiar with all the base concepts, you can... @@ -46,9 +40,7 @@ Once you're familiar with all the base concepts, you can... * Explore the :doc:`Extensions ` to widen the possibilities. * Open the :doc:`References ` and start hacking like crazy. - -You're not alone! -::::::::::::::::: +**You're not alone!** Good documentation is not easy to write. From 870db691502d096737603a576674d7781802a606 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 1 Jan 2018 22:19:20 +0100 Subject: [PATCH 145/145] Release: 0.6.0 --- Makefile | 2 +- bonobo/_version.py | 2 +- requirements-docker.txt | 2 +- requirements-sqlalchemy.txt | 2 +- requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 2a58251..288ff1a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Generated by Medikit 0.4.5 on 2017-12-30. +# Generated by Medikit 0.4.5 on 2018-01-01. # All changes will be overriden. PACKAGE ?= bonobo diff --git a/bonobo/_version.py b/bonobo/_version.py index 0520fdb..ef7eb44 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.6.0b1' +__version__ = '0.6.0' diff --git a/requirements-docker.txt b/requirements-docker.txt index b81c208..baa97e3 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -14,7 +14,7 @@ markupsafe==1.0 mondrian==0.6.1 packaging==16.8 pbr==3.1.1 -psutil==5.4.2 +psutil==5.4.3 pyparsing==2.2.0 python-slugify==1.2.4 pytz==2017.3 diff --git a/requirements-sqlalchemy.txt b/requirements-sqlalchemy.txt index 46888e2..8508033 100644 --- a/requirements-sqlalchemy.txt +++ b/requirements-sqlalchemy.txt @@ -12,7 +12,7 @@ markupsafe==1.0 mondrian==0.6.1 packaging==16.8 pbr==3.1.1 -psutil==5.4.2 +psutil==5.4.3 pyparsing==2.2.0 python-slugify==1.2.4 pytz==2017.3 diff --git a/requirements.txt b/requirements.txt index ef36dda..07f51aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ markupsafe==1.0 mondrian==0.6.1 packaging==16.8 pbr==3.1.1 -psutil==5.4.2 +psutil==5.4.3 pyparsing==2.2.0 python-slugify==1.2.4 pytz==2017.3