wxflow 0.1.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.
- wxflow/__init__.py +21 -0
- wxflow/attrdict.py +171 -0
- wxflow/configuration.py +179 -0
- wxflow/exceptions.py +87 -0
- wxflow/executable.py +357 -0
- wxflow/factory.py +133 -0
- wxflow/file_utils.py +77 -0
- wxflow/fsutils.py +87 -0
- wxflow/jinja.py +255 -0
- wxflow/logger.py +277 -0
- wxflow/schema.py +886 -0
- wxflow/task.py +93 -0
- wxflow/template.py +191 -0
- wxflow/timetools.py +315 -0
- wxflow/yaml_file.py +210 -0
- wxflow-0.1.0.dist-info/LICENSE.md +157 -0
- wxflow-0.1.0.dist-info/METADATA +93 -0
- wxflow-0.1.0.dist-info/RECORD +20 -0
- wxflow-0.1.0.dist-info/WHEEL +5 -0
- wxflow-0.1.0.dist-info/top_level.txt +1 -0
wxflow/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from .attrdict import AttrDict
|
|
4
|
+
from .configuration import (Configuration, cast_as_dtype,
|
|
5
|
+
cast_strdict_as_dtypedict)
|
|
6
|
+
from .exceptions import WorkflowException, msg_except_handle
|
|
7
|
+
from .executable import CommandNotFoundError, Executable, which
|
|
8
|
+
from .factory import Factory
|
|
9
|
+
from .file_utils import FileHandler
|
|
10
|
+
from .fsutils import chdir, cp, mkdir, mkdir_p, rm_p, rmdir
|
|
11
|
+
from .jinja import Jinja
|
|
12
|
+
from .logger import Logger, logit
|
|
13
|
+
from .task import Task
|
|
14
|
+
from .template import Template, TemplateConstants
|
|
15
|
+
from .timetools import *
|
|
16
|
+
from .yaml_file import (YAMLFile, dump_as_yaml, parse_j2yaml, parse_yaml,
|
|
17
|
+
parse_yamltmpl, save_as_yaml, vanilla_yaml)
|
|
18
|
+
|
|
19
|
+
__docformat__ = "restructuredtext"
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
wxflow_directory = os.path.dirname(__file__)
|
wxflow/attrdict.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# attrdict is a Python module that gives you dictionaries whose values are both
|
|
2
|
+
# gettable and settable using attributes, in addition to standard item-syntax.
|
|
3
|
+
# https://github.com/mewwts/addict
|
|
4
|
+
# addict/addict.py -> attrdict.py
|
|
5
|
+
# hash: 7e8d23d
|
|
6
|
+
# License: MIT
|
|
7
|
+
# class Dict -> class AttrDict to prevent name collisions w/ typing.Dict
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
|
|
11
|
+
__all__ = ['AttrDict']
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AttrDict(dict):
|
|
15
|
+
|
|
16
|
+
def __init__(__self, *args, **kwargs):
|
|
17
|
+
object.__setattr__(__self, '__parent', kwargs.pop('__parent', None))
|
|
18
|
+
object.__setattr__(__self, '__key', kwargs.pop('__key', None))
|
|
19
|
+
object.__setattr__(__self, '__frozen', False)
|
|
20
|
+
for arg in args:
|
|
21
|
+
if not arg:
|
|
22
|
+
continue
|
|
23
|
+
elif isinstance(arg, dict):
|
|
24
|
+
for key, val in arg.items():
|
|
25
|
+
__self[key] = __self._hook(val)
|
|
26
|
+
elif isinstance(arg, tuple) and (not isinstance(arg[0], tuple)):
|
|
27
|
+
__self[arg[0]] = __self._hook(arg[1])
|
|
28
|
+
else:
|
|
29
|
+
for key, val in iter(arg):
|
|
30
|
+
__self[key] = __self._hook(val)
|
|
31
|
+
|
|
32
|
+
for key, val in kwargs.items():
|
|
33
|
+
__self[key] = __self._hook(val)
|
|
34
|
+
|
|
35
|
+
def __setattr__(self, name, value):
|
|
36
|
+
if hasattr(self.__class__, name):
|
|
37
|
+
raise AttributeError("'AttrDict' object attribute "
|
|
38
|
+
"'{0}' is read-only".format(name))
|
|
39
|
+
else:
|
|
40
|
+
self[name] = value
|
|
41
|
+
|
|
42
|
+
def __setitem__(self, name, value):
|
|
43
|
+
isFrozen = (hasattr(self, '__frozen') and
|
|
44
|
+
object.__getattribute__(self, '__frozen'))
|
|
45
|
+
if isFrozen and name not in super(AttrDict, self).keys():
|
|
46
|
+
raise KeyError(name)
|
|
47
|
+
if isinstance(value, dict):
|
|
48
|
+
value = AttrDict(value)
|
|
49
|
+
super(AttrDict, self).__setitem__(name, value)
|
|
50
|
+
try:
|
|
51
|
+
p = object.__getattribute__(self, '__parent')
|
|
52
|
+
key = object.__getattribute__(self, '__key')
|
|
53
|
+
except AttributeError:
|
|
54
|
+
p = None
|
|
55
|
+
key = None
|
|
56
|
+
if p is not None:
|
|
57
|
+
p[key] = self
|
|
58
|
+
object.__delattr__(self, '__parent')
|
|
59
|
+
object.__delattr__(self, '__key')
|
|
60
|
+
|
|
61
|
+
def __add__(self, other):
|
|
62
|
+
if not self.keys():
|
|
63
|
+
return other
|
|
64
|
+
else:
|
|
65
|
+
self_type = type(self).__name__
|
|
66
|
+
other_type = type(other).__name__
|
|
67
|
+
msg = "unsupported operand type(s) for +: '{}' and '{}'"
|
|
68
|
+
raise TypeError(msg.format(self_type, other_type))
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def _hook(cls, item):
|
|
72
|
+
if isinstance(item, dict):
|
|
73
|
+
return cls(item)
|
|
74
|
+
elif isinstance(item, (list, tuple)):
|
|
75
|
+
return type(item)(cls._hook(elem) for elem in item)
|
|
76
|
+
return item
|
|
77
|
+
|
|
78
|
+
def __getattr__(self, item):
|
|
79
|
+
return self.__getitem__(item)
|
|
80
|
+
|
|
81
|
+
def __missing__(self, name):
|
|
82
|
+
if object.__getattribute__(self, '__frozen'):
|
|
83
|
+
raise KeyError(name)
|
|
84
|
+
return self.__class__(__parent=self, __key=name)
|
|
85
|
+
|
|
86
|
+
def __delattr__(self, name):
|
|
87
|
+
del self[name]
|
|
88
|
+
|
|
89
|
+
def to_dict(self):
|
|
90
|
+
base = {}
|
|
91
|
+
for key, value in self.items():
|
|
92
|
+
if isinstance(value, type(self)):
|
|
93
|
+
base[key] = value.to_dict()
|
|
94
|
+
elif isinstance(value, (list, tuple)):
|
|
95
|
+
base[key] = type(value)(
|
|
96
|
+
item.to_dict() if isinstance(item, type(self)) else
|
|
97
|
+
item for item in value)
|
|
98
|
+
else:
|
|
99
|
+
base[key] = value
|
|
100
|
+
return base
|
|
101
|
+
|
|
102
|
+
def copy(self):
|
|
103
|
+
return copy.copy(self)
|
|
104
|
+
|
|
105
|
+
def deepcopy(self):
|
|
106
|
+
return copy.deepcopy(self)
|
|
107
|
+
|
|
108
|
+
def __deepcopy__(self, memo):
|
|
109
|
+
other = self.__class__()
|
|
110
|
+
memo[id(self)] = other
|
|
111
|
+
for key, value in self.items():
|
|
112
|
+
other[copy.deepcopy(key, memo)] = copy.deepcopy(value, memo)
|
|
113
|
+
return other
|
|
114
|
+
|
|
115
|
+
def update(self, *args, **kwargs):
|
|
116
|
+
other = {}
|
|
117
|
+
if args:
|
|
118
|
+
if len(args) > 1:
|
|
119
|
+
raise TypeError()
|
|
120
|
+
other.update(args[0])
|
|
121
|
+
other.update(kwargs)
|
|
122
|
+
for k, v in other.items():
|
|
123
|
+
if ((k not in self) or
|
|
124
|
+
(not isinstance(self[k], dict)) or
|
|
125
|
+
(not isinstance(v, dict))):
|
|
126
|
+
self[k] = v
|
|
127
|
+
else:
|
|
128
|
+
self[k].update(v)
|
|
129
|
+
|
|
130
|
+
def __getnewargs__(self):
|
|
131
|
+
return tuple(self.items())
|
|
132
|
+
|
|
133
|
+
def __getstate__(self):
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def __setstate__(self, state):
|
|
137
|
+
self.update(state)
|
|
138
|
+
|
|
139
|
+
def __or__(self, other):
|
|
140
|
+
if not isinstance(other, (AttrDict, dict)):
|
|
141
|
+
return NotImplemented
|
|
142
|
+
new = AttrDict(self)
|
|
143
|
+
new.update(other)
|
|
144
|
+
return new
|
|
145
|
+
|
|
146
|
+
def __ror__(self, other):
|
|
147
|
+
if not isinstance(other, (AttrDict, dict)):
|
|
148
|
+
return NotImplemented
|
|
149
|
+
new = AttrDict(other)
|
|
150
|
+
new.update(self)
|
|
151
|
+
return new
|
|
152
|
+
|
|
153
|
+
def __ior__(self, other):
|
|
154
|
+
self.update(other)
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def setdefault(self, key, default=None):
|
|
158
|
+
if key in self:
|
|
159
|
+
return self[key]
|
|
160
|
+
else:
|
|
161
|
+
self[key] = default
|
|
162
|
+
return default
|
|
163
|
+
|
|
164
|
+
def freeze(self, shouldFreeze=True):
|
|
165
|
+
object.__setattr__(self, '__frozen', shouldFreeze)
|
|
166
|
+
for key, val in self.items():
|
|
167
|
+
if isinstance(val, AttrDict):
|
|
168
|
+
val.freeze(shouldFreeze)
|
|
169
|
+
|
|
170
|
+
def unfreeze(self):
|
|
171
|
+
self.freeze(False)
|
wxflow/configuration.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import os
|
|
3
|
+
import random
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from pprint import pprint
|
|
7
|
+
from typing import Any, Dict, List, Union
|
|
8
|
+
|
|
9
|
+
from .attrdict import AttrDict
|
|
10
|
+
from .timetools import to_datetime
|
|
11
|
+
|
|
12
|
+
__all__ = ['Configuration', 'cast_as_dtype', 'cast_strdict_as_dtypedict']
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ShellScriptException(Exception):
|
|
16
|
+
def __init__(self, scripts, errors):
|
|
17
|
+
self.scripts = scripts
|
|
18
|
+
self.errors = errors
|
|
19
|
+
super(ShellScriptException, self).__init__(
|
|
20
|
+
str(errors) +
|
|
21
|
+
': error processing' +
|
|
22
|
+
(' '.join(scripts)))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UnknownConfigError(Exception):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Configuration:
|
|
30
|
+
"""
|
|
31
|
+
Configuration parser for the global-workflow
|
|
32
|
+
(or generally for sourcing a shell script into a python dictionary)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, config_dir: Union[str, Path]):
|
|
36
|
+
"""
|
|
37
|
+
Given a directory containing config files (config.XYZ),
|
|
38
|
+
return a list of config_files minus the ones ending with ".default"
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
self.config_dir = config_dir
|
|
42
|
+
self.config_files = self._get_configs
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def _get_configs(self) -> List[str]:
|
|
46
|
+
"""
|
|
47
|
+
Given a directory containing config files (config.XYZ),
|
|
48
|
+
return a list of config_files minus the ones ending with ".default"
|
|
49
|
+
"""
|
|
50
|
+
result = list()
|
|
51
|
+
for config in glob.glob(f'{self.config_dir}/config.*'):
|
|
52
|
+
if not config.endswith('.default'):
|
|
53
|
+
result.append(config)
|
|
54
|
+
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
def find_config(self, config_name: str) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Given a config file name, find the full path of the config file
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
for config in self.config_files:
|
|
63
|
+
if config_name == os.path.basename(config):
|
|
64
|
+
return config
|
|
65
|
+
|
|
66
|
+
raise UnknownConfigError(
|
|
67
|
+
f'{config_name} does not exist (known: {repr(config_name)}), ABORT!')
|
|
68
|
+
|
|
69
|
+
def parse_config(self, files: Union[str, bytes, list]) -> Dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Given the name of config file(s), key-value pair of all variables in the config file(s)
|
|
72
|
+
are returned as a dictionary
|
|
73
|
+
:param files: config file or list of config files
|
|
74
|
+
:type files: list or str or unicode
|
|
75
|
+
:return: Key value pairs representing the environment variables defined
|
|
76
|
+
in the script.
|
|
77
|
+
:rtype: dict
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(files, (str, bytes)):
|
|
80
|
+
files = [files]
|
|
81
|
+
files = [self.find_config(file) for file in files]
|
|
82
|
+
return cast_strdict_as_dtypedict(self._get_script_env(files))
|
|
83
|
+
|
|
84
|
+
def print_config(self, files: Union[str, bytes, list]) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Given the name of config file(s), key-value pair of all variables in the config file(s) are printed
|
|
87
|
+
Same signature as parse_config
|
|
88
|
+
:param files: config file or list of config files
|
|
89
|
+
:type files: list or str or unicode
|
|
90
|
+
:return: None
|
|
91
|
+
"""
|
|
92
|
+
config = self.parse_config(files)
|
|
93
|
+
pprint(config, width=4)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def _get_script_env(cls, scripts: List) -> Dict[str, Any]:
|
|
97
|
+
default_env = cls._get_shell_env([])
|
|
98
|
+
and_script_env = cls._get_shell_env(scripts)
|
|
99
|
+
vars_just_in_script = set(and_script_env) - set(default_env)
|
|
100
|
+
union_env = dict(default_env)
|
|
101
|
+
union_env.update(and_script_env)
|
|
102
|
+
return dict([(v, union_env[v]) for v in vars_just_in_script])
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _get_shell_env(scripts: List) -> Dict[str, Any]:
|
|
106
|
+
varbls = dict()
|
|
107
|
+
runme = ''.join([f'source {s} ; ' for s in scripts])
|
|
108
|
+
magic = f'--- ENVIRONMENT BEGIN {random.randint(0,64**5)} ---'
|
|
109
|
+
runme += f'/bin/echo -n "{magic}" ; /usr/bin/env -0'
|
|
110
|
+
with open('/dev/null', 'w') as null:
|
|
111
|
+
env = subprocess.Popen(runme, shell=True, stdin=null.fileno(),
|
|
112
|
+
stdout=subprocess.PIPE)
|
|
113
|
+
(out, err) = env.communicate()
|
|
114
|
+
out = out.decode()
|
|
115
|
+
begin = out.find(magic)
|
|
116
|
+
if begin < 0:
|
|
117
|
+
raise ShellScriptException(scripts, 'Cannot find magic string; '
|
|
118
|
+
'at least one script failed: ' + repr(out))
|
|
119
|
+
for entry in out[begin + len(magic):].split('\x00'):
|
|
120
|
+
iequal = entry.find('=')
|
|
121
|
+
varbls[entry[0:iequal]] = entry[iequal + 1:]
|
|
122
|
+
return varbls
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cast_strdict_as_dtypedict(ctx: Dict[str, str]) -> Dict[str, Any]:
|
|
126
|
+
"""
|
|
127
|
+
Environment variables are typically stored as str
|
|
128
|
+
This method attempts to translate those into datatypes
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
ctx : dict
|
|
132
|
+
dictionary with values as str
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
varbles : dict
|
|
136
|
+
dictionary with values as datatypes
|
|
137
|
+
"""
|
|
138
|
+
varbles = AttrDict()
|
|
139
|
+
for key, value in ctx.items():
|
|
140
|
+
varbles[key] = cast_as_dtype(value)
|
|
141
|
+
return varbles
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def cast_as_dtype(string: str) -> Union[str, int, float, bool, Any]:
|
|
145
|
+
"""
|
|
146
|
+
Cast a value into known datatype
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
string: str
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
value : str or int or float or datetime
|
|
153
|
+
default: str
|
|
154
|
+
"""
|
|
155
|
+
TRUTHS = ['y', 'yes', 't', 'true', '.t.', '.true.']
|
|
156
|
+
BOOLS = ['n', 'no', 'f', 'false', '.f.', '.false.'] + TRUTHS
|
|
157
|
+
BOOLS = [x.upper() for x in BOOLS] + BOOLS + ['Yes', 'No', 'True', 'False']
|
|
158
|
+
|
|
159
|
+
def _cast_or_not(type: Any, string: str):
|
|
160
|
+
try:
|
|
161
|
+
return type(string)
|
|
162
|
+
except ValueError:
|
|
163
|
+
return string
|
|
164
|
+
|
|
165
|
+
def _true_or_not(string: str):
|
|
166
|
+
try:
|
|
167
|
+
return string.lower() in TRUTHS
|
|
168
|
+
except AttributeError:
|
|
169
|
+
return string
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
return to_datetime(string) # Try as a datetime
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
if string in BOOLS: # Likely a boolean, convert to True/False
|
|
175
|
+
return _true_or_not(string)
|
|
176
|
+
elif '.' in string: # Likely a number and that too a float
|
|
177
|
+
return _cast_or_not(float, string)
|
|
178
|
+
else: # Still could be a number, may be an integer
|
|
179
|
+
return _cast_or_not(int, string)
|
wxflow/exceptions.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# pylint: disable=unused-argument
|
|
2
|
+
|
|
3
|
+
# ----
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from .logger import Logger, logit
|
|
8
|
+
|
|
9
|
+
logger = Logger(level="error", colored_log=True)
|
|
10
|
+
|
|
11
|
+
__all__ = ["WorkflowException", "msg_except_handle"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WorkflowException(Exception):
|
|
15
|
+
"""
|
|
16
|
+
Description
|
|
17
|
+
-----------
|
|
18
|
+
|
|
19
|
+
This is the base-class for all exceptions; it is a sub-class of
|
|
20
|
+
Exceptions.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
|
|
25
|
+
msg: str
|
|
26
|
+
|
|
27
|
+
A Python string containing a message to accompany the
|
|
28
|
+
exception.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@logit(logger)
|
|
33
|
+
def __init__(self: Exception, msg: str):
|
|
34
|
+
"""
|
|
35
|
+
Description
|
|
36
|
+
-----------
|
|
37
|
+
|
|
38
|
+
Creates a new WorkflowException object.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Define the base-class attributes.
|
|
43
|
+
logger.error(msg=msg)
|
|
44
|
+
super().__init__()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ----
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def msg_except_handle(err_cls: object) -> Callable:
|
|
51
|
+
"""
|
|
52
|
+
Description
|
|
53
|
+
-----------
|
|
54
|
+
|
|
55
|
+
This function provides a decorator to be used to raise specified
|
|
56
|
+
exceptions.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
|
|
61
|
+
err_cls: object
|
|
62
|
+
|
|
63
|
+
A Python object containing the WorkflowException subclass to
|
|
64
|
+
be used for exception raises.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
|
|
69
|
+
decorator: Callable
|
|
70
|
+
|
|
71
|
+
A Python decorator.
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
# Define the decorator function.
|
|
76
|
+
def decorator(func: Callable):
|
|
77
|
+
|
|
78
|
+
# Execute the caller function; proceed accordingly.
|
|
79
|
+
def call_function(msg: str) -> None:
|
|
80
|
+
|
|
81
|
+
# If an exception is encountered, raise the respective
|
|
82
|
+
# exception.
|
|
83
|
+
raise err_cls(msg=msg)
|
|
84
|
+
|
|
85
|
+
return call_function
|
|
86
|
+
|
|
87
|
+
return decorator
|