hcs-core 0.1.250__py3-none-any.whl → 0.1.316__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.
- hcs_core/__init__.py +1 -0
- hcs_core/ctxp/__init__.py +12 -4
- hcs_core/ctxp/_init.py +94 -22
- hcs_core/ctxp/built_in_cmds/_ut.py +4 -3
- hcs_core/ctxp/built_in_cmds/context.py +16 -1
- hcs_core/ctxp/built_in_cmds/profile.py +30 -11
- hcs_core/ctxp/cli_options.py +34 -13
- hcs_core/ctxp/cli_processor.py +33 -20
- hcs_core/ctxp/cmd_util.py +87 -0
- hcs_core/ctxp/config.py +1 -1
- hcs_core/ctxp/context.py +82 -3
- hcs_core/ctxp/data_util.py +56 -20
- hcs_core/ctxp/dispatcher.py +82 -0
- hcs_core/ctxp/duration.py +65 -0
- hcs_core/ctxp/extension.py +7 -6
- hcs_core/ctxp/fn_util.py +57 -0
- hcs_core/ctxp/fstore.py +39 -22
- hcs_core/ctxp/jsondot.py +259 -78
- hcs_core/ctxp/logger.py +7 -6
- hcs_core/ctxp/profile.py +53 -21
- hcs_core/ctxp/profile_store.py +1 -0
- hcs_core/ctxp/recent.py +3 -3
- hcs_core/ctxp/state.py +4 -3
- hcs_core/ctxp/task_schd.py +168 -0
- hcs_core/ctxp/telemetry.py +145 -0
- hcs_core/ctxp/template_util.py +21 -0
- hcs_core/ctxp/timeutil.py +11 -0
- hcs_core/ctxp/util.py +194 -33
- hcs_core/ctxp/var_template.py +3 -4
- hcs_core/plan/__init__.py +11 -5
- hcs_core/plan/base_provider.py +1 -0
- hcs_core/plan/core.py +29 -26
- hcs_core/plan/dag.py +15 -12
- hcs_core/plan/helper.py +4 -2
- hcs_core/plan/kop.py +21 -8
- hcs_core/plan/provider/dev/dummy.py +3 -3
- hcs_core/sglib/auth.py +137 -95
- hcs_core/sglib/cli_options.py +20 -5
- hcs_core/sglib/client_util.py +230 -62
- hcs_core/sglib/csp.py +73 -6
- hcs_core/sglib/ez_client.py +139 -41
- hcs_core/sglib/hcs_client.py +3 -9
- hcs_core/sglib/init.py +17 -0
- hcs_core/sglib/login_support.py +22 -83
- hcs_core/sglib/payload_util.py +3 -1
- hcs_core/sglib/requtil.py +38 -0
- hcs_core/sglib/utils.py +107 -0
- hcs_core/util/check_license.py +0 -2
- hcs_core/util/duration.py +6 -3
- hcs_core/util/job_view.py +35 -15
- hcs_core/util/pki_util.py +48 -1
- hcs_core/util/query_util.py +54 -8
- hcs_core/util/scheduler.py +3 -3
- hcs_core/util/ssl_util.py +1 -1
- hcs_core/util/versions.py +15 -12
- hcs_core-0.1.316.dist-info/METADATA +54 -0
- hcs_core-0.1.316.dist-info/RECORD +69 -0
- {hcs_core-0.1.250.dist-info → hcs_core-0.1.316.dist-info}/WHEEL +1 -2
- hcs_core-0.1.250.dist-info/METADATA +0 -36
- hcs_core-0.1.250.dist-info/RECORD +0 -59
- hcs_core-0.1.250.dist-info/top_level.txt +0 -1
hcs_core/ctxp/context.py
CHANGED
|
@@ -13,8 +13,12 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from . import jsondot
|
|
21
|
+
from .profile_store import fstore, profile_store
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
def _store() -> fstore:
|
|
@@ -25,11 +29,13 @@ def list() -> list:
|
|
|
25
29
|
return _store().keys()
|
|
26
30
|
|
|
27
31
|
|
|
28
|
-
def get(name: str, reload: bool = False, default=None) -> dotdict:
|
|
32
|
+
def get(name: str, reload: bool = False, default=None) -> jsondot.dotdict:
|
|
29
33
|
return _store().get(key=name, reload=reload, default=default)
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
def set(name: str, data: dict):
|
|
37
|
+
if data is None or len(data) == 0:
|
|
38
|
+
return _store().delete(name)
|
|
33
39
|
return _store().save(name, data)
|
|
34
40
|
|
|
35
41
|
|
|
@@ -43,3 +49,76 @@ def file(name: str):
|
|
|
43
49
|
|
|
44
50
|
def clear():
|
|
45
51
|
return _store().clear()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Context:
|
|
55
|
+
def __init__(self, name):
|
|
56
|
+
self.name = name
|
|
57
|
+
self._store = _store()
|
|
58
|
+
self._data = None
|
|
59
|
+
self._changed = False
|
|
60
|
+
|
|
61
|
+
def get(self, key: str, default=None):
|
|
62
|
+
return self._data.get(key, default)
|
|
63
|
+
|
|
64
|
+
def set(self, key: str, value: Any):
|
|
65
|
+
existing = self._data.get(key)
|
|
66
|
+
if existing != value:
|
|
67
|
+
self._changed = True
|
|
68
|
+
self._data[key] = value
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def remove(self, key: str):
|
|
72
|
+
if key in self._data:
|
|
73
|
+
self._changed = True
|
|
74
|
+
del self._data[key]
|
|
75
|
+
|
|
76
|
+
def __enter__(self):
|
|
77
|
+
self._data = self._store.get(key=self.name, reload=False, default={})
|
|
78
|
+
|
|
79
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
80
|
+
if self._changed:
|
|
81
|
+
self._store.save(key=self.name, data=self.data)
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def apply_variables(self, object: any, additional_vars: dict = None):
|
|
85
|
+
|
|
86
|
+
mapping = self._data.copy()
|
|
87
|
+
if additional_vars:
|
|
88
|
+
mapping.update(additional_vars)
|
|
89
|
+
|
|
90
|
+
# The Python default Template.substitute has too many limitations...
|
|
91
|
+
def substitute(text: str):
|
|
92
|
+
pattern = re.compile(r".*\${(.+?)}.*")
|
|
93
|
+
while True:
|
|
94
|
+
m = pattern.match(text)
|
|
95
|
+
if not m:
|
|
96
|
+
break
|
|
97
|
+
name = m.group(1)
|
|
98
|
+
text = text.replace("${" + name + "}", self.get(name))
|
|
99
|
+
return text
|
|
100
|
+
|
|
101
|
+
t = type(object)
|
|
102
|
+
try:
|
|
103
|
+
if t is str:
|
|
104
|
+
return substitute(object)
|
|
105
|
+
|
|
106
|
+
if t is dict:
|
|
107
|
+
text = json.dumps(object)
|
|
108
|
+
text = substitute(text)
|
|
109
|
+
return json.loads(text)
|
|
110
|
+
|
|
111
|
+
if t is jsondot.dotdict:
|
|
112
|
+
text = json.dumps(object)
|
|
113
|
+
text = substitute(text)
|
|
114
|
+
return jsondot.parse(text)
|
|
115
|
+
except KeyError as e:
|
|
116
|
+
raise Exception("Variable not defined in context") from e
|
|
117
|
+
|
|
118
|
+
def load_template(self, name: str):
|
|
119
|
+
# with open(name) as f:
|
|
120
|
+
# text = f.read()
|
|
121
|
+
# return apply_variables(text)
|
|
122
|
+
|
|
123
|
+
tmpl = jsondot.load(name)
|
|
124
|
+
return self.apply_variables(tmpl)
|
hcs_core/ctxp/data_util.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
from io import TextIOWrapper
|
|
2
|
-
from os import path, chmod
|
|
3
|
-
from typing import Tuple, Any
|
|
4
1
|
import json
|
|
5
|
-
import
|
|
2
|
+
import os
|
|
6
3
|
import re
|
|
4
|
+
from io import TextIOWrapper
|
|
5
|
+
from os import chmod, path
|
|
6
|
+
from typing import Any, Tuple
|
|
7
|
+
|
|
7
8
|
from .util import CtxpException
|
|
8
9
|
|
|
9
10
|
|
|
@@ -23,9 +24,13 @@ def load_data_file(file, default=None, format="auto"):
|
|
|
23
24
|
if format == "json":
|
|
24
25
|
data = json.loads(text)
|
|
25
26
|
elif format == "yaml" or format == "yml":
|
|
27
|
+
import yaml
|
|
28
|
+
|
|
26
29
|
data = yaml.safe_load(text)
|
|
27
30
|
else:
|
|
28
31
|
try:
|
|
32
|
+
import yaml
|
|
33
|
+
|
|
29
34
|
data = yaml.safe_load(text)
|
|
30
35
|
except Exception:
|
|
31
36
|
data = json.loads(text)
|
|
@@ -45,6 +50,8 @@ def load_data_file(file, default=None, format="auto"):
|
|
|
45
50
|
if ext == ".yaml" or ext == ".yml" or format == "yml" or format == "yaml":
|
|
46
51
|
if format != "auto" and format != "yaml" and format != "yml":
|
|
47
52
|
raise CtxpException(f"File extension does not match specified format. File={file}, format={format}")
|
|
53
|
+
import yaml
|
|
54
|
+
|
|
48
55
|
data = yaml.safe_load(text)
|
|
49
56
|
return default if data is None else data # handle empty file
|
|
50
57
|
return text
|
|
@@ -53,6 +60,8 @@ def load_data_file(file, default=None, format="auto"):
|
|
|
53
60
|
def save_data_file(data, file_name: str, format: str = "yaml", file_mod: int = 0):
|
|
54
61
|
with open(file_name, "w") as file:
|
|
55
62
|
if format == "yaml":
|
|
63
|
+
import yaml
|
|
64
|
+
|
|
56
65
|
# TODO
|
|
57
66
|
# yaml.safe_dump(data, file, sort_keys=False)
|
|
58
67
|
yaml.safe_dump(json.loads(json.dumps(data, default=vars)), file, sort_keys=False)
|
|
@@ -91,9 +100,7 @@ def strict_dict_to_class(data: dict, class_type):
|
|
|
91
100
|
value = strict_dict_to_class(value, field_type)
|
|
92
101
|
setattr(inst, field_name, value)
|
|
93
102
|
continue
|
|
94
|
-
raise ValueError(
|
|
95
|
-
f"Field '{class_type.__name__}.{field_name}' has an incorrect type. Declared: {field_type}, actual: {type(value)}"
|
|
96
|
-
)
|
|
103
|
+
raise ValueError(f"Field '{class_type.__name__}.{field_name}' has an incorrect type. Declared: {field_type}, actual: {type(value)}")
|
|
97
104
|
return inst
|
|
98
105
|
|
|
99
106
|
|
|
@@ -152,7 +159,15 @@ def deep_set_attr(obj: dict, path: str, value, raise_on_not_found: bool = True):
|
|
|
152
159
|
try:
|
|
153
160
|
for i in range(len(parts)):
|
|
154
161
|
k = parts[i]
|
|
155
|
-
|
|
162
|
+
try:
|
|
163
|
+
sub_obj = _get_obj_attr(obj, k)
|
|
164
|
+
except KeyError:
|
|
165
|
+
if raise_on_not_found:
|
|
166
|
+
raise
|
|
167
|
+
sub_obj = {}
|
|
168
|
+
obj[k] = sub_obj
|
|
169
|
+
|
|
170
|
+
obj = sub_obj
|
|
156
171
|
|
|
157
172
|
if i == len(parts) - 2:
|
|
158
173
|
# found the one before the leaf.
|
|
@@ -244,22 +259,26 @@ def process_variables(obj: dict, fn_get_var=None, use_env: bool = True):
|
|
|
244
259
|
fn_get_var = _fn_get_var
|
|
245
260
|
|
|
246
261
|
if use_env:
|
|
247
|
-
import os
|
|
248
262
|
|
|
249
263
|
prev_fn_get_var = fn_get_var
|
|
250
264
|
|
|
251
265
|
def _fn_get_var_from_env(name: str):
|
|
252
266
|
if not name.startswith("env."):
|
|
253
267
|
return prev_fn_get_var(name)
|
|
254
|
-
v = None
|
|
255
268
|
actual_name = name[4:]
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
if actual_name.endswith("?"):
|
|
270
|
+
# optional
|
|
271
|
+
actual_name = actual_name[:-1]
|
|
272
|
+
required = False
|
|
273
|
+
else:
|
|
274
|
+
# required
|
|
275
|
+
required = True
|
|
276
|
+
|
|
277
|
+
if actual_name not in os.environ:
|
|
278
|
+
if required:
|
|
279
|
+
raise CtxpException(f"Environment variable '{actual_name}' is used in template, but not found. ")
|
|
280
|
+
return None, True
|
|
281
|
+
return os.environ[actual_name], True
|
|
263
282
|
|
|
264
283
|
fn_get_var = _fn_get_var_from_env
|
|
265
284
|
|
|
@@ -320,9 +339,7 @@ def resolve_expression(expr, fn_get_value, referencing_attr_path) -> Tuple[Any,
|
|
|
320
339
|
f"Invalid variable value for expression. Expect list, actual {type(target_value).__name__}. attr_path={referencing_attr_path}, src_var_name={src_var_name}"
|
|
321
340
|
)
|
|
322
341
|
if not mapped_value.startswith(tmp_var_name + "."):
|
|
323
|
-
raise CtxpException(
|
|
324
|
-
f"Unsupported expression. attr_path={referencing_attr_path}, src_var_name={src_var_name}"
|
|
325
|
-
)
|
|
342
|
+
raise CtxpException(f"Unsupported expression. attr_path={referencing_attr_path}, src_var_name={src_var_name}")
|
|
326
343
|
new_attr_path = mapped_value[len(tmp_var_name) + 1 :]
|
|
327
344
|
ret = []
|
|
328
345
|
for i in target_value:
|
|
@@ -375,6 +392,25 @@ def get_common_items(iter1, iter2):
|
|
|
375
392
|
return common_items
|
|
376
393
|
|
|
377
394
|
|
|
395
|
+
def get_delta(dict_base, dict_update):
|
|
396
|
+
delta = {}
|
|
397
|
+
for k, v2 in dict_update.items():
|
|
398
|
+
v1 = dict_base.get(k)
|
|
399
|
+
if not deep_equals(v1, v2):
|
|
400
|
+
delta[k] = v2
|
|
401
|
+
return delta
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def deep_equals(v1, v2):
|
|
405
|
+
if v1 is None and v2 is None:
|
|
406
|
+
return True
|
|
407
|
+
if v1 is None or v2 is None:
|
|
408
|
+
return False
|
|
409
|
+
if v1 == v2:
|
|
410
|
+
return True
|
|
411
|
+
return json.dumps(v1) == json.dumps(v2)
|
|
412
|
+
|
|
413
|
+
|
|
378
414
|
def _evaluate(value, smart_search):
|
|
379
415
|
# If we found an exact match, return 2 immediately
|
|
380
416
|
if value == smart_search:
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from weakref import WeakSet
|
|
3
|
+
|
|
4
|
+
log = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Dispatcher:
|
|
8
|
+
_registry: dict = {}
|
|
9
|
+
|
|
10
|
+
def register(self, key: str, fn: callable):
|
|
11
|
+
listeners = self._registry.get(key)
|
|
12
|
+
if not listeners:
|
|
13
|
+
listeners = WeakSet()
|
|
14
|
+
self._registry[key] = listeners
|
|
15
|
+
listeners.add(fn)
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
def register_map(self, map: dict):
|
|
19
|
+
for k, v in map.items():
|
|
20
|
+
self.register(k, v)
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
def unregister(self, key: str, fn: callable):
|
|
24
|
+
listeners = self._registry.get(key)
|
|
25
|
+
if listeners:
|
|
26
|
+
listeners.remove(fn)
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
def unregister_map(self, map: dict):
|
|
30
|
+
for k, v in map.items():
|
|
31
|
+
self.unregister(k, v)
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def emit(self, key: str, *args, **kwargs):
|
|
35
|
+
listeners = self._registry.get(key)
|
|
36
|
+
if listeners:
|
|
37
|
+
copy = listeners.copy()
|
|
38
|
+
for fn in copy:
|
|
39
|
+
try:
|
|
40
|
+
fn(*args, **kwargs)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
log.error(f"Error handling event: {key}", e)
|
|
43
|
+
log.exception(e)
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def scope(self, events):
|
|
47
|
+
return StrictDispatcher(self, events)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class StrictDispatcher:
|
|
51
|
+
def __init__(self, impl: Dispatcher, events: set):
|
|
52
|
+
self._impl = impl
|
|
53
|
+
self._events = events
|
|
54
|
+
|
|
55
|
+
def register(self, key: str, fn: callable):
|
|
56
|
+
self._validate(key)
|
|
57
|
+
self._impl.register(key, fn)
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def register_map(self, map: dict):
|
|
61
|
+
for k, v in map.items():
|
|
62
|
+
self.register(k, v)
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def unregister(self, key: str, fn: callable):
|
|
66
|
+
self._validate(key)
|
|
67
|
+
self._impl.unregister(key, fn)
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def unregister_map(self, map: dict):
|
|
71
|
+
for k, v in map.items():
|
|
72
|
+
self.unregister(k, v)
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def emit(self, key: str, *args, **kwargs):
|
|
76
|
+
self._validate(key)
|
|
77
|
+
self._impl.emit(key, *args, **kwargs)
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def _validate(self, key: str):
|
|
81
|
+
if key not in self._events:
|
|
82
|
+
raise Exception("Event is not in dispatcher scope: %s. The scoped events are: %s " % (key, self._events))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Utility for Java Duration interoperability
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
PATTERN = "([-+]?)P(?:([-+]?[0-9]+)D)?(T(?:([-+]?[0-9]+)H)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)(?:[.,]([0-9]{0,9}))?S)?)?"
|
|
6
|
+
|
|
7
|
+
# Examples
|
|
8
|
+
|
|
9
|
+
# PT0.555S
|
|
10
|
+
# PT9M15S
|
|
11
|
+
# PT9H15M
|
|
12
|
+
# PT555H
|
|
13
|
+
# PT13320H
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def to_seconds(duration_string: str):
|
|
17
|
+
duration_string = duration_string.upper()
|
|
18
|
+
|
|
19
|
+
p = re.compile(PATTERN)
|
|
20
|
+
matcher = p.fullmatch(duration_string)
|
|
21
|
+
if matcher is None:
|
|
22
|
+
raise Exception("Unsupported duration format: %s" % duration_string)
|
|
23
|
+
|
|
24
|
+
if matcher.start(3) >= 0 and matcher.end(3) == matcher.start(3) + 1 and duration_string[matcher.start(3)] == "T":
|
|
25
|
+
raise Exception("Unsupported duration format: %s" % duration_string)
|
|
26
|
+
|
|
27
|
+
day_start = matcher.start(2)
|
|
28
|
+
day_end = matcher.end(2)
|
|
29
|
+
hour_start = matcher.start(4)
|
|
30
|
+
hour_end = matcher.end(4)
|
|
31
|
+
minute_start = matcher.start(5)
|
|
32
|
+
minute_end = matcher.end(5)
|
|
33
|
+
second_start = matcher.start(6)
|
|
34
|
+
second_end = matcher.end(6)
|
|
35
|
+
|
|
36
|
+
total_seconds = 0
|
|
37
|
+
if day_start >= 0:
|
|
38
|
+
days = int(duration_string[day_start:day_end])
|
|
39
|
+
total_seconds += days * 24 * 3600
|
|
40
|
+
if hour_start >= 0:
|
|
41
|
+
hours = int(duration_string[hour_start:hour_end])
|
|
42
|
+
total_seconds += hours * 3600
|
|
43
|
+
if minute_start >= 0:
|
|
44
|
+
minutes = int(duration_string[minute_start:minute_end])
|
|
45
|
+
total_seconds += minutes * 60
|
|
46
|
+
if second_start >= 0:
|
|
47
|
+
seconds = int(duration_string[second_start:second_end])
|
|
48
|
+
total_seconds += seconds
|
|
49
|
+
|
|
50
|
+
return total_seconds
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
assert to_seconds("PT5S") == 5
|
|
55
|
+
assert to_seconds("PT5M") == 5 * 60
|
|
56
|
+
assert to_seconds("PT5H") == 5 * 3600
|
|
57
|
+
assert to_seconds("PT2M3S") == 2 * 60 + 3
|
|
58
|
+
assert to_seconds("PT2H3M") == 2 * 3600 + 3 * 60
|
|
59
|
+
assert to_seconds("PT2H3S") == 2 * 3600 + 3
|
|
60
|
+
assert to_seconds("PT2H3M4S") == 2 * 3600 + 3 * 60 + 4
|
|
61
|
+
assert to_seconds("P1D") == 1 * 24 * 60 * 60
|
|
62
|
+
assert to_seconds("P1DT1M") == 1 * 24 * 60 * 60 + 1 * 60
|
|
63
|
+
assert to_seconds("P1DT1S") == 1 * 24 * 60 * 60 + 1
|
|
64
|
+
assert to_seconds("P1DT1H1S") == 1 * 24 * 60 * 60 + 1 * 3600 + 1
|
|
65
|
+
print("SUCCESS")
|
hcs_core/ctxp/extension.py
CHANGED
|
@@ -13,16 +13,18 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
import click
|
|
17
|
-
import sys
|
|
18
16
|
import subprocess
|
|
19
|
-
|
|
20
|
-
import
|
|
17
|
+
import sys
|
|
18
|
+
from importlib import import_module, reload
|
|
19
|
+
from importlib.metadata import distributions
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
|
|
21
23
|
from .util import CtxpException
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def ensure_extension(required: str, parent_module_name: str = None):
|
|
25
|
-
installed = {
|
|
27
|
+
installed = {dist.metadata["Name"].lower() for dist in distributions()}
|
|
26
28
|
if required in installed:
|
|
27
29
|
return
|
|
28
30
|
|
|
@@ -43,4 +45,3 @@ def ensure_extension(required: str, parent_module_name: str = None):
|
|
|
43
45
|
reload(m)
|
|
44
46
|
else:
|
|
45
47
|
import_module(required.replace("-", "_"))
|
|
46
|
-
reload(pkg_resources)
|
hcs_core/ctxp/fn_util.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import os
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def debounce(graceful_seconds):
|
|
7
|
+
"""
|
|
8
|
+
Delay a function call by graceful_seconds. If an additional call happens during this time,
|
|
9
|
+
the delay will be reset.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def decorator(function):
|
|
13
|
+
def wrapper(*args, **kwargs):
|
|
14
|
+
def call_impl():
|
|
15
|
+
wrapper._timer = None
|
|
16
|
+
return function(*args, **kwargs)
|
|
17
|
+
|
|
18
|
+
# Cancel existing timer, if any
|
|
19
|
+
if wrapper._timer is not None:
|
|
20
|
+
wrapper._timer.cancel()
|
|
21
|
+
|
|
22
|
+
# Schedule debounced run
|
|
23
|
+
wrapper._timer = threading.Timer(graceful_seconds, call_impl)
|
|
24
|
+
wrapper._timer.start()
|
|
25
|
+
|
|
26
|
+
wrapper._timer = None
|
|
27
|
+
return wrapper
|
|
28
|
+
|
|
29
|
+
return decorator
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def once(fn):
|
|
33
|
+
"""
|
|
34
|
+
Raise exception if the function called more than once.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def wrapper(*args, **kwargs):
|
|
38
|
+
if wrapper._invoked:
|
|
39
|
+
raise Exception("Duplicated invocation: %s, %s" % (_get_caller_file(), fn.__name__))
|
|
40
|
+
wrapper._invoked = True
|
|
41
|
+
return fn(*args, **kwargs)
|
|
42
|
+
|
|
43
|
+
wrapper._invoked = False
|
|
44
|
+
return wrapper
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_caller_file():
|
|
48
|
+
# first get the full filename (including path and file extension)
|
|
49
|
+
caller_frame = inspect.stack()[2]
|
|
50
|
+
caller_filename_full = caller_frame.filename
|
|
51
|
+
|
|
52
|
+
# now get rid of the directory (via basename)
|
|
53
|
+
# then split filename and extension (via splitext)
|
|
54
|
+
caller_filename_only = os.path.splitext(os.path.basename(caller_filename_full))[0]
|
|
55
|
+
|
|
56
|
+
# return both filename versions as tuple
|
|
57
|
+
return caller_filename_only
|
hcs_core/ctxp/fstore.py
CHANGED
|
@@ -13,16 +13,15 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
import os
|
|
17
16
|
import logging
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
20
19
|
from collections.abc import Generator
|
|
20
|
+
from os import listdir, path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
21
23
|
from . import jsondot
|
|
22
24
|
from .jsondot import dotdict
|
|
23
|
-
import yaml
|
|
24
|
-
import shutil
|
|
25
|
-
|
|
26
25
|
|
|
27
26
|
log = logging.getLogger(__name__)
|
|
28
27
|
|
|
@@ -30,12 +29,15 @@ log = logging.getLogger(__name__)
|
|
|
30
29
|
def _validate_key(key: str):
|
|
31
30
|
# if key.find(os.pathsep) >= 0:
|
|
32
31
|
if key.find("/") >= 0 or key.find("\\") >= 0:
|
|
33
|
-
raise Exception("Invalid
|
|
32
|
+
raise Exception("Invalid key: " + key)
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
def _load_yaml(file_name: str):
|
|
37
36
|
if not os.path.exists(file_name):
|
|
38
37
|
return
|
|
38
|
+
|
|
39
|
+
import yaml
|
|
40
|
+
|
|
39
41
|
with open(file_name, "r") as file:
|
|
40
42
|
ret = yaml.safe_load(file)
|
|
41
43
|
return jsondot.dotify(ret)
|
|
@@ -120,21 +122,12 @@ class fstore:
|
|
|
120
122
|
|
|
121
123
|
Args:
|
|
122
124
|
store_path (str): The path to store state files. If None, state will not be stored.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
Raises:
|
|
126
|
-
Exception: [description]
|
|
125
|
+
create (bool, optional): If store_path is specified, try creating it if not exist. Defaults to True.
|
|
127
126
|
"""
|
|
128
127
|
if store_path:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
os.makedirs(store_path)
|
|
133
|
-
else:
|
|
134
|
-
raise Exception(f"Store path does not exist: {store_path}")
|
|
135
|
-
elif not os.path.isdir(store_path):
|
|
136
|
-
raise Exception(f"Store path is not a directory: {store_path}")
|
|
137
|
-
self._path = store_path
|
|
128
|
+
self._path = path.realpath(store_path)
|
|
129
|
+
self._create = create
|
|
130
|
+
self._path_checked = False
|
|
138
131
|
else:
|
|
139
132
|
self._path = None
|
|
140
133
|
self._cache = {}
|
|
@@ -165,6 +158,23 @@ class fstore:
|
|
|
165
158
|
return None
|
|
166
159
|
return path.join(self._path, key)
|
|
167
160
|
|
|
161
|
+
def _ensure_dir(self):
|
|
162
|
+
if not self._path or self._path_checked:
|
|
163
|
+
return
|
|
164
|
+
if path.exists(self._path):
|
|
165
|
+
if not os.path.isdir(self._path):
|
|
166
|
+
raise Exception(f"Store path is not a directory: {self._path}")
|
|
167
|
+
else:
|
|
168
|
+
# Good.
|
|
169
|
+
self._path_checked = True
|
|
170
|
+
return
|
|
171
|
+
else:
|
|
172
|
+
if self._create:
|
|
173
|
+
os.makedirs(self._path, exist_ok=True)
|
|
174
|
+
else:
|
|
175
|
+
raise Exception(f"Store path does not exist: {self._path}")
|
|
176
|
+
self._path_checked = True
|
|
177
|
+
|
|
168
178
|
def save(self, key: str, data: Any, format: str = "auto") -> Any:
|
|
169
179
|
_validate_key(key)
|
|
170
180
|
|
|
@@ -178,6 +188,7 @@ class fstore:
|
|
|
178
188
|
|
|
179
189
|
self._cache[key] = data
|
|
180
190
|
if self._path:
|
|
191
|
+
self._ensure_dir()
|
|
181
192
|
file_path = self._get_path(key)
|
|
182
193
|
log.debug(f"Write {file_path}")
|
|
183
194
|
if format == "json":
|
|
@@ -187,8 +198,10 @@ class fstore:
|
|
|
187
198
|
# TODO lock
|
|
188
199
|
outfile.write(str(data))
|
|
189
200
|
elif format == "json-compact":
|
|
190
|
-
jsondot.save(data, file_path,
|
|
191
|
-
elif format == "yaml":
|
|
201
|
+
jsondot.save(data=data, file=file_path, pretty=False, lock=True)
|
|
202
|
+
elif format == "yaml" or format == "yml":
|
|
203
|
+
import yaml
|
|
204
|
+
|
|
192
205
|
with open(file_path, "w", encoding="utf-8") as outfile:
|
|
193
206
|
# TODO lock
|
|
194
207
|
yaml.safe_dump(data, outfile)
|
|
@@ -208,12 +221,16 @@ class fstore:
|
|
|
208
221
|
|
|
209
222
|
def keys(self) -> list[str]:
|
|
210
223
|
if self._path:
|
|
224
|
+
if not os.path.exists(self._path):
|
|
225
|
+
return []
|
|
211
226
|
return [f for f in listdir(self._path) if path.isfile(path.join(self._path, f))]
|
|
212
227
|
return list(self._cache.keys())
|
|
213
228
|
|
|
214
229
|
def children(self, depth: int = 0) -> list[str]:
|
|
215
230
|
"""List child stores"""
|
|
216
231
|
if self._path:
|
|
232
|
+
if not os.path.exists(self._path):
|
|
233
|
+
return []
|
|
217
234
|
if depth == 0:
|
|
218
235
|
return [f for f in listdir(self._path) if path.isdir(path.join(self._path, f))]
|
|
219
236
|
else:
|