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 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
+