'
);
},
});
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 '
'\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(' ')
+ '
'
);
},
});
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 '
'\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/ext/opendatasoft.py b/bonobo/contrib/opendatasoft/__init__.py
similarity index 68%
rename from bonobo/ext/opendatasoft.py
rename to bonobo/contrib/opendatasoft/__init__.py
index 2dc54c0..5144e59 100644
--- a/bonobo/ext/opendatasoft.py
+++ b/bonobo/contrib/opendatasoft/__init__.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/errors.py b/bonobo/errors.py
index 08b97d4..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,22 @@ 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
+
+
class UnrecoverableValueError(UnrecoverableError, ValueError):
pass
diff --git a/bonobo/examples/__init__.py b/bonobo/examples/__init__.py
index 49b1544..ec68fc5 100644
--- a/bonobo/examples/__init__.py
+++ b/bonobo/examples/__init__.py
@@ -1,23 +1,32 @@
-def require(package, requirement=None):
- requirement = requirement or package
+import bonobo
- 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
+
+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/__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/clock.py b/bonobo/examples/clock.py
new file mode 100644
index 0000000..1977cba
--- /dev/null
+++ b/bonobo/examples/clock.py
@@ -0,0 +1,27 @@
+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/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..b5b3b4f
--- /dev/null
+++ b/bonobo/examples/datasets/__main__.py
@@ -0,0 +1,62 @@
+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, get_datasets_dir, get_minor_version
+
+graph_factories = {
+ 'coffeeshops': get_coffeeshops_graph,
+ 'fablabs': get_fablabs_graph,
+}
+
+if __name__ == '__main__':
+ parser = examples.get_argument_parser()
+ 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)
+ graph_names = list(
+ options['target']
+ 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']:
+ # 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(
+ get_minor_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/_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.json b/bonobo/examples/datasets/coffeeshops.json
deleted file mode 100644
index 391b5e8..0000000
--- a/bonobo/examples/datasets/coffeeshops.json
+++ /dev/null
@@ -1,182 +0,0 @@
-{"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
diff --git a/bonobo/examples/datasets/coffeeshops.py b/bonobo/examples/datasets/coffeeshops.py
index dc3db52..93aa0d5 100644
--- a/bonobo/examples/datasets/coffeeshops.py
+++ b/bonobo/examples/datasets/coffeeshops.py
@@ -1,29 +1,64 @@
-"""
-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.run import get_default_services
-from bonobo.ext.opendatasoft import OpenDataSoftAPI
+from bonobo import examples
+from bonobo.contrib.opendatasoft import OpenDataSoftAPI as ODSReader
+from bonobo.examples.datasets.services import get_services
-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),
-)
+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__':
- 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/coffeeshops.txt b/bonobo/examples/datasets/coffeeshops.txt
deleted file mode 100644
index 9e3c181..0000000
--- a/bonobo/examples/datasets/coffeeshops.txt
+++ /dev/null
@@ -1,182 +0,0 @@
-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
diff --git a/bonobo/examples/datasets/fablabs.py b/bonobo/examples/datasets/fablabs.py
index 986aea9..0a6e188 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.run import get_default_services
-from bonobo.ext.opendatasoft import OpenDataSoftAPI
+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,31 @@ 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..6412156
--- /dev/null
+++ b/bonobo/examples/datasets/services.py
@@ -0,0 +1,20 @@
+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', get_minor_version(), *dirs
+ )
+ os.makedirs(target_dir, exist_ok=True)
+ return target_dir
+
+
+def get_services():
+ 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
diff --git a/bonobo/examples/env_vars/get_passed_env.py b/bonobo/examples/env_vars/get_passed_env.py
deleted file mode 100644
index 54a3280..0000000
--- a/bonobo/examples/env_vars/get_passed_env.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import os
-
-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')
- return env_test_user, env_test_number, env_test_string
-
-
-def load(s: str):
- print(s)
-
-
-graph = bonobo.Graph(extract, load)
-
-if __name__ == '__main__':
- bonobo.run(graph)
diff --git a/bonobo/examples/environ.py b/bonobo/examples/environ.py
new file mode 100644
index 0000000..280d2e1
--- /dev/null
+++ b/bonobo/examples/environ.py
@@ -0,0 +1,27 @@
+"""
+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():
+ graph = bonobo.Graph()
+ graph.add_chain(extract_environ, print)
+
+ return graph
+
+
+if __name__ == '__main__':
+ parser = bonobo.get_argument_parser()
+ with bonobo.parse_args(parser):
+ bonobo.run(get_graph())
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 33412c3..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.run 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 27dc38e..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.run 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 71a2b9a..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.run 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 6ca6ef8..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.run 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/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/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/tut02e02_write.py b/bonobo/examples/tutorials/tut02e02_write.py
index e5a8445..a33a11b 100644
--- a/bonobo/examples/tutorials/tut02e02_write.py
+++ b/bonobo/examples/tutorials/tut02e02_write.py
@@ -2,15 +2,13 @@ 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'
- ),
+ bonobo.JsonWriter('coffeeshops.json', fs='fs.output'),
)
diff --git a/bonobo/examples/tutorials/tut02e03_writeasmap.py b/bonobo/examples/tutorials/tut02e03_writeasmap.py
index e234f22..afc251e 100644
--- a/bonobo/examples/tutorials/tut02e03_writeasmap.py
+++ b/bonobo/examples/tutorials/tut02e03_writeasmap.py
@@ -11,16 +11,17 @@ 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]
+ self, fs, file, lineno,
+ json.dumps(row)[1:-1]
)
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'),
)
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/__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/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 1903151..6675a77 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():
@@ -23,17 +23,19 @@ 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)
-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/__init__.py b/bonobo/execution/__init__.py
index b8a83dd..43ffbf3 100644
--- a/bonobo/execution/__init__.py
+++ b/bonobo/execution/__init__.py
@@ -1 +1,5 @@
-from bonobo.execution.graph import GraphExecutionContext, NodeExecutionContext, PluginExecutionContext
+import logging
+
+logger = logging.getLogger(__name__)
+
+__all__ = []
diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py
deleted file mode 100644
index abb3516..0000000
--- a/bonobo/execution/base.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import traceback
-from contextlib import contextmanager
-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.errors import print_error
-from bonobo.util.objects import Wrapper, get_name
-
-
-@contextmanager
-def recoverable(error_handler):
- try:
- yield
- except Exception as exc: # pylint: disable=broad-except
- error_handler(exc, traceback.format_exc())
-
-
-@contextmanager
-def unrecoverable(error_handler):
- try:
- yield
- except Exception as exc: # pylint: disable=broad-except
- error_handler(exc, traceback.format_exc())
- raise # raise unrecoverableerror from x ?
-
-
-class LoopingExecutionContext(Wrapper):
- alive = True
- PERIOD = 0.25
-
- @property
- def started(self):
- return self._started
-
- @property
- def stopped(self):
- return self._stopped
-
- 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 = False, False
- self._stack = None
-
- # XXX enhancers
- self._enhancers = get_enhancers(self.wrapped)
-
- def __enter__(self):
- self.start()
- return self
-
- def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
- self.stop()
-
- def start(self):
- if self.started:
- raise RuntimeError('Cannot start a node twice ({}).'.format(get_name(self)))
-
- self._started = True
-
- self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context())
- 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:
- self.step()
- sleep(self.PERIOD)
-
- 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)))
-
- if self._stopped:
- return
-
- try:
- if self._stack:
- self._stack.teardown()
- finally:
- self._stopped = True
-
- def handle_error(self, exc, trace):
- return print_error(exc, trace, context=self.wrapped)
-
- 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 ()
diff --git a/bonobo/execution/contexts/__init__.py b/bonobo/execution/contexts/__init__.py
new file mode 100644
index 0000000..4c462c5
--- /dev/null
+++ 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/base.py b/bonobo/execution/contexts/base.py
new file mode 100644
index 0000000..953f13c
--- /dev/null
+++ b/bonobo/execution/contexts/base.py
@@ -0,0 +1,136 @@
+import logging
+import sys
+from contextlib import contextmanager
+from logging import ERROR
+
+from mondrian import term
+
+from bonobo.util import deprecated
+from bonobo.util.objects import Wrapper, get_name
+
+
+@contextmanager
+def recoverable(error_handler):
+ try:
+ yield
+ except Exception as exc: # pylint: disable=broad-except
+ error_handler(*sys.exc_info(), level=ERROR)
+
+
+@contextmanager
+def unrecoverable(error_handler):
+ try:
+ yield
+ except Exception as exc: # pylint: disable=broad-except
+ error_handler(*sys.exc_info(), level=ERROR)
+ raise # raise unrecoverableerror from x ?
+
+
+class Lifecycle:
+ def __init__(self):
+ self._started = False
+ self._stopped = False
+ self._killed = False
+ self._defunct = False
+
+ @property
+ def started(self):
+ return self._started
+
+ @property
+ def stopped(self):
+ return self._stopped
+
+ @property
+ def killed(self):
+ return self._killed
+
+ @property
+ def defunct(self):
+ return self._defunct
+
+ @property
+ 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. """
+ if self._defunct:
+ return '!'
+ if not self.started:
+ return ' '
+ if not self.stopped:
+ return '+'
+ return '-'
+
+ def __enter__(self):
+ self.start()
+ return self
+
+ 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('This context is already started ({}).'.format(get_name(self)))
+
+ self._started = True
+
+ def stop(self):
+ if not self.started:
+ raise RuntimeError('This context cannot be stopped as it never started ({}).'.format(get_name(self)))
+
+ self._stopped = True
+
+ if self._stopped: # Stopping twice has no effect
+ return
+
+ def kill(self):
+ if not self.started:
+ raise RuntimeError('Cannot kill an unstarted context.')
+
+ if self.stopped:
+ raise RuntimeError('Cannot kill a stopped context.')
+
+ 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
+
+ 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/graph.py b/bonobo/execution/contexts/graph.py
new file mode 100644
index 0000000..a6559a3
--- /dev/null
+++ b/bonobo/execution/contexts/graph.py
@@ -0,0 +1,115 @@
+from functools import partial
+from time import sleep
+
+from bonobo.config import create_container
+from bonobo.constants import BEGIN, END
+from bonobo.execution import events
+from bonobo.execution.contexts.node import NodeExecutionContext
+from bonobo.execution.contexts.plugin import PluginExecutionContext
+from whistle import EventDispatcher
+
+
+class GraphExecutionContext:
+ NodeExecutionContextType = NodeExecutionContext
+ PluginExecutionContextType = PluginExecutionContext
+
+ TICK_PERIOD = 0.25
+
+ @property
+ def started(self):
+ return any(node.started for node in self.nodes)
+
+ @property
+ def stopped(self):
+ return all(node.started and node.stopped for node in self.nodes)
+
+ @property
+ def alive(self):
+ return any(node.alive for node in self.nodes)
+
+ 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 ()]
+ 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):
+ 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)
+
+ def __getitem__(self, item):
+ return self.nodes[item]
+
+ def __len__(self):
+ return len(self.nodes)
+
+ 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):
+ if isinstance(plugin, type):
+ plugin = 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."""
+
+ for i in self.graph.outputs_of(BEGIN):
+ 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(pause=False)
+ for node in self.nodes:
+ if starter is None:
+ node.start()
+ else:
+ starter(node)
+ self.dispatch(events.STARTED)
+
+ def tick(self, pause=True):
+ self.dispatch(events.TICK)
+ if pause:
+ 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):
+ self.dispatch(events.STOP)
+ for node_context in self.nodes:
+ if stopper is None:
+ node_context.stop()
+ else:
+ stopper(node_context)
+ self.tick(pause=False)
+ 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/contexts/node.py b/bonobo/execution/contexts/node.py
new file mode 100644
index 0000000..316d9b8
--- /dev/null
+++ b/bonobo/execution/contexts/node.py
@@ -0,0 +1,377 @@
+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, 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, 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')
+
+ # 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 []
+
+ # Types
+ self._input_type, self._input_length = None, None
+ self._output_type = None
+
+ # Stack: context decorators for the execution
+ self._stack = None
+
+ def __str__(self):
+ 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.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:
+ 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(
+ 'Configurables should be instanciated before execution starts.\nGot {!r}.\n'.format(self.wrapped)
+ )
+ self._stack.setup(self)
+ except Exception:
+ # 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()
+ except InactiveReadableError:
+ break
+ except Empty:
+ sleep(TICK_PERIOD) # XXX: How do we determine this constant?
+ continue
+ except (
+ NotImplementedError,
+ UnrecoverableError,
+ ):
+ self.fatal(sys.exc_info()) # exit loop
+ except Exception: # pylint: disable=broad-except
+ self.error(sys.exc_info()) # does not exit loop
+ except BaseException:
+ self.fatal(sys.exc_info()) # exit loop
+
+ logger.debug('Node loop ends for {!r}.'.format(self))
+
+ def step(self):
+ """
+ A single step in the loop.
+
+ 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
+ results = self._stack(input_bag)
+
+ # 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
+ else:
+ # Push data (in case of an iterator)
+ self._send(self._cast(input_bag, result))
+ elif 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:
+ try:
+ self._stack.teardown()
+ except:
+ self.fatal(sys.exc_info())
+
+ super().stop()
+
+ def send(self, *_output, _input=None):
+ return self._send(self._cast(_input, _output))
+
+ ### 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):
+ """
+ Push a message list to this context's input queue.
+
+ :param mixed value: message
+ """
+ for message in messages:
+ 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 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 the real slim output.
+
+ :param _input: Bag
+ :param _output: mixed
+ :return: Bag
+ """
+
+ tokens, _output = split_token(_output)
+
+ 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):
+ """
+ Sends a message to all of this context's outputs.
+
+ :param mixed value: message
+ :param _control: if true, won't count in statistics.
+ """
+
+ if not _control:
+ self.increment('out')
+
+ for output in self.outputs:
+ output.put(value)
+
+ def _get_initial_context(self):
+ if self.parent:
+ return UnboundArguments((), self.parent.services.kwargs_for(self.wrapped))
+ if self.services:
+ return UnboundArguments((), self.services.kwargs_for(self.wrapped))
+ return UnboundArguments((), {})
+
+
+def isflag(param):
+ return isinstance(param, Flag)
+
+
+def split_token(output):
+ """
+ Split an output into token tuple, real output tuple.
+
+ :param output:
+ :return: tuple, tuple
+ """
+
+ output = ensure_tuple(output)
+
+ 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
+
+ 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/execution/contexts/plugin.py b/bonobo/execution/contexts/plugin.py
new file mode 100644
index 0000000..3551d0d
--- /dev/null
+++ b/bonobo/execution/contexts/plugin.py
@@ -0,0 +1,13 @@
+from bonobo.execution.contexts.base import BaseContext
+
+
+class PluginExecutionContext(BaseContext):
+ @property
+ def dispatcher(self):
+ return self.parent.dispatcher
+
+ def register(self):
+ return self.wrapped.register(self.dispatcher)
+
+ def unregister(self):
+ return self.wrapped.unregister(self.dispatcher)
diff --git a/bonobo/execution/events.py b/bonobo/execution/events.py
new file mode 100644
index 0000000..3bf3986
--- /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, context):
+ self.context = context
diff --git a/bonobo/execution/graph.py b/bonobo/execution/graph.py
deleted file mode 100644
index 91e4aef..0000000
--- a/bonobo/execution/graph.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from functools import partial
-
-from bonobo.config import create_container
-from bonobo.constants import BEGIN, END
-from bonobo.execution.node import NodeExecutionContext
-from bonobo.execution.plugin import PluginExecutionContext
-
-
-class GraphExecutionContext:
- @property
- def started(self):
- return any(node.started for node in self.nodes)
-
- @property
- def stopped(self):
- return all(node.started and node.stopped for node in self.nodes)
-
- @property
- def alive(self):
- return any(node.alive for node in self.nodes)
-
- 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.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)]
- 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):
- return self.nodes[item]
-
- def __len__(self):
- return len(self.nodes)
-
- def __iter__(self):
- yield from self.nodes
-
- 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."""
-
- for i in self.graph.outputs_of(BEGIN):
- for message in messages:
- self[i].write(message)
-
- def start(self):
- # todo use strategy
- for node in self.nodes:
- node.start()
-
- def stop(self):
- # todo use strategy
- for node in self.nodes:
- node.stop()
-
- def loop(self):
- # todo use strategy
- for node in self.nodes:
- node.loop()
diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py
deleted file mode 100644
index e8869ac..0000000
--- a/bonobo/execution/node.py
+++ /dev/null
@@ -1,154 +0,0 @@
-import traceback
-from queue import Empty
-from time import sleep
-
-from bonobo.constants import INHERIT_INPUT, NOT_MODIFIED
-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.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
-
-
-class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
- """
- todo: make the counter dependant of parent context?
- """
-
- @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 __init__(self, wrapped, parent=None, services=None):
- LoopingExecutionContext.__init__(self, wrapped, parent=parent, services=services)
- WithStatistics.__init__(self, 'in', 'out', 'err')
-
- self.input = Input()
- self.outputs = []
-
- def __str__(self):
- return self.alive_str + ' ' + 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=' '))
-
- def write(self, *messages):
- """
- Push a message list to this context's input queue.
-
- :param mixed value: message
- """
- for message in messages:
- self.input.put(message)
-
- # XXX deprecated alias
- recv = deprecated_alias('recv', write)
-
- def send(self, value, _control=False):
- """
- Sends a message to all of this context's outputs.
-
- :param mixed value: message
- :param _control: if true, won't count in statistics.
- """
-
- 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)
-
- push = deprecated_alias('push', send)
-
- def get(self): # recv() ? input_data = self.receive()
- """
- 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)
- self.increment('in')
- return row
-
- def loop(self):
- while True:
- try:
- self.step()
- except KeyboardInterrupt:
- raise
- except InactiveReadableError:
- break
- except Empty:
- sleep(self.PERIOD)
- continue
- except UnrecoverableError as exc:
- self.handle_error(exc, traceback.format_exc())
- self.input.shutdown()
- break
- except Exception as exc: # pylint: disable=broad-except
- self.handle_error(exc, traceback.format_exc())
-
- 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()
-
- # todo add timer
- self.handle_results(input_bag, input_bag.apply(self._stack))
-
- 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
- try:
- result = next(results)
- except StopIteration:
- break
- else:
- self.send(_resolve(input_bag, result))
-
-
-def _resolve(input_bag, output):
- # NotModified means to send the input unmodified to output.
- if output is NOT_MODIFIED:
- return input_bag
-
- if iserrorbag(output):
- return output
-
- # If it does not look like a bag, let's create one for easier manipulation
- if hasattr(output, 'apply'):
- # 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
diff --git a/bonobo/execution/plugin.py b/bonobo/execution/plugin.py
deleted file mode 100644
index a207f23..0000000
--- a/bonobo/execution/plugin.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from bonobo.execution.base import LoopingExecutionContext, recoverable
-
-
-class PluginExecutionContext(LoopingExecutionContext):
- PERIOD = 0.5
-
- 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 start(self):
- super().start()
-
- with recoverable(self.handle_error):
- self.wrapped.initialize()
-
- def shutdown(self):
- if self.started:
- with recoverable(self.handle_error):
- self.wrapped.finalize()
- self.alive = False
-
- def step(self):
- with recoverable(self.handle_error):
- self.wrapped.run()
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/execution/strategies/base.py b/bonobo/execution/strategies/base.py
new file mode 100644
index 0000000..0a8d2a5
--- /dev/null
+++ b/bonobo/execution/strategies/base.py
@@ -0,0 +1,18 @@
+from bonobo.execution.contexts.graph import GraphExecutionContext
+
+
+class Strategy:
+ """
+ Base class for execution strategies.
+
+ """
+ GraphExecutionContextType = GraphExecutionContext
+
+ 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/execution/strategies/executor.py b/bonobo/execution/strategies/executor.py
new file mode 100644
index 0000000..1e2d45f
--- /dev/null
+++ b/bonobo/execution/strategies/executor.py
@@ -0,0 +1,74 @@
+import functools
+import logging
+import sys
+from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
+
+from bonobo.constants import BEGIN, END
+from bonobo.execution.strategies.base import Strategy
+
+logger = logging.getLogger(__name__)
+
+
+class ExecutorStrategy(Strategy):
+ """
+ Strategy based on a concurrent.futures.Executor subclass (or similar interface).
+
+ """
+
+ executor_factory = Executor
+
+ def create_executor(self):
+ return self.executor_factory()
+
+ def execute(self, graph, **kwargs):
+ context = self.create_graph_execution_context(graph, **kwargs)
+ context.write(BEGIN, (), END)
+
+ futures = []
+
+ with self.create_executor() as executor:
+ try:
+ context.start(self.get_starter(executor, futures))
+ except:
+ logger.critical('Exception caught while starting execution context.', exc_info=sys.exc_info())
+
+ while context.alive:
+ try:
+ context.tick()
+ except KeyboardInterrupt:
+ logging.getLogger(__name__).warning(
+ 'KeyboardInterrupt received. Trying to terminate the nodes gracefully.'
+ )
+ context.kill()
+ break
+
+ context.stop()
+
+ return context
+
+ def get_starter(self, executor, futures):
+ def starter(node):
+ @functools.wraps(node)
+ def _runner():
+ 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))
+ except:
+ logging.getLogger(__name__).critical('futures.append', exc_info=sys.exc_info())
+
+ return starter
+
+
+class ThreadPoolExecutorStrategy(ExecutorStrategy):
+ executor_factory = ThreadPoolExecutor
+
+
+class ProcessPoolExecutorStrategy(ExecutorStrategy):
+ executor_factory = ProcessPoolExecutor
diff --git a/bonobo/execution/strategies/naive.py b/bonobo/execution/strategies/naive.py
new file mode 100644
index 0000000..01b0416
--- /dev/null
+++ b/bonobo/execution/strategies/naive.py
@@ -0,0 +1,25 @@
+from bonobo.constants import BEGIN, END
+from bonobo.execution.strategies.base import Strategy
+
+
+class NaiveStrategy(Strategy):
+ # TODO: how to run plugins in "naive" mode ?
+
+ def execute(self, graph, **kwargs):
+ context = self.create_graph_execution_context(graph, **kwargs)
+ context.write(BEGIN, (), END)
+
+ # start
+ context.start()
+
+ # 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/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/console.py b/bonobo/ext/console.py
deleted file mode 100644
index e42a2b7..0000000
--- a/bonobo/ext/console.py
+++ /dev/null
@@ -1,163 +0,0 @@
-import io
-import sys
-from contextlib import redirect_stdout
-
-from colorama import Style, Fore, init
-
-init(wrap=True)
-
-from bonobo import settings
-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()
-
-
-class ConsoleOutputPlugin(Plugin):
- """
- Outputs status information to the connected stdout. Can be a TTY, with or without support for colors/cursor
- movements, or a non tty (pipe, file, ...). The features are adapted to terminal capabilities.
-
- On Windows, we'll play a bit differently because we don't know how to manipulate cursor position. We'll only
- display stats at the very end, and there won't be this "buffering" logic we need to display both stats and stdout.
-
- .. attribute:: prefix
-
- String prefix of output lines.
-
- """
-
- def __init__(self, context):
- super(ConsoleOutputPlugin, self).__init__(context)
- self._reset()
-
- def _reset(self):
- self.prefix = ''
- self.counter = 0
- self._append_cache = ''
- self.isatty = sys.stdout.isatty()
- self.iswindows = (sys.platform == 'win32')
-
- def initialize(self):
- self._reset()
- self._stdout = sys.stdout
- self.stdout = IOBuffer()
- self.redirect_stdout = redirect_stdout(self._stdout if self.iswindows else self.stdout)
- self.redirect_stdout.__enter__()
-
- def run(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):
- self._write(self.context.parent, rewind=False)
- 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)
-
- alive_color = Style.BRIGHT
- dead_color = Style.BRIGHT + Fore.BLACK
-
- 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(),
- Style.RESET_ALL,
- ' ',
- )
- )
- else:
- _line = ''.join(
- (
- ' ',
- dead_color,
- '-',
- ' ',
- node.name,
- name_suffix,
- ' ',
- node.get_statistics_as_string(),
- Style.RESET_ALL,
- ' ',
- )
- )
- print(prefix + _line + '\033[0K', file=sys.stderr)
-
- if append:
- # todo handle multiline
- print(
- ''.join(
- (
- ' `-> ', ' '.join('{}{}{}: {}'.format(Style.BRIGHT, k, Style.RESET_ALL, v) for k, v in append),
- CLEAR_EOL
- )
- ),
- file=sys.stderr
- )
- t_cnt += 1
-
- if rewind:
- print(CLEAR_EOL, file=sys.stderr)
- print(MOVE_CURSOR_UP(t_cnt + 2), file=sys.stderr)
-
- def _write(self, graph_context, rewind):
- if settings.PROFILE.get():
- 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))),
- )
- else:
- append = ()
- self.write(graph_context, prefix=self.prefix, append=append, rewind=rewind)
- self.counter += 1
-
-
-def memory_usage():
- import os, psutil
- process = psutil.Process(os.getpid())
- return process.memory_info()[0] / float(2**20)
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/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())
diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py
index e23dd05..9708996 100644
--- a/bonobo/nodes/basics.py
+++ b/bonobo/nodes/basics.py
@@ -1,23 +1,29 @@
import functools
+import html
import itertools
+import pprint
from bonobo import settings
-from bonobo.config import Configurable, Option
-from bonobo.config.processors import ContextProcessor
-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
-
-from bonobo.constants import NOT_MODIFIED
+from mondrian import term
__all__ = [
+ 'FixedWindow',
+ 'Format',
'Limit',
+ 'OrderFields',
'PrettyPrinter',
+ 'Rename',
+ 'SetFields',
'Tee',
- 'arg0_to_kwargs',
+ 'UnpackItems',
'count',
'identity',
- 'kwargs_to_arg0',
'noop',
]
@@ -34,6 +40,8 @@ class Limit(Configurable):
Number of rows to let go through.
+ TODO: simplify into a closure building factory?
+
"""
limit = Option(positional=True, default=10)
@@ -41,7 +49,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
@@ -59,55 +67,261 @@ 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] + '...'
+ return s
class PrettyPrinter(Configurable):
- def call(self, *args, **kwargs):
- formater = self._format_quiet if settings.QUIET.get() else self._format_console
+ max_width = Option(
+ int,
+ default=term.get_size()[0],
+ required=False,
+ __doc__='''
+ If set, truncates the output values longer than this to this width.
+ '''
+ )
- for i, (item, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())):
- print(formater(i, item, value))
+ 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.
+ '''
+ )
- def _format_quiet(self, i, item, value):
- return ' '.join(((' ' if i else '-'), str(item), ':', str(value).strip()))
+ @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 _format_console(self, i, item, value):
- return ' '.join(
- ((' ' if i else '•'), str(item), '=', str(value).strip().replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL)
- )
+ def __call__(self, context, *args, **kwargs):
+ 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 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 print_console(self, context, *args, **kwargs):
+ 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('\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]):
+ key = '{}{}'.format(fields[key], term.lightblack('[{}]'.format(key)))
+ else:
+ key = str(index)
+
+ prefix = '\u2502 {} = '.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), '\u2502' + ' ' * (len(prefix) - 1))
+ ).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))),
+ '
',
+ ]
-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):
+class FixedWindow(Configurable):
"""
- 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).
+ 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], ...
- :param row:
- :return: bonobo.Bag
"""
- return Bag(**row)
+
+ length = Option(int, positional=True) # type: int
+
+ @ContextProcessor
+ def buffer(self, context):
+ buffer = yield ValueHolder([])
+ if len(buffer):
+ last_value = buffer.get()
+ last_value += [None] * (self.length - len(last_value))
+ context.send(*last_value)
+
+ @use_raw_input
+ def __call__(self, buffer, bag):
+ buffer.append(bag)
+ if len(buffer) >= self.length:
+ yield tuple(buffer.get())
+ buffer.set([])
-def kwargs_to_arg0(**row):
+@transformation_factory
+def OrderFields(fields):
"""
- 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) .
+ Transformation factory to reorder fields in a data stream.
- :param **row:
- :return: bonobo.Bag
+ :param fields:
+ :return: callable
"""
- return Bag(row)
+ 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):
+ 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/factory.py b/bonobo/nodes/factory.py
deleted file mode 100644
index 2a1c30b..0000000
--- a/bonobo/nodes/factory.py
+++ /dev/null
@@ -1,219 +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 dict(self, x):
- return x if isinstance(x, dict) else dict(x)
-
- @operation('int')
- def int(self):
- pass
-
- @operation('str')
- def str(self, x):
- return x if isinstance(x, str) else str(x)
-
- @operation('list')
- def list(self):
- pass
-
- @operation('tuple')
- def tuple(self):
- pass
-
- 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(
- __file__ +
- ' 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):
- 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/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/__init__.py b/bonobo/nodes/io/__init__.py
index f364dd9..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 .file import FileReader, FileWriter
-from .json import JsonReader, JsonWriter
from .csv import CsvReader, CsvWriter
+from .file import FileReader, FileWriter
+from .json import JsonReader, JsonWriter, LdjsonReader, LdjsonWriter
from .pickle import PickleReader, PickleWriter
__all__ = [
@@ -12,6 +12,8 @@ __all__ = [
'FileWriter',
'JsonReader',
'JsonWriter',
+ 'LdjsonReader',
+ 'LdjsonWriter',
'PickleReader',
'PickleWriter',
]
diff --git a/bonobo/nodes/io/base.py b/bonobo/nodes/io/base.py
index c59195a..037992f 100644
--- a/bonobo/nodes/io/base.py
+++ b/bonobo/nodes/io/base.py
@@ -1,50 +1,15 @@
-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, __doc__='''
- Input/output format for rows. This will be removed in 0.6, so please use the kwargs format.
- ''')
-
- def get_input(self, *args, **kwargs):
- if self.ioformat == settings.IOFORMAT_ARG0:
- if len(args) != 1 or len(kwargs):
- raise UnrecoverableValueError(
- 'Wrong input formating: IOFORMAT=ARG0 implies one arg and no kwargs, got args={!r} and kwargs={!r}.'.
- format(args, kwargs)
- )
- return args[0]
-
- if self.ioformat == settings.IOFORMAT_KWARGS:
- if len(args) or not len(kwargs):
- raise UnrecoverableValueError(
- 'Wrong input formating: IOFORMAT=KWARGS ioformat implies no arg, got args={!r} and kwargs={!r}.'.
- format(args, kwargs)
- )
- return kwargs
-
- raise UnrecoverableNotImplementedError('Unsupported format.')
-
- def get_output(self, row):
- if self.ioformat == settings.IOFORMAT_ARG0:
- return row
-
- if self.ioformat == settings.IOFORMAT_KWARGS:
- return Bag(**row)
-
- raise UnrecoverableNotImplementedError('Unsupported format.')
class FileHandler(Configurable):
"""Abstract component factory for file-related components.
Args:
- eol (str): which
- mode (str): which mode to use when opening the file.
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.
+ encoding (str): which encoding to use when opening the file.
"""
path = Option(str, required=True, positional=True, __doc__='''
@@ -64,7 +29,7 @@ class FileHandler(Configurable):
''') # type: str
@ContextProcessor
- def file(self, context, fs):
+ def file(self, context, *, fs):
with self.open(fs) as file:
yield file
@@ -73,22 +38,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 e141af5..34db6cc 100644
--- a/bonobo/nodes/io/csv.py
+++ b/bonobo/nodes/io/csv.py
@@ -1,28 +1,58 @@
import csv
-from bonobo.config import Option
-from bonobo.config.processors import ContextProcessor
+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, IOFormatEnabled
+from bonobo.nodes.io.base import FileHandler
from bonobo.nodes.io.file import FileReader, FileWriter
-from bonobo.util.objects import ValueHolder
+from bonobo.util import ensure_tuple
+from bonobo.util.bags import BagType
class CsvHandler(FileHandler):
- delimiter = Option(str, default=';', __doc__='''
- Delimiter used between values.
- ''')
- quotechar = Option(str, default='"', __doc__='''
- Character used for quoting values.
- ''')
- headers = Option(tuple, required=False, __doc__='''
- Tuple of headers to use, if provided.
- Readers will try to guess that from first line, unless this option is provided.
- Writers will guess from kwargs keys, unless this option is provided.
- ''')
+ """
+
+ .. attribute:: delimiter
+
+ The CSV delimiter.
+
+ .. attribute:: quotechar
+
+ The CSV quote character.
+
+ .. attribute:: fields
+
+ The list of column names, if the CSV does not contain it as its first line.
+
+ """
+
+ # 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,
+ }
-class CsvReader(IOFormatEnabled, FileReader, CsvHandler):
+@use_context
+class CsvReader(FileReader, CsvHandler):
"""
Reads a CSV and yield the values as dicts.
"""
@@ -31,45 +61,71 @@ class CsvReader(IOFormatEnabled, FileReader, CsvHandler):
If set and greater than zero, the reader will skip this amount of lines.
''')
- @ContextProcessor
- def csv_headers(self, context, fs, file):
- yield ValueHolder(self.headers)
+ @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.
+
+ Defaults to builtin csv.reader(...), but can be overriden to fit your special needs.
+ '''
+ )
+ def reader_factory(self, file):
+ return csv.reader(file, **self.get_dialect_kwargs())
- def read(self, fs, file, headers):
- reader = csv.reader(file, delimiter=self.delimiter, quotechar=self.quotechar)
+ def read(self, file, context, *, fs):
+ context.setdefault('skipped', 0)
+ reader = self.reader_factory(file)
+ skip = self.skip
- 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)
+ if not context.output_type:
+ context.set_output_fields(self.fields or next(reader))
for row in reader:
- if len(row) != field_count:
- raise ValueError('Got a line with %d fields, expecting %d.' % (
- len(row),
- field_count,
- ))
+ if context.skipped < skip:
+ context.skipped += 1
+ continue
+ yield tuple(row)
- yield self.get_output(dict(zip(_headers, row)))
+ __call__ = read
-class CsvWriter(IOFormatEnabled, 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
+@use_context
+class CsvWriter(FileWriter, CsvHandler):
+ @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, **self.get_dialect_kwargs()).writerow
+
+ 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)
+
+ if fields:
+ context.writer(fields)
+ context.lineno += 1
+
+ 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
+ )
+ )
+ context.writer(values)
+ else:
+ for arg in values:
+ context.writer(ensure_tuple(arg))
- def write(self, fs, file, lineno, writer, headers, *args, **kwargs):
- row = self.get_input(*args, **kwargs)
- if not lineno:
- headers.set(headers.value or row.keys())
- writer.writerow(headers.get())
- writer.writerow(row[header] for header in headers.get())
- lineno += 1
return NOT_MODIFIED
+
+ __call__ = write
diff --git a/bonobo/nodes/io/file.py b/bonobo/nodes/io/file.py
index 34a585f..7cbf410 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):
@@ -16,7 +16,44 @@ class FileReader(Reader, FileHandler):
What mode to use for open() call.
''') # type: str
- 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.
@@ -24,7 +61,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.
@@ -36,18 +76,16 @@ class FileWriter(Writer, FileHandler):
What mode to use for open() call.
''') # type: str
- @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 f1c6df0..86d3262 100644
--- a/bonobo/nodes/io/json.py
+++ b/bonobo/nodes/io/json.py
@@ -1,10 +1,11 @@
import json
+from collections import OrderedDict
-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, IOFormatEnabled
+from bonobo.nodes.io.base import FileHandler
from bonobo.nodes.io.file import FileReader, FileWriter
-from bonobo.structs.bags import Bag
class JsonHandler(FileHandler):
@@ -12,35 +13,74 @@ class JsonHandler(FileHandler):
prefix, suffix = '[', ']'
-class JsonReader(IOFormatEnabled, FileReader, JsonHandler):
- loader = staticmethod(json.load)
-
- def read(self, fs, file):
- for line in self.loader(file):
- yield self.get_output(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(IOFormatEnabled, 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, *args, **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 = self.get_input(*args, **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
+
+ __call__ = write
+
+
+@use_context
+class LdjsonWriter(LdjsonHandler, JsonWriter):
+ """
+ Write a stream of Line-delimited JSON objects (one object per line).
+
+ Not to be mistaken with JSON-LD (where LD stands for linked data).
+
+ """
diff --git a/bonobo/nodes/io/pickle.py b/bonobo/nodes/io/pickle.py
index 28db8af..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, IOFormatEnabled
+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,21 +15,18 @@ class PickleHandler(FileHandler):
"""
- item_names = Option(tuple, required=False)
+ fields = Option(tuple, required=False)
-class PickleReader(IOFormatEnabled, FileReader, PickleHandler):
+@use_context
+class PickleReader(FileReader, PickleHandler):
"""
Reads a Python pickle object and yields the items in dicts.
"""
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(IOFormatEnabled, 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 self.get_output(dict(zip(i)) if is_dict else dict(zip(pickle_headers.value, i)))
+ __call__ = read
-class PickleWriter(IOFormatEnabled, FileWriter, PickleHandler):
+@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.py b/bonobo/plugins.py
deleted file mode 100644
index 4fa1e18..0000000
--- a/bonobo/plugins.py
+++ /dev/null
@@ -1,40 +0,0 @@
-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
- 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.
-
- 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/plugins/__init__.py b/bonobo/plugins/__init__.py
new file mode 100644
index 0000000..68f91b2
--- /dev/null
+++ b/bonobo/plugins/__init__.py
@@ -0,0 +1,26 @@
+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.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!
+
+ """
+
+ def register(self, dispatcher):
+ """
+ :param dispatcher: whistle.EventDispatcher
+ """
+ pass
+
+ def unregister(self, dispatcher):
+ """
+ :param dispatcher: whistle.EventDispatcher
+ """
+ pass
diff --git a/bonobo/plugins/console.py b/bonobo/plugins/console.py
new file mode 100644
index 0000000..69a044c
--- /dev/null
+++ b/bonobo/plugins/console.py
@@ -0,0 +1,168 @@
+import io
+import sys
+from contextlib import redirect_stdout, redirect_stderr
+
+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
+
+initialize_colorama_output_wrappers(wrap=True)
+
+
+class ConsoleOutputPlugin(Plugin):
+ """
+ Outputs status information to the connected stdout. Can be a TTY, with or without support for colors/cursor
+ movements, or a non tty (pipe, file, ...). The features are adapted to terminal capabilities.
+
+ On Windows, we'll play a bit differently because we don't know how to manipulate cursor position. We'll only
+ display stats at the very end, and there won't be this "buffering" logic we need to display both stats and stdout.
+
+ .. attribute:: prefix
+
+ String prefix of output lines.
+
+ """
+
+ # Standard outputs descriptors backup here, also used to override if needed.
+ _stdout = sys.stdout
+ _stderr = sys.stderr
+
+ # 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.
+ iswindows = (sys.platform == 'win32')
+
+ 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):
+ # 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 = ''
+
+ self.stdout = IOBuffer()
+ self.redirect_stdout = redirect_stdout(self._stdout if self.iswindows else self.stdout)
+ self.redirect_stdout.__enter__()
+
+ self.stderr = IOBuffer()
+ self.redirect_stderr = redirect_stderr(self._stderr if self.iswindows else self.stderr)
+ self.redirect_stderr.__enter__()
+
+ def tick(self, event):
+ if self.isatty and not self.iswindows:
+ 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.context, 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:
+ 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
+
+ for i in context.graph.topologically_sorted_indexes:
+ node = context[i]
+ name_suffix = '({})'.format(i) if settings.DEBUG.get() else ''
+
+ 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:
+ # todo handle multiline
+ print(
+ ''.join((
+ ' `-> ', ' '.join('{}{}{}: {}'.format(Style.BRIGHT, k, Style.RESET_ALL, v) for k, v in append),
+ CLEAR_EOL
+ )),
+ file=self._stderr
+ )
+ t_cnt += 1
+
+ if rewind:
+ print(CLEAR_EOL, file=self._stderr)
+ print(MOVE_CURSOR_UP(t_cnt + 2), file=self._stderr)
+
+ def _write(self, context, rewind):
+ if settings.PROFILE.get():
+ 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))),
+ )
+ else:
+ append = ()
+ self.write(context, prefix=self.prefix, append=append, rewind=rewind)
+ 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)
diff --git a/bonobo/plugins/jupyter.py b/bonobo/plugins/jupyter.py
new file mode 100644
index 0000000..245ac95
--- /dev/null
+++ b/bonobo/plugins/jupyter.py
@@ -0,0 +1,33 @@
+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/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/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/settings.py b/bonobo/settings.py
index e5edd83..799ba3d 100644
--- a/bonobo/settings.py
+++ b/bonobo/settings.py
@@ -1,4 +1,5 @@
import logging
+
import os
from bonobo.errors import ValidationError
@@ -42,12 +43,24 @@ class Setting:
def __repr__(self):
return ''.format(self.name, self.get())
+ 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):
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/strategies/base.py b/bonobo/strategies/base.py
deleted file mode 100644
index 4b345d4..0000000
--- a/bonobo/strategies/base.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from bonobo.execution.graph import GraphExecutionContext
-
-
-class Strategy:
- """
- Base class for execution strategies.
-
- """
- graph_execution_context_factory = GraphExecutionContext
-
- def create_graph_execution_context(self, graph, *args, **kwargs):
- return self.graph_execution_context_factory(graph, *args, **kwargs)
-
- def execute(self, graph, *args, **kwargs):
- raise NotImplementedError
diff --git a/bonobo/strategies/executor.py b/bonobo/strategies/executor.py
deleted file mode 100644
index a0bd4f4..0000000
--- a/bonobo/strategies/executor.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import time
-import traceback
-from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
-
-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):
- """
- Strategy based on a concurrent.futures.Executor subclass (or similar interface).
-
- """
-
- executor_factory = Executor
-
- 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)
- 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))
-
- while context.alive:
- time.sleep(0.1)
-
- for plugin_context in context.plugins:
- plugin_context.shutdown()
-
- executor.shutdown()
-
- return context
-
-
-class ThreadPoolExecutorStrategy(ExecutorStrategy):
- executor_factory = ThreadPoolExecutor
-
-
-class ProcessPoolExecutorStrategy(ExecutorStrategy):
- executor_factory = ProcessPoolExecutor
diff --git a/bonobo/strategies/naive.py b/bonobo/strategies/naive.py
deleted file mode 100644
index cab9c57..0000000
--- a/bonobo/strategies/naive.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from bonobo.constants import BEGIN, END
-from bonobo.strategies.base import Strategy
-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)
- context.write(BEGIN, Bag(), END)
-
- # TODO: how to run plugins in "naive" mode ?
- context.start()
- context.loop()
- context.stop()
-
- return context
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/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 28d2e02..0000000
--- a/bonobo/structs/bags.py
+++ /dev/null
@@ -1,120 +0,0 @@
-import itertools
-
-from bonobo.constants import INHERIT_INPUT, LOOPBACK
-
-__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 = ()
-
- 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
-
- @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
-
- def apply(self, func_or_iter, *args, **kwargs):
- if callable(func_or_iter):
- return func_or_iter(*args, *self.args, **kwargs, **self.kwargs)
-
- 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):
- return isinstance(other, Bag) and other.args == self.args and other.kwargs == self.kwargs
-
- 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, )
-
-
-class ErrorBag(Bag):
- pass
diff --git a/bonobo/structs/graphs.py b/bonobo/structs/graphs.py
index fe7c1df..39de1fe 100644
--- a/bonobo/structs/graphs.py
+++ b/bonobo/structs/graphs.py
@@ -1,6 +1,15 @@
+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:
@@ -46,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)
@@ -62,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()
@@ -110,6 +124,32 @@ 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):
+ return str(self.graphviz)
+
+ def _repr_html_(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.
"""
@@ -126,3 +166,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/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/structs/tokens.py b/bonobo/structs/tokens.py
deleted file mode 100644
index 9ef6f64..0000000
--- a/bonobo/structs/tokens.py
+++ /dev/null
@@ -1,9 +0,0 @@
-class Token:
- """Token factory."""
-
- def __init__(self, name):
- self.__name__ = name
- self.__doc__ = 'The {!r} token.'.format(name)
-
- def __repr__(self):
- return '<{}>'.format(self.__name__)
diff --git a/bonobo/util/__init__.py b/bonobo/util/__init__.py
index df14e9a..b03c0a9 100644
--- a/bonobo/util/__init__.py
+++ b/bonobo/util/__init__.py
@@ -1,33 +1,35 @@
-from bonobo.util.collections import sortedlist
+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,
- isbag,
isconfigurable,
isconfigurabletype,
iscontextprocessor,
- iserrorbag,
- isloopbackbag,
+ isdict,
ismethod,
isoption,
+ istuple,
istype,
)
from bonobo.util.objects import (get_name, get_attribute_or_create, ValueHolder)
-from bonobo.util.python import require
# Bonobo's util API
__all__ = [
'ValueHolder',
+ 'cast',
+ 'deprecated',
+ 'deprecated_alias',
+ 'ensure_tuple',
'get_attribute_or_create',
'get_name',
'inspect_node',
- 'isbag',
'isconfigurable',
'isconfigurabletype',
'iscontextprocessor',
- 'iserrorbag',
- 'isloopbackbag',
+ 'isdict',
'ismethod',
'isoption',
'istype',
- 'require',
+ 'sortedlist',
+ 'tuplize',
]
diff --git a/bonobo/util/api.py b/bonobo/util/api.py
new file mode 100644
index 0000000..1561207
--- /dev/null
+++ b/bonobo/util/api.py
@@ -0,0 +1,37 @@
+from bonobo.util import get_name
+
+
+class ApiHelper:
+ # TODO __all__ kwarg only
+ 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 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(
+ 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/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 b97630a..de706f4 100644
--- a/bonobo/util/collections.py
+++ b/bonobo/util/collections.py
@@ -1,6 +1,61 @@
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, *, cls=tuple):
+ """
+ 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, cls):
+ 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 cast(type_):
+ def _wrap_cast(f):
+ @functools.wraps(f)
+ def _wrapped_cast(*args, **kwargs):
+ nonlocal f, type_
+ return type_(f(*args, **kwargs))
+
+ return _wrapped_cast
+
+ return _wrap_cast
+
+
+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.
+
+>>> 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/environ.py b/bonobo/util/environ.py
new file mode 100644
index 0000000..b344d29
--- /dev/null
+++ b/bonobo/util/environ.py
@@ -0,0 +1,164 @@
+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
+
+
+@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/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)
diff --git a/bonobo/util/inspect.py b/bonobo/util/inspect.py
index f9ae4d8..a7f27c4 100644
--- a/bonobo/util/inspect.py
+++ b/bonobo/util/inspect.py
@@ -12,16 +12,30 @@ 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
+
+ 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):
@@ -68,37 +82,24 @@ def istype(mixed):
return isinstance(mixed, type)
-def isbag(mixed):
+def isdict(mixed):
"""
- Check if the given argument is an instance of a :class:`bonobo.Bag`.
+ Check if the given argument is a dict.
:param mixed:
:return: bool
"""
- from bonobo.structs.bags import Bag
- return isinstance(mixed, Bag)
+ return isinstance(mixed, dict)
-def iserrorbag(mixed):
+def istuple(mixed):
"""
- Check if the given argument is an instance of an :class:`bonobo.ErrorBag`.
+ Check if the given argument is a tuple.
: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
+ return isinstance(mixed, tuple)
ConfigurableInspection = namedtuple(
@@ -129,7 +130,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/iterators.py b/bonobo/util/iterators.py
deleted file mode 100644
index ee45614..0000000
--- a/bonobo/util/iterators.py
+++ /dev/null
@@ -1,48 +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
-
-
-def iter_if_not_sequence(mixed):
- if isinstance(mixed, (
- dict,
- list,
- str,
- bytes,
- )):
- raise TypeError(type(mixed).__name__)
- return iter(mixed)
diff --git a/bonobo/util/objects.py b/bonobo/util/objects.py
index 34fc6e7..f3ffa5e 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__
@@ -220,6 +216,18 @@ 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 __getattr__(self, item):
+ return getattr(self._value, item)
+
def get_attribute_or_create(obj, attr, default):
try:
diff --git a/bonobo/util/python.py b/bonobo/util/python.py
deleted file mode 100644
index a496e19..0000000
--- a/bonobo/util/python.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import inspect
-import os
-import runpy
-
-
-class _RequiredModule:
- def __init__(self, dct):
- self.__dict__ = dct
-
-
-class _RequiredModulesRegistry(dict):
- 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')
- self[name] = _RequiredModule(runpy.run_path(filename, run_name=name))
- return self[name]
-
-
-registry = _RequiredModulesRegistry()
-require = registry.require
diff --git a/bonobo/util/resolvers.py b/bonobo/util/resolvers.py
new file mode 100644
index 0000000..5cf2738
--- /dev/null
+++ b/bonobo/util/resolvers.py
@@ -0,0 +1,81 @@
+"""
+This package is considered private, and should only be used within bonobo.
+
+"""
+
+import json
+import os
+import runpy
+
+import bonobo
+from bonobo.util import cast
+
+
+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):
+ """
+ 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()
+
+
+@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.
+
+ Syntax for each string is path.to.package:attribute
+
+ :param transformations: tuple(str)
+ :return: tuple(object)
+ """
+ registry = _ModulesRegistry()
+ transformations = transformations or []
+ 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/bonobo/util/statistics.py b/bonobo/util/statistics.py
index 5d71a0f..31da8b9 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:
@@ -27,5 +28,25 @@ 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:
+ """
+ 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/testing.py b/bonobo/util/testing.py
index 7c07256..8000e89 100644
--- a/bonobo/util/testing.py
+++ b/bonobo/util/testing.py
@@ -1,14 +1,19 @@
-from contextlib import contextmanager
-from unittest.mock import MagicMock
+import contextlib
+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
-from bonobo.execution.node import NodeExecutionContext
+import pytest
-
-class CapturingNodeExecutionContext(NodeExecutionContext):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.send = MagicMock()
+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
@contextmanager
@@ -21,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):
@@ -35,3 +40,224 @@ 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
+
+ def get_buffer_args_as_dicts(self):
+ return [row._asdict() if hasattr(row, '_asdict') else dict(row) for row in 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)
+
+
+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'))))
+
+
+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/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'
diff --git a/config/conda.yml b/config/conda.yml
deleted file mode 100644
index 09b92de..0000000
--- a/config/conda.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-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
-- pip:
- - colorama ==0.3.9
- - fs ==2.0.3
- - psutil ==5.2.2
- - requests ==2.13.0
- - stevedore ==1.21.0
- # for examples
- - pycountry ==17.9.23
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
index f658da9..3de53da 100644
--- a/docs/_static/custom.css
+++ b/docs/_static/custom.css
@@ -1,3 +1,47 @@
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;
+ font-size: 0.9em;
+}
+
+div.sphinxsidebar h3 {
+ margin: 30px 0 10px 0;
+}
+
+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/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/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html
index ef4ad45..05a2198 100644
--- a/docs/_templates/sidebarintro.html
+++ b/docs/_templates/sidebarintro.html
@@ -1,22 +1,21 @@
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+, your swiss-army knife for everyday's data.
Other Formats
- You can download the documentation in other formats as well:
+ Download the docs...