formating, better consistency in readers, ability to read files from http (fast and dirty).
This commit is contained in:
@ -4,7 +4,6 @@
|
|||||||
# transformations using a simple directed graph of python callables.
|
# transformations using a simple directed graph of python callables.
|
||||||
#
|
#
|
||||||
# Licensed under Apache License 2.0, read the LICENSE file in the root of the source tree.
|
# Licensed under Apache License 2.0, read the LICENSE file in the root of the source tree.
|
||||||
|
|
||||||
"""Bonobo data-processing toolkit main module."""
|
"""Bonobo data-processing toolkit main module."""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@ -62,8 +61,9 @@ def create_strategy(name=None):
|
|||||||
try:
|
try:
|
||||||
factory = STRATEGIES[name]
|
factory = STRATEGIES[name]
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
raise RuntimeError('Invalid strategy {}. Available choices: {}.'.format(repr(name), ', '.join(
|
raise RuntimeError(
|
||||||
sorted(STRATEGIES.keys())))) from exc
|
'Invalid strategy {}. Available choices: {}.'.format(repr(name), ', '.join(sorted(STRATEGIES.keys())))
|
||||||
|
) from exc
|
||||||
|
|
||||||
return factory()
|
return factory()
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ def entrypoint(args=None):
|
|||||||
subparsers.required = True
|
subparsers.required = True
|
||||||
|
|
||||||
commands = {}
|
commands = {}
|
||||||
|
|
||||||
def register_extension(ext, commands=commands):
|
def register_extension(ext, commands=commands):
|
||||||
try:
|
try:
|
||||||
parser = subparsers.add_parser(ext.name)
|
parser = subparsers.add_parser(ext.name)
|
||||||
@ -18,7 +19,9 @@ def entrypoint(args=None):
|
|||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('Error while loading command {}.'.format(ext.name))
|
logging.exception('Error while loading command {}.'.format(ext.name))
|
||||||
|
|
||||||
mgr = ExtensionManager(namespace='bonobo.commands', )
|
mgr = ExtensionManager(
|
||||||
|
namespace='bonobo.commands',
|
||||||
|
)
|
||||||
mgr.map(register_extension)
|
mgr.map(register_extension)
|
||||||
|
|
||||||
args = parser.parse_args(args).__dict__
|
args = parser.parse_args(args).__dict__
|
||||||
|
|||||||
@ -24,9 +24,10 @@ def execute(file, quiet=False):
|
|||||||
|
|
||||||
graphs = dict((k, v) for k, v in context.items() if isinstance(v, Graph))
|
graphs = dict((k, v) for k, v in context.items() if isinstance(v, Graph))
|
||||||
|
|
||||||
assert len(graphs) == 1, ('Having zero or more than one graph definition in one file is unsupported for now, '
|
assert len(graphs) == 1, (
|
||||||
'but it is something that will be implemented in the future.\n\nExpected: 1, got: {}.').format(
|
'Having zero or more than one graph definition in one file is unsupported for now, '
|
||||||
len(graphs))
|
'but it is something that will be implemented in the future.\n\nExpected: 1, got: {}.'
|
||||||
|
).format(len(graphs))
|
||||||
|
|
||||||
name, graph = list(graphs.items())[0]
|
name, graph = list(graphs.items())[0]
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ __all__ = [
|
|||||||
'Option',
|
'Option',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Option:
|
class Option:
|
||||||
def __init__(self, type=None, *, required=False, default=None):
|
def __init__(self, type=None, *, required=False, default=None):
|
||||||
self.name = None
|
self.name = None
|
||||||
@ -11,7 +12,9 @@ class Option:
|
|||||||
self.default = default
|
self.default = default
|
||||||
|
|
||||||
def __get__(self, inst, typ):
|
def __get__(self, inst, typ):
|
||||||
return inst.__options_values__.get(self.name, self.default)
|
if not self.name in inst.__options_values__:
|
||||||
|
inst.__options_values__[self.name] = self.default() if callable(self.default) else self.default
|
||||||
|
return inst.__options_values__[self.name]
|
||||||
|
|
||||||
def __set__(self, inst, value):
|
def __set__(self, inst, value):
|
||||||
inst.__options_values__[self.name] = self.type(value) if self.type else value
|
inst.__options_values__[self.name] = self.type(value) if self.type else value
|
||||||
|
|||||||
@ -101,8 +101,8 @@ class LoopingExecutionContext(Wrapper):
|
|||||||
self._started, self._stopped, self._context, self._stack = False, False, None, []
|
self._started, self._stopped, self._context, self._stack = False, False, None, []
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
assert self.state == (False, False), ('{}.start() can only be called on a new node.'
|
assert self.state == (False,
|
||||||
).format(type(self).__name__)
|
False), ('{}.start() can only be called on a new node.').format(type(self).__name__)
|
||||||
assert self._context is None
|
assert self._context is None
|
||||||
|
|
||||||
self._started = True
|
self._started = True
|
||||||
@ -175,6 +175,7 @@ class PluginExecutionContext(LoopingExecutionContext):
|
|||||||
LoopingExecutionContext.__init__(self, wrapped, parent)
|
LoopingExecutionContext.__init__(self, wrapped, parent)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
|
self.wrapped.finalize(self)
|
||||||
self.alive = False
|
self.alive = False
|
||||||
|
|
||||||
def step(self):
|
def step(self):
|
||||||
|
|||||||
@ -21,10 +21,7 @@ class Bag:
|
|||||||
def args(self):
|
def args(self):
|
||||||
if self._parent is None:
|
if self._parent is None:
|
||||||
return self._args
|
return self._args
|
||||||
return (
|
return (*self._parent.args, *self._args, )
|
||||||
*self._parent.args,
|
|
||||||
*self._args,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def kwargs(self):
|
def kwargs(self):
|
||||||
|
|||||||
@ -39,9 +39,11 @@ class ExecutorStrategy(Strategy):
|
|||||||
futures.append(executor.submit(_runner))
|
futures.append(executor.submit(_runner))
|
||||||
|
|
||||||
for node_context in context.nodes:
|
for node_context in context.nodes:
|
||||||
|
|
||||||
def _runner(node_context=node_context):
|
def _runner(node_context=node_context):
|
||||||
node_context.start()
|
node_context.start()
|
||||||
node_context.loop()
|
node_context.loop()
|
||||||
|
|
||||||
futures.append(executor.submit(_runner))
|
futures.append(executor.submit(_runner))
|
||||||
|
|
||||||
while context.alive:
|
while context.alive:
|
||||||
|
|||||||
@ -1,17 +1,16 @@
|
|||||||
from os.path import dirname, realpath, join
|
from os.path import dirname, realpath, join
|
||||||
|
|
||||||
from bonobo import console_run
|
import bonobo
|
||||||
from bonobo.ext.opendatasoft import from_opendatasoft_api
|
from bonobo.ext.opendatasoft import OpenDataSoftAPI
|
||||||
from bonobo.io.file import FileWriter
|
|
||||||
|
|
||||||
OUTPUT_FILENAME = realpath(join(dirname(__file__), 'coffeeshops.txt'))
|
OUTPUT_FILENAME = realpath(join(dirname(__file__), 'coffeeshops.txt'))
|
||||||
|
|
||||||
console_run(
|
graph = bonobo.Graph(
|
||||||
from_opendatasoft_api(
|
OpenDataSoftAPI(dataset='liste-des-cafes-a-un-euro', netloc='opendata.paris.fr'),
|
||||||
'liste-des-cafes-a-un-euro', netloc='opendata.paris.fr'
|
|
||||||
),
|
|
||||||
lambda row: '{nom_du_cafe}, {adresse}, {arrondissement} Paris, France'.format(**row),
|
lambda row: '{nom_du_cafe}, {adresse}, {arrondissement} Paris, France'.format(**row),
|
||||||
FileWriter(OUTPUT_FILENAME),
|
bonobo.FileWriter(path=OUTPUT_FILENAME),
|
||||||
)
|
)
|
||||||
|
|
||||||
print('Import done, read {} for results.'.format(OUTPUT_FILENAME))
|
if __name__ == '__main__':
|
||||||
|
bonobo.run(graph)
|
||||||
|
print('Import done, read {} for results.'.format(OUTPUT_FILENAME))
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import os
|
|||||||
from blessings import Terminal
|
from blessings import Terminal
|
||||||
|
|
||||||
from bonobo import Tee, JsonWriter, Graph, get_examples_path
|
from bonobo import Tee, JsonWriter, Graph, get_examples_path
|
||||||
from bonobo.ext.opendatasoft import from_opendatasoft_api
|
from bonobo.ext.opendatasoft import OpenDataSoftAPI
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pycountry
|
import pycountry
|
||||||
@ -44,8 +44,7 @@ def display(row):
|
|||||||
address = list(
|
address = list(
|
||||||
filter(
|
filter(
|
||||||
None, (
|
None, (
|
||||||
' '.join(filter(None, (row.get('postal_code', None), row.get('city', None)))),
|
' '.join(filter(None, (row.get('postal_code', None), row.get('city', None)))), row.get('county', None),
|
||||||
row.get('county', None),
|
|
||||||
row.get('country'),
|
row.get('country'),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -58,9 +57,7 @@ def display(row):
|
|||||||
|
|
||||||
|
|
||||||
graph = Graph(
|
graph = Graph(
|
||||||
from_opendatasoft_api(
|
OpenDataSoftAPI(dataset=API_DATASET, netloc=API_NETLOC, timezone='Europe/Paris'),
|
||||||
API_DATASET, netloc=API_NETLOC, timezone='Europe/Paris'
|
|
||||||
),
|
|
||||||
normalize,
|
normalize,
|
||||||
filter_france,
|
filter_france,
|
||||||
Tee(display),
|
Tee(display),
|
||||||
|
|||||||
30
bonobo/examples/datasets/passwd.txt
Normal file
30
bonobo/examples/datasets/passwd.txt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
root:x:0:0:root:/root:/bin/bash
|
||||||
|
daemon:x:105:1:daemon:/usr/sbin:/usr/sbin/nologin
|
||||||
|
bin:x:2:2:bin:/bin:/usr/sbin/nologin
|
||||||
|
sys:x:3:3:sys:/dev:/usr/sbin/nologin
|
||||||
|
sync:x:4:65534:sync:/bin:/bin/sync
|
||||||
|
games:x:5:60:games:/usr/games:/usr/sbin/nologin
|
||||||
|
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
|
||||||
|
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
|
||||||
|
mail:x:0:8:mail:/var/mail:/usr/sbin/nologin
|
||||||
|
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
|
||||||
|
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
|
||||||
|
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
|
||||||
|
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
|
||||||
|
backup:x:33:34:backup:/var/backups:/usr/sbin/nologin
|
||||||
|
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
|
||||||
|
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
|
||||||
|
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
|
||||||
|
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
|
||||||
|
systemd-timesync:x:33:103:systemd Time Synchronization,,,:/run/systemd:/bin/false
|
||||||
|
systemd-network:x:101:104:systemd Network Management,,,:/run/systemd/netif:/bin/false
|
||||||
|
systemd-resolve:x:102:105:systemd Resolver,,,:/run/systemd/resolve:/bin/false
|
||||||
|
systemd-bus-proxy:x:103:106:systemd Bus Proxy,,,:/run/systemd:/bin/false
|
||||||
|
sshd:x:104:65534::/var/run/sshd:/usr/sbin/nologin
|
||||||
|
ntp:x:105:110::/home/ntp:/bin/false
|
||||||
|
postfix:x:105:112::/var/spool/postfix:/bin/false
|
||||||
|
messagebus:x:107:114::/var/run/dbus:/bin/false
|
||||||
|
debian-security-support:x:108:115:Debian security support check,,,:/var/lib/debian-security-support:/bin/false
|
||||||
|
snmp:x:109:116::/var/lib/snmp:/usr/sbin/nologin
|
||||||
|
postgres:x:105:117:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
|
||||||
|
redis:x:111:118::/var/lib/redis:/bin/false
|
||||||
8
bonobo/examples/files/json.py
Normal file
8
bonobo/examples/files/json.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import bonobo as bb
|
||||||
|
|
||||||
|
url = 'https://data.toulouse-metropole.fr/explore/dataset/theatres-et-salles-de-spectacles/download?format=json&timezone=Europe/Berlin&use_labels_for_header=true'
|
||||||
|
|
||||||
|
graph = bb.Graph(bb.JsonReader(path=url), print)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
bb.run(graph)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
from bonobo import FileReader, Graph
|
from bonobo import FileReader, Graph, get_examples_path
|
||||||
|
|
||||||
|
|
||||||
def skip_comments(line):
|
def skip_comments(line):
|
||||||
@ -7,7 +7,7 @@ def skip_comments(line):
|
|||||||
|
|
||||||
|
|
||||||
graph = Graph(
|
graph = Graph(
|
||||||
FileReader(path='/etc/passwd'),
|
FileReader(path=get_examples_path('datasets/passwd.txt')),
|
||||||
skip_comments,
|
skip_comments,
|
||||||
lambda s: s.split(':'),
|
lambda s: s.split(':'),
|
||||||
lambda l: l[0],
|
lambda l: l[0],
|
||||||
|
|||||||
@ -13,7 +13,6 @@ Example on how to use :class:`bonobo.Bag` instances to pass flexible args/kwargs
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
from bonobo import Bag, Graph
|
from bonobo import Bag, Graph
|
||||||
@ -26,10 +25,7 @@ def extract():
|
|||||||
|
|
||||||
|
|
||||||
def transform(topic: str):
|
def transform(topic: str):
|
||||||
return Bag.inherit(
|
return Bag.inherit(title=topic.title(), rand=randint(10, 99))
|
||||||
title=topic.title(),
|
|
||||||
rand=randint(10, 99)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load(topic: str, title: str, rand: int):
|
def load(topic: str, title: str, rand: int):
|
||||||
|
|||||||
@ -35,11 +35,7 @@ def load(row: dict):
|
|||||||
print(row)
|
print(row)
|
||||||
|
|
||||||
|
|
||||||
graph = Graph(
|
graph = Graph(extract, transform, load)
|
||||||
extract,
|
|
||||||
transform,
|
|
||||||
load
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from bonobo import run
|
from bonobo import run
|
||||||
|
|||||||
@ -31,11 +31,7 @@ def load(s: str):
|
|||||||
print(s)
|
print(s)
|
||||||
|
|
||||||
|
|
||||||
graph = Graph(
|
graph = Graph(extract, transform, load)
|
||||||
extract,
|
|
||||||
transform,
|
|
||||||
load
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from bonobo import run
|
from bonobo import run
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
from .plugin import ConsoleOutputPlugin
|
from .plugin import ConsoleOutputPlugin
|
||||||
|
|
||||||
__all__ = ['ConsoleOutputPlugin', ]
|
__all__ = [
|
||||||
|
'ConsoleOutputPlugin',
|
||||||
|
]
|
||||||
|
|||||||
@ -80,30 +80,16 @@ class ConsoleOutputPlugin(Plugin):
|
|||||||
if component.alive:
|
if component.alive:
|
||||||
_line = ''.join(
|
_line = ''.join(
|
||||||
(
|
(
|
||||||
t.black('({})'.format(i + 1)),
|
t.black('({})'.format(i + 1)), ' ', t.bold(t.white('+')), ' ', component.name, ' ',
|
||||||
' ',
|
component.get_statistics_as_string(debug=debug, profile=profile), ' ',
|
||||||
t.bold(t.white('+')),
|
|
||||||
' ',
|
|
||||||
component.name,
|
|
||||||
' ',
|
|
||||||
component.get_statistics_as_string(
|
|
||||||
debug=debug, profile=profile
|
|
||||||
),
|
|
||||||
' ',
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_line = t.black(
|
_line = t.black(
|
||||||
''.join(
|
''.join(
|
||||||
(
|
(
|
||||||
'({})'.format(i + 1),
|
'({})'.format(i + 1), ' - ', component.name, ' ',
|
||||||
' - ',
|
component.get_statistics_as_string(debug=debug, profile=profile), ' ',
|
||||||
component.name,
|
|
||||||
' ',
|
|
||||||
component.get_statistics_as_string(
|
|
||||||
debug=debug, profile=profile
|
|
||||||
),
|
|
||||||
' ',
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,4 +5,6 @@ def _jupyter_nbextension_paths():
|
|||||||
return [{'section': 'notebook', 'src': 'static', 'dest': 'bonobo-jupyter', 'require': 'bonobo-jupyter/extension'}]
|
return [{'section': 'notebook', 'src': 'static', 'dest': 'bonobo-jupyter', 'require': 'bonobo-jupyter/extension'}]
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['JupyterOutputPlugin', ]
|
__all__ = [
|
||||||
|
'JupyterOutputPlugin',
|
||||||
|
]
|
||||||
|
|||||||
@ -2,28 +2,39 @@ from urllib.parse import urlencode
|
|||||||
|
|
||||||
import requests # todo: make this a service so we can substitute it ?
|
import requests # todo: make this a service so we can substitute it ?
|
||||||
|
|
||||||
|
from bonobo.config import Configurable, Option
|
||||||
|
from bonobo.context import ContextProcessor, contextual
|
||||||
|
from bonobo.util.compat import deprecated
|
||||||
|
from bonobo.util.objects import ValueHolder
|
||||||
|
|
||||||
def from_opendatasoft_api(
|
|
||||||
dataset=None,
|
|
||||||
endpoint='{scheme}://{netloc}{path}',
|
|
||||||
scheme='https',
|
|
||||||
netloc='data.opendatasoft.com',
|
|
||||||
path='/api/records/1.0/search/',
|
|
||||||
rows=100,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
path = path if path.startswith('/') else '/' + path
|
|
||||||
params = (
|
|
||||||
('dataset', dataset),
|
|
||||||
('rows', rows),
|
|
||||||
) + tuple(sorted(kwargs.items()))
|
|
||||||
base_url = endpoint.format(scheme=scheme, netloc=netloc, path=path) + '?' + urlencode(params)
|
|
||||||
|
|
||||||
def _extract_ods():
|
def path_str(path):
|
||||||
nonlocal base_url, rows
|
return path if path.startswith('/') else '/' + path
|
||||||
start = 0
|
|
||||||
|
|
||||||
|
@contextual
|
||||||
|
class OpenDataSoftAPI(Configurable):
|
||||||
|
dataset = Option(str, required=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=100)
|
||||||
|
kwargs = Option(dict, default=dict)
|
||||||
|
|
||||||
|
@ContextProcessor
|
||||||
|
def compute_path(self, context):
|
||||||
|
params = (('dataset', self.dataset), ('rows', self.rows), ) + tuple(sorted(self.kwargs.items()))
|
||||||
|
yield self.endpoint.format(scheme=self.scheme, netloc=self.netloc, path=self.path) + '?' + urlencode(params)
|
||||||
|
|
||||||
|
@ContextProcessor
|
||||||
|
def start(self, context, base_url):
|
||||||
|
yield ValueHolder(0)
|
||||||
|
|
||||||
|
def __call__(self, base_url, start, *args, **kwargs):
|
||||||
while True:
|
while True:
|
||||||
resp = requests.get('{}&start={start}'.format(base_url, start=start))
|
url = '{}&start={start}'.format(base_url, start=start.value)
|
||||||
|
resp = requests.get(url)
|
||||||
records = resp.json().get('records', [])
|
records = resp.json().get('records', [])
|
||||||
|
|
||||||
if not len(records):
|
if not len(records):
|
||||||
@ -32,7 +43,14 @@ def from_opendatasoft_api(
|
|||||||
for row in records:
|
for row in records:
|
||||||
yield {**row.get('fields', {}), 'geometry': row.get('geometry', {})}
|
yield {**row.get('fields', {}), 'geometry': row.get('geometry', {})}
|
||||||
|
|
||||||
start += rows
|
start.value += self.rows
|
||||||
|
|
||||||
_extract_ods.__name__ = 'extract_' + dataset.replace('-', '_')
|
|
||||||
return _extract_ods
|
@deprecated
|
||||||
|
def from_opendatasoft_api(dataset, **kwargs):
|
||||||
|
return OpenDataSoftAPI(dataset=dataset, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'OpenDataSoftAPI',
|
||||||
|
]
|
||||||
|
|||||||
@ -55,10 +55,7 @@ class CsvReader(CsvHandler, FileReader):
|
|||||||
|
|
||||||
for row in reader:
|
for row in reader:
|
||||||
if len(row) != field_count:
|
if len(row) != field_count:
|
||||||
raise ValueError('Got a line with %d fields, expecting %d.' % (
|
raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count, ))
|
||||||
len(row),
|
|
||||||
field_count,
|
|
||||||
))
|
|
||||||
|
|
||||||
yield dict(zip(headers.value, row))
|
yield dict(zip(headers.value, row))
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
from io import BytesIO
|
||||||
|
|
||||||
from bonobo.config import Configurable, Option
|
from bonobo.config import Configurable, Option
|
||||||
from bonobo.context import ContextProcessor
|
from bonobo.context import ContextProcessor
|
||||||
from bonobo.context.processors import contextual
|
from bonobo.context.processors import contextual
|
||||||
|
from bonobo.util.file import create_reader
|
||||||
from bonobo.util.objects import ValueHolder
|
from bonobo.util.objects import ValueHolder
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -22,8 +25,13 @@ class FileHandler(Configurable):
|
|||||||
|
|
||||||
@ContextProcessor
|
@ContextProcessor
|
||||||
def file(self, context):
|
def file(self, context):
|
||||||
with self.open() as file:
|
if self.path.find('http://') == 0 or self.path.find('https://') == 0:
|
||||||
yield file
|
import requests
|
||||||
|
response = requests.get(self.path)
|
||||||
|
yield BytesIO(response.content)
|
||||||
|
else:
|
||||||
|
with self.open() as file:
|
||||||
|
yield file
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
return open(self.path, self.mode)
|
return open(self.path, self.mode)
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import json
|
|||||||
from bonobo.context import ContextProcessor, contextual
|
from bonobo.context import ContextProcessor, contextual
|
||||||
from .file import FileWriter, FileReader
|
from .file import FileWriter, FileReader
|
||||||
|
|
||||||
__all__ = ['JsonWriter', ]
|
__all__ = [
|
||||||
|
'JsonWriter',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class JsonHandler:
|
class JsonHandler:
|
||||||
@ -11,8 +13,10 @@ class JsonHandler:
|
|||||||
|
|
||||||
|
|
||||||
class JsonReader(JsonHandler, FileReader):
|
class JsonReader(JsonHandler, FileReader):
|
||||||
|
loader = staticmethod(json.load)
|
||||||
|
|
||||||
def read(self, file):
|
def read(self, file):
|
||||||
for line in json.load(file):
|
for line in self.loader(file):
|
||||||
yield line
|
yield line
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -65,9 +65,8 @@ def PrettyPrint(title_keys=('title', 'name', 'id'), print_values=True, sort=True
|
|||||||
if print_values:
|
if print_values:
|
||||||
for k in sorted(row) if sort else row:
|
for k in sorted(row) if sort else row:
|
||||||
print(
|
print(
|
||||||
' • {t.blue}{k}{t.normal} : {t.black}({tp}){t.normal} {v}{t.clear_eol}'.format(
|
' • {t.blue}{k}{t.normal} : {t.black}({tp}){t.normal} {v}{t.clear_eol}'.
|
||||||
k=k, v=repr(row[k]), t=term, tp=type(row[k]).__name__
|
format(k=k, v=repr(row[k]), t=term, tp=type(row[k]).__name__)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield NOT_MODIFIED
|
yield NOT_MODIFIED
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
import functools
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
def is_platform_little_endian():
|
def is_platform_little_endian():
|
||||||
@ -22,3 +24,20 @@ def is_platform_mac():
|
|||||||
|
|
||||||
def is_platform_32bit():
|
def is_platform_32bit():
|
||||||
return struct.calcsize("P") * 8 < 64
|
return struct.calcsize("P") * 8 < 64
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated(func):
|
||||||
|
"""This is a decorator which can be used to mark functions
|
||||||
|
as deprecated. It will result in a warning being emmitted
|
||||||
|
when the function is used."""
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def new_func(*args, **kwargs):
|
||||||
|
warnings.simplefilter('always', DeprecationWarning) # turn off filter
|
||||||
|
warnings.warn(
|
||||||
|
"Call to deprecated function {}.".format(func.__name__), category=DeprecationWarning, stacklevel=2
|
||||||
|
)
|
||||||
|
warnings.simplefilter('default', DeprecationWarning) # reset filter
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return new_func
|
||||||
|
|||||||
@ -19,4 +19,4 @@ class Wrapper:
|
|||||||
class ValueHolder:
|
class ValueHolder:
|
||||||
def __init__(self, value, *, type=None):
|
def __init__(self, value, *, type=None):
|
||||||
self.value = value
|
self.value = value
|
||||||
self.type = type
|
self.type = type
|
||||||
|
|||||||
@ -140,7 +140,9 @@ latex_elements = {
|
|||||||
# Grouping the document tree into LaTeX files. List of tuples
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [(master_doc, 'Bonobo.tex', 'Bonobo Documentation', 'Romain Dorgueil', 'manual'), ]
|
latex_documents = [
|
||||||
|
(master_doc, 'Bonobo.tex', 'Bonobo Documentation', 'Romain Dorgueil', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
# -- Options for manual page output ---------------------------------------
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
@ -181,7 +183,4 @@ epub_copyright = copyright
|
|||||||
epub_exclude_files = ['search.html']
|
epub_exclude_files = ['search.html']
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
|
||||||
'python': ('https://docs.python.org/3', None)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
36
setup.py
36
setup.py
@ -34,37 +34,35 @@ setup(
|
|||||||
description='Bonobo',
|
description='Bonobo',
|
||||||
license='Apache License, Version 2.0',
|
license='Apache License, Version 2.0',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'blessings >=1.6,<1.7', 'psutil >=5.0,<5.1', 'requests >=2.12,<2.13',
|
'blessings >=1.6,<1.7', 'psutil >=5.0,<5.1', 'requests >=2.12,<2.13', 'stevedore >=1.19,<1.20',
|
||||||
'stevedore >=1.19,<1.20', 'toolz >=0.8,<0.9'
|
'toolz >=0.8,<0.9'
|
||||||
],
|
],
|
||||||
version=version,
|
version=version,
|
||||||
long_description=read('README.rst'),
|
long_description=read('README.rst'),
|
||||||
classifiers=read('classifiers.txt', tolines),
|
classifiers=read('classifiers.txt', tolines),
|
||||||
packages=find_packages(exclude=['ez_setup', 'example', 'test']),
|
packages=find_packages(exclude=['ez_setup', 'example', 'test']),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
data_files=[('share/jupyter/nbextensions/bonobo-jupyter', [
|
data_files=[
|
||||||
'bonobo/ext/jupyter/static/extension.js',
|
(
|
||||||
'bonobo/ext/jupyter/static/index.js',
|
'share/jupyter/nbextensions/bonobo-jupyter', [
|
||||||
'bonobo/ext/jupyter/static/index.js.map'
|
'bonobo/ext/jupyter/static/extension.js', 'bonobo/ext/jupyter/static/index.js',
|
||||||
])],
|
'bonobo/ext/jupyter/static/index.js.map'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'dev': [
|
'dev': [
|
||||||
'coverage >=4.3,<4.4', 'mock >=2.0,<2.1', 'nose >=1.3,<1.4',
|
'coverage >=4.3,<4.4', 'mock >=2.0,<2.1', 'nose >=1.3,<1.4', 'pylint >=1.6,<1.7', 'pytest >=3,<4',
|
||||||
'pylint >=1.6,<1.7', 'pytest >=3,<4', 'pytest-cov >=2.4,<2.5',
|
'pytest-cov >=2.4,<2.5', 'pytest-timeout >=1.2,<1.3', 'sphinx', 'sphinx_rtd_theme', 'yapf'
|
||||||
'pytest-timeout >=1.2,<1.3', 'sphinx', 'sphinx_rtd_theme', 'yapf'
|
|
||||||
],
|
],
|
||||||
'jupyter': ['jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5']
|
'jupyter': ['jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5']
|
||||||
},
|
},
|
||||||
entry_points={
|
entry_points={
|
||||||
'bonobo.commands': [
|
'bonobo.commands': ['init = bonobo.commands.init:register', 'run = bonobo.commands.run:register'],
|
||||||
'init = bonobo.commands.init:register',
|
|
||||||
'run = bonobo.commands.run:register'
|
|
||||||
],
|
|
||||||
'console_scripts': ['bonobo = bonobo.commands:entrypoint'],
|
'console_scripts': ['bonobo = bonobo.commands:entrypoint'],
|
||||||
'edgy.project.features':
|
'edgy.project.features': ['bonobo = '
|
||||||
['bonobo = '
|
'bonobo.ext.edgy.project.feature:BonoboFeature']
|
||||||
'bonobo.ext.edgy.project.feature:BonoboFeature']
|
|
||||||
},
|
},
|
||||||
url='https://bonobo-project.org/',
|
url='https://bonobo-project.org/',
|
||||||
download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'.
|
download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'.format(version=version),
|
||||||
format(version=version), )
|
)
|
||||||
|
|||||||
@ -3,10 +3,7 @@ from mock import Mock
|
|||||||
from bonobo import Bag
|
from bonobo import Bag
|
||||||
from bonobo.core.bags import INHERIT_INPUT
|
from bonobo.core.bags import INHERIT_INPUT
|
||||||
|
|
||||||
args = (
|
args = ('foo', 'bar', )
|
||||||
'foo',
|
|
||||||
'bar',
|
|
||||||
)
|
|
||||||
kwargs = dict(acme='corp')
|
kwargs = dict(acme='corp')
|
||||||
|
|
||||||
|
|
||||||
@ -39,17 +36,11 @@ def test_inherit():
|
|||||||
assert bag.kwargs == {'a': 1}
|
assert bag.kwargs == {'a': 1}
|
||||||
assert bag.flags is ()
|
assert bag.flags is ()
|
||||||
|
|
||||||
assert bag2.args == (
|
assert bag2.args == ('a', 'b', )
|
||||||
'a',
|
|
||||||
'b',
|
|
||||||
)
|
|
||||||
assert bag2.kwargs == {'a': 1, 'b': 2}
|
assert bag2.kwargs == {'a': 1, 'b': 2}
|
||||||
assert INHERIT_INPUT in bag2.flags
|
assert INHERIT_INPUT in bag2.flags
|
||||||
|
|
||||||
assert bag3.args == (
|
assert bag3.args == ('a', 'c', )
|
||||||
'a',
|
|
||||||
'c',
|
|
||||||
)
|
|
||||||
assert bag3.kwargs == {'a': 1, 'c': 3}
|
assert bag3.kwargs == {'a': 1, 'c': 3}
|
||||||
assert bag3.flags is ()
|
assert bag3.flags is ()
|
||||||
|
|
||||||
@ -58,19 +49,12 @@ def test_inherit():
|
|||||||
assert bag4.flags is ()
|
assert bag4.flags is ()
|
||||||
|
|
||||||
bag4.set_parent(bag)
|
bag4.set_parent(bag)
|
||||||
assert bag4.args == (
|
assert bag4.args == ('a', 'd', )
|
||||||
'a',
|
|
||||||
'd',
|
|
||||||
)
|
|
||||||
assert bag4.kwargs == {'a': 1, 'd': 4}
|
assert bag4.kwargs == {'a': 1, 'd': 4}
|
||||||
assert bag4.flags is ()
|
assert bag4.flags is ()
|
||||||
|
|
||||||
bag4.set_parent(bag3)
|
bag4.set_parent(bag3)
|
||||||
assert bag4.args == (
|
assert bag4.args == ('a', 'c', 'd', )
|
||||||
'a',
|
|
||||||
'c',
|
|
||||||
'd',
|
|
||||||
)
|
|
||||||
assert bag4.kwargs == {'a': 1, 'c': 3, 'd': 4}
|
assert bag4.kwargs == {'a': 1, 'c': 3, 'd': 4}
|
||||||
assert bag4.flags is ()
|
assert bag4.flags is ()
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,7 @@ from bonobo.core.statistics import WithStatistics
|
|||||||
|
|
||||||
class MyThingWithStats(WithStatistics):
|
class MyThingWithStats(WithStatistics):
|
||||||
def get_statistics(self, *args, **kwargs):
|
def get_statistics(self, *args, **kwargs):
|
||||||
return (
|
return (('foo', 42), ('bar', 69), )
|
||||||
('foo', 42),
|
|
||||||
('bar', 69),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_with_statistics():
|
def test_with_statistics():
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from mock import patch
|
from mock import patch
|
||||||
|
|
||||||
from bonobo.ext.opendatasoft import from_opendatasoft_api
|
from bonobo.ext.opendatasoft import OpenDataSoftAPI
|
||||||
|
from bonobo.util.objects import ValueHolder
|
||||||
|
|
||||||
|
|
||||||
class ResponseMock:
|
class ResponseMock:
|
||||||
@ -13,11 +14,13 @@ class ResponseMock:
|
|||||||
return {}
|
return {}
|
||||||
else:
|
else:
|
||||||
self.count += 1
|
self.count += 1
|
||||||
return {'records': self.json_value, }
|
return {
|
||||||
|
'records': self.json_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_read_from_opendatasoft_api():
|
def test_read_from_opendatasoft_api():
|
||||||
extract = from_opendatasoft_api('http://example.com/', 'test-a-set')
|
extract = OpenDataSoftAPI(dataset='test-a-set')
|
||||||
with patch(
|
with patch(
|
||||||
'requests.get', return_value=ResponseMock([
|
'requests.get', return_value=ResponseMock([
|
||||||
{
|
{
|
||||||
@ -32,5 +35,5 @@ def test_read_from_opendatasoft_api():
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
):
|
):
|
||||||
for line in extract():
|
for line in extract('http://example.com/', ValueHolder(0)):
|
||||||
assert 'foo' in line
|
assert 'foo' in line
|
||||||
|
|||||||
@ -5,9 +5,7 @@ import bonobo as bb
|
|||||||
|
|
||||||
@pytest.mark.timeout(2)
|
@pytest.mark.timeout(2)
|
||||||
def test_run_graph_noop():
|
def test_run_graph_noop():
|
||||||
graph = bb.Graph(
|
graph = bb.Graph(bb.noop)
|
||||||
bb.noop
|
|
||||||
)
|
|
||||||
assert len(graph) == 1
|
assert len(graph) == 1
|
||||||
|
|
||||||
result = bb.run(graph, strategy='threadpool')
|
result = bb.run(graph, strategy='threadpool')
|
||||||
|
|||||||
@ -14,14 +14,17 @@ def test_entrypoint():
|
|||||||
assert 'init' in commands
|
assert 'init' in commands
|
||||||
assert 'run' in commands
|
assert 'run' in commands
|
||||||
|
|
||||||
|
|
||||||
def test_no_command(capsys):
|
def test_no_command(capsys):
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
entrypoint([])
|
entrypoint([])
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
assert 'error: the following arguments are required: command' in err
|
assert 'error: the following arguments are required: command' in err
|
||||||
|
|
||||||
|
|
||||||
def test_init():
|
def test_init():
|
||||||
pass # need ext dir
|
pass # need ext dir
|
||||||
|
|
||||||
|
|
||||||
def test_run(capsys):
|
def test_run(capsys):
|
||||||
entrypoint(['run', '--quiet', get_examples_path('types/strings.py')])
|
entrypoint(['run', '--quiet', get_examples_path('types/strings.py')])
|
||||||
|
|||||||
Reference in New Issue
Block a user