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 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)
@@ -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