4
Makefile
4
Makefile
@ -1,5 +1,5 @@
|
|||||||
# This file has been auto-generated by Medikit. All changes will be lost.
|
# Generated by Medikit 0.4a5 on 2017-10-22.
|
||||||
# Updated on 2017-10-21.
|
# All changes will be overriden.
|
||||||
|
|
||||||
PACKAGE ?= bonobo
|
PACKAGE ?= bonobo
|
||||||
PYTHON ?= $(shell which python)
|
PYTHON ?= $(shell which python)
|
||||||
|
|||||||
@ -1,105 +1,34 @@
|
|||||||
import mimetypes
|
|
||||||
import os
|
|
||||||
|
|
||||||
import bonobo
|
import bonobo
|
||||||
from bonobo.commands.util.arguments import parse_variable_argument
|
from bonobo.registry import READER, WRITER, default_registry
|
||||||
from bonobo.util import require
|
from bonobo.util.resolvers import _resolve_transformations, _resolve_options
|
||||||
from bonobo.util.iterators import tuplize
|
|
||||||
from bonobo.util.python import WorkingDirectoryModulesRegistry
|
|
||||||
|
|
||||||
SHORTCUTS = {
|
|
||||||
'csv': 'text/csv',
|
|
||||||
'json': 'application/json',
|
|
||||||
'pickle': 'pickle',
|
|
||||||
'plain': 'text/plain',
|
|
||||||
'text': 'text/plain',
|
|
||||||
'txt': 'text/plain',
|
|
||||||
}
|
|
||||||
|
|
||||||
REGISTRY = {
|
|
||||||
'application/json': (bonobo.JsonReader, bonobo.JsonWriter),
|
|
||||||
'pickle': (bonobo.PickleReader, bonobo.PickleWriter),
|
|
||||||
'text/csv': (bonobo.CsvReader, bonobo.CsvWriter),
|
|
||||||
'text/plain': (bonobo.FileReader, bonobo.FileWriter),
|
|
||||||
}
|
|
||||||
|
|
||||||
READER = 'reader'
|
|
||||||
WRITER = 'writer'
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_factory(name, filename, factory_type, options=None):
|
|
||||||
"""
|
|
||||||
Try to resolve which transformation factory to use for this filename. User eventually provided a name, which has
|
|
||||||
priority, otherwise we try to detect it using the mimetype detection on filename.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if name is None:
|
|
||||||
name = mimetypes.guess_type(filename)[0]
|
|
||||||
|
|
||||||
if name in SHORTCUTS:
|
|
||||||
name = SHORTCUTS[name]
|
|
||||||
|
|
||||||
if name is None:
|
|
||||||
_, _ext = os.path.splitext(filename)
|
|
||||||
if _ext:
|
|
||||||
_ext = _ext[1:]
|
|
||||||
if _ext in SHORTCUTS:
|
|
||||||
name = SHORTCUTS[_ext]
|
|
||||||
|
|
||||||
if options:
|
|
||||||
options = dict(map(parse_variable_argument, options))
|
|
||||||
else:
|
|
||||||
options = dict()
|
|
||||||
|
|
||||||
if not name in REGISTRY:
|
|
||||||
raise RuntimeError(
|
|
||||||
'Could not resolve {factory_type} factory for {filename} ({name}). Try providing it explicitely using -{opt} <format>.'.
|
|
||||||
format(name=name, filename=filename, factory_type=factory_type, opt=factory_type[0])
|
|
||||||
)
|
|
||||||
|
|
||||||
if factory_type == READER:
|
|
||||||
return REGISTRY[name][0], options
|
|
||||||
elif factory_type == WRITER:
|
|
||||||
return REGISTRY[name][1], options
|
|
||||||
else:
|
|
||||||
raise ValueError('Invalid factory type.')
|
|
||||||
|
|
||||||
|
|
||||||
@tuplize
|
|
||||||
def resolve_filters(filters):
|
|
||||||
registry = WorkingDirectoryModulesRegistry()
|
|
||||||
for f in filters:
|
|
||||||
try:
|
|
||||||
mod, attr = f.split(':', 1)
|
|
||||||
yield getattr(registry.require(mod), attr)
|
|
||||||
except ValueError:
|
|
||||||
yield getattr(bonobo, f)
|
|
||||||
|
|
||||||
|
|
||||||
def execute(
|
def execute(
|
||||||
input,
|
input_filename,
|
||||||
output,
|
output_filename,
|
||||||
reader=None,
|
reader=None,
|
||||||
reader_option=None,
|
reader_option=None,
|
||||||
writer=None,
|
writer=None,
|
||||||
writer_option=None,
|
writer_option=None,
|
||||||
option=None,
|
option=None,
|
||||||
filter=None,
|
transformation=None,
|
||||||
):
|
):
|
||||||
reader_factory, reader_option = resolve_factory(reader, input, READER, (option or []) + (reader_option or []))
|
reader_factory = default_registry.get_reader_factory_for(input_filename, format=reader)
|
||||||
|
reader_options = _resolve_options((option or []) + (reader_option or []))
|
||||||
|
|
||||||
if output == '-':
|
if output_filename == '-':
|
||||||
writer_factory, writer_option = bonobo.PrettyPrinter, {}
|
writer_factory = bonobo.PrettyPrinter
|
||||||
else:
|
else:
|
||||||
writer_factory, writer_option = resolve_factory(writer, output, WRITER, (option or []) + (writer_option or []))
|
writer_factory = default_registry.get_writer_factory_for(output_filename, format=writer)
|
||||||
|
writer_options = _resolve_options((option or []) + (writer_option or []))
|
||||||
|
|
||||||
filters = resolve_filters(filter)
|
transformations = _resolve_transformations(transformation)
|
||||||
|
|
||||||
graph = bonobo.Graph()
|
graph = bonobo.Graph()
|
||||||
graph.add_chain(
|
graph.add_chain(
|
||||||
reader_factory(input, **reader_option),
|
reader_factory(input_filename, **reader_options),
|
||||||
*filters,
|
*transformations,
|
||||||
writer_factory(output, **writer_option),
|
writer_factory(output_filename, **writer_options),
|
||||||
)
|
)
|
||||||
|
|
||||||
return bonobo.run(
|
return bonobo.run(
|
||||||
@ -110,8 +39,8 @@ def execute(
|
|||||||
|
|
||||||
|
|
||||||
def register(parser):
|
def register(parser):
|
||||||
parser.add_argument('input', help='Input filename.')
|
parser.add_argument('input-filename', help='Input filename.')
|
||||||
parser.add_argument('output', help='Output filename.')
|
parser.add_argument('output-filename', help='Output filename.')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--' + READER,
|
'--' + READER,
|
||||||
'-r',
|
'-r',
|
||||||
@ -124,11 +53,11 @@ def register(parser):
|
|||||||
'Choose the writer factory if it cannot be detected from extension, or if detection is wrong (use - for console pretty print).'
|
'Choose the writer factory if it cannot be detected from extension, or if detection is wrong (use - for console pretty print).'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--filter',
|
'--transformation',
|
||||||
'-f',
|
'-t',
|
||||||
dest='filter',
|
dest='transformation',
|
||||||
action='append',
|
action='append',
|
||||||
help='Add a filter between input and output',
|
help='Add a transformation between input and output (can be used multiple times, order is preserved).',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--option',
|
'--option',
|
||||||
|
|||||||
@ -46,7 +46,17 @@ def _install_requirements(requirements):
|
|||||||
importlib.reload(site)
|
importlib.reload(site)
|
||||||
|
|
||||||
|
|
||||||
def read(filename, module, install=False, quiet=False, verbose=False, default_env_file=None, default_env=None, env_file=None, env=None):
|
def read(
|
||||||
|
filename,
|
||||||
|
module,
|
||||||
|
install=False,
|
||||||
|
quiet=False,
|
||||||
|
verbose=False,
|
||||||
|
default_env_file=None,
|
||||||
|
default_env=None,
|
||||||
|
env_file=None,
|
||||||
|
env=None
|
||||||
|
):
|
||||||
|
|
||||||
import runpy
|
import runpy
|
||||||
from bonobo import Graph, settings
|
from bonobo import Graph, settings
|
||||||
@ -129,8 +139,20 @@ def set_env_var(e, override=False):
|
|||||||
os.environ.setdefault(ename, evalue)
|
os.environ.setdefault(ename, evalue)
|
||||||
|
|
||||||
|
|
||||||
def execute(filename, module, install=False, quiet=False, verbose=False, default_env_file=None, default_env=None, env_file=None, env=None):
|
def execute(
|
||||||
graph, plugins, services = read(filename, module, install, quiet, verbose, default_env_file, default_env, env_file, env)
|
filename,
|
||||||
|
module,
|
||||||
|
install=False,
|
||||||
|
quiet=False,
|
||||||
|
verbose=False,
|
||||||
|
default_env_file=None,
|
||||||
|
default_env=None,
|
||||||
|
env_file=None,
|
||||||
|
env=None
|
||||||
|
):
|
||||||
|
graph, plugins, services = read(
|
||||||
|
filename, module, install, quiet, verbose, default_env_file, default_env, env_file, env
|
||||||
|
)
|
||||||
|
|
||||||
return bonobo.run(graph, plugins=plugins, services=services)
|
return bonobo.run(graph, plugins=plugins, services=services)
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
def parse_variable_argument(arg):
|
|
||||||
try:
|
|
||||||
key, val = arg.split('=', 1)
|
|
||||||
except ValueError:
|
|
||||||
return arg, True
|
|
||||||
|
|
||||||
try:
|
|
||||||
val = json.loads(val)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return key, val
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_variable_argument():
|
|
||||||
assert parse_variable_argument('foo=bar') == ('foo', 'bar')
|
|
||||||
assert parse_variable_argument('foo="bar"') == ('foo', 'bar')
|
|
||||||
assert parse_variable_argument('sep=";"') == ('sep', ';')
|
|
||||||
assert parse_variable_argument('foo') == ('foo', True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_parse_var()
|
|
||||||
3
bonobo/events.py
Normal file
3
bonobo/events.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ON_START = 'bonobo.on_start'
|
||||||
|
ON_TICK = 'bonobo.on_tick'
|
||||||
|
ON_STOP = 'bonobo.on_stop'
|
||||||
@ -8,7 +8,9 @@ def extract():
|
|||||||
test_user_password = os.getenv('TEST_USER_PASSWORD')
|
test_user_password = os.getenv('TEST_USER_PASSWORD')
|
||||||
path = os.getenv('PATH')
|
path = os.getenv('PATH')
|
||||||
|
|
||||||
return my_secret, test_user_password, path
|
yield my_secret
|
||||||
|
yield test_user_password
|
||||||
|
yield path
|
||||||
|
|
||||||
|
|
||||||
def load(s: str):
|
def load(s: str):
|
||||||
|
|||||||
@ -8,7 +8,11 @@ def extract():
|
|||||||
env_test_number = os.getenv('ENV_TEST_NUMBER', 'number')
|
env_test_number = os.getenv('ENV_TEST_NUMBER', 'number')
|
||||||
env_test_string = os.getenv('ENV_TEST_STRING', 'string')
|
env_test_string = os.getenv('ENV_TEST_STRING', 'string')
|
||||||
env_user = os.getenv('USER')
|
env_user = os.getenv('USER')
|
||||||
return env_test_user, env_test_number, env_test_string, env_user
|
|
||||||
|
yield env_test_user
|
||||||
|
yield env_test_number
|
||||||
|
yield env_test_string
|
||||||
|
yield env_user
|
||||||
|
|
||||||
|
|
||||||
def load(s: str):
|
def load(s: str):
|
||||||
|
|||||||
@ -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__))
|
|
||||||
@ -4,8 +4,7 @@ from time import sleep
|
|||||||
|
|
||||||
from bonobo.config import create_container
|
from bonobo.config import create_container
|
||||||
from bonobo.config.processors import ContextCurrifier
|
from bonobo.config.processors import ContextCurrifier
|
||||||
from bonobo.plugins import get_enhancers
|
from bonobo.util import isconfigurabletype
|
||||||
from bonobo.util import inspect_node, isconfigurabletype
|
|
||||||
from bonobo.util.errors import print_error
|
from bonobo.util.errors import print_error
|
||||||
from bonobo.util.objects import Wrapper, get_name
|
from bonobo.util.objects import Wrapper, get_name
|
||||||
|
|
||||||
@ -56,9 +55,6 @@ class LoopingExecutionContext(Wrapper):
|
|||||||
self._started, self._stopped = False, False
|
self._started, self._stopped = False, False
|
||||||
self._stack = None
|
self._stack = None
|
||||||
|
|
||||||
# XXX enhancers
|
|
||||||
self._enhancers = get_enhancers(self.wrapped)
|
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.start()
|
self.start()
|
||||||
return self
|
return self
|
||||||
@ -79,15 +75,9 @@ class LoopingExecutionContext(Wrapper):
|
|||||||
raise TypeError(
|
raise TypeError(
|
||||||
'The Configurable should be fully instanciated by now, unfortunately I got a PartiallyConfigured object...'
|
'The Configurable should be fully instanciated by now, unfortunately I got a PartiallyConfigured object...'
|
||||||
)
|
)
|
||||||
# XXX enhance that, maybe giving hints on what's missing.
|
|
||||||
# print(inspect_node(self.wrapped))
|
|
||||||
|
|
||||||
self._stack.setup(self)
|
self._stack.setup(self)
|
||||||
|
|
||||||
for enhancer in self._enhancers:
|
|
||||||
with unrecoverable(self.handle_error):
|
|
||||||
enhancer.start(self)
|
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
"""Generic loop. A bit boring. """
|
"""Generic loop. A bit boring. """
|
||||||
while self.alive:
|
while self.alive:
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import traceback
|
import traceback
|
||||||
from queue import Empty
|
from queue import Empty
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
from types import GeneratorType
|
||||||
|
|
||||||
from bonobo import settings
|
from bonobo.constants import NOT_MODIFIED, BEGIN, END
|
||||||
from bonobo.constants import INHERIT_INPUT, NOT_MODIFIED, BEGIN, END
|
|
||||||
from bonobo.errors import InactiveReadableError, UnrecoverableError
|
from bonobo.errors import InactiveReadableError, UnrecoverableError
|
||||||
from bonobo.execution.base import LoopingExecutionContext
|
from bonobo.execution.base import LoopingExecutionContext
|
||||||
from bonobo.structs.bags import Bag
|
from bonobo.structs.bags import Bag
|
||||||
from bonobo.structs.inputs import Input
|
from bonobo.structs.inputs import Input
|
||||||
from bonobo.util import get_name, iserrorbag, isloopbackbag, isdict, istuple
|
from bonobo.structs.tokens import Token
|
||||||
|
from bonobo.util import get_name, iserrorbag, isloopbackbag, isbag
|
||||||
from bonobo.util.compat import deprecated_alias
|
from bonobo.util.compat import deprecated_alias
|
||||||
from bonobo.util.iterators import iter_if_not_sequence
|
|
||||||
from bonobo.util.statistics import WithStatistics
|
from bonobo.util.statistics import WithStatistics
|
||||||
|
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
|
|||||||
:param mixed value: message
|
:param mixed value: message
|
||||||
"""
|
"""
|
||||||
for message in messages:
|
for message in messages:
|
||||||
self.input.put(message)
|
self.input.put(message if isinstance(message, (Bag, Token)) else Bag(message))
|
||||||
|
|
||||||
def write_sync(self, *messages):
|
def write_sync(self, *messages):
|
||||||
self.write(BEGIN, *messages, END)
|
self.write(BEGIN, *messages, END)
|
||||||
@ -120,23 +120,21 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
|
|||||||
def handle_results(self, input_bag, results):
|
def handle_results(self, input_bag, results):
|
||||||
# self._exec_time += timer.duration
|
# self._exec_time += timer.duration
|
||||||
# Put data onto output channels
|
# Put data onto output channels
|
||||||
try:
|
|
||||||
results = iter_if_not_sequence(results)
|
if isinstance(results, GeneratorType):
|
||||||
except TypeError: # not an iterator
|
while True:
|
||||||
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:
|
try:
|
||||||
result = next(results)
|
result = next(results)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.send(_resolve(input_bag, result))
|
self.send(_resolve(input_bag, result))
|
||||||
|
elif results:
|
||||||
|
self.send(_resolve(input_bag, results))
|
||||||
|
else:
|
||||||
|
# case with no result, an execution went through anyway, use for stats.
|
||||||
|
# self._exec_count += 1
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _resolve(input_bag, output):
|
def _resolve(input_bag, output):
|
||||||
@ -144,22 +142,7 @@ def _resolve(input_bag, output):
|
|||||||
if output is NOT_MODIFIED:
|
if output is NOT_MODIFIED:
|
||||||
return input_bag
|
return input_bag
|
||||||
|
|
||||||
if iserrorbag(output):
|
if isbag(output):
|
||||||
return output
|
return output
|
||||||
|
|
||||||
# If it does not look like a bag, let's create one for easier manipulation
|
|
||||||
if hasattr(output, 'apply'): # XXX TODO use isbag() ?
|
|
||||||
# Already a bag? Check if we need to set parent.
|
|
||||||
if INHERIT_INPUT in output.flags:
|
|
||||||
output.set_parent(input_bag)
|
|
||||||
return output
|
|
||||||
|
|
||||||
# If we're using kwargs ioformat, then a dict means kwargs.
|
|
||||||
if settings.IOFORMAT == settings.IOFORMAT_KWARGS and isdict(output):
|
|
||||||
return Bag(**output)
|
|
||||||
|
|
||||||
if istuple(output):
|
|
||||||
return Bag(*output)
|
|
||||||
|
|
||||||
# Either we use arg0 format, either it's "just" a value.
|
|
||||||
return Bag(output)
|
return Bag(output)
|
||||||
|
|||||||
@ -13,14 +13,14 @@ class PluginExecutionContext(LoopingExecutionContext):
|
|||||||
super().start()
|
super().start()
|
||||||
|
|
||||||
with recoverable(self.handle_error):
|
with recoverable(self.handle_error):
|
||||||
self.wrapped.initialize()
|
self.wrapped.on_start()
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
if self.started:
|
if self.started:
|
||||||
with recoverable(self.handle_error):
|
with recoverable(self.handle_error):
|
||||||
self.wrapped.finalize()
|
self.wrapped.on_stop()
|
||||||
self.alive = False
|
self.alive = False
|
||||||
|
|
||||||
def step(self):
|
def step(self):
|
||||||
with recoverable(self.handle_error):
|
with recoverable(self.handle_error):
|
||||||
self.wrapped.run()
|
self.wrapped.on_tick()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import io
|
import io
|
||||||
import sys
|
import sys
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout, redirect_stderr
|
||||||
|
|
||||||
from colorama import Style, Fore, init
|
from colorama import Style, Fore, init
|
||||||
|
|
||||||
@ -50,35 +50,50 @@ class ConsoleOutputPlugin(Plugin):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def initialize(self):
|
# Standard outputs descriptors backup here, also used to override if needed.
|
||||||
|
_stdout = sys.stdout
|
||||||
|
_stderr = sys.stderr
|
||||||
|
|
||||||
|
# When the plugin is started, we'll set the real value of this.
|
||||||
|
isatty = False
|
||||||
|
|
||||||
|
# Whether we're on windows, or a real operating system.
|
||||||
|
iswindows = (sys.platform == 'win32')
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
self.prefix = ''
|
self.prefix = ''
|
||||||
self.counter = 0
|
self.counter = 0
|
||||||
self._append_cache = ''
|
self._append_cache = ''
|
||||||
self.isatty = sys.stdout.isatty()
|
|
||||||
self.iswindows = (sys.platform == 'win32')
|
|
||||||
|
|
||||||
self._stdout = sys.stdout
|
self.isatty = self._stdout.isatty()
|
||||||
|
|
||||||
self.stdout = IOBuffer()
|
self.stdout = IOBuffer()
|
||||||
self.redirect_stdout = redirect_stdout(self._stdout if self.iswindows else self.stdout)
|
self.redirect_stdout = redirect_stdout(self._stdout if self.iswindows else self.stdout)
|
||||||
self.redirect_stdout.__enter__()
|
self.redirect_stdout.__enter__()
|
||||||
|
|
||||||
def run(self):
|
self.stderr = IOBuffer()
|
||||||
|
self.redirect_stderr = redirect_stderr(self._stderr if self.iswindows else self.stderr)
|
||||||
|
self.redirect_stderr.__enter__()
|
||||||
|
|
||||||
|
def on_tick(self):
|
||||||
if self.isatty and not self.iswindows:
|
if self.isatty and not self.iswindows:
|
||||||
self._write(self.context.parent, rewind=True)
|
self._write(self.context.parent, rewind=True)
|
||||||
else:
|
else:
|
||||||
pass # not a tty, or windows, so we'll ignore stats output
|
pass # not a tty, or windows, so we'll ignore stats output
|
||||||
|
|
||||||
def finalize(self):
|
def on_stop(self):
|
||||||
self._write(self.context.parent, rewind=False)
|
self._write(self.context.parent, rewind=False)
|
||||||
|
self.redirect_stderr.__exit__(None, None, None)
|
||||||
self.redirect_stdout.__exit__(None, None, None)
|
self.redirect_stdout.__exit__(None, None, None)
|
||||||
|
|
||||||
def write(self, context, prefix='', rewind=True, append=None):
|
def write(self, context, prefix='', rewind=True, append=None):
|
||||||
t_cnt = len(context)
|
t_cnt = len(context)
|
||||||
|
|
||||||
if not self.iswindows:
|
if not self.iswindows:
|
||||||
buffered = self.stdout.switch()
|
for line in self.stdout.switch().split('\n')[:-1]:
|
||||||
for line in buffered.split('\n')[:-1]:
|
print(line + CLEAR_EOL, file=self._stdout)
|
||||||
print(line + CLEAR_EOL, file=sys.stderr)
|
for line in self.stderr.switch().split('\n')[:-1]:
|
||||||
|
print(line + CLEAR_EOL, file=self._stderr)
|
||||||
|
|
||||||
alive_color = Style.BRIGHT
|
alive_color = Style.BRIGHT
|
||||||
dead_color = Style.BRIGHT + Fore.BLACK
|
dead_color = Style.BRIGHT + Fore.BLACK
|
||||||
@ -117,7 +132,7 @@ class ConsoleOutputPlugin(Plugin):
|
|||||||
' ',
|
' ',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
print(prefix + _line + '\033[0K', file=sys.stderr)
|
print(prefix + _line + CLEAR_EOL, file=self._stderr)
|
||||||
|
|
||||||
if append:
|
if append:
|
||||||
# todo handle multiline
|
# todo handle multiline
|
||||||
@ -128,13 +143,13 @@ class ConsoleOutputPlugin(Plugin):
|
|||||||
CLEAR_EOL
|
CLEAR_EOL
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
file=sys.stderr
|
file=self._stderr
|
||||||
)
|
)
|
||||||
t_cnt += 1
|
t_cnt += 1
|
||||||
|
|
||||||
if rewind:
|
if rewind:
|
||||||
print(CLEAR_EOL, file=sys.stderr)
|
print(CLEAR_EOL, file=self._stderr)
|
||||||
print(MOVE_CURSOR_UP(t_cnt + 2), file=sys.stderr)
|
print(MOVE_CURSOR_UP(t_cnt + 2), file=self._stderr)
|
||||||
|
|
||||||
def _write(self, graph_context, rewind):
|
def _write(self, graph_context, rewind):
|
||||||
if settings.PROFILE.get():
|
if settings.PROFILE.get():
|
||||||
|
|||||||
73
bonobo/ext/django.py
Normal file
73
bonobo/ext/django.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from colorama import Fore, Back, Style
|
||||||
|
from django.core.management.base import BaseCommand, OutputWrapper
|
||||||
|
|
||||||
|
import bonobo
|
||||||
|
import bonobo.util
|
||||||
|
from bonobo.commands.run import get_default_services
|
||||||
|
from bonobo.ext.console import ConsoleOutputPlugin
|
||||||
|
from bonobo.util.term import CLEAR_EOL
|
||||||
|
|
||||||
|
|
||||||
|
class ETLCommand(BaseCommand):
|
||||||
|
GraphType = bonobo.Graph
|
||||||
|
|
||||||
|
def create_or_update(self, model, *, defaults=None, save=True, **kwargs):
|
||||||
|
"""
|
||||||
|
Create or update a django model instance.
|
||||||
|
|
||||||
|
:param model:
|
||||||
|
:param defaults:
|
||||||
|
:param kwargs:
|
||||||
|
:return: object, created, updated
|
||||||
|
|
||||||
|
"""
|
||||||
|
obj, created = model._default_manager.get_or_create(defaults=defaults, **kwargs)
|
||||||
|
|
||||||
|
updated = False
|
||||||
|
if not created:
|
||||||
|
for k, v in defaults.items():
|
||||||
|
if getattr(obj, k) != v:
|
||||||
|
setattr(obj, k, v)
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
if updated and save:
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
return obj, created, updated
|
||||||
|
|
||||||
|
def get_graph(self, *args, **options):
|
||||||
|
def not_implemented():
|
||||||
|
raise NotImplementedError('You must implement {}.get_graph() method.'.format(self))
|
||||||
|
|
||||||
|
return self.GraphType(not_implemented)
|
||||||
|
|
||||||
|
def get_services(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logger(self):
|
||||||
|
try:
|
||||||
|
return self._logger
|
||||||
|
except AttributeError:
|
||||||
|
self._logger = getLogger(type(self).__module__)
|
||||||
|
return self._logger
|
||||||
|
|
||||||
|
def info(self, *args, **kwargs):
|
||||||
|
self.logger.info(*args, **kwargs)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
_stdout_backup, _stderr_backup = self.stdout, self.stderr
|
||||||
|
|
||||||
|
self.stdout = OutputWrapper(ConsoleOutputPlugin._stdout, ending=CLEAR_EOL + '\n')
|
||||||
|
self.stderr = OutputWrapper(ConsoleOutputPlugin._stderr, ending=CLEAR_EOL + '\n')
|
||||||
|
self.stderr.style_func = lambda x: Fore.LIGHTRED_EX + Back.RED + '!' + Style.RESET_ALL + ' ' + x
|
||||||
|
|
||||||
|
result = bonobo.run(
|
||||||
|
self.get_graph(*args, **options),
|
||||||
|
services=self.get_services(),
|
||||||
|
)
|
||||||
|
self.stdout = _stdout_backup
|
||||||
|
|
||||||
|
return '\nReturn Value: ' + str(result)
|
||||||
43
bonobo/ext/google.py
Normal file
43
bonobo/ext/google.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import httplib2
|
||||||
|
from apiclient import discovery
|
||||||
|
from oauth2client import client, tools
|
||||||
|
from oauth2client.file import Storage
|
||||||
|
from oauth2client.tools import argparser
|
||||||
|
|
||||||
|
HOME_DIR = os.path.expanduser('~')
|
||||||
|
GOOGLE_SCOPES = ('https://www.googleapis.com/auth/spreadsheets', )
|
||||||
|
GOOGLE_SECRETS = os.path.join(HOME_DIR, '.cache/secrets/client_secrets.json')
|
||||||
|
|
||||||
|
|
||||||
|
def get_credentials():
|
||||||
|
"""Gets valid user credentials from storage.
|
||||||
|
|
||||||
|
If nothing has been stored, or if the stored credentials are invalid,
|
||||||
|
the OAuth2 flow is completed to obtain the new credentials.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Credentials, the obtained credential.
|
||||||
|
"""
|
||||||
|
credential_dir = os.path.join(HOME_DIR, '.cache', __package__, 'credentials')
|
||||||
|
if not os.path.exists(credential_dir):
|
||||||
|
os.makedirs(credential_dir)
|
||||||
|
credential_path = os.path.join(credential_dir, 'googleapis.json')
|
||||||
|
|
||||||
|
store = Storage(credential_path)
|
||||||
|
credentials = store.get()
|
||||||
|
if not credentials or credentials.invalid:
|
||||||
|
flow = client.flow_from_clientsecrets(GOOGLE_SECRETS, GOOGLE_SCOPES)
|
||||||
|
flow.user_agent = 'Bonobo ETL (https://www.bonobo-project.org/)'
|
||||||
|
flags = argparser.parse_args(['--noauth_local_webserver'])
|
||||||
|
credentials = tools.run_flow(flow, store, flags)
|
||||||
|
print('Storing credentials to ' + credential_path)
|
||||||
|
return credentials
|
||||||
|
|
||||||
|
|
||||||
|
def get_google_spreadsheets_api_client():
|
||||||
|
credentials = get_credentials()
|
||||||
|
http = credentials.authorize(httplib2.Http())
|
||||||
|
discoveryUrl = 'https://sheets.googleapis.com/$discovery/rest?version=v4'
|
||||||
|
return discovery.build('sheets', 'v4', http=http, discoveryServiceUrl=discoveryUrl, cache_discovery=False)
|
||||||
@ -14,14 +14,14 @@ def path_str(path):
|
|||||||
|
|
||||||
class OpenDataSoftAPI(Configurable):
|
class OpenDataSoftAPI(Configurable):
|
||||||
dataset = Option(str, positional=True)
|
dataset = Option(str, positional=True)
|
||||||
endpoint = Option(str, default='{scheme}://{netloc}{path}')
|
endpoint = Option(str, required=False, default='{scheme}://{netloc}{path}')
|
||||||
scheme = Option(str, default='https')
|
scheme = Option(str, required=False, default='https')
|
||||||
netloc = Option(str, default='data.opendatasoft.com')
|
netloc = Option(str, required=False, default='data.opendatasoft.com')
|
||||||
path = Option(path_str, default='/api/records/1.0/search/')
|
path = Option(path_str, required=False, default='/api/records/1.0/search/')
|
||||||
rows = Option(int, default=500)
|
rows = Option(int, required=False, default=500)
|
||||||
limit = Option(int, required=False)
|
limit = Option(int, required=False)
|
||||||
timezone = Option(str, default='Europe/Paris')
|
timezone = Option(str, required=False, default='Europe/Paris')
|
||||||
kwargs = Option(dict, default=dict)
|
kwargs = Option(dict, required=False, default=dict)
|
||||||
|
|
||||||
@ContextProcessor
|
@ContextProcessor
|
||||||
def compute_path(self, context):
|
def compute_path(self, context):
|
||||||
@ -44,7 +44,11 @@ class OpenDataSoftAPI(Configurable):
|
|||||||
break
|
break
|
||||||
|
|
||||||
for row in records:
|
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
|
start += self.rows
|
||||||
|
|
||||||
|
|||||||
@ -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 '<operation {} on {}>'.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 '<factory operation {}>'.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'
|
|
||||||
})
|
|
||||||
|
|
||||||
'''
|
|
||||||
@ -1,7 +1,3 @@
|
|||||||
from bonobo.config import Configurable
|
|
||||||
from bonobo.util.objects import get_attribute_or_create
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin:
|
class Plugin:
|
||||||
"""
|
"""
|
||||||
A plugin is an extension to the core behavior of bonobo. If you're writing transformations, you should not need
|
A plugin is an extension to the core behavior of bonobo. If you're writing transformations, you should not need
|
||||||
@ -11,30 +7,8 @@ class Plugin:
|
|||||||
respectively permits an interactive output on an ANSI console and a rich output in a jupyter notebook.
|
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!
|
Warning: THE PLUGIN API IS PRE-ALPHA AND WILL EVOLVE BEFORE 1.0, DO NOT RELY ON IT BEING STABLE!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, context):
|
def __init__(self, context):
|
||||||
self.context = 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
|
|
||||||
|
|||||||
90
bonobo/registry.py
Normal file
90
bonobo/registry.py
Normal file
@ -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()
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
|
from bonobo.structs.tokens import Token
|
||||||
from bonobo.constants import INHERIT_INPUT, LOOPBACK
|
from bonobo.constants import INHERIT_INPUT, LOOPBACK
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -35,11 +36,55 @@ class Bag:
|
|||||||
|
|
||||||
default_flags = ()
|
default_flags = ()
|
||||||
|
|
||||||
|
def __new__(cls, *args, _flags=None, _parent=None, **kwargs):
|
||||||
|
# Handle the special case where we call Bag's constructor with only one bag or token as argument.
|
||||||
|
if len(args) == 1 and len(kwargs) == 0:
|
||||||
|
if isinstance(args[0], Bag):
|
||||||
|
raise ValueError('Bag cannot be instanciated with a bag (for now ...).')
|
||||||
|
|
||||||
|
if isinstance(args[0], Token):
|
||||||
|
return args[0]
|
||||||
|
|
||||||
|
# Otherwise, type will handle that for us.
|
||||||
|
return super().__new__(cls)
|
||||||
|
|
||||||
def __init__(self, *args, _flags=None, _parent=None, **kwargs):
|
def __init__(self, *args, _flags=None, _parent=None, **kwargs):
|
||||||
self._flags = type(self).default_flags + (_flags or ())
|
self._flags = type(self).default_flags + (_flags or ())
|
||||||
self._parent = _parent
|
self._parent = _parent
|
||||||
self._args = args
|
|
||||||
self._kwargs = kwargs
|
if len(args) == 1 and len(kwargs) == 0:
|
||||||
|
# If we only have one argument, that may be because we're using the shorthand syntax.
|
||||||
|
mixed = args[0]
|
||||||
|
|
||||||
|
if isinstance(mixed, Bag):
|
||||||
|
# Just duplicate the bag.
|
||||||
|
self._args = mixed.args
|
||||||
|
self._kwargs = mixed.kwargs
|
||||||
|
elif isinstance(mixed, tuple):
|
||||||
|
if not len(mixed):
|
||||||
|
# Empty bag.
|
||||||
|
self._args = ()
|
||||||
|
self._kwargs = {}
|
||||||
|
elif isinstance(mixed[-1], dict):
|
||||||
|
# Args + Kwargs
|
||||||
|
self._args = mixed[:-1]
|
||||||
|
self._kwargs = mixed[-1]
|
||||||
|
else:
|
||||||
|
# Args only
|
||||||
|
self._args = mixed
|
||||||
|
self._kwargs = {}
|
||||||
|
elif isinstance(mixed, dict):
|
||||||
|
# Kwargs only
|
||||||
|
self._args = ()
|
||||||
|
self._kwargs = mixed
|
||||||
|
else:
|
||||||
|
self._args = args
|
||||||
|
self._kwargs = {}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Otherwise, lets get args/kwargs from the constructor.
|
||||||
|
self._args = args
|
||||||
|
self._kwargs = kwargs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def args(self):
|
def args(self):
|
||||||
@ -105,23 +150,24 @@ class Bag:
|
|||||||
if isinstance(other, Bag) and other.args == self.args and other.kwargs == self.kwargs:
|
if isinstance(other, Bag) and other.args == self.args and other.kwargs == self.kwargs:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# tuple of (tuple, dict)
|
# tuple
|
||||||
if isinstance(other, tuple) and len(other) == 2 and other[0] == self.args and other[1] == self.kwargs:
|
if isinstance(other, tuple):
|
||||||
return True
|
# self == ()
|
||||||
|
if not len(other):
|
||||||
|
return not self.args and not self.kwargs
|
||||||
|
|
||||||
# tuple (aka args)
|
if isinstance(other[-1], dict):
|
||||||
if isinstance(other, tuple) and other == self.args:
|
# self == (*args, {**kwargs}) ?
|
||||||
return True
|
return other[:-1] == self.args and other[-1] == self.kwargs
|
||||||
|
|
||||||
|
# self == (*args) ?
|
||||||
|
return other == self.args and not self.kwargs
|
||||||
|
|
||||||
# dict (aka kwargs)
|
# dict (aka kwargs)
|
||||||
if isinstance(other, dict) and not self.args and other == self.kwargs:
|
if isinstance(other, dict) and not self.args and other == self.kwargs:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# arg0
|
return len(self.args) == 1 and not self.kwargs and self.args[0] == other
|
||||||
if len(self.args) == 1 and not len(self.kwargs) and self.args[0] == other:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<{} ({})>'.format(
|
return '<{} ({})>'.format(
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from bonobo.util.collections import sortedlist
|
from bonobo.util.collections import sortedlist, ensure_tuple
|
||||||
from bonobo.util.iterators import ensure_tuple
|
|
||||||
from bonobo.util.compat import deprecated, deprecated_alias
|
from bonobo.util.compat import deprecated, deprecated_alias
|
||||||
from bonobo.util.inspect import (
|
from bonobo.util.inspect import (
|
||||||
inspect_node,
|
inspect_node,
|
||||||
|
|||||||
@ -1,6 +1,48 @@
|
|||||||
import bisect
|
import bisect
|
||||||
|
import functools
|
||||||
|
|
||||||
|
|
||||||
class sortedlist(list):
|
class sortedlist(list):
|
||||||
def insort(self, x):
|
def insort(self, x):
|
||||||
bisect.insort(self, x)
|
bisect.insort(self, x)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_tuple(tuple_or_mixed):
|
||||||
|
"""
|
||||||
|
If it's not a tuple, let's make a tuple of one item.
|
||||||
|
Otherwise, not changed.
|
||||||
|
|
||||||
|
:param tuple_or_mixed:
|
||||||
|
:return: tuple
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(tuple_or_mixed, tuple):
|
||||||
|
return tuple_or_mixed
|
||||||
|
return (tuple_or_mixed, )
|
||||||
|
|
||||||
|
|
||||||
|
def tuplize(generator):
|
||||||
|
""" Takes a generator and make it a tuple-returning function. As a side
|
||||||
|
effect, it can also decorate any iterator-returning function to force
|
||||||
|
return value to be a tuple.
|
||||||
|
|
||||||
|
>>> tuplized_lambda = tuplize(lambda: [1, 2, 3])
|
||||||
|
>>> tuplized_lambda()
|
||||||
|
(1, 2, 3)
|
||||||
|
|
||||||
|
>>> @tuplize
|
||||||
|
... def my_generator():
|
||||||
|
... yield 1
|
||||||
|
... yield 2
|
||||||
|
... yield 3
|
||||||
|
...
|
||||||
|
>>> my_generator()
|
||||||
|
(1, 2, 3)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(generator)
|
||||||
|
def tuplized(*args, **kwargs):
|
||||||
|
return tuple(generator(*args, **kwargs))
|
||||||
|
|
||||||
|
return tuplized
|
||||||
|
|||||||
@ -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)
|
|
||||||
@ -1,7 +1,3 @@
|
|||||||
import functools
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
|
|
||||||
def get_name(mixed):
|
def get_name(mixed):
|
||||||
try:
|
try:
|
||||||
return mixed.__name__
|
return mixed.__name__
|
||||||
@ -220,6 +216,15 @@ class ValueHolder:
|
|||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self._value)
|
return len(self._value)
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return item in self._value
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
return self._value[item]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self._value[key] = value
|
||||||
|
|
||||||
|
|
||||||
def get_attribute_or_create(obj, attr, default):
|
def get_attribute_or_create(obj, attr, default):
|
||||||
try:
|
try:
|
||||||
|
|||||||
61
bonobo/util/resolvers.py
Normal file
61
bonobo/util/resolvers.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
This package is considered private, and should only be used within bonobo.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import bonobo
|
||||||
|
from bonobo.util.collections import tuplize
|
||||||
|
from bonobo.util.python import WorkingDirectoryModulesRegistry
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_option(option):
|
||||||
|
"""
|
||||||
|
Parse a 'key=val' option string into a python (key, val) pair
|
||||||
|
|
||||||
|
:param option: str
|
||||||
|
:return: tuple
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
key, val = option.split('=', 1)
|
||||||
|
except ValueError:
|
||||||
|
return option, True
|
||||||
|
|
||||||
|
try:
|
||||||
|
val = json.loads(val)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return key, val
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_options(options=None):
|
||||||
|
"""
|
||||||
|
Resolve a collection of option strings (eventually coming from command line) into a python dictionary.
|
||||||
|
|
||||||
|
:param options: tuple[str]
|
||||||
|
:return: dict
|
||||||
|
"""
|
||||||
|
if options:
|
||||||
|
return dict(map(_parse_option, options))
|
||||||
|
return dict()
|
||||||
|
|
||||||
|
|
||||||
|
@tuplize
|
||||||
|
def _resolve_transformations(transformations):
|
||||||
|
"""
|
||||||
|
Resolve a collection of strings into the matching python objects, defaulting to bonobo namespace if no package is provided.
|
||||||
|
|
||||||
|
Syntax for each string is path.to.package:attribute
|
||||||
|
|
||||||
|
:param transformations: tuple(str)
|
||||||
|
:return: tuple(object)
|
||||||
|
"""
|
||||||
|
registry = WorkingDirectoryModulesRegistry()
|
||||||
|
for t in transformations:
|
||||||
|
try:
|
||||||
|
mod, attr = t.split(':', 1)
|
||||||
|
yield getattr(registry.require(mod), attr)
|
||||||
|
except ValueError:
|
||||||
|
yield getattr(bonobo, t)
|
||||||
@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import sys
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('..'))
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
sys.path.insert(0, os.path.abspath('_themes'))
|
sys.path.insert(0, os.path.abspath('_themes'))
|
||||||
@ -36,8 +37,8 @@ master_doc = 'index'
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'Bonobo'
|
project = 'Bonobo'
|
||||||
copyright = '2012-2017, Romain Dorgueil'
|
|
||||||
author = 'Romain Dorgueil'
|
author = 'Romain Dorgueil'
|
||||||
|
copyright = '2012-{}, {}'.format(datetime.datetime.now().year, author)
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
|||||||
@ -32,6 +32,100 @@ Iterable
|
|||||||
Something we can iterate on, in python, so basically anything you'd be able to use in a `for` loop.
|
Something we can iterate on, in python, so basically anything you'd be able to use in a `for` loop.
|
||||||
|
|
||||||
|
|
||||||
|
Concepts
|
||||||
|
::::::::
|
||||||
|
|
||||||
|
Whatever kind of transformation you want to use, there are a few common concepts you should know about.
|
||||||
|
|
||||||
|
Input
|
||||||
|
-----
|
||||||
|
|
||||||
|
All input is retrieved via the call arguments. Each line of input means one call to the callable provided. Arguments
|
||||||
|
will be, in order:
|
||||||
|
|
||||||
|
* Injected dependencies (database, http, filesystem, ...)
|
||||||
|
* Position based arguments
|
||||||
|
* Keyword based arguments
|
||||||
|
|
||||||
|
You'll see below how to pass each of those.
|
||||||
|
|
||||||
|
Output
|
||||||
|
------
|
||||||
|
|
||||||
|
Each callable can return/yield different things (all examples will use yield, but if there is only one output per input
|
||||||
|
line, you can also return your output row and expect the exact same behaviour).
|
||||||
|
|
||||||
|
Let's see the rules (first to match wins).
|
||||||
|
|
||||||
|
1. A flag, eventually followed by something else, marks a special behaviour. If it supports it, the remaining part of
|
||||||
|
the output line will be interpreted using the same rules, and some flags can be combined.
|
||||||
|
|
||||||
|
**NOT_MODIFIED**
|
||||||
|
|
||||||
|
**NOT_MODIFIED** tells bonobo to use the input row unmodified as the output.
|
||||||
|
|
||||||
|
*CANNOT be combined*
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from bonobo import NOT_MODIFIED
|
||||||
|
|
||||||
|
def output_will_be_same_as_input(*args, **kwargs):
|
||||||
|
yield NOT_MODIFIED
|
||||||
|
|
||||||
|
**APPEND**
|
||||||
|
|
||||||
|
**APPEND** tells bonobo to append this output to the input (positional arguments will equal `input_args + output_args`,
|
||||||
|
keyword arguments will equal `{**input_kwargs, **output_kwargs}`).
|
||||||
|
|
||||||
|
*CAN be combined, but not with itself*
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from bonobo import APPEND
|
||||||
|
|
||||||
|
def output_will_be_appended_to_input(*args, **kwargs):
|
||||||
|
yield APPEND, 'foo', 'bar', {'eat_at': 'joe'}
|
||||||
|
|
||||||
|
**LOOPBACK**
|
||||||
|
|
||||||
|
**LOOPBACK** tells bonobo that this output must be looped back into our own input queue, allowing to create the stream
|
||||||
|
processing version of recursive algorithms.
|
||||||
|
|
||||||
|
*CAN be combined, but not with itself*
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from bonobo import LOOPBACK
|
||||||
|
|
||||||
|
def output_will_be_sent_to_self(*args, **kwargs):
|
||||||
|
yield LOOPBACK, 'Hello, I am the future "you".'
|
||||||
|
|
||||||
|
**CHANNEL(...)**
|
||||||
|
|
||||||
|
**CHANNEL(...)** tells bonobo that this output does not use the default channel and is routed through another path.
|
||||||
|
This is something you should probably not use unless your data flow design is complex, and if you're not certain
|
||||||
|
about it, it probably means that it is not the feature you're looking for.
|
||||||
|
|
||||||
|
*CAN be combined, but not with itself*
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from bonobo import CHANNEL
|
||||||
|
|
||||||
|
def output_will_be_sent_to_self(*args, **kwargs):
|
||||||
|
yield CHANNEL("errors"), 'That is not cool.'
|
||||||
|
|
||||||
|
2. Once all flags are "consumed", the remaining part is interpreted.
|
||||||
|
|
||||||
|
* If it is a :class:`bonobo.Bag` instance, then it's used directly.
|
||||||
|
* If it is a :class:`dict` then a kwargs-only :class:`bonobo.Bag` will be created.
|
||||||
|
* If it is a :class:`tuple` then an args-only :class:`bonobo.Bag` will be created, unless its last argument is a
|
||||||
|
:class:`dict` in which case a args+kwargs :class:`bonobo.Bag` will be created.
|
||||||
|
* If it's something else, it will be used to create a one-arg-only :class:`bonobo.Bag`.
|
||||||
|
|
||||||
Function based transformations
|
Function based transformations
|
||||||
::::::::::::::::::::::::::::::
|
::::::::::::::::::::::::::::::
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
-e .[docker]
|
-e .[docker]
|
||||||
appdirs==1.4.3
|
appdirs==1.4.3
|
||||||
bonobo-docker==0.2.12
|
bonobo-docker==0.5.0
|
||||||
certifi==2017.7.27.1
|
certifi==2017.7.27.1
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
colorama==0.3.9
|
colorama==0.3.9
|
||||||
|
|||||||
@ -12,7 +12,7 @@ def test_node_string():
|
|||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
assert output[0] == (('foo', ), {})
|
assert output[0] == 'foo'
|
||||||
|
|
||||||
def g():
|
def g():
|
||||||
yield 'foo'
|
yield 'foo'
|
||||||
@ -23,8 +23,8 @@ def test_node_string():
|
|||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
assert output[0] == (('foo', ), {})
|
assert output[0] == 'foo'
|
||||||
assert output[1] == (('bar', ), {})
|
assert output[1] == 'bar'
|
||||||
|
|
||||||
|
|
||||||
def test_node_bytes():
|
def test_node_bytes():
|
||||||
@ -36,7 +36,7 @@ def test_node_bytes():
|
|||||||
|
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
assert output[0] == ((b'foo', ), {})
|
assert output[0] == b'foo'
|
||||||
|
|
||||||
def g():
|
def g():
|
||||||
yield b'foo'
|
yield b'foo'
|
||||||
@ -47,8 +47,8 @@ def test_node_bytes():
|
|||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
assert output[0] == ((b'foo', ), {})
|
assert output[0] == b'foo'
|
||||||
assert output[1] == ((b'bar', ), {})
|
assert output[1] == b'bar'
|
||||||
|
|
||||||
|
|
||||||
def test_node_dict():
|
def test_node_dict():
|
||||||
@ -102,3 +102,80 @@ def test_node_dict_chained():
|
|||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
assert output[0] == {'id': 1, 'name': 'FOO'}
|
assert output[0] == {'id': 1, 'name': 'FOO'}
|
||||||
assert output[1] == {'id': 2, 'name': 'BAR'}
|
assert output[1] == {'id': 2, 'name': 'BAR'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_tuple():
|
||||||
|
def f():
|
||||||
|
return 'foo', 'bar'
|
||||||
|
|
||||||
|
with BufferingNodeExecutionContext(f) as context:
|
||||||
|
context.write_sync(Bag())
|
||||||
|
output = context.get_buffer()
|
||||||
|
|
||||||
|
assert len(output) == 1
|
||||||
|
assert output[0] == ('foo', 'bar')
|
||||||
|
|
||||||
|
def g():
|
||||||
|
yield 'foo', 'bar'
|
||||||
|
yield 'foo', 'baz'
|
||||||
|
|
||||||
|
with BufferingNodeExecutionContext(g) as context:
|
||||||
|
context.write_sync(Bag())
|
||||||
|
output = context.get_buffer()
|
||||||
|
|
||||||
|
assert len(output) == 2
|
||||||
|
assert output[0] == ('foo', 'bar')
|
||||||
|
assert output[1] == ('foo', 'baz')
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_tuple_chained():
|
||||||
|
strategy = NaiveStrategy(GraphExecutionContextType=BufferingGraphExecutionContext)
|
||||||
|
|
||||||
|
def uppercase(*args):
|
||||||
|
return tuple(map(str.upper, args))
|
||||||
|
|
||||||
|
def f():
|
||||||
|
return 'foo', 'bar'
|
||||||
|
|
||||||
|
graph = Graph(f, uppercase)
|
||||||
|
context = strategy.execute(graph)
|
||||||
|
output = context.get_buffer()
|
||||||
|
|
||||||
|
assert len(output) == 1
|
||||||
|
assert output[0] == ('FOO', 'BAR')
|
||||||
|
|
||||||
|
def g():
|
||||||
|
yield 'foo', 'bar'
|
||||||
|
yield 'foo', 'baz'
|
||||||
|
|
||||||
|
graph = Graph(g, uppercase)
|
||||||
|
context = strategy.execute(graph)
|
||||||
|
output = context.get_buffer()
|
||||||
|
|
||||||
|
assert len(output) == 2
|
||||||
|
assert output[0] == ('FOO', 'BAR')
|
||||||
|
assert output[1] == ('FOO', 'BAZ')
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_tuple_dict():
|
||||||
|
def f():
|
||||||
|
return 'foo', 'bar', {'id': 1}
|
||||||
|
|
||||||
|
with BufferingNodeExecutionContext(f) as context:
|
||||||
|
context.write_sync(Bag())
|
||||||
|
output = context.get_buffer()
|
||||||
|
|
||||||
|
assert len(output) == 1
|
||||||
|
assert output[0] == ('foo', 'bar', {'id': 1})
|
||||||
|
|
||||||
|
def g():
|
||||||
|
yield 'foo', 'bar', {'id': 1}
|
||||||
|
yield 'foo', 'baz', {'id': 2}
|
||||||
|
|
||||||
|
with BufferingNodeExecutionContext(g) as context:
|
||||||
|
context.write_sync(Bag())
|
||||||
|
output = context.get_buffer()
|
||||||
|
|
||||||
|
assert len(output) == 2
|
||||||
|
assert output[0] == ('foo', 'bar', {'id': 1})
|
||||||
|
assert output[1] == ('foo', 'baz', {'id': 2})
|
||||||
|
|||||||
@ -28,9 +28,7 @@ def test_write_csv_to_file_kwargs(tmpdir, add_kwargs):
|
|||||||
fs, filename, services = csv_tester.get_services_for_writer(tmpdir)
|
fs, filename, services = csv_tester.get_services_for_writer(tmpdir)
|
||||||
|
|
||||||
with NodeExecutionContext(CsvWriter(filename, **add_kwargs), services=services) as context:
|
with NodeExecutionContext(CsvWriter(filename, **add_kwargs), services=services) as context:
|
||||||
context.write(BEGIN, Bag(**{'foo': 'bar'}), Bag(**{'foo': 'baz', 'ignore': 'this'}), END)
|
context.write_sync({'foo': 'bar'}, {'foo': 'baz', 'ignore': 'this'})
|
||||||
context.step()
|
|
||||||
context.step()
|
|
||||||
|
|
||||||
with fs.open(filename) as fp:
|
with fs.open(filename) as fp:
|
||||||
assert fp.read() == 'foo\nbar\nbaz\n'
|
assert fp.read() == 'foo\nbar\nbaz\n'
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bonobo import Bag, JsonReader, JsonWriter, settings
|
from bonobo import JsonReader, JsonWriter, settings
|
||||||
from bonobo.constants import BEGIN, END
|
|
||||||
from bonobo.execution.node import NodeExecutionContext
|
from bonobo.execution.node import NodeExecutionContext
|
||||||
from bonobo.util.testing import FilesystemTester
|
from bonobo.util.testing import FilesystemTester
|
||||||
|
|
||||||
@ -29,8 +28,7 @@ def test_write_json_kwargs(tmpdir, add_kwargs):
|
|||||||
fs, filename, services = json_tester.get_services_for_writer(tmpdir)
|
fs, filename, services = json_tester.get_services_for_writer(tmpdir)
|
||||||
|
|
||||||
with NodeExecutionContext(JsonWriter(filename, **add_kwargs), services=services) as context:
|
with NodeExecutionContext(JsonWriter(filename, **add_kwargs), services=services) as context:
|
||||||
context.write(BEGIN, Bag(**{'foo': 'bar'}), END)
|
context.write_sync({'foo': 'bar'})
|
||||||
context.step()
|
|
||||||
|
|
||||||
with fs.open(filename) as fp:
|
with fs.open(filename) as fp:
|
||||||
assert fp.read() == '[{"foo": "bar"}]'
|
assert fp.read() == '[{"foo": "bar"}]'
|
||||||
|
|||||||
@ -14,7 +14,7 @@ def test_write_pickled_dict_to_file(tmpdir):
|
|||||||
fs, filename, services = pickle_tester.get_services_for_writer(tmpdir)
|
fs, filename, services = pickle_tester.get_services_for_writer(tmpdir)
|
||||||
|
|
||||||
with NodeExecutionContext(PickleWriter(filename), services=services) as context:
|
with NodeExecutionContext(PickleWriter(filename), services=services) as context:
|
||||||
context.write_sync(Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}))
|
context.write_sync(Bag(({'foo': 'bar'}, {})), Bag(({'foo': 'baz', 'ignore': 'this'}, {})))
|
||||||
|
|
||||||
with fs.open(filename, 'rb') as fp:
|
with fs.open(filename, 'rb') as fp:
|
||||||
assert pickle.loads(fp.read()) == {'foo': 'bar'}
|
assert pickle.loads(fp.read()) == {'foo': 'bar'}
|
||||||
@ -27,7 +27,7 @@ def test_read_pickled_list_from_file(tmpdir):
|
|||||||
fs, filename, services = pickle_tester.get_services_for_reader(tmpdir)
|
fs, filename, services = pickle_tester.get_services_for_reader(tmpdir)
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(PickleReader(filename), services=services) as context:
|
with BufferingNodeExecutionContext(PickleReader(filename), services=services) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(())
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import pickle
|
import pickle
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from bonobo import Bag
|
from bonobo import Bag
|
||||||
from bonobo.constants import INHERIT_INPUT
|
from bonobo.constants import INHERIT_INPUT, BEGIN
|
||||||
from bonobo.structs import Token
|
from bonobo.structs import Token
|
||||||
|
|
||||||
args = (
|
args = (
|
||||||
@ -31,6 +33,32 @@ def test_basic():
|
|||||||
my_callable2.assert_called_once_with(*args, **kwargs)
|
my_callable2.assert_called_once_with(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_constructor_empty():
|
||||||
|
a, b = Bag(), Bag()
|
||||||
|
assert a == b
|
||||||
|
assert a.args is ()
|
||||||
|
assert a.kwargs == {}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('arg_in', 'arg_out'), (
|
||||||
|
((), ()),
|
||||||
|
({}, ()),
|
||||||
|
(('a', 'b', 'c'), None),
|
||||||
|
))
|
||||||
|
def test_constructor_shorthand(arg_in, arg_out):
|
||||||
|
if arg_out is None:
|
||||||
|
arg_out = arg_in
|
||||||
|
assert Bag(arg_in) == arg_out
|
||||||
|
|
||||||
|
|
||||||
|
def test_constructor_kwargs_only():
|
||||||
|
assert Bag(foo='bar') == {'foo': 'bar'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_constructor_identity():
|
||||||
|
assert Bag(BEGIN) is BEGIN
|
||||||
|
|
||||||
|
|
||||||
def test_inherit():
|
def test_inherit():
|
||||||
bag = Bag('a', a=1)
|
bag = Bag('a', a=1)
|
||||||
bag2 = Bag.inherit('b', b=2, _parent=bag)
|
bag2 = Bag.inherit('b', b=2, _parent=bag)
|
||||||
@ -92,7 +120,7 @@ def test_pickle():
|
|||||||
assert unpickled == bag
|
assert unpickled == bag
|
||||||
|
|
||||||
|
|
||||||
def test_eq_operator():
|
def test_eq_operator_bag():
|
||||||
assert Bag('foo') == Bag('foo')
|
assert Bag('foo') == Bag('foo')
|
||||||
assert Bag('foo') != Bag('bar')
|
assert Bag('foo') != Bag('bar')
|
||||||
assert Bag('foo') is not Bag('foo')
|
assert Bag('foo') is not Bag('foo')
|
||||||
@ -100,6 +128,35 @@ def test_eq_operator():
|
|||||||
assert Token('foo') != Bag('foo')
|
assert Token('foo') != Bag('foo')
|
||||||
|
|
||||||
|
|
||||||
|
def test_eq_operator_tuple_mixed():
|
||||||
|
assert Bag('foo', bar='baz') == ('foo', {'bar': 'baz'})
|
||||||
|
assert Bag('foo') == ('foo', {})
|
||||||
|
assert Bag() == ({}, )
|
||||||
|
|
||||||
|
|
||||||
|
def test_eq_operator_tuple_not_mixed():
|
||||||
|
assert Bag('foo', 'bar') == ('foo', 'bar')
|
||||||
|
assert Bag('foo') == ('foo', )
|
||||||
|
assert Bag() == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_eq_operator_dict():
|
||||||
|
assert Bag(foo='bar') == {'foo': 'bar'}
|
||||||
|
assert Bag(
|
||||||
|
foo='bar', corp='acme'
|
||||||
|
) == {
|
||||||
|
'foo': 'bar',
|
||||||
|
'corp': 'acme',
|
||||||
|
}
|
||||||
|
assert Bag(
|
||||||
|
foo='bar', corp='acme'
|
||||||
|
) == {
|
||||||
|
'corp': 'acme',
|
||||||
|
'foo': 'bar',
|
||||||
|
}
|
||||||
|
assert Bag() == {}
|
||||||
|
|
||||||
|
|
||||||
def test_repr():
|
def test_repr():
|
||||||
bag = Bag('a', a=1)
|
bag = Bag('a', a=1)
|
||||||
assert repr(bag) == "<Bag ('a', a=1)>"
|
assert repr(bag) == "<Bag ('a', a=1)>"
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
import functools
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import runpy
|
import runpy
|
||||||
import sys
|
import sys
|
||||||
|
from contextlib import redirect_stdout, redirect_stderr
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
@ -10,12 +13,27 @@ from bonobo import __main__, __version__, get_examples_path
|
|||||||
from bonobo.commands import entrypoint
|
from bonobo.commands import entrypoint
|
||||||
|
|
||||||
|
|
||||||
def runner_entrypoint(*args):
|
def runner(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def wrapped_runner(*args):
|
||||||
|
with redirect_stdout(io.StringIO()) as stdout, redirect_stderr(io.StringIO()) as stderr:
|
||||||
|
try:
|
||||||
|
f(list(args))
|
||||||
|
except BaseException as exc:
|
||||||
|
return stdout.getvalue(), stderr.getvalue(), exc
|
||||||
|
return stdout.getvalue(), stderr.getvalue()
|
||||||
|
|
||||||
|
return wrapped_runner
|
||||||
|
|
||||||
|
|
||||||
|
@runner
|
||||||
|
def runner_entrypoint(args):
|
||||||
""" Run bonobo using the python command entrypoint directly (bonobo.commands.entrypoint). """
|
""" Run bonobo using the python command entrypoint directly (bonobo.commands.entrypoint). """
|
||||||
return entrypoint(list(args))
|
return entrypoint(args)
|
||||||
|
|
||||||
|
|
||||||
def runner_module(*args):
|
@runner
|
||||||
|
def runner_module(args):
|
||||||
""" Run bonobo using the bonobo.__main__ file, which is equivalent as doing "python -m bonobo ..."."""
|
""" Run bonobo using the bonobo.__main__ file, which is equivalent as doing "python -m bonobo ..."."""
|
||||||
with patch.object(sys, 'argv', ['bonobo', *args]):
|
with patch.object(sys, 'argv', ['bonobo', *args]):
|
||||||
return runpy.run_path(__main__.__file__, run_name='__main__')
|
return runpy.run_path(__main__.__file__, run_name='__main__')
|
||||||
@ -40,17 +58,15 @@ def test_entrypoint():
|
|||||||
|
|
||||||
|
|
||||||
@all_runners
|
@all_runners
|
||||||
def test_no_command(runner, capsys):
|
def test_no_command(runner):
|
||||||
with pytest.raises(SystemExit):
|
_, err, exc = runner()
|
||||||
runner()
|
assert type(exc) == SystemExit
|
||||||
_, err = capsys.readouterr()
|
|
||||||
assert 'error: the following arguments are required: command' in err
|
assert 'error: the following arguments are required: command' in err
|
||||||
|
|
||||||
|
|
||||||
@all_runners
|
@all_runners
|
||||||
def test_run(runner, capsys):
|
def test_run(runner):
|
||||||
runner('run', '--quiet', get_examples_path('types/strings.py'))
|
out, err = runner('run', '--quiet', get_examples_path('types/strings.py'))
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0].startswith('Foo ')
|
assert out[0].startswith('Foo ')
|
||||||
assert out[1].startswith('Bar ')
|
assert out[1].startswith('Bar ')
|
||||||
@ -58,9 +74,8 @@ def test_run(runner, capsys):
|
|||||||
|
|
||||||
|
|
||||||
@all_runners
|
@all_runners
|
||||||
def test_run_module(runner, capsys):
|
def test_run_module(runner):
|
||||||
runner('run', '--quiet', '-m', 'bonobo.examples.types.strings')
|
out, err = runner('run', '--quiet', '-m', 'bonobo.examples.types.strings')
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0].startswith('Foo ')
|
assert out[0].startswith('Foo ')
|
||||||
assert out[1].startswith('Bar ')
|
assert out[1].startswith('Bar ')
|
||||||
@ -68,9 +83,8 @@ def test_run_module(runner, capsys):
|
|||||||
|
|
||||||
|
|
||||||
@all_runners
|
@all_runners
|
||||||
def test_run_path(runner, capsys):
|
def test_run_path(runner):
|
||||||
runner('run', '--quiet', get_examples_path('types'))
|
out, err = runner('run', '--quiet', get_examples_path('types'))
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0].startswith('Foo ')
|
assert out[0].startswith('Foo ')
|
||||||
assert out[1].startswith('Bar ')
|
assert out[1].startswith('Bar ')
|
||||||
@ -94,9 +108,8 @@ def test_install_requirements_for_file(runner):
|
|||||||
|
|
||||||
|
|
||||||
@all_runners
|
@all_runners
|
||||||
def test_version(runner, capsys):
|
def test_version(runner):
|
||||||
runner('version')
|
out, err = runner('version')
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.strip()
|
out = out.strip()
|
||||||
assert out.startswith('bonobo ')
|
assert out.startswith('bonobo ')
|
||||||
assert __version__ in out
|
assert __version__ in out
|
||||||
@ -104,48 +117,47 @@ def test_version(runner, capsys):
|
|||||||
|
|
||||||
@all_runners
|
@all_runners
|
||||||
class TestDefaultEnvFile(object):
|
class TestDefaultEnvFile(object):
|
||||||
def test_run_file_with_default_env_file(self, runner, capsys):
|
def test_run_file_with_default_env_file(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet', '--default-env-file', '.env_one',
|
'run', '--quiet', '--default-env-file', '.env_one',
|
||||||
get_examples_path('environment/env_files/get_passed_env_file.py')
|
get_examples_path('environment/env_files/get_passed_env_file.py')
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == '321'
|
assert out[0] == '321'
|
||||||
assert out[1] == 'sweetpassword'
|
assert out[1] == 'sweetpassword'
|
||||||
assert out[2] != 'marzo'
|
assert out[2] != 'marzo'
|
||||||
|
|
||||||
def test_run_file_with_multiple_default_env_files(self, runner, capsys):
|
def test_run_file_with_multiple_default_env_files(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet', '--default-env-file', '.env_one',
|
'run', '--quiet', '--default-env-file', '.env_one', '--default-env-file', '.env_two',
|
||||||
'--default-env-file', '.env_two',
|
|
||||||
get_examples_path('environment/env_files/get_passed_env_file.py')
|
get_examples_path('environment/env_files/get_passed_env_file.py')
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == '321'
|
assert out[0] == '321'
|
||||||
assert out[1] == 'sweetpassword'
|
assert out[1] == 'sweetpassword'
|
||||||
assert out[2] != 'marzo'
|
assert out[2] != 'marzo'
|
||||||
|
|
||||||
def test_run_module_with_default_env_file(self, runner, capsys):
|
def test_run_module_with_default_env_file(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet', '-m',
|
'run', '--quiet', '-m', 'bonobo.examples.environment.env_files.get_passed_env_file', '--default-env-file',
|
||||||
'bonobo.examples.environment.env_files.get_passed_env_file',
|
'.env_one'
|
||||||
'--default-env-file', '.env_one'
|
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == '321'
|
assert out[0] == '321'
|
||||||
assert out[1] == 'sweetpassword'
|
assert out[1] == 'sweetpassword'
|
||||||
assert out[2] != 'marzo'
|
assert out[2] != 'marzo'
|
||||||
|
|
||||||
def test_run_module_with_multiple_default_env_files(self, runner, capsys):
|
def test_run_module_with_multiple_default_env_files(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet', '-m',
|
'run',
|
||||||
|
'--quiet',
|
||||||
|
'-m',
|
||||||
'bonobo.examples.environment.env_files.get_passed_env_file',
|
'bonobo.examples.environment.env_files.get_passed_env_file',
|
||||||
'--default-env-file', '.env_one', '--default-env-file', '.env_two',
|
'--default-env-file',
|
||||||
|
'.env_one',
|
||||||
|
'--default-env-file',
|
||||||
|
'.env_two',
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == '321'
|
assert out[0] == '321'
|
||||||
assert out[1] == 'sweetpassword'
|
assert out[1] == 'sweetpassword'
|
||||||
@ -154,49 +166,59 @@ class TestDefaultEnvFile(object):
|
|||||||
|
|
||||||
@all_runners
|
@all_runners
|
||||||
class TestEnvFile(object):
|
class TestEnvFile(object):
|
||||||
def test_run_file_with_file(self, runner, capsys):
|
def test_run_file_with_file(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet',
|
'run',
|
||||||
|
'--quiet',
|
||||||
get_examples_path('environment/env_files/get_passed_env_file.py'),
|
get_examples_path('environment/env_files/get_passed_env_file.py'),
|
||||||
'--env-file', '.env_one',
|
'--env-file',
|
||||||
|
'.env_one',
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == '321'
|
assert out[0] == '321'
|
||||||
assert out[1] == 'sweetpassword'
|
assert out[1] == 'sweetpassword'
|
||||||
assert out[2] == 'marzo'
|
assert out[2] == 'marzo'
|
||||||
|
|
||||||
def test_run_file_with_multiple_files(self, runner, capsys):
|
def test_run_file_with_multiple_files(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet',
|
'run',
|
||||||
|
'--quiet',
|
||||||
get_examples_path('environment/env_files/get_passed_env_file.py'),
|
get_examples_path('environment/env_files/get_passed_env_file.py'),
|
||||||
'--env-file', '.env_one', '--env-file', '.env_two',
|
'--env-file',
|
||||||
|
'.env_one',
|
||||||
|
'--env-file',
|
||||||
|
'.env_two',
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == '321'
|
assert out[0] == '321'
|
||||||
assert out[1] == 'not_sweet_password'
|
assert out[1] == 'not_sweet_password'
|
||||||
assert out[2] == 'abril'
|
assert out[2] == 'abril'
|
||||||
|
|
||||||
def test_run_module_with_file(self, runner, capsys):
|
def test_run_module_with_file(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet', '-m',
|
'run',
|
||||||
|
'--quiet',
|
||||||
|
'-m',
|
||||||
'bonobo.examples.environment.env_files.get_passed_env_file',
|
'bonobo.examples.environment.env_files.get_passed_env_file',
|
||||||
'--env-file', '.env_one',
|
'--env-file',
|
||||||
|
'.env_one',
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == '321'
|
assert out[0] == '321'
|
||||||
assert out[1] == 'sweetpassword'
|
assert out[1] == 'sweetpassword'
|
||||||
assert out[2] == 'marzo'
|
assert out[2] == 'marzo'
|
||||||
|
|
||||||
def test_run_module_with_multiple_files(self, runner, capsys):
|
def test_run_module_with_multiple_files(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet', '-m',
|
'run',
|
||||||
|
'--quiet',
|
||||||
|
'-m',
|
||||||
'bonobo.examples.environment.env_files.get_passed_env_file',
|
'bonobo.examples.environment.env_files.get_passed_env_file',
|
||||||
'--env-file', '.env_one', '--env-file', '.env_two',
|
'--env-file',
|
||||||
|
'.env_one',
|
||||||
|
'--env-file',
|
||||||
|
'.env_two',
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == '321'
|
assert out[0] == '321'
|
||||||
assert out[1] == 'not_sweet_password'
|
assert out[1] == 'not_sweet_password'
|
||||||
@ -204,28 +226,36 @@ class TestEnvFile(object):
|
|||||||
|
|
||||||
|
|
||||||
@all_runners
|
@all_runners
|
||||||
class TestEnvFileCombinations(object):
|
class TestEnvFileCombinations:
|
||||||
def test_run_file_with_default_env_file_and_env_file(self, runner, capsys):
|
def test_run_file_with_default_env_file_and_env_file(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet',
|
'run',
|
||||||
|
'--quiet',
|
||||||
get_examples_path('environment/env_files/get_passed_env_file.py'),
|
get_examples_path('environment/env_files/get_passed_env_file.py'),
|
||||||
'--default-env-file', '.env_one', '--env-file', '.env_two',
|
'--default-env-file',
|
||||||
|
'.env_one',
|
||||||
|
'--env-file',
|
||||||
|
'.env_two',
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == '321'
|
assert out[0] == '321'
|
||||||
assert out[1] == 'not_sweet_password'
|
assert out[1] == 'not_sweet_password'
|
||||||
assert out[2] == 'abril'
|
assert out[2] == 'abril'
|
||||||
|
|
||||||
def test_run_file_with_default_env_file_and_env_file_and_env_vars(self, runner, capsys):
|
def test_run_file_with_default_env_file_and_env_file_and_env_vars(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet',
|
'run',
|
||||||
|
'--quiet',
|
||||||
get_examples_path('environment/env_files/get_passed_env_file.py'),
|
get_examples_path('environment/env_files/get_passed_env_file.py'),
|
||||||
'--default-env-file', '.env_one', '--env-file', '.env_two',
|
'--default-env-file',
|
||||||
'--env', 'TEST_USER_PASSWORD=SWEETpassWORD', '--env',
|
'.env_one',
|
||||||
|
'--env-file',
|
||||||
|
'.env_two',
|
||||||
|
'--env',
|
||||||
|
'TEST_USER_PASSWORD=SWEETpassWORD',
|
||||||
|
'--env',
|
||||||
'MY_SECRET=444',
|
'MY_SECRET=444',
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == '444'
|
assert out[0] == '444'
|
||||||
assert out[1] == 'SWEETpassWORD'
|
assert out[1] == 'SWEETpassWORD'
|
||||||
@ -233,54 +263,45 @@ class TestEnvFileCombinations(object):
|
|||||||
|
|
||||||
|
|
||||||
@all_runners
|
@all_runners
|
||||||
class TestDefaultEnvVars(object):
|
class TestDefaultEnvVars:
|
||||||
def test_run_file_with_default_env_var(self, runner, capsys):
|
def test_run_file_with_default_env_var(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet',
|
'run', '--quiet',
|
||||||
get_examples_path('environment/env_vars/get_passed_env.py'),
|
get_examples_path('environment/env_vars/get_passed_env.py'), '--default-env', 'USER=clowncity', '--env',
|
||||||
'--default-env', 'USER=clowncity', '--env', 'USER=ted'
|
'USER=ted'
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == 'user'
|
assert out[0] == 'user'
|
||||||
assert out[1] == 'number'
|
assert out[1] == 'number'
|
||||||
assert out[2] == 'string'
|
assert out[2] == 'string'
|
||||||
assert out[3] != 'clowncity'
|
assert out[3] != 'clowncity'
|
||||||
|
|
||||||
def test_run_file_with_default_env_vars(self, runner, capsys):
|
def test_run_file_with_default_env_vars(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet',
|
'run', '--quiet',
|
||||||
get_examples_path('environment/env_vars/get_passed_env.py'),
|
get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123', '--env',
|
||||||
'--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews',
|
'ENV_TEST_USER=cwandrews', '--default-env', "ENV_TEST_STRING='my_test_string'"
|
||||||
'--default-env', "ENV_TEST_STRING='my_test_string'"
|
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == 'cwandrews'
|
assert out[0] == 'cwandrews'
|
||||||
assert out[1] == '123'
|
assert out[1] == '123'
|
||||||
assert out[2] == 'my_test_string'
|
assert out[2] == 'my_test_string'
|
||||||
|
|
||||||
def test_run_module_with_default_env_var(self, runner, capsys):
|
def test_run_module_with_default_env_var(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet', '-m',
|
'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env',
|
||||||
'bonobo.examples.environment.env_vars.get_passed_env',
|
'ENV_TEST_NUMBER=123', '--default-env', 'ENV_TEST_STRING=string'
|
||||||
'--env', 'ENV_TEST_NUMBER=123',
|
|
||||||
'--default-env', 'ENV_TEST_STRING=string'
|
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == 'cwandrews'
|
assert out[0] == 'cwandrews'
|
||||||
assert out[1] == '123'
|
assert out[1] == '123'
|
||||||
assert out[2] != 'string'
|
assert out[2] != 'string'
|
||||||
|
|
||||||
def test_run_module_with_default_env_vars(self, runner, capsys):
|
def test_run_module_with_default_env_vars(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet', '-m',
|
'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env',
|
||||||
'bonobo.examples.environment.env_vars.get_passed_env',
|
'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', '--default-env', "ENV_TEST_STRING='string'"
|
||||||
'--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews',
|
|
||||||
'--default-env', "ENV_TEST_STRING='string'"
|
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == 'cwandrews'
|
assert out[0] == 'cwandrews'
|
||||||
assert out[1] == '123'
|
assert out[1] == '123'
|
||||||
@ -288,52 +309,43 @@ class TestDefaultEnvVars(object):
|
|||||||
|
|
||||||
|
|
||||||
@all_runners
|
@all_runners
|
||||||
class TestEnvVars(object):
|
class TestEnvVars:
|
||||||
def test_run_file_with_env_var(self, runner, capsys):
|
def test_run_file_with_env_var(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet',
|
'run', '--quiet',
|
||||||
get_examples_path('environment/env_vars/get_passed_env.py'),
|
get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123'
|
||||||
'--env', 'ENV_TEST_NUMBER=123'
|
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] != 'test_user'
|
assert out[0] != 'test_user'
|
||||||
assert out[1] == '123'
|
assert out[1] == '123'
|
||||||
assert out[2] == 'my_test_string'
|
assert out[2] == 'my_test_string'
|
||||||
|
|
||||||
def test_run_file_with_env_vars(self, runner, capsys):
|
def test_run_file_with_env_vars(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet',
|
'run', '--quiet',
|
||||||
get_examples_path('environment/env_vars/get_passed_env.py'),
|
get_examples_path('environment/env_vars/get_passed_env.py'), '--env', 'ENV_TEST_NUMBER=123', '--env',
|
||||||
'--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews',
|
'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'"
|
||||||
'--env', "ENV_TEST_STRING='my_test_string'"
|
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == 'cwandrews'
|
assert out[0] == 'cwandrews'
|
||||||
assert out[1] == '123'
|
assert out[1] == '123'
|
||||||
assert out[2] == 'my_test_string'
|
assert out[2] == 'my_test_string'
|
||||||
|
|
||||||
def test_run_module_with_env_var(self, runner, capsys):
|
def test_run_module_with_env_var(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet', '-m',
|
'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env',
|
||||||
'bonobo.examples.environment.env_vars.get_passed_env',
|
'ENV_TEST_NUMBER=123'
|
||||||
'--env', 'ENV_TEST_NUMBER=123'
|
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == 'cwandrews'
|
assert out[0] == 'cwandrews'
|
||||||
assert out[1] == '123'
|
assert out[1] == '123'
|
||||||
assert out[2] == 'my_test_string'
|
assert out[2] == 'my_test_string'
|
||||||
|
|
||||||
def test_run_module_with_env_vars(self, runner, capsys):
|
def test_run_module_with_env_vars(self, runner):
|
||||||
runner(
|
out, err = runner(
|
||||||
'run', '--quiet', '-m',
|
'run', '--quiet', '-m', 'bonobo.examples.environment.env_vars.get_passed_env', '--env',
|
||||||
'bonobo.examples.environment.env_vars.get_passed_env',
|
'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews', '--env', "ENV_TEST_STRING='my_test_string'"
|
||||||
'--env', 'ENV_TEST_NUMBER=123', '--env', 'ENV_TEST_USER=cwandrews',
|
|
||||||
'--env', "ENV_TEST_STRING='my_test_string'"
|
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
|
||||||
out = out.split('\n')
|
out = out.split('\n')
|
||||||
assert out[0] == 'cwandrews'
|
assert out[0] == 'cwandrews'
|
||||||
assert out[1] == '123'
|
assert out[1] == '123'
|
||||||
|
|||||||
30
tests/util/test_collections.py
Normal file
30
tests/util/test_collections.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from bonobo.util import sortedlist, ensure_tuple
|
||||||
|
from bonobo.util.collections import tuplize
|
||||||
|
|
||||||
|
|
||||||
|
def test_sortedlist():
|
||||||
|
l = sortedlist()
|
||||||
|
l.insort(2)
|
||||||
|
l.insort(1)
|
||||||
|
l.insort(3)
|
||||||
|
l.insort(2)
|
||||||
|
assert l == [1, 2, 2, 3]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ensure_tuple():
|
||||||
|
assert ensure_tuple('a') == ('a', )
|
||||||
|
assert ensure_tuple(('a', )) == ('a', )
|
||||||
|
assert ensure_tuple(()) is ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_tuplize():
|
||||||
|
tuplized_lambda = tuplize(lambda: [1, 2, 3])
|
||||||
|
assert tuplized_lambda() == (1, 2, 3)
|
||||||
|
|
||||||
|
@tuplize
|
||||||
|
def some_generator():
|
||||||
|
yield 'c'
|
||||||
|
yield 'b'
|
||||||
|
yield 'a'
|
||||||
|
|
||||||
|
assert some_generator() == ('c', 'b', 'a')
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import types
|
|
||||||
|
|
||||||
from bonobo.util.iterators import force_iterator
|
|
||||||
|
|
||||||
|
|
||||||
def test_force_iterator_with_string():
|
|
||||||
assert force_iterator('foo') == ['foo']
|
|
||||||
|
|
||||||
|
|
||||||
def test_force_iterator_with_none():
|
|
||||||
assert force_iterator(None) == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_force_iterator_with_generator():
|
|
||||||
def generator():
|
|
||||||
yield 'aaa'
|
|
||||||
yield 'bbb'
|
|
||||||
yield 'ccc'
|
|
||||||
|
|
||||||
iterator = force_iterator(generator())
|
|
||||||
assert isinstance(iterator, types.GeneratorType)
|
|
||||||
assert list(iterator) == ['aaa', 'bbb', 'ccc']
|
|
||||||
@ -2,7 +2,7 @@ import operator
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bonobo.util.objects import Wrapper, get_name, ValueHolder
|
from bonobo.util.objects import Wrapper, get_name, ValueHolder, get_attribute_or_create
|
||||||
from bonobo.util.testing import optional_contextmanager
|
from bonobo.util.testing import optional_contextmanager
|
||||||
|
|
||||||
|
|
||||||
@ -59,6 +59,73 @@ def test_valueholder():
|
|||||||
assert repr(x) == repr(y) == repr(43)
|
assert repr(x) == repr(y) == repr(43)
|
||||||
|
|
||||||
|
|
||||||
|
def test_valueholder_notequal():
|
||||||
|
x = ValueHolder(42)
|
||||||
|
assert x != 41
|
||||||
|
assert not (x != 42)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('rlo,rhi', [
|
||||||
|
(1, 2),
|
||||||
|
('a', 'b'),
|
||||||
|
])
|
||||||
|
def test_valueholder_ordering(rlo, rhi):
|
||||||
|
vlo, vhi = ValueHolder(rlo), ValueHolder(rhi)
|
||||||
|
|
||||||
|
for lo in (rlo, vlo):
|
||||||
|
for hi in (rhi, vhi):
|
||||||
|
assert lo < hi
|
||||||
|
assert hi > lo
|
||||||
|
assert lo <= lo
|
||||||
|
assert not (lo < lo)
|
||||||
|
assert lo >= lo
|
||||||
|
|
||||||
|
|
||||||
|
def test_valueholder_negpos():
|
||||||
|
neg, zero, pos = ValueHolder(-1), ValueHolder(0), ValueHolder(1)
|
||||||
|
|
||||||
|
assert -neg == pos
|
||||||
|
assert -pos == neg
|
||||||
|
assert -zero == zero
|
||||||
|
assert +pos == pos
|
||||||
|
assert +neg == neg
|
||||||
|
|
||||||
|
|
||||||
|
def test_valueholders_containers():
|
||||||
|
x = ValueHolder({1, 2, 3, 5, 8, 13})
|
||||||
|
|
||||||
|
assert 5 in x
|
||||||
|
assert 42 not in x
|
||||||
|
|
||||||
|
y = ValueHolder({'foo': 'bar', 'corp': 'acme'})
|
||||||
|
|
||||||
|
assert 'foo' in y
|
||||||
|
assert y['foo'] == 'bar'
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
y['no']
|
||||||
|
y['no'] = 'oh, wait'
|
||||||
|
assert 'no' in y
|
||||||
|
assert 'oh, wait' == y['no']
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_attribute_or_create():
|
||||||
|
class X:
|
||||||
|
pass
|
||||||
|
|
||||||
|
x = X()
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
x.foo
|
||||||
|
|
||||||
|
foo = get_attribute_or_create(x, 'foo', 'bar')
|
||||||
|
assert foo == 'bar'
|
||||||
|
assert x.foo == 'bar'
|
||||||
|
|
||||||
|
foo = get_attribute_or_create(x, 'foo', 'baz')
|
||||||
|
assert foo == 'bar'
|
||||||
|
assert x.foo == 'bar'
|
||||||
|
|
||||||
|
|
||||||
unsupported_operations = {
|
unsupported_operations = {
|
||||||
int: {operator.matmul},
|
int: {operator.matmul},
|
||||||
str: {
|
str: {
|
||||||
|
|||||||
18
tests/util/test_resolvers.py
Normal file
18
tests/util/test_resolvers.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import bonobo
|
||||||
|
from bonobo.util.resolvers import _parse_option, _resolve_options, _resolve_transformations
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_option():
|
||||||
|
assert _parse_option('foo=bar') == ('foo', 'bar')
|
||||||
|
assert _parse_option('foo="bar"') == ('foo', 'bar')
|
||||||
|
assert _parse_option('sep=";"') == ('sep', ';')
|
||||||
|
assert _parse_option('foo') == ('foo', True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_options():
|
||||||
|
assert _resolve_options(('foo=bar', 'bar="baz"')) == {'foo': 'bar', 'bar': 'baz'}
|
||||||
|
assert _resolve_options() == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_transformations():
|
||||||
|
assert _resolve_transformations(('PrettyPrinter', )) == (bonobo.PrettyPrinter, )
|
||||||
Reference in New Issue
Block a user