oryxflow 26.6.6__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.
- oryxflow/__init__.py +1015 -0
- oryxflow/cache.py +1 -0
- oryxflow/core.py +633 -0
- oryxflow/functional.py +327 -0
- oryxflow/log.py +97 -0
- oryxflow/parameter.py +253 -0
- oryxflow/settings.py +26 -0
- oryxflow/targets/__init__.py +449 -0
- oryxflow/targets/dt.py +15 -0
- oryxflow/targets/h5.py +13 -0
- oryxflow/targets/tf.py +12 -0
- oryxflow/targets/torch.py +37 -0
- oryxflow/tasks/__init__.py +560 -0
- oryxflow/tasks/dt.py +10 -0
- oryxflow/tasks/h5.py +9 -0
- oryxflow/tasks/tf.py +9 -0
- oryxflow/tasks/torch.py +10 -0
- oryxflow/utils.py +194 -0
- oryxflow-26.6.6.dist-info/METADATA +256 -0
- oryxflow-26.6.6.dist-info/RECORD +23 -0
- oryxflow-26.6.6.dist-info/WHEEL +5 -0
- oryxflow-26.6.6.dist-info/licenses/LICENSE +21 -0
- oryxflow-26.6.6.dist-info/top_level.txt +1 -0
oryxflow/__init__.py
ADDED
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from oryxflow import core
|
|
3
|
+
from oryxflow.core import flatten, RunResult, MultiRunResult, TaskFailure
|
|
4
|
+
from oryxflow.log import logger, enable_logging, disable_logging
|
|
5
|
+
from oryxflow.parameter import (
|
|
6
|
+
Parameter,
|
|
7
|
+
IntParameter, FloatParameter, BoolParameter,
|
|
8
|
+
DateParameter, DictParameter, ListParameter, ChoiceParameter, EnumParameter,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import oryxflow.targets, oryxflow.tasks, oryxflow.settings
|
|
14
|
+
import oryxflow.utils
|
|
15
|
+
from oryxflow.cache import data as data
|
|
16
|
+
import oryxflow.cache
|
|
17
|
+
|
|
18
|
+
def set_dir(dir=None):
|
|
19
|
+
"""
|
|
20
|
+
Initialize oryxflow
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
dir (str): data output directory
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
if dir is None:
|
|
27
|
+
dirpath = oryxflow.settings.dirpath
|
|
28
|
+
dirpath.mkdir(exist_ok=True)
|
|
29
|
+
else:
|
|
30
|
+
dirpath = Path(dir)
|
|
31
|
+
oryxflow.settings.dir = dir
|
|
32
|
+
oryxflow.settings.dirpath = dirpath
|
|
33
|
+
|
|
34
|
+
oryxflow.settings.isinit = True
|
|
35
|
+
return dirpath
|
|
36
|
+
|
|
37
|
+
def enable_cloud_storage(protocol, bucket, prefix=None):
|
|
38
|
+
"""
|
|
39
|
+
Initialize cloud storage
|
|
40
|
+
Uses https://github.com/orgs/fsspec/repositories
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
protocol (str): fsspec eg gcs, s3, dropbox etc. See https://pypi.org/project/universal-pathlib/
|
|
44
|
+
bucket (str): bucket name
|
|
45
|
+
prefix (str): prefix similar to folder
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
from pathlib import PurePosixPath
|
|
49
|
+
fs_path = PurePosixPath(bucket)
|
|
50
|
+
if prefix is not None:
|
|
51
|
+
fs_path = fs_path / prefix
|
|
52
|
+
oryxflow.settings.cloud_fs_prefix = f'{protocol}://{fs_path}'
|
|
53
|
+
oryxflow.settings.cloud_fs_enabled = True
|
|
54
|
+
|
|
55
|
+
return oryxflow.settings.cloud_fs_prefix
|
|
56
|
+
|
|
57
|
+
def enable_gcs(bucket, prefix=None):
|
|
58
|
+
"""
|
|
59
|
+
Initialize google cloud storage, for reference see https://cloud.google.com/storage/docs/listing-objects
|
|
60
|
+
Uses https://gcsfs.readthedocs.io/en/latest/
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
bucket (str): bucket name
|
|
64
|
+
prefix (str): prefix similar to folder
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
return enable_cloud_storage('gcs', bucket, prefix)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def preview(tasks, indent='', last=True, show_params=True, clip_params=False, print_it=True):
|
|
73
|
+
"""
|
|
74
|
+
Preview task flows
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
tasks (obj, list): task or list of tasks
|
|
78
|
+
"""
|
|
79
|
+
msg = '\n ===== oryxflow Execution Preview ===== \n'
|
|
80
|
+
if not isinstance(tasks, (list,)):
|
|
81
|
+
tasks = [tasks]
|
|
82
|
+
for t in tasks:
|
|
83
|
+
msg += oryxflow.utils.print_tree(t, indent=indent, last=last, show_params=show_params, clip_params=clip_params)
|
|
84
|
+
msg += '\n ===== oryxflow Execution Preview ===== \n'
|
|
85
|
+
if print_it:
|
|
86
|
+
print(msg)
|
|
87
|
+
else:
|
|
88
|
+
return msg
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def run(tasks, forced=None, forced_all=False, forced_all_upstream=False, confirm=False, workers=1, abort=True,
|
|
94
|
+
execution_summary=None, main_thread_only=False, **kwargs):
|
|
95
|
+
"""
|
|
96
|
+
Run tasks locally. Runs the DAG sequentially in dependency order.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
tasks (obj, list): task or list of tasks
|
|
100
|
+
forced (list): list of forced tasks
|
|
101
|
+
forced_all (bool): force all tasks
|
|
102
|
+
forced_all_upstream (bool): force all tasks including upstream
|
|
103
|
+
confirm (list): confirm invalidating tasks
|
|
104
|
+
workers (int): number of workers
|
|
105
|
+
abort (bool): on errors raise exception
|
|
106
|
+
execution_summary (bool): log execution summary
|
|
107
|
+
main_thread_only (bool): if True, only works in main thread of the main interpreter. Default false so it can run in apps and workers.
|
|
108
|
+
kwargs: keywords to pass to core.build
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
if not isinstance(tasks, (list,)):
|
|
113
|
+
tasks = [tasks]
|
|
114
|
+
|
|
115
|
+
# if forced_all_upstream is true we are going to force run tasks anyway
|
|
116
|
+
# in the second if condition.
|
|
117
|
+
# So in this case we are going to skip running forced tasks.
|
|
118
|
+
if forced_all and not forced_all_upstream:
|
|
119
|
+
forced = tasks
|
|
120
|
+
if forced_all_upstream:
|
|
121
|
+
for t in tasks:
|
|
122
|
+
invalidate_upstream(t, confirm=confirm)
|
|
123
|
+
if forced is not None:
|
|
124
|
+
if not isinstance(forced, (list,)):
|
|
125
|
+
forced = [forced]
|
|
126
|
+
invalidate = []
|
|
127
|
+
for tf in forced:
|
|
128
|
+
for tup in tasks:
|
|
129
|
+
invalidate.append(oryxflow.taskflow_downstream(tf, tup))
|
|
130
|
+
|
|
131
|
+
invalidate = set().union(*invalidate)
|
|
132
|
+
invalidate = {t for t in invalidate if t.complete()}
|
|
133
|
+
if len(invalidate) > 0:
|
|
134
|
+
if confirm:
|
|
135
|
+
print('Forced tasks', invalidate)
|
|
136
|
+
c = input('Confirm invalidating forced tasks (y/n)')
|
|
137
|
+
else:
|
|
138
|
+
c = 'y'
|
|
139
|
+
if c == 'y':
|
|
140
|
+
for t in invalidate:
|
|
141
|
+
logger.info("invalidating forced task: {}", t.task_id)
|
|
142
|
+
t.invalidate(confirm=False)
|
|
143
|
+
else:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
execution_summary = execution_summary if execution_summary is not None else oryxflow.settings.execution_summary
|
|
147
|
+
opts = {**{'workers': workers, 'local_scheduler': True}, **kwargs}
|
|
148
|
+
opts['detailed_summary'] = execution_summary # gates the summary LOG, not a print
|
|
149
|
+
result = core.build(tasks, **opts)
|
|
150
|
+
success = result.scheduling_succeeded
|
|
151
|
+
if abort and not success:
|
|
152
|
+
raise RuntimeError(
|
|
153
|
+
'Exception found running flow, check trace. For more details see https://oryxflow.readthedocs.io/en/latest/run.html#debugging-failures') from result.first_exception
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def taskflow_upstream(task, only_complete=False):
|
|
158
|
+
"""
|
|
159
|
+
Get all upstream inputs for a task
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
task (obj): task
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
tasks = oryxflow.utils.traverse(task)
|
|
167
|
+
if only_complete:
|
|
168
|
+
tasks = [t for t in tasks if t.complete()]
|
|
169
|
+
return tasks
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def taskflow_downstream(task, task_downstream, only_complete=False):
|
|
173
|
+
"""
|
|
174
|
+
Get all downstream outputs for a task
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
task (obj): task
|
|
178
|
+
task_downstream (obj): downstream target task
|
|
179
|
+
|
|
180
|
+
"""
|
|
181
|
+
tasks = core.find_deps(task_downstream, task.task_family)
|
|
182
|
+
if only_complete:
|
|
183
|
+
tasks = {t for t in tasks if t.complete()}
|
|
184
|
+
return tasks
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def invalidate_all(confirm=False):
|
|
188
|
+
"""
|
|
189
|
+
Invalidate all tasks by deleting all files in data directory
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
confirm (bool): confirm operation
|
|
193
|
+
|
|
194
|
+
"""
|
|
195
|
+
# record all tasks that run and their output vs files present
|
|
196
|
+
raise NotImplementedError()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def invalidate_orphans(confirm=False):
|
|
200
|
+
"""
|
|
201
|
+
Invalidate all unused task outputs
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
confirm (bool): confirm operation
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
# record all tasks that run and their output vs files present
|
|
208
|
+
raise NotImplementedError()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def show(task):
|
|
212
|
+
"""
|
|
213
|
+
Show task execution status
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
tasks (obj, list): task or list of tasks
|
|
217
|
+
"""
|
|
218
|
+
preview(task)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _as_families(x):
|
|
222
|
+
"""Normalize a single task/family (class or instance) or an iterable of them to a tuple,
|
|
223
|
+
for use with ``isinstance`` / family matching. Shared by invalidate_upstream (``only=``)
|
|
224
|
+
and invalidate_downstream (family list)."""
|
|
225
|
+
return tuple(x) if isinstance(x, (list, tuple, set)) else (x,)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def invalidate_upstream(task, confirm=False, only=None):
|
|
229
|
+
"""
|
|
230
|
+
Invalidate all tasks upstream tasks in a flow.
|
|
231
|
+
|
|
232
|
+
For example, you have 3 dependant tasks. Normally you run Task3 but you've changed parameters for Task1. By invalidating Task3 it will check the full DAG and realize Task1 needs to be invalidated and therefore Task2 and Task3 also.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
task (obj): task to invalidate. This should be an upstream task for which you want to check upstream dependencies for invalidation conditions
|
|
236
|
+
confirm (bool): confirm operation
|
|
237
|
+
only (class, list): if set, only invalidate upstream tasks of these task family/families
|
|
238
|
+
|
|
239
|
+
"""
|
|
240
|
+
tasks = taskflow_upstream(task, only_complete=False)
|
|
241
|
+
if only is not None:
|
|
242
|
+
tasks = [t for t in tasks if isinstance(t, _as_families(only))]
|
|
243
|
+
if len(tasks) == 0:
|
|
244
|
+
print('no tasks to invalidate')
|
|
245
|
+
return True
|
|
246
|
+
if confirm:
|
|
247
|
+
print('Completed tasks to invalidate:')
|
|
248
|
+
for t in tasks:
|
|
249
|
+
print(t)
|
|
250
|
+
c = input('Confirm invalidating tasks (y/n)')
|
|
251
|
+
else:
|
|
252
|
+
c = 'y'
|
|
253
|
+
if c == 'y':
|
|
254
|
+
for t in tasks:
|
|
255
|
+
logger.info("invalidating upstream task: {}", t.task_id)
|
|
256
|
+
t.invalidate(confirm=False)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def invalidate_downstream(task, task_downstream, confirm=False):
|
|
260
|
+
"""
|
|
261
|
+
Invalidate all downstream tasks in a flow.
|
|
262
|
+
|
|
263
|
+
For example, you have 3 dependant tasks. Normally you run Task3 but you've changed parameters for Task1. By invalidating Task3 it will check the full DAG and realize Task1 needs to be invalidated and therefore Task2 and Task3 also.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
task (obj, class, list): task/family — or list of families — to invalidate downstream of.
|
|
267
|
+
Only the family is used (a class is fine), so a list resets several families and
|
|
268
|
+
everything downstream of each, in one call.
|
|
269
|
+
task_downstream (obj): downstream task target
|
|
270
|
+
confirm (bool): confirm operation
|
|
271
|
+
|
|
272
|
+
"""
|
|
273
|
+
tasks = set()
|
|
274
|
+
for fam in _as_families(task):
|
|
275
|
+
tasks |= taskflow_downstream(fam, task_downstream, only_complete=True)
|
|
276
|
+
tasks = list(tasks)
|
|
277
|
+
if len(tasks) == 0:
|
|
278
|
+
print('no tasks to invalidate')
|
|
279
|
+
return True
|
|
280
|
+
if confirm:
|
|
281
|
+
print('Completed tasks to invalidate:')
|
|
282
|
+
for t in tasks:
|
|
283
|
+
print(t)
|
|
284
|
+
c = input('Confirm invalidating tasks (y/n)')
|
|
285
|
+
else:
|
|
286
|
+
c = 'y'
|
|
287
|
+
if c == 'y':
|
|
288
|
+
for t in tasks:
|
|
289
|
+
logger.info("invalidating downstream task: {}", t.task_id)
|
|
290
|
+
t.invalidate(confirm=False)
|
|
291
|
+
return True
|
|
292
|
+
else:
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def clone_parent(cls):
|
|
297
|
+
warnings.warn("This is replaced with `@oryxflow.requires()`", DeprecationWarning, stacklevel=2)
|
|
298
|
+
|
|
299
|
+
def requires(self):
|
|
300
|
+
return self.clone_parent()
|
|
301
|
+
|
|
302
|
+
setattr(cls, 'requires', requires)
|
|
303
|
+
return cls
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# Like core.inherits but for handling dictionaries
|
|
307
|
+
class dict_inherits:
|
|
308
|
+
def __init__(self, *tasks_to_inherit):
|
|
309
|
+
super(dict_inherits, self).__init__()
|
|
310
|
+
if not tasks_to_inherit:
|
|
311
|
+
raise TypeError("tasks_to_inherit cannot be empty")
|
|
312
|
+
# We know the first arg is a dict.
|
|
313
|
+
self.tasks_to_inherit = tasks_to_inherit[0]
|
|
314
|
+
|
|
315
|
+
def __call__(self, task_that_inherits):
|
|
316
|
+
for task_to_inherit in self.tasks_to_inherit:
|
|
317
|
+
for param_name, param_obj in self.tasks_to_inherit[task_to_inherit].get_params():
|
|
318
|
+
# Check if the parameter exists in the inheriting task
|
|
319
|
+
if not hasattr(task_that_inherits, param_name):
|
|
320
|
+
# If not, add it to the inheriting task
|
|
321
|
+
setattr(task_that_inherits, param_name, param_obj)
|
|
322
|
+
|
|
323
|
+
# adding dictionary functionality
|
|
324
|
+
def clone_parents_dict(_self, **kwargs):
|
|
325
|
+
return {
|
|
326
|
+
task_to_inherit: _self.clone(cls=self.tasks_to_inherit[task_to_inherit], **kwargs)
|
|
327
|
+
for task_to_inherit in self.tasks_to_inherit
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
task_that_inherits.clone_parents_dict = clone_parents_dict
|
|
331
|
+
return task_that_inherits
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# Like core.requires but for handling dictionaries
|
|
335
|
+
class dict_requires:
|
|
336
|
+
def __init__(self, *tasks_to_require):
|
|
337
|
+
super(dict_requires, self).__init__()
|
|
338
|
+
if not tasks_to_require:
|
|
339
|
+
raise TypeError("tasks_to_require cannot be empty")
|
|
340
|
+
|
|
341
|
+
self.tasks_to_require = tasks_to_require[0] # Assign the dictionary
|
|
342
|
+
|
|
343
|
+
def __call__(self, task_that_requires):
|
|
344
|
+
task_that_requires = dict_inherits(self.tasks_to_require)(task_that_requires)
|
|
345
|
+
|
|
346
|
+
def requires(_self):
|
|
347
|
+
return _self.clone_parents_dict()
|
|
348
|
+
|
|
349
|
+
task_that_requires.requires = requires
|
|
350
|
+
|
|
351
|
+
return task_that_requires
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def inherits(*tasks_to_inherit):
|
|
355
|
+
if isinstance(tasks_to_inherit[0], dict):
|
|
356
|
+
return dict_inherits(*tasks_to_inherit)
|
|
357
|
+
return core.inherits(*tasks_to_inherit)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def requires(*tasks_to_require):
|
|
361
|
+
# Check the type; if a dictionary call our custom requires decorator
|
|
362
|
+
is_dict = isinstance(tasks_to_require[0], dict)
|
|
363
|
+
if is_dict:
|
|
364
|
+
return dict_requires(*tasks_to_require)
|
|
365
|
+
return core.requires(*tasks_to_require)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class Workflow(object):
|
|
369
|
+
"""
|
|
370
|
+
The class is used to orchestrate tasks and define a task pipeline
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
def __init__(self, task=None, params=None, path=None, env=None):
|
|
374
|
+
# Set Env
|
|
375
|
+
if path is not None and env is not None:
|
|
376
|
+
path = str(path) + f"/env={env}"
|
|
377
|
+
# Will overide other tasks with this task's main path
|
|
378
|
+
elif env is not None:
|
|
379
|
+
path = getattr(task, 'path', oryxflow.settings.dirpath)
|
|
380
|
+
path = str(path) + f"/env={env}"
|
|
381
|
+
|
|
382
|
+
# Set Params
|
|
383
|
+
self.params = {} if params is None else params
|
|
384
|
+
self.params = self.params if path is None else dict(**self.params, **{'path': path})
|
|
385
|
+
# Add flows to params
|
|
386
|
+
self.params = dict(**self.params, **{'flows': {}})
|
|
387
|
+
# Default Task
|
|
388
|
+
self.default_task = task
|
|
389
|
+
# If Task is set, Try to send Flow path to all other tasks
|
|
390
|
+
if task:
|
|
391
|
+
# Attach to tasks
|
|
392
|
+
if not isinstance(task, (list,)):
|
|
393
|
+
task = [task]
|
|
394
|
+
self._attach_to_tasks(task, path=path)
|
|
395
|
+
|
|
396
|
+
def preview(self, tasks=None, indent='', last=True, show_params=True, clip_params=False, print_it=True):
|
|
397
|
+
"""
|
|
398
|
+
Preview task flows with the workflow parameters
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
tasks (class, list): task class or list of tasks class
|
|
402
|
+
"""
|
|
403
|
+
if not isinstance(tasks, (list,)):
|
|
404
|
+
tasks = [tasks]
|
|
405
|
+
tasks_inst = [self.get_task(x) for x in tasks]
|
|
406
|
+
return preview(tasks=tasks_inst, indent=indent, last=last, show_params=show_params, clip_params=clip_params, print_it=print_it)
|
|
407
|
+
|
|
408
|
+
def run(self, tasks=None, forced=None, forced_all=False, forced_all_upstream=False, confirm=False, workers=1,
|
|
409
|
+
abort=True, execution_summary=None, **kwargs):
|
|
410
|
+
"""
|
|
411
|
+
Run tasks with the workflow parameters. Runs the DAG sequentially in dependency order.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
tasks (class, list): task class or list of tasks class
|
|
415
|
+
forced (list): list of forced tasks
|
|
416
|
+
forced_all (bool): force all tasks
|
|
417
|
+
forced_all_upstream (bool): force all tasks including upstream
|
|
418
|
+
confirm (list): confirm invalidating tasks
|
|
419
|
+
workers (int): number of workers
|
|
420
|
+
abort (bool): on errors raise exception
|
|
421
|
+
execution_summary (bool): log execution summary
|
|
422
|
+
kwargs: keywords to pass to core.build
|
|
423
|
+
|
|
424
|
+
"""
|
|
425
|
+
if not isinstance(tasks, (list,)):
|
|
426
|
+
tasks = [tasks]
|
|
427
|
+
tasks_inst = [self.get_task(x) for x in tasks]
|
|
428
|
+
|
|
429
|
+
# Before Running if Path/Flow Param is set, Set it to all other tasks
|
|
430
|
+
path_param = None
|
|
431
|
+
flow_param = None
|
|
432
|
+
if 'path' in self.params.keys():
|
|
433
|
+
path_param = self.params['path']
|
|
434
|
+
if self.params['flows']:
|
|
435
|
+
flow_param = self.params['flows']
|
|
436
|
+
|
|
437
|
+
# Attach to tasks
|
|
438
|
+
self._attach_to_tasks(tasks, flows=flow_param, path=path_param)
|
|
439
|
+
|
|
440
|
+
return run(tasks_inst, forced=forced, forced_all=forced_all, forced_all_upstream=forced_all_upstream,
|
|
441
|
+
confirm=confirm, workers=workers, abort=abort, execution_summary=execution_summary, **kwargs)
|
|
442
|
+
|
|
443
|
+
def outputLoad(self, task=None, keys=None, as_dict=False, cached=False):
|
|
444
|
+
"""
|
|
445
|
+
Load output from task with the workflow parameters
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
task (class): task class
|
|
449
|
+
keys (list): list of data to load
|
|
450
|
+
as_dict (bool): cache data in memory
|
|
451
|
+
cached (bool): cache data in memory
|
|
452
|
+
|
|
453
|
+
Returns: list or dict of all task output
|
|
454
|
+
"""
|
|
455
|
+
return self.get_task(task).outputLoad(keys=keys, as_dict=as_dict, cached=cached)
|
|
456
|
+
|
|
457
|
+
def outputPath(self, task=None):
|
|
458
|
+
"""
|
|
459
|
+
Ouputs the Path given a task
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
task (class): task class
|
|
463
|
+
|
|
464
|
+
Returns: list or dict of all task paths
|
|
465
|
+
"""
|
|
466
|
+
# Get Output
|
|
467
|
+
output = self.get_task(task).output()
|
|
468
|
+
|
|
469
|
+
# If Output is Dict, we have multiple outputs
|
|
470
|
+
if type(output) is dict:
|
|
471
|
+
# Get Paths
|
|
472
|
+
for output_name, output_target in output.items():
|
|
473
|
+
output[output_name] = output_target.path
|
|
474
|
+
|
|
475
|
+
return output
|
|
476
|
+
else:
|
|
477
|
+
return output.path
|
|
478
|
+
|
|
479
|
+
def complete(self, task=None, cascade=True):
|
|
480
|
+
return self.get_task(task).complete(cascade=cascade)
|
|
481
|
+
|
|
482
|
+
def output(self, task=None):
|
|
483
|
+
return self.get_task(task).output()
|
|
484
|
+
|
|
485
|
+
def outputLoadMeta(self, task=None):
|
|
486
|
+
return self.get_task(task).outputLoadMeta()
|
|
487
|
+
|
|
488
|
+
def outputLoadMetaJson(self, task=None):
|
|
489
|
+
return self.get_task(task).outputLoadMetaJson()
|
|
490
|
+
|
|
491
|
+
def outputLoadAll(self, task=None, keys=None, as_dict=False, cached=False):
|
|
492
|
+
"""
|
|
493
|
+
Load all output from task with the workflow parameters
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
task (class): task class
|
|
497
|
+
keys (list): list of data to load
|
|
498
|
+
as_dict (bool): cache data in memory
|
|
499
|
+
cached (bool): cache data in memory
|
|
500
|
+
|
|
501
|
+
Returns: list or dict of all task output
|
|
502
|
+
"""
|
|
503
|
+
task_inst = self.get_task(task)
|
|
504
|
+
data_dict = {}
|
|
505
|
+
tasks = taskflow_upstream(task_inst)
|
|
506
|
+
for task in tasks:
|
|
507
|
+
data_dict[type(task).__name__] = task.outputLoad(keys=keys, as_dict=as_dict, cached=cached)
|
|
508
|
+
return data_dict
|
|
509
|
+
|
|
510
|
+
def reset(self, task=None, confirm=False):
|
|
511
|
+
task_inst = self.get_task(task)
|
|
512
|
+
return task_inst.reset(confirm)
|
|
513
|
+
|
|
514
|
+
def reset_downstream(self, task, task_downstream=None, confirm=False):
|
|
515
|
+
"""
|
|
516
|
+
Invalidate all downstream tasks in a flow.
|
|
517
|
+
|
|
518
|
+
For example, you have 3 dependant tasks. Normally you run Task3 but you've changed parameters for Task1. By invalidating Task3 it will check the full DAG and realize Task1 needs to be invalidated and therefore Task2 and Task3 also.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
task (obj, class, list): task/family — or list of families — to invalidate downstream
|
|
522
|
+
of. Only the family is used (a class is fine — it is not instantiated), so this
|
|
523
|
+
works for tasks whose params are internal to the DAG (e.g. a per-``country`` task
|
|
524
|
+
you can't name from flow params); a list resets several families at once.
|
|
525
|
+
task_downstream (obj): terminal downstream task the walk stops at. Defaults to the
|
|
526
|
+
flow's default task; must be set (here or as the default task) so it knows where
|
|
527
|
+
"down" ends.
|
|
528
|
+
confirm (bool): confirm operation
|
|
529
|
+
"""
|
|
530
|
+
# invalidate_downstream only needs task.task_family (available on the class), so don't
|
|
531
|
+
# instantiate `task` — that would fail for families with DAG-internal params.
|
|
532
|
+
task_downstream_inst = self.get_task(task_downstream)
|
|
533
|
+
return invalidate_downstream(task, task_downstream_inst, confirm)
|
|
534
|
+
|
|
535
|
+
def reset_upstream(self, task, confirm=False, only=None):
|
|
536
|
+
task_inst = self.get_task(task)
|
|
537
|
+
return invalidate_upstream(task_inst, confirm, only=only)
|
|
538
|
+
|
|
539
|
+
def set_default(self, task):
|
|
540
|
+
"""
|
|
541
|
+
Set default task for the workflow object
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
task(obj) The task to be set as a default task
|
|
545
|
+
"""
|
|
546
|
+
self.default_task = task
|
|
547
|
+
|
|
548
|
+
def get_task(self, task=None):
|
|
549
|
+
"""
|
|
550
|
+
Get task with the workflow parameters
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
task(class)
|
|
554
|
+
|
|
555
|
+
Retuns: An instance of task class with the workflow parameters
|
|
556
|
+
"""
|
|
557
|
+
if task is None:
|
|
558
|
+
if self.default_task is None:
|
|
559
|
+
raise RuntimeError('no default tasks set')
|
|
560
|
+
else:
|
|
561
|
+
task = self.default_task
|
|
562
|
+
return task(**self.params)
|
|
563
|
+
|
|
564
|
+
# Add a Flow to the Params of the Workflow
|
|
565
|
+
def attach_flow(self, flow=None, flow_name="flow"):
|
|
566
|
+
if self.params['flows']:
|
|
567
|
+
self.params['flows'][flow_name] = flow
|
|
568
|
+
else:
|
|
569
|
+
self.params['flows'] = {flow_name: flow}
|
|
570
|
+
|
|
571
|
+
# Attach Flow/Path to the Tasks
|
|
572
|
+
def _attach_to_tasks(self, tasks, flows=None, path=None):
|
|
573
|
+
# If Both not set
|
|
574
|
+
if not flows and not path:
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
# Get all paths
|
|
578
|
+
for t_task in tasks:
|
|
579
|
+
task_inst = self.get_task(t_task)
|
|
580
|
+
tasks = taskflow_upstream(task_inst)
|
|
581
|
+
# Overide param of all tasks
|
|
582
|
+
for temp_task in tasks:
|
|
583
|
+
if flows:
|
|
584
|
+
temp_task.flows = self.params['flows']
|
|
585
|
+
if path:
|
|
586
|
+
temp_task.path = self.params['path']
|
|
587
|
+
|
|
588
|
+
class WorkflowMulti(object):
|
|
589
|
+
"""
|
|
590
|
+
A multi experiment workflow can be defined with multiple flows and separate parameters for each flow and a default task. It is mandatory to define the flows and parameters for each of the flows.
|
|
591
|
+
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
def __init__(self, task=None, params=None, path=None, env=None):
|
|
595
|
+
self.params = params
|
|
596
|
+
self._task_name = task.task_family if task else 'WorkflowMulti default task'
|
|
597
|
+
if params is not None and type(params) not in [dict, list]:
|
|
598
|
+
raise Exception("Params has to be a dictionary with key defining the flow name or a list")
|
|
599
|
+
if type(params) == dict:
|
|
600
|
+
if type(list(params.values())[0]) == list:
|
|
601
|
+
# single-key grid (e.g. {'country': [...]}) -> one flow per value;
|
|
602
|
+
# multi-key grid -> cartesian product of the value lists
|
|
603
|
+
if len(params) == 1:
|
|
604
|
+
self.params = oryxflow.utils.params_generator_single(params)
|
|
605
|
+
else:
|
|
606
|
+
self.params = oryxflow.utils.generate_exps_for_multi_param(params)
|
|
607
|
+
if type(params) == list:
|
|
608
|
+
params = {i: v for i, v in enumerate(params)}
|
|
609
|
+
self.params = params
|
|
610
|
+
if params is None or len(params.keys()) == 0:
|
|
611
|
+
raise Exception("Need to pass task parameters or use oryxflow.Workflow")
|
|
612
|
+
self.default_task = task
|
|
613
|
+
if params is not None:
|
|
614
|
+
self.workflow_objs = {k: Workflow(task=task, params=v, path=path, env=env) for k, v in self.params.items()}
|
|
615
|
+
|
|
616
|
+
def run(self, tasks=None, flow=None, forced=None, forced_all=False, forced_all_upstream=False, confirm=False,
|
|
617
|
+
workers=1, abort=True, execution_summary=None, **kwargs):
|
|
618
|
+
"""
|
|
619
|
+
Run tasks with the workflow parameters for a flow. Runs the DAG sequentially in dependency order.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
flow (string): The name of the experiment for which the flow is to be run. If nothing is passed, all the flows are run
|
|
623
|
+
tasks (class, list): task class or list of tasks class
|
|
624
|
+
forced (list): list of forced tasks
|
|
625
|
+
forced_all (bool): force all tasks
|
|
626
|
+
forced_all_upstream (bool): force all tasks including upstream
|
|
627
|
+
confirm (list): confirm invalidating tasks
|
|
628
|
+
workers (int): number of workers
|
|
629
|
+
abort (bool): on errors raise exception
|
|
630
|
+
execution_summary (bool): log execution summary
|
|
631
|
+
kwargs: keywords to pass to core.build
|
|
632
|
+
|
|
633
|
+
"""
|
|
634
|
+
|
|
635
|
+
if flow is not None:
|
|
636
|
+
return self.workflow_objs[flow].run(tasks=tasks, forced=forced, forced_all=forced_all,
|
|
637
|
+
forced_all_upstream=forced_all_upstream, confirm=confirm,
|
|
638
|
+
workers=workers,
|
|
639
|
+
abort=abort,
|
|
640
|
+
execution_summary=execution_summary, **kwargs)
|
|
641
|
+
result = MultiRunResult()
|
|
642
|
+
for exp_name in self.params.keys():
|
|
643
|
+
result[exp_name] = self.workflow_objs[exp_name].run(tasks, forced, forced_all, forced_all_upstream,
|
|
644
|
+
confirm, workers, abort,
|
|
645
|
+
execution_summary, **kwargs)
|
|
646
|
+
return result
|
|
647
|
+
|
|
648
|
+
def outputLoad(self, task=None, flow=None, keys=None, as_dict=False, cached=False):
|
|
649
|
+
"""
|
|
650
|
+
Load output from task with the workflow parameters for a flow
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
flow (string): The name of the experiment for which the flow is to be run. If nothing is passed, all the flows are run
|
|
654
|
+
task (class): task class
|
|
655
|
+
keys (list): list of data to load
|
|
656
|
+
as_dict (bool): cache data in memory
|
|
657
|
+
cached (bool): cache data in memory
|
|
658
|
+
|
|
659
|
+
Returns: list or dict of all task output
|
|
660
|
+
"""
|
|
661
|
+
if flow is not None:
|
|
662
|
+
return self.workflow_objs[flow].outputLoad(task, keys, as_dict, cached)
|
|
663
|
+
data = {}
|
|
664
|
+
for exp_name in self.params.keys():
|
|
665
|
+
data[exp_name] = self.workflow_objs[exp_name].outputLoad(task, keys, as_dict, cached)
|
|
666
|
+
return data
|
|
667
|
+
|
|
668
|
+
def outputPath(self, task=None, flow=None):
|
|
669
|
+
"""
|
|
670
|
+
Ouputs the Path given a task
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
task (class): task class
|
|
674
|
+
flow (string): The name of the experiment for which the flow is to be run. If nothing is passed, all the flows are run
|
|
675
|
+
|
|
676
|
+
Returns: list or dict of all task paths
|
|
677
|
+
"""
|
|
678
|
+
if flow is not None:
|
|
679
|
+
return self.workflow_objs[flow].outputPath(task)
|
|
680
|
+
|
|
681
|
+
data = {}
|
|
682
|
+
for exp_name in self.params.keys():
|
|
683
|
+
data[exp_name] = self.workflow_objs[exp_name].outputPath(task)
|
|
684
|
+
|
|
685
|
+
return data
|
|
686
|
+
|
|
687
|
+
def outputLoadMeta(self, task=None, flow=None):
|
|
688
|
+
if flow is not None:
|
|
689
|
+
return self.workflow_objs[flow].outputLoadMeta(task)
|
|
690
|
+
data = {}
|
|
691
|
+
for exp_name in self.params.keys():
|
|
692
|
+
data[exp_name] = self.workflow_objs[exp_name].outputLoadMeta(task)
|
|
693
|
+
return data
|
|
694
|
+
|
|
695
|
+
def outputLoadMetaJson(self, task=None, flow=None):
|
|
696
|
+
if flow is not None:
|
|
697
|
+
return self.workflow_objs[flow].outputLoadMetaJson(task)
|
|
698
|
+
data = {}
|
|
699
|
+
for exp_name in self.params.keys():
|
|
700
|
+
data[exp_name] = self.workflow_objs[exp_name].outputLoadMetaJson(task)
|
|
701
|
+
return data
|
|
702
|
+
|
|
703
|
+
def outputLoadAll(self, task=None, flow=None, keys=None, as_dict=False, cached=False):
|
|
704
|
+
"""
|
|
705
|
+
Load all output from task with the workflow parameters for a flow
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
flow (string): The name of the experiment for which the flow is to be run. If nothing is passed, all the flows are run
|
|
709
|
+
task (class): task class
|
|
710
|
+
keys (list): list of data to load
|
|
711
|
+
as_dict (bool): cache data in memory
|
|
712
|
+
cached (bool): cache data in memory
|
|
713
|
+
|
|
714
|
+
Returns: list or dict of all task output
|
|
715
|
+
"""
|
|
716
|
+
if flow is not None:
|
|
717
|
+
return self.workflow_objs[flow].outputLoadAll(task, keys, as_dict, cached)
|
|
718
|
+
data = {}
|
|
719
|
+
for exp_name in self.params.keys():
|
|
720
|
+
data[exp_name] = self.workflow_objs[exp_name].outputLoadAll(task, keys, as_dict, cached)
|
|
721
|
+
return data
|
|
722
|
+
|
|
723
|
+
def outputLoadConcat(self, task=None, keys=None, as_dict=False, cached=False,
|
|
724
|
+
concat_fn=None, tagkeys=None):
|
|
725
|
+
"""Load `task` output for every flow and concatenate into one DataFrame,
|
|
726
|
+
tagging each flow's rows with that flow's raw params."""
|
|
727
|
+
per_flow = self.outputLoad(task=task, keys=keys, as_dict=as_dict, cached=cached)
|
|
728
|
+
items = ((flow, self.params[flow], per_flow[flow]) for flow in self.params.keys())
|
|
729
|
+
return oryxflow.utils.concat_iter(items, concat_fn=concat_fn, keys=tagkeys)
|
|
730
|
+
|
|
731
|
+
def _confirm_reset(self, confirm, operation_name="reset"):
|
|
732
|
+
"""
|
|
733
|
+
Helper method to handle confirmation logic for reset operations
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
confirm (bool): whether to ask for confirmation
|
|
737
|
+
operation_name (str): name of the operation for the confirmation message
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
bool: True if confirmed, False otherwise
|
|
741
|
+
"""
|
|
742
|
+
if confirm:
|
|
743
|
+
c = input(
|
|
744
|
+
'Confirm invalidating task: {} (y/n). PS You can disable this message by passing confirm=False'.format(
|
|
745
|
+
self._task_name))
|
|
746
|
+
else:
|
|
747
|
+
c = 'y'
|
|
748
|
+
return c == 'y'
|
|
749
|
+
|
|
750
|
+
def reset(self, task=None, flow=None, confirm=False):
|
|
751
|
+
if flow is not None:
|
|
752
|
+
return self.workflow_objs[flow].reset(task, confirm)
|
|
753
|
+
|
|
754
|
+
# For multiple flows, ask for confirmation once if confirm=True
|
|
755
|
+
if not self._confirm_reset(confirm, "reset"):
|
|
756
|
+
return False
|
|
757
|
+
|
|
758
|
+
return {self.workflow_objs[exp_name].reset(task, confirm=False) for exp_name in self.params.keys()}
|
|
759
|
+
|
|
760
|
+
def reset_downstream(self, task=None, task_downstream=None, flow=None, confirm=False):
|
|
761
|
+
if flow is not None:
|
|
762
|
+
return self.workflow_objs[flow].reset_downstream(task, task_downstream, confirm)
|
|
763
|
+
|
|
764
|
+
# For multiple flows, ask for confirmation once if confirm=True
|
|
765
|
+
if not self._confirm_reset(confirm, "reset_downstream"):
|
|
766
|
+
return False
|
|
767
|
+
|
|
768
|
+
return {self.workflow_objs[exp_name].reset_downstream(task, task_downstream, confirm=False) for exp_name in self.params.keys()}
|
|
769
|
+
|
|
770
|
+
def reset_upstream(self, task=None, flow=None, confirm=False, only=None):
|
|
771
|
+
if flow is not None:
|
|
772
|
+
return self.workflow_objs[flow].reset_upstream(task, confirm, only=only)
|
|
773
|
+
|
|
774
|
+
# For multiple flows, ask for confirmation once if confirm=True
|
|
775
|
+
if not self._confirm_reset(confirm, "reset_upstream"):
|
|
776
|
+
return False
|
|
777
|
+
|
|
778
|
+
return {self.workflow_objs[exp_name].reset_upstream(task, confirm=False, only=only) for exp_name in self.params.keys()}
|
|
779
|
+
|
|
780
|
+
def preview(self, tasks=None, flow=None, indent='', last=True, show_params=True, clip_params=False, print_it=True):
|
|
781
|
+
"""
|
|
782
|
+
Preview task flows with the workflow parameters for a flow
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
flow (string): The name of the experiment for which the flow is to be run. If nothing is passed, all the flows are run
|
|
786
|
+
tasks (class, list): task class or list of tasks class
|
|
787
|
+
"""
|
|
788
|
+
if not isinstance(tasks, (list,)):
|
|
789
|
+
tasks = [tasks]
|
|
790
|
+
if flow is not None:
|
|
791
|
+
return self.workflow_objs[flow].preview(tasks, print_it=print_it)
|
|
792
|
+
data = {}
|
|
793
|
+
for exp_name in self.params.keys():
|
|
794
|
+
data[exp_name] = self.workflow_objs[exp_name].preview(tasks=tasks, indent=indent, last=last,
|
|
795
|
+
show_params=show_params, clip_params=clip_params, print_it=print_it)
|
|
796
|
+
return data
|
|
797
|
+
|
|
798
|
+
def set_default(self, task):
|
|
799
|
+
"""
|
|
800
|
+
Set default task for the workflow. The default task is set for all the experiments
|
|
801
|
+
|
|
802
|
+
Args:
|
|
803
|
+
task(obj) The task to be set as a default task
|
|
804
|
+
"""
|
|
805
|
+
self.default_task = task
|
|
806
|
+
for exp_name in self.params.keys():
|
|
807
|
+
self.workflow_objs[exp_name].set_default(task)
|
|
808
|
+
|
|
809
|
+
def get_task(self, task=None, flow=None):
|
|
810
|
+
"""
|
|
811
|
+
Get task with the workflow parameters for a flow
|
|
812
|
+
|
|
813
|
+
Args:
|
|
814
|
+
flow (string): The name of the experiment for which the flow is to be run. If nothing is passed, all the flows are run
|
|
815
|
+
task(class): task class
|
|
816
|
+
|
|
817
|
+
Retuns: An instance of task class with the workflow parameters
|
|
818
|
+
"""
|
|
819
|
+
if task is None:
|
|
820
|
+
if self.default_task is None:
|
|
821
|
+
raise RuntimeError('no default tasks set')
|
|
822
|
+
else:
|
|
823
|
+
task = self.default_task
|
|
824
|
+
if flow is None:
|
|
825
|
+
return {exp_name: self.workflow_objs[exp_name].get_task(task) for exp_name in self.params.keys()}
|
|
826
|
+
return self.workflow_objs[flow].get_task(task)
|
|
827
|
+
|
|
828
|
+
def get_flow(self, flow):
|
|
829
|
+
"""
|
|
830
|
+
Get flow by name
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
flow (string): The name of the experiment for which the flow is to be run. If nothing is passed, all the flows are run
|
|
834
|
+
|
|
835
|
+
Retuns: An instance of Workflow
|
|
836
|
+
"""
|
|
837
|
+
return self.workflow_objs[flow]
|
|
838
|
+
|
|
839
|
+
class FlowExport(object):
|
|
840
|
+
"""
|
|
841
|
+
Auto generate task files to quickly share workflows with others.
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
tasks (obj): task or list of tasks to share
|
|
845
|
+
flows (obj): flow or list of flows to get tasks from.
|
|
846
|
+
save (bool): save to tasks file
|
|
847
|
+
path_export (str): filename for tasks to export.
|
|
848
|
+
"""
|
|
849
|
+
def __init__(self, tasks=None, flows=None, save=False, path_export='tasks_export.py'):
|
|
850
|
+
|
|
851
|
+
tasks = [] if tasks is None else tasks
|
|
852
|
+
flows = [] if flows is None else flows
|
|
853
|
+
if not isinstance(tasks, (list,)):
|
|
854
|
+
tasks = [tasks]
|
|
855
|
+
if not isinstance(flows, (list,)):
|
|
856
|
+
flows = [flows]
|
|
857
|
+
for flow in flows:
|
|
858
|
+
task_inst = flow.get_task()
|
|
859
|
+
t_tasks = taskflow_upstream(task_inst)
|
|
860
|
+
for task in t_tasks:
|
|
861
|
+
tasks.append(task)
|
|
862
|
+
|
|
863
|
+
self.tasks = tasks
|
|
864
|
+
self.save = save
|
|
865
|
+
self.path_export = path_export
|
|
866
|
+
|
|
867
|
+
# file templates
|
|
868
|
+
self.tmpl_tasks = '''
|
|
869
|
+
import oryxflow
|
|
870
|
+
import datetime
|
|
871
|
+
|
|
872
|
+
{% for task in tasks -%}
|
|
873
|
+
|
|
874
|
+
class {{task.name}}({{task.class}}):
|
|
875
|
+
external=True
|
|
876
|
+
persists={{task.obj.persist}}
|
|
877
|
+
{% if task.path -%}
|
|
878
|
+
path="{{task.path}}"
|
|
879
|
+
{% endif -%}
|
|
880
|
+
{% if task.obj.task_group -%}
|
|
881
|
+
task_group="{{task.obj.task_group}}"
|
|
882
|
+
{% endif -%}
|
|
883
|
+
{% for param in task.params -%}
|
|
884
|
+
{{param.name}}={{param.class}}(default={{param.value}})
|
|
885
|
+
{% endfor %}
|
|
886
|
+
{% endfor %}
|
|
887
|
+
'''
|
|
888
|
+
|
|
889
|
+
def generate(self):
|
|
890
|
+
"""
|
|
891
|
+
Generate output files
|
|
892
|
+
"""
|
|
893
|
+
try:
|
|
894
|
+
from jinja2 import Template
|
|
895
|
+
except ModuleNotFoundError:
|
|
896
|
+
print("module 'jinja2' is not installed. Run: pip install Jinja2")
|
|
897
|
+
|
|
898
|
+
tasksPrint = []
|
|
899
|
+
for task in self.tasks:
|
|
900
|
+
if getattr(task, 'export', True):
|
|
901
|
+
class_ = next(c for c in type(task).__mro__ if 'oryxflow.tasks.' in str(c)) # type(task).__mro__[1]
|
|
902
|
+
|
|
903
|
+
# Get Path
|
|
904
|
+
task_path = getattr(task, 'path', None)
|
|
905
|
+
task_path = Path(task_path) if task_path else None
|
|
906
|
+
|
|
907
|
+
# Create Dict
|
|
908
|
+
taskPrint = {'name': task.__class__.__name__, 'class': class_.__module__ + "." + class_.__name__,
|
|
909
|
+
'obj': task, 'persist': task.persist, 'path': task_path,
|
|
910
|
+
'params': [{'name': param[0],
|
|
911
|
+
'class': f'{param[1].__class__.__module__}.{param[1].__class__.__name__}',
|
|
912
|
+
'value': repr(getattr(task,param[0]))} for param in task.get_params()]} # param[1]._default
|
|
913
|
+
tasksPrint.append(taskPrint)
|
|
914
|
+
|
|
915
|
+
tasksPrint[-1], tasksPrint[0:-1] = tasksPrint[0], tasksPrint[1:]
|
|
916
|
+
|
|
917
|
+
# Print or Save to File
|
|
918
|
+
if not self.save:
|
|
919
|
+
print(Template(self.tmpl_tasks).render(tasks=tasksPrint))
|
|
920
|
+
else:
|
|
921
|
+
# Write Tasks
|
|
922
|
+
with open(self.path_export, 'w') as fh:
|
|
923
|
+
fh.write(Template(self.tmpl_tasks).render(tasks=tasksPrint))
|
|
924
|
+
|
|
925
|
+
import importlib.util
|
|
926
|
+
import inspect
|
|
927
|
+
class FlowImport(object):
|
|
928
|
+
"""
|
|
929
|
+
Import a specific module from a directory.
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
path (str): path to the dir to import from
|
|
933
|
+
module (str): the module name to import
|
|
934
|
+
path_data (str): path to the data file; if not absolute will be appended to path
|
|
935
|
+
"""
|
|
936
|
+
def __init__(self, path=None, module=None, path_data=None):
|
|
937
|
+
# INIT
|
|
938
|
+
self.path = path
|
|
939
|
+
self.module = module
|
|
940
|
+
self.tasks = {}
|
|
941
|
+
|
|
942
|
+
# if path_data is an absolute path, use that, else append to path)
|
|
943
|
+
if Path(path_data).is_absolute():
|
|
944
|
+
self.dirpath = path_data
|
|
945
|
+
else:
|
|
946
|
+
self.dirpath = Path(path) / Path(path_data)
|
|
947
|
+
|
|
948
|
+
# Check if name ends with .py
|
|
949
|
+
if not str(module).endswith(".py"):
|
|
950
|
+
module = str(module) + ".py"
|
|
951
|
+
# Check if exists
|
|
952
|
+
path_to_file = Path(path) / Path(module)
|
|
953
|
+
if (path_to_file).exists():
|
|
954
|
+
path = Path(path) / Path(module)
|
|
955
|
+
else:
|
|
956
|
+
raise ValueError("Path {} not found.".format((Path(path) / Path(module))))
|
|
957
|
+
|
|
958
|
+
# Get Module, Tasks, Dirpath
|
|
959
|
+
self.module_obj = self._module_from_file(module, path)
|
|
960
|
+
self._get_tasks()
|
|
961
|
+
|
|
962
|
+
def _get_tasks(self):
|
|
963
|
+
tasks = {}
|
|
964
|
+
for name, obj in inspect.getmembers(self.module_obj):
|
|
965
|
+
if inspect.isclass(obj) and issubclass(obj, oryxflow.tasks.TaskData):
|
|
966
|
+
tasks[name] = obj
|
|
967
|
+
# Convert to DotDict so that we can use .
|
|
968
|
+
self.tasks = dotdict(tasks)
|
|
969
|
+
|
|
970
|
+
def _module_from_file(self, module_name, file_path):
|
|
971
|
+
try:
|
|
972
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
973
|
+
module = importlib.util.module_from_spec(spec)
|
|
974
|
+
spec.loader.exec_module(module)
|
|
975
|
+
return module
|
|
976
|
+
except:
|
|
977
|
+
print("Module {} not found.".format(module_name))
|
|
978
|
+
return None
|
|
979
|
+
|
|
980
|
+
# Helper Class
|
|
981
|
+
class dotdict(dict):
|
|
982
|
+
"""dot.notation access to dictionary attributes"""
|
|
983
|
+
__getattr__ = dict.get
|
|
984
|
+
__setattr__ = dict.__setitem__
|
|
985
|
+
__delattr__ = dict.__delitem__
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def runLoad(task, params=None, load=True, taskLoad=None, reset=False):
|
|
989
|
+
|
|
990
|
+
params = dict() if params is None else params
|
|
991
|
+
taskLoad = task if taskLoad is None else taskLoad
|
|
992
|
+
|
|
993
|
+
flow = oryxflow.Workflow(task, params)
|
|
994
|
+
if reset:
|
|
995
|
+
flow.reset(task)
|
|
996
|
+
flow.run()
|
|
997
|
+
|
|
998
|
+
if load:
|
|
999
|
+
r = flow.outputLoad(taskLoad)
|
|
1000
|
+
return r
|
|
1001
|
+
|
|
1002
|
+
def runIt(task, params=None, reset=False):
|
|
1003
|
+
return runLoad(task, params=params, reset=reset, load=False)
|
|
1004
|
+
|
|
1005
|
+
def runIterConcat(task, params, load=True, taskLoad=None, reset=False,
|
|
1006
|
+
concat_fn=None, tagkeys=None):
|
|
1007
|
+
"""Run `task` across a grid of params (one flow per param set) and return the
|
|
1008
|
+
per-flow outputs concatenated into one DataFrame, each flow tagged with its params."""
|
|
1009
|
+
taskLoad = task if taskLoad is None else taskLoad
|
|
1010
|
+
flow = oryxflow.WorkflowMulti(task, params)
|
|
1011
|
+
if reset:
|
|
1012
|
+
flow.reset(task)
|
|
1013
|
+
flow.run()
|
|
1014
|
+
return flow.outputLoadConcat(taskLoad, concat_fn=concat_fn, tagkeys=tagkeys) if load else flow
|
|
1015
|
+
|