streamline 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- streamline/__init__.py +1 -0
- streamline/cli.py +326 -0
- streamline/consumers.py +169 -0
- streamline/core.py +45 -0
- streamline/entries.py +71 -0
- streamline/executors.py +513 -0
- streamline/extractor.py +71 -0
- streamline/generators.py +164 -0
- streamline/streamers.py +793 -0
- streamline/utils.py +78 -0
- streamline-1.2.0.dist-info/METADATA +289 -0
- streamline-1.2.0.dist-info/RECORD +14 -0
- streamline-1.2.0.dist-info/WHEEL +4 -0
- streamline-1.2.0.dist-info/entry_points.txt +2 -0
streamline/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
streamline/cli.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import yaml
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from . import utils
|
|
10
|
+
from . import executors
|
|
11
|
+
from . import generators
|
|
12
|
+
from . import consumers
|
|
13
|
+
from . import streamers
|
|
14
|
+
from .core import pipe
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__file__)
|
|
17
|
+
|
|
18
|
+
SHORTHAND_PATTERN = r'^(?P<target_attr>[-\[\]_\w]+=)?(?P<streamer>[-\[\]_.\w]+)\((?P<input_extract>[-\[\]._*\w]+), ?(?P<output_extract>[-._*\w\[\]]+)\)$'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OptionsProcessor():
|
|
22
|
+
def __init__(self, args):
|
|
23
|
+
self.args = args
|
|
24
|
+
|
|
25
|
+
def parse(self, parser):
|
|
26
|
+
# Support '--' separator between commands
|
|
27
|
+
if self.args and self.args[0] == '--':
|
|
28
|
+
self.args = self.args[1:]
|
|
29
|
+
|
|
30
|
+
parsed_args, remaining = parser.parse_known_args(self.args)
|
|
31
|
+
self.args = remaining
|
|
32
|
+
return parsed_args
|
|
33
|
+
|
|
34
|
+
def has_remaining_args(self):
|
|
35
|
+
return len(self.args) > 0
|
|
36
|
+
|
|
37
|
+
def remaining_args(self):
|
|
38
|
+
return self.args.copy()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parser_main():
|
|
42
|
+
cmd_parser = argparse.ArgumentParser(prog='streamline', add_help=False)
|
|
43
|
+
cmd_parser.add_argument(
|
|
44
|
+
'--generator',
|
|
45
|
+
help='Entry Generator Module',
|
|
46
|
+
)
|
|
47
|
+
cmd_parser.add_argument(
|
|
48
|
+
'--consumer',
|
|
49
|
+
help='Entry Consumer/Writer Module',
|
|
50
|
+
)
|
|
51
|
+
cmd_parser.add_argument(
|
|
52
|
+
'-s', '--streamers',
|
|
53
|
+
help='Additional streamers to apply (-s is optional)',
|
|
54
|
+
nargs='*',
|
|
55
|
+
)
|
|
56
|
+
cmd_parser.add_argument(
|
|
57
|
+
'-h', '--help',
|
|
58
|
+
action='store_true',
|
|
59
|
+
help='Print help',
|
|
60
|
+
)
|
|
61
|
+
cmd_parser.add_argument(
|
|
62
|
+
'-p', '--progress',
|
|
63
|
+
choices=['buffer', 'stream-output', 'streaming'],
|
|
64
|
+
help='Print progress to stdout. ("buffer": buffers input and output, "stream-output" buffers only input, "stream" for no buffering at all)',
|
|
65
|
+
)
|
|
66
|
+
cmd_parser.add_argument(
|
|
67
|
+
'-w', '--workers',
|
|
68
|
+
type=int,
|
|
69
|
+
help='Number of concurrent workers for any one async execution module to have',
|
|
70
|
+
)
|
|
71
|
+
cmd_parser.add_argument(
|
|
72
|
+
'-y', '--yaml',
|
|
73
|
+
help='Take options from a yaml or json config file',
|
|
74
|
+
)
|
|
75
|
+
return cmd_parser
|
|
76
|
+
|
|
77
|
+
def _generator_parser(generator):
|
|
78
|
+
if not hasattr(generator, 'args'):
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
generator_parser = argparse.ArgumentParser(
|
|
82
|
+
prog=generator.__name__,
|
|
83
|
+
add_help=False,
|
|
84
|
+
usage='streamline [streamers...] --generator %(prog)s [generator options]',
|
|
85
|
+
)
|
|
86
|
+
generator.args(generator_parser)
|
|
87
|
+
return generator_parser
|
|
88
|
+
|
|
89
|
+
def _consumer_parser(consumer):
|
|
90
|
+
if not hasattr(consumer, 'args'):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
consumer_parser = argparse.ArgumentParser(
|
|
94
|
+
prog=consumer.__name__,
|
|
95
|
+
add_help=False,
|
|
96
|
+
usage='streamline [streamers...] --consumer %(prog)s [consumer options]',
|
|
97
|
+
)
|
|
98
|
+
consumer.args(consumer_parser)
|
|
99
|
+
return consumer_parser
|
|
100
|
+
|
|
101
|
+
def _streamer_parser(Streamer):
|
|
102
|
+
streamer_parser = argparse.ArgumentParser(
|
|
103
|
+
prog=Streamer.__name__,
|
|
104
|
+
add_help=False,
|
|
105
|
+
usage='streamline -s %(prog)s -- [options]',
|
|
106
|
+
)
|
|
107
|
+
if hasattr(Streamer, 'args'):
|
|
108
|
+
Streamer.args(streamer_parser)
|
|
109
|
+
return streamer_parser
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_config(*sources, ignore_nulls=True):
|
|
113
|
+
config = {}
|
|
114
|
+
for source in sources:
|
|
115
|
+
if source is None:
|
|
116
|
+
continue
|
|
117
|
+
if not isinstance(source, dict):
|
|
118
|
+
source = source.__dict__
|
|
119
|
+
if ignore_nulls:
|
|
120
|
+
source = utils.strip_nulls(source)
|
|
121
|
+
config.update(source)
|
|
122
|
+
return config
|
|
123
|
+
|
|
124
|
+
def wrap_streamer(streamer, input_path=None, output_path=None, target=None):
|
|
125
|
+
if not (input_path or output_path or target):
|
|
126
|
+
return [streamer]
|
|
127
|
+
|
|
128
|
+
if target and not (input_path or output_path):
|
|
129
|
+
combiner = streamers.Combiner(path=target, target=-2, source=-1)
|
|
130
|
+
return [streamer, combiner]
|
|
131
|
+
|
|
132
|
+
subpipe = []
|
|
133
|
+
# Compose the sub-command
|
|
134
|
+
subpipe.append(streamers.history_push)
|
|
135
|
+
if input_path != 'value':
|
|
136
|
+
input_extract = streamers.ExtractionStreamer(
|
|
137
|
+
selector=input_path,
|
|
138
|
+
)
|
|
139
|
+
subpipe.append(input_extract.stream)
|
|
140
|
+
subpipe.append(streamer)
|
|
141
|
+
if output_path != 'value':
|
|
142
|
+
output_extract = streamers.ExtractionStreamer(
|
|
143
|
+
selector=output_path
|
|
144
|
+
)
|
|
145
|
+
subpipe.append(output_extract.stream)
|
|
146
|
+
subpipe.append(streamers.history_pop)
|
|
147
|
+
if target:
|
|
148
|
+
combiner = streamers.Combiner(path=target, target=-2, source=-1)
|
|
149
|
+
subpipe.append(combiner.stream)
|
|
150
|
+
return subpipe
|
|
151
|
+
|
|
152
|
+
def streamline_command(args):
|
|
153
|
+
if args and '-s' not in args:
|
|
154
|
+
args.insert(0, '-s'),
|
|
155
|
+
options_processor = OptionsProcessor(args)
|
|
156
|
+
main_parser = _parser_main()
|
|
157
|
+
main_args = options_processor.parse(main_parser)
|
|
158
|
+
|
|
159
|
+
yaml_config = {}
|
|
160
|
+
use_yaml = main_args.yaml is not None
|
|
161
|
+
if main_args.yaml:
|
|
162
|
+
with open(main_args.yaml, 'r') as f:
|
|
163
|
+
yaml_config = yaml.safe_load(f.read())
|
|
164
|
+
|
|
165
|
+
yaml_generator_config = yaml_config.get('generator', {})
|
|
166
|
+
yaml_consumer_config = yaml_config.get('consumer', {})
|
|
167
|
+
yaml_streamers = yaml_config.get('streamers', [])
|
|
168
|
+
command_config = load_config(
|
|
169
|
+
{
|
|
170
|
+
'generator': 'file',
|
|
171
|
+
'consumer': 'file',
|
|
172
|
+
'workers': streamers.AsyncExecutor.DEFAULT_WORKERS,
|
|
173
|
+
'streamers': [],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
'generator': yaml_generator_config.get('name', None),
|
|
177
|
+
'consumer': yaml_consumer_config.get('name', None),
|
|
178
|
+
},
|
|
179
|
+
main_args,
|
|
180
|
+
ignore_nulls=True,
|
|
181
|
+
)
|
|
182
|
+
ae_args = {'workers': command_config.get('workers')}
|
|
183
|
+
|
|
184
|
+
# Load Generator & Consumer
|
|
185
|
+
Generator = generators.load_generator(command_config['generator'])
|
|
186
|
+
if 'options' in yaml_generator_config:
|
|
187
|
+
generator_options = yaml_generator_config.get('options')
|
|
188
|
+
else:
|
|
189
|
+
generator_options = options_processor.parse(_generator_parser(Generator)).__dict__
|
|
190
|
+
generator = Generator(**generator_options)
|
|
191
|
+
|
|
192
|
+
Consumer = consumers.load_consumer(command_config['consumer'])
|
|
193
|
+
if 'options' in yaml_consumer_config:
|
|
194
|
+
consumer_options = yaml_consumer_config.get('options')
|
|
195
|
+
else:
|
|
196
|
+
consumer_options = options_processor.parse(_consumer_parser(Consumer)).__dict__
|
|
197
|
+
consumer = Consumer(**consumer_options)
|
|
198
|
+
|
|
199
|
+
if main_args.help:
|
|
200
|
+
print('=' * 15 + ' Streamline ' + '=' * 15)
|
|
201
|
+
print('\n')
|
|
202
|
+
main_parser.print_help()
|
|
203
|
+
streamer_list = command_config.get('streamers', [])
|
|
204
|
+
if streamer_list:
|
|
205
|
+
for streamer_name in streamer_list:
|
|
206
|
+
print('\n\n')
|
|
207
|
+
print('=' * 15 + ' Streamer::{} '.format(streamer_name) + '=' * 15)
|
|
208
|
+
print('\n')
|
|
209
|
+
load_streamer(streamer_name, print_help=True, ae_args=ae_args)
|
|
210
|
+
else:
|
|
211
|
+
print('\n')
|
|
212
|
+
print('=' * 15 + ' Streamers ' + '=' * 15)
|
|
213
|
+
for streamer_name, streamer in streamers.STREAMERS.items():
|
|
214
|
+
description = getattr(streamer, '_arg_description', 'An undocumented module')
|
|
215
|
+
example = getattr(streamer, '_arg_example', '')
|
|
216
|
+
invocation = 'streamline -s {} --'.format(streamer_name)
|
|
217
|
+
print('\n::{}::\n\tDescription: {}\n\tExample: {} {}'.format(
|
|
218
|
+
streamer_name,
|
|
219
|
+
description,
|
|
220
|
+
invocation,
|
|
221
|
+
example,
|
|
222
|
+
))
|
|
223
|
+
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Parse streamers
|
|
227
|
+
command_streamers = []
|
|
228
|
+
if use_yaml:
|
|
229
|
+
for streamer_conf in yaml_streamers:
|
|
230
|
+
streamer = load_streamer(
|
|
231
|
+
streamer_conf['name'],
|
|
232
|
+
options=streamer_conf.get('options', {}),
|
|
233
|
+
ae_args=ae_args,
|
|
234
|
+
)
|
|
235
|
+
sub_pipeline = wrap_streamer(
|
|
236
|
+
streamer,
|
|
237
|
+
input_path=streamer_conf.get('input'),
|
|
238
|
+
output_path=streamer_conf.get('output'),
|
|
239
|
+
target=streamer_conf.get('target'),
|
|
240
|
+
)
|
|
241
|
+
command_streamers.extend(sub_pipeline)
|
|
242
|
+
else:
|
|
243
|
+
for streamer_name in command_config.get('streamers',[]):
|
|
244
|
+
# Support shorthand syntax "attr=streamer(input.path, output.path)"
|
|
245
|
+
shorthand_match = re.match(SHORTHAND_PATTERN, streamer_name)
|
|
246
|
+
if shorthand_match:
|
|
247
|
+
shorthand_options = shorthand_match.groupdict()
|
|
248
|
+
|
|
249
|
+
# Load the main streamer
|
|
250
|
+
streamer_name = shorthand_options.get('streamer')
|
|
251
|
+
main_streamer = load_streamer(shorthand_options['streamer'], options_processor, ae_args=ae_args)
|
|
252
|
+
|
|
253
|
+
target_attr = shorthand_options.get('target_attr', '')
|
|
254
|
+
if target_attr:
|
|
255
|
+
target_attr = target_attr[:-1]
|
|
256
|
+
|
|
257
|
+
command_streamers.extend(wrap_streamer(
|
|
258
|
+
main_streamer,
|
|
259
|
+
input_path=shorthand_options.get('input_extract', None),
|
|
260
|
+
output_path=shorthand_options.get('output_extract', None),
|
|
261
|
+
target=target_attr,
|
|
262
|
+
))
|
|
263
|
+
else:
|
|
264
|
+
streamer = load_streamer(streamer_name, options_processor, ae_args=ae_args)
|
|
265
|
+
command_streamers.append(streamer)
|
|
266
|
+
|
|
267
|
+
# Ensure we don't have any extra arguments
|
|
268
|
+
if options_processor.has_remaining_args():
|
|
269
|
+
sys.stderr.write('Extra arguments found: {}\n'.format(' '.join(options_processor.remaining_args())))
|
|
270
|
+
sys.exit(2)
|
|
271
|
+
|
|
272
|
+
# Override streamers
|
|
273
|
+
progress_option = command_config.get('progress', None)
|
|
274
|
+
if command_config.get('progress', None):
|
|
275
|
+
buffer_start = progress_option in ('buffer', 'stream-output')
|
|
276
|
+
buffer_end = progress_option in ('buffer')
|
|
277
|
+
progress = streamers.ProgressStreamer(buffer_start=buffer_start, buffer_end=buffer_end)
|
|
278
|
+
command_streamers = [progress.streamer_start, *command_streamers, progress.streamer_end]
|
|
279
|
+
|
|
280
|
+
future = pipe(generator.stream(), command_streamers, consumer=consumer.stream)
|
|
281
|
+
asyncio.run(future)
|
|
282
|
+
|
|
283
|
+
def load_streamer(path, options_processor=None, options=None, print_help=False, ae_args=None):
|
|
284
|
+
kwargs = {}
|
|
285
|
+
if options:
|
|
286
|
+
kwargs.update(options)
|
|
287
|
+
if path is None:
|
|
288
|
+
return None
|
|
289
|
+
elif '.' in path:
|
|
290
|
+
Streamer = utils.import_obj(path)
|
|
291
|
+
else:
|
|
292
|
+
Streamer = streamers.STREAMERS.get(path)
|
|
293
|
+
if Streamer is None:
|
|
294
|
+
raise ValueError('Invalid streamer: {}'.format(path))
|
|
295
|
+
|
|
296
|
+
if print_help:
|
|
297
|
+
_streamer_parser(Streamer).print_help()
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
if options_processor:
|
|
301
|
+
streamer_parser = _streamer_parser(Streamer)
|
|
302
|
+
streamer_args = options_processor.parse(streamer_parser)
|
|
303
|
+
kwargs.update(load_config(streamer_args.__dict__))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
if hasattr(Streamer, 'async_handler'):
|
|
307
|
+
# This is really a handler that needs wrapped with AsyncExecutor
|
|
308
|
+
Executor = Streamer
|
|
309
|
+
if Executor and hasattr(Executor, 'handle'):
|
|
310
|
+
executor = Executor(**kwargs).handle
|
|
311
|
+
else:
|
|
312
|
+
executor = Executor
|
|
313
|
+
|
|
314
|
+
# Now build the wrapper
|
|
315
|
+
ae = streamers.AsyncExecutor(executor, **ae_args)
|
|
316
|
+
return ae.stream
|
|
317
|
+
else:
|
|
318
|
+
if type(Streamer) == type:
|
|
319
|
+
return Streamer(**kwargs).stream
|
|
320
|
+
return Streamer
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def main():
|
|
324
|
+
"""Target entry point for standard pyproject.toml scripts."""
|
|
325
|
+
args = sys.argv[1:]
|
|
326
|
+
streamline_command(args)
|
streamline/consumers.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import csv
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from . import utils
|
|
6
|
+
|
|
7
|
+
def stringify_all(source):
|
|
8
|
+
return [utils.force_string(v) for v in source]
|
|
9
|
+
|
|
10
|
+
class FileWriter():
|
|
11
|
+
DEFAULT_OUTPUT = '-'
|
|
12
|
+
DELIMITER = '\n'
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def args(cls, parser):
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
'--output',
|
|
18
|
+
default=cls.DEFAULT_OUTPUT,
|
|
19
|
+
help='Set target of output (Default stdout)',
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def __init__(self, output=None):
|
|
23
|
+
self.target_name = output
|
|
24
|
+
self.target = None
|
|
25
|
+
self.target_template = None
|
|
26
|
+
self.first_written = False
|
|
27
|
+
|
|
28
|
+
self.force_closing_newline = os.environ.get('STREAMLINE_CLOSING_NEWLINE')
|
|
29
|
+
|
|
30
|
+
def _output_value(self, entry):
|
|
31
|
+
return utils.force_string(entry.value)
|
|
32
|
+
|
|
33
|
+
async def stream(self, source):
|
|
34
|
+
output = ''
|
|
35
|
+
if '{' in self.target_name:
|
|
36
|
+
self.target_template = self.target_name
|
|
37
|
+
else:
|
|
38
|
+
self.target = utils.get_file_io(self.target_name, write=True)
|
|
39
|
+
async for entry in source:
|
|
40
|
+
output = self._output_value(entry)
|
|
41
|
+
if self.target_template:
|
|
42
|
+
|
|
43
|
+
file_name = self.target_template.format(input=entry.original_value, index=entry.index)
|
|
44
|
+
with open(file_name, 'w') as target_file:
|
|
45
|
+
target_file.write(output)
|
|
46
|
+
else:
|
|
47
|
+
if self.force_closing_newline:
|
|
48
|
+
output = output + self.DELIMITER
|
|
49
|
+
elif self.first_written:
|
|
50
|
+
output = self.DELIMITER + output
|
|
51
|
+
else:
|
|
52
|
+
self.first_written = True
|
|
53
|
+
self.target.write(output)
|
|
54
|
+
if hasattr(self.target, 'flush'):
|
|
55
|
+
self.target.flush()
|
|
56
|
+
|
|
57
|
+
if self.target and hasattr(self.target, 'close'):
|
|
58
|
+
self.target.close()
|
|
59
|
+
|
|
60
|
+
class CSVWriter():
|
|
61
|
+
DEFAULT_OUTPUT = '-'
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def args(cls, parser):
|
|
65
|
+
parser.add_argument('--output', help='Set target of output (Default stdout)', default=cls.DEFAULT_OUTPUT)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
'--input-column',
|
|
68
|
+
action='store_true',
|
|
69
|
+
default=False,
|
|
70
|
+
help='Automatically add a column for input value',
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def __init__(self, output=DEFAULT_OUTPUT, input_column=False):
|
|
74
|
+
self.target_name = output
|
|
75
|
+
self.input_column = input_column
|
|
76
|
+
|
|
77
|
+
def _parse_fields(self, entry):
|
|
78
|
+
if not isinstance(entry.value, dict):
|
|
79
|
+
return None
|
|
80
|
+
else:
|
|
81
|
+
return entry.value.keys()
|
|
82
|
+
|
|
83
|
+
def _get_values(self, entry, fields):
|
|
84
|
+
if fields is None:
|
|
85
|
+
return stringify_all((entry.original_value, entry.value))
|
|
86
|
+
|
|
87
|
+
# Return blanks for all invalid rows
|
|
88
|
+
if not isinstance(entry.value, dict):
|
|
89
|
+
return [''] * len(fields)
|
|
90
|
+
|
|
91
|
+
values = [entry.value.get(field, '') for field in fields]
|
|
92
|
+
if self.input_column:
|
|
93
|
+
values.insert(0, entry.original_value)
|
|
94
|
+
return stringify_all(values)
|
|
95
|
+
|
|
96
|
+
async def stream(self, source):
|
|
97
|
+
self.target = utils.get_file_io(self.target_name, write=True)
|
|
98
|
+
writer = csv.writer(self.target, 'unix', quoting=csv.QUOTE_MINIMAL)
|
|
99
|
+
|
|
100
|
+
fields = None
|
|
101
|
+
header_written = False
|
|
102
|
+
async for entry in source:
|
|
103
|
+
# Write csv column names
|
|
104
|
+
if not header_written:
|
|
105
|
+
fields = self._parse_fields(entry)
|
|
106
|
+
if fields is None:
|
|
107
|
+
writer.writerow(['input', 'value'])
|
|
108
|
+
else:
|
|
109
|
+
header = list(fields)
|
|
110
|
+
if self.input_column:
|
|
111
|
+
header.insert(0, 'input')
|
|
112
|
+
writer.writerow(header)
|
|
113
|
+
header_written = True
|
|
114
|
+
|
|
115
|
+
# Write values
|
|
116
|
+
entry_values = self._get_values(entry, fields)
|
|
117
|
+
writer.writerow(entry_values)
|
|
118
|
+
|
|
119
|
+
if hasattr(self.target, 'close'):
|
|
120
|
+
self.target.close()
|
|
121
|
+
|
|
122
|
+
class JsonWriter():
|
|
123
|
+
DEFAULT_OUTPUT = '-'
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def args(cls, parser):
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
'--output',
|
|
129
|
+
default=cls.DEFAULT_OUTPUT,
|
|
130
|
+
help='Set target of output (Default stdout)',
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def __init__(self, output=DEFAULT_OUTPUT):
|
|
134
|
+
self.target_name = output
|
|
135
|
+
|
|
136
|
+
async def stream(self, source):
|
|
137
|
+
self.target = utils.get_file_io(self.target_name, write=True)
|
|
138
|
+
self.target.write('[\n')
|
|
139
|
+
first = True
|
|
140
|
+
async for entry in source:
|
|
141
|
+
if not first:
|
|
142
|
+
self.target.write(',\n ')
|
|
143
|
+
else:
|
|
144
|
+
self.target.write(' ')
|
|
145
|
+
self.target.write(json.dumps(entry.value))
|
|
146
|
+
first = False
|
|
147
|
+
self.target.write('\n]')
|
|
148
|
+
|
|
149
|
+
if hasattr(self.target, 'close'):
|
|
150
|
+
self.target.close()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
CONSUMERS = {
|
|
154
|
+
'file': FileWriter,
|
|
155
|
+
'csv': CSVWriter,
|
|
156
|
+
'json': JsonWriter,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
def load_consumer(path):
|
|
160
|
+
if path is None:
|
|
161
|
+
return None
|
|
162
|
+
elif '.' in path:
|
|
163
|
+
Consumer = utils.import_obj(path)
|
|
164
|
+
else:
|
|
165
|
+
Consumer = CONSUMERS.get(path)
|
|
166
|
+
if Consumer is None:
|
|
167
|
+
raise ValueError('Invalid consumer: {}'.format(path))
|
|
168
|
+
return Consumer
|
|
169
|
+
|
streamline/core.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
async def drain(generator):
|
|
4
|
+
""" A no-op drain of a generator """
|
|
5
|
+
items = []
|
|
6
|
+
async for item in generator:
|
|
7
|
+
items.append(item)
|
|
8
|
+
return items
|
|
9
|
+
|
|
10
|
+
async def transync(synchronous_iter):
|
|
11
|
+
"""
|
|
12
|
+
This is a converter which takes a synchronous iterator and yields the items
|
|
13
|
+
asynchronously which allows a standard pipe to convert to an async pipe.
|
|
14
|
+
"""
|
|
15
|
+
for item in synchronous_iter:
|
|
16
|
+
yield item
|
|
17
|
+
|
|
18
|
+
async def static_pipe(stream, inputs):
|
|
19
|
+
source = transync(inputs)
|
|
20
|
+
outputs = await drain(stream(source))
|
|
21
|
+
return outputs
|
|
22
|
+
|
|
23
|
+
async def pipe(generator, streamers, consumer=None):
|
|
24
|
+
"""
|
|
25
|
+
The "pipe" function is the central piece of a stream, the pipe connects
|
|
26
|
+
the generators together in a chain allowing each to pass the result of the
|
|
27
|
+
previous to the next.
|
|
28
|
+
|
|
29
|
+
The first generator is special as it does not take an input stream.
|
|
30
|
+
|
|
31
|
+
The last piece of the stream is the consumer which is not a generator but
|
|
32
|
+
is expected to drain the generator given to it, writing any valued output.
|
|
33
|
+
"""
|
|
34
|
+
if consumer is None:
|
|
35
|
+
consumer = drain
|
|
36
|
+
|
|
37
|
+
pipe = generator
|
|
38
|
+
for streamer in streamers:
|
|
39
|
+
pipe = streamer(pipe)
|
|
40
|
+
|
|
41
|
+
await consumer(pipe)
|
|
42
|
+
|
|
43
|
+
def sync_exec(future):
|
|
44
|
+
result = asyncio.run(future)
|
|
45
|
+
return result
|
streamline/entries.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
class EntryFactory():
|
|
4
|
+
def __init__(self, error_value=None):
|
|
5
|
+
self.error_value = error_value
|
|
6
|
+
self.index = 0
|
|
7
|
+
|
|
8
|
+
def __call__(self, value):
|
|
9
|
+
entry = Entry(
|
|
10
|
+
value,
|
|
11
|
+
error_value=self.error_value,
|
|
12
|
+
index=self.index
|
|
13
|
+
)
|
|
14
|
+
self.index += 1
|
|
15
|
+
return entry
|
|
16
|
+
|
|
17
|
+
class Entry():
|
|
18
|
+
def __init__(self, value=None, index=None, error_value=None):
|
|
19
|
+
self.index = index
|
|
20
|
+
self.history = [[value]]
|
|
21
|
+
self.errors = []
|
|
22
|
+
self.error_value = error_value
|
|
23
|
+
|
|
24
|
+
def push(self, value=None):
|
|
25
|
+
if value is None:
|
|
26
|
+
value = self.value
|
|
27
|
+
self.history.append([value])
|
|
28
|
+
|
|
29
|
+
def pop(self):
|
|
30
|
+
if len(self.history) == 1:
|
|
31
|
+
raise ValueError('Attempted to pop an entry history that is only 1 level deep!')
|
|
32
|
+
value = self.value
|
|
33
|
+
self.history = self.history[:-1]
|
|
34
|
+
self.value = value
|
|
35
|
+
|
|
36
|
+
def reset(self):
|
|
37
|
+
self.history = [[self.history[0][0]]]
|
|
38
|
+
|
|
39
|
+
def collapse(self):
|
|
40
|
+
self.history[-1] = [self.history[-1][-1]]
|
|
41
|
+
|
|
42
|
+
def get_history(self):
|
|
43
|
+
return self.history[-1]
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def original_value(self):
|
|
47
|
+
return self.history[-1][0]
|
|
48
|
+
|
|
49
|
+
def get_value(self):
|
|
50
|
+
return self.history[-1][-1]
|
|
51
|
+
|
|
52
|
+
def set_value(self, new_value):
|
|
53
|
+
self.history[-1].append(new_value)
|
|
54
|
+
|
|
55
|
+
def error(self, e):
|
|
56
|
+
self.errors.append(e)
|
|
57
|
+
self.value = self.error_value
|
|
58
|
+
|
|
59
|
+
def clone(self):
|
|
60
|
+
new_clone = Entry(index=self.index, error_value=self.error_value)
|
|
61
|
+
new_clone.errors = self.errors.copy()
|
|
62
|
+
new_clone.history = [h.copy() for h in self.history]
|
|
63
|
+
return new_clone
|
|
64
|
+
|
|
65
|
+
value = property(get_value, set_value)
|
|
66
|
+
|
|
67
|
+
def entry_wrap(items):
|
|
68
|
+
return [Entry(item) for item in items]
|
|
69
|
+
|
|
70
|
+
def entry_unwrap(entries):
|
|
71
|
+
return [entry.value for entry in entries]
|