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/jsondot.py
CHANGED
|
@@ -13,15 +13,6 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
import json
|
|
17
|
-
import os.path
|
|
18
|
-
import copy
|
|
19
|
-
import time
|
|
20
|
-
import random
|
|
21
|
-
import portalocker
|
|
22
|
-
from typing import Any
|
|
23
|
-
|
|
24
|
-
|
|
25
16
|
"""
|
|
26
17
|
jsondot is utility to make json/dict object accessible in the "." way.
|
|
27
18
|
|
|
@@ -44,133 +35,323 @@ print(my_dict.key1.key2)
|
|
|
44
35
|
"""
|
|
45
36
|
|
|
46
37
|
|
|
38
|
+
import copy
|
|
39
|
+
import json
|
|
40
|
+
import os.path
|
|
41
|
+
import random
|
|
42
|
+
import time
|
|
43
|
+
from typing import Any, Dict, Optional, Union
|
|
44
|
+
|
|
45
|
+
|
|
47
46
|
class dotdict(dict):
|
|
48
|
-
"""dot.notation access to dictionary attributes
|
|
47
|
+
"""dot.notation access to dictionary attributes with enhanced safety.
|
|
48
|
+
|
|
49
|
+
A dictionary subclass that provides attribute-style access to its elements
|
|
50
|
+
while maintaining compatibility with regular dict operations.
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
>>> d = dotdict({'a': 1, 'b': {'c': 2}})
|
|
54
|
+
>>> d.a
|
|
55
|
+
1
|
|
56
|
+
>>> d.b.c
|
|
57
|
+
2
|
|
58
|
+
>>> d.b.c = 3
|
|
59
|
+
>>> d.b.c
|
|
60
|
+
3
|
|
61
|
+
"""
|
|
49
62
|
|
|
50
63
|
__getattr__ = dict.get
|
|
51
64
|
__setattr__ = dict.__setitem__
|
|
52
65
|
__delattr__ = dict.__delitem__
|
|
53
66
|
|
|
54
|
-
def __lt__(self, other):
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
def __lt__(self, other: Union[dict, "dotdict"]) -> bool:
|
|
68
|
+
"""Support proper sorting by comparing underlying dictionaries."""
|
|
69
|
+
if isinstance(other, dict):
|
|
70
|
+
return dict(self) < dict(other)
|
|
71
|
+
return NotImplemented
|
|
57
72
|
|
|
58
|
-
def
|
|
59
|
-
|
|
73
|
+
def __eq__(self, other: Union[dict, "dotdict"]) -> bool:
|
|
74
|
+
"""Support equality comparison with other dictionaries."""
|
|
75
|
+
if isinstance(other, dict):
|
|
76
|
+
return dict(self) == dict(other)
|
|
77
|
+
return NotImplemented
|
|
78
|
+
|
|
79
|
+
def __deepcopy__(self, memo: Optional[Dict] = None) -> "dotdict":
|
|
80
|
+
"""Support deep copying of the dictionary.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
memo: Memoization dictionary for handling circular references
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A deep copy of the dotdict instance
|
|
87
|
+
"""
|
|
60
88
|
new = {}
|
|
61
89
|
for key in self.keys():
|
|
62
90
|
new[key] = copy.deepcopy(self[key], memo=memo)
|
|
63
|
-
return new
|
|
91
|
+
return dotdict(new)
|
|
92
|
+
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
"""Return a string representation of the dictionary.
|
|
64
95
|
|
|
96
|
+
Returns:
|
|
97
|
+
A string showing this is a dotdict instance with its contents
|
|
98
|
+
"""
|
|
99
|
+
return dict.__repr__(self)
|
|
65
100
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
return dumper.represent_dict(data.items())
|
|
101
|
+
# def __getstate__(self):
|
|
102
|
+
# """Return state for serialization - just return the underlying dict"""
|
|
103
|
+
# return dict(self)
|
|
70
104
|
|
|
105
|
+
# def __setstate__(self, state):
|
|
106
|
+
# """Restore state from serialization"""
|
|
107
|
+
# self.update(state)
|
|
71
108
|
|
|
72
|
-
|
|
109
|
+
# def __reduce_ex__(self, protocol):
|
|
110
|
+
# return dict, (dict(self),)
|
|
73
111
|
|
|
74
|
-
|
|
112
|
+
# def __getattribute__(self, name):
|
|
113
|
+
# """Make this look like a plain dict to introspection"""
|
|
114
|
+
# if name in ('__getstate__', '__setstate__', '__reduce__', '__reduce_ex__', '__class__'):
|
|
115
|
+
# return getattr(dict, name)
|
|
116
|
+
# return super().__getattribute__(name)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _yaml_interoperability() -> None:
|
|
120
|
+
"""Set up YAML serialization support for dotdict objects.
|
|
121
|
+
|
|
122
|
+
Registers a custom representer with PyYAML to properly serialize
|
|
123
|
+
dotdict objects as regular dictionaries. This ensures dotdict
|
|
124
|
+
objects can be seamlessly used with YAML operations.
|
|
125
|
+
|
|
126
|
+
Note:
|
|
127
|
+
This function is automatically called during module initialization.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
import yaml
|
|
131
|
+
|
|
132
|
+
def _represent_dict(dumper: yaml.SafeDumper, data: dotdict) -> yaml.nodes.MappingNode:
|
|
133
|
+
return dumper.represent_dict(data.items())
|
|
134
|
+
|
|
135
|
+
yaml.SafeDumper.add_representer(dotdict, _represent_dict)
|
|
136
|
+
except ImportError:
|
|
137
|
+
pass # PyYAML not available, silently skip registration
|
|
75
138
|
|
|
76
139
|
|
|
77
140
|
def dotify(target: Any) -> Any:
|
|
78
|
-
"""Deeply convert an object from dict to dotdict
|
|
141
|
+
"""Deeply convert an object from dict to dotdict.
|
|
79
142
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return target
|
|
83
|
-
if isinstance(target, list):
|
|
84
|
-
for i in range(len(target)):
|
|
85
|
-
target[i] = dotify(target[i])
|
|
86
|
-
return target
|
|
87
|
-
if isinstance(target, dict):
|
|
88
|
-
for k in target:
|
|
89
|
-
target[k] = dotify(target[k])
|
|
90
|
-
return dotdict(target)
|
|
143
|
+
Recursively converts all nested dictionaries to dotdict instances
|
|
144
|
+
while preserving other data types. Handles circular references safely.
|
|
91
145
|
|
|
92
|
-
|
|
93
|
-
|
|
146
|
+
Args:
|
|
147
|
+
target: The object to convert
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The converted object with all dicts replaced by dotdict instances
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
RecursionError: If maximum recursion depth is exceeded
|
|
154
|
+
"""
|
|
155
|
+
# Handle circular references with recursion depth check
|
|
156
|
+
if getattr(dotify, "_depth", 0) > getattr(dotify, "_max_depth", 1000):
|
|
157
|
+
raise RecursionError("Maximum recursion depth exceeded")
|
|
158
|
+
|
|
159
|
+
dotify._depth = getattr(dotify, "_depth", 0) + 1
|
|
160
|
+
try:
|
|
161
|
+
if isinstance(target, dotdict):
|
|
162
|
+
return target
|
|
163
|
+
if isinstance(target, list):
|
|
164
|
+
return [dotify(item) for item in target]
|
|
165
|
+
if isinstance(target, dict):
|
|
166
|
+
return dotdict({k: dotify(v) for k, v in target.items()})
|
|
167
|
+
return target
|
|
168
|
+
finally:
|
|
169
|
+
dotify._depth -= 1
|
|
94
170
|
|
|
95
171
|
|
|
96
172
|
def undot(target: Any) -> Any:
|
|
97
|
-
"""Deeply convert an object from dotdict to plain
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
173
|
+
"""Deeply convert an object from dotdict to plain dict.
|
|
174
|
+
|
|
175
|
+
Recursively converts all nested dotdict instances to regular dictionaries
|
|
176
|
+
while preserving other data types. Handles circular references safely.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
target: The object to convert
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
The converted object with all dotdict instances replaced by plain dicts
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
RecursionError: If maximum recursion depth is exceeded
|
|
186
|
+
"""
|
|
187
|
+
# Handle circular references with recursion depth check
|
|
188
|
+
if getattr(undot, "_depth", 0) > getattr(undot, "_max_depth", 1000):
|
|
189
|
+
raise RecursionError("Maximum recursion depth exceeded")
|
|
190
|
+
|
|
191
|
+
undot._depth = getattr(undot, "_depth", 0) + 1
|
|
192
|
+
try:
|
|
193
|
+
if isinstance(target, list):
|
|
194
|
+
return [undot(item) for item in target]
|
|
195
|
+
if isinstance(target, dict):
|
|
196
|
+
return {k: undot(v) for k, v in target.items()}
|
|
101
197
|
return target
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for k in target:
|
|
105
|
-
ret[k] = undot(target[k])
|
|
106
|
-
return ret
|
|
107
|
-
return target
|
|
198
|
+
finally:
|
|
199
|
+
undot._depth -= 1
|
|
108
200
|
|
|
109
201
|
|
|
110
|
-
def _is_primitive(obj):
|
|
111
|
-
|
|
202
|
+
def _is_primitive(obj: Any) -> bool:
|
|
203
|
+
"""Check if the object is a primitive type.
|
|
204
|
+
|
|
205
|
+
Determines if an object is a basic Python type that doesn't need
|
|
206
|
+
conversion when serializing/deserializing.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
obj: Any Python object
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
bool: True if the object is a primitive type (str, bool, int, float, None)
|
|
213
|
+
"""
|
|
214
|
+
return obj is None or isinstance(obj, (str, bool, int, float))
|
|
112
215
|
|
|
113
216
|
|
|
114
217
|
def plain(target: Any) -> Any:
|
|
115
|
-
"""
|
|
218
|
+
"""Convert a complex object structure to plain Python types.
|
|
219
|
+
|
|
220
|
+
Recursively converts all objects to their basic Python equivalents,
|
|
221
|
+
making the structure suitable for serialization. Handles nested
|
|
222
|
+
dictionaries, lists, and primitive types.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
target: The object to convert
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
The converted object with all complex types replaced by basic Python types
|
|
229
|
+
|
|
230
|
+
Examples:
|
|
231
|
+
>>> d = dotdict({'a': 1, 'b': {'c': 2}})
|
|
232
|
+
>>> plain(d)
|
|
233
|
+
{'a': 1, 'b': {'c': 2}}
|
|
234
|
+
"""
|
|
116
235
|
if _is_primitive(target):
|
|
117
236
|
return target
|
|
118
|
-
|
|
119
237
|
if isinstance(target, list):
|
|
120
|
-
for
|
|
121
|
-
target[i] = plain(target[i])
|
|
122
|
-
return target
|
|
238
|
+
return [plain(item) for item in target]
|
|
123
239
|
if isinstance(target, dict):
|
|
124
|
-
for k in target
|
|
125
|
-
|
|
126
|
-
return dict(target)
|
|
240
|
+
return {k: plain(v) for k, v in target.items()}
|
|
241
|
+
return target
|
|
127
242
|
|
|
128
243
|
|
|
129
244
|
def parse(text: str) -> dotdict:
|
|
130
|
-
|
|
131
|
-
|
|
245
|
+
"""Parse a JSON string into a dotdict object.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
text: JSON string to parse
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
dotdict: The parsed and converted data
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
JSONDecodeError: If the string contains invalid JSON
|
|
255
|
+
TypeError: If input is not a string
|
|
256
|
+
"""
|
|
257
|
+
if not isinstance(text, str):
|
|
258
|
+
raise TypeError("Input must be a string")
|
|
259
|
+
try:
|
|
260
|
+
dict_data = json.loads(text)
|
|
261
|
+
return dotify(dict_data)
|
|
262
|
+
except json.JSONDecodeError as e:
|
|
263
|
+
raise Exception(f"Invalid JSON: {str(e)}") from e
|
|
132
264
|
|
|
133
265
|
|
|
134
|
-
def _lock_with_retry(file, for_read: bool):
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
266
|
+
def _lock_with_retry(file: str, for_read: bool, max_retry: int = 6, initial_backoff: float = 0.05) -> None:
|
|
267
|
+
"""Attempt to lock a file with retries using exponential backoff.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
file: File handle to lock
|
|
271
|
+
for_read: If True, acquire shared lock; if False, acquire exclusive lock
|
|
272
|
+
max_retry: Maximum number of retry attempts
|
|
273
|
+
initial_backoff: Initial backoff time in seconds
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
Exception: If unable to acquire lock after max retries
|
|
277
|
+
portalocker.LockException: If locking fails
|
|
278
|
+
"""
|
|
279
|
+
import portalocker
|
|
280
|
+
|
|
281
|
+
flags = portalocker.LockFlags.SHARED if for_read else portalocker.LockFlags.EXCLUSIVE
|
|
139
282
|
|
|
140
|
-
max_retry = 6
|
|
141
283
|
retry = 0
|
|
142
|
-
backoff_seconds =
|
|
284
|
+
backoff_seconds = initial_backoff
|
|
143
285
|
while True:
|
|
144
286
|
try:
|
|
145
287
|
portalocker.lock(file, flags)
|
|
146
288
|
return
|
|
147
|
-
except
|
|
289
|
+
except portalocker.LockException as e:
|
|
148
290
|
retry += 1
|
|
149
291
|
if retry > max_retry:
|
|
150
|
-
raise Exception(f"
|
|
292
|
+
raise Exception(f"Failed to acquire lock for file {file} after {max_retry} attempts") from e
|
|
151
293
|
random_delay = backoff_seconds + random.uniform(0, 0.1)
|
|
152
294
|
time.sleep(random_delay)
|
|
153
|
-
backoff_seconds
|
|
295
|
+
backoff_seconds *= 2
|
|
296
|
+
|
|
154
297
|
|
|
298
|
+
def load(file: str, default: Any = None, lock: bool = False) -> dotdict:
|
|
299
|
+
"""Load and parse a JSON file into a dotdict object.
|
|
155
300
|
|
|
156
|
-
|
|
301
|
+
Args:
|
|
302
|
+
file: Path to the JSON file
|
|
303
|
+
default: Value to return if file doesn't exist
|
|
304
|
+
lock: Whether to use file locking
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
dotdict: The loaded and converted data
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
FileNotFoundError: If file doesn't exist and no default provided
|
|
311
|
+
JSONDecodeError: If file contains invalid JSON
|
|
312
|
+
IOError: If file access fails
|
|
313
|
+
"""
|
|
157
314
|
try:
|
|
158
315
|
if not os.path.exists(file):
|
|
316
|
+
if default is None:
|
|
317
|
+
raise FileNotFoundError(f"File not found: {file}")
|
|
159
318
|
return dotify(default)
|
|
319
|
+
|
|
160
320
|
with open(file) as json_file:
|
|
161
321
|
if lock:
|
|
162
322
|
_lock_with_retry(json_file, True)
|
|
163
323
|
dict = json.load(json_file)
|
|
164
324
|
return dotify(dict)
|
|
325
|
+
except json.JSONDecodeError as e:
|
|
326
|
+
raise Exception(f"Invalid JSON in file {file}: {str(e)}") from e
|
|
327
|
+
except IOError as e:
|
|
328
|
+
raise Exception(f"Error accessing file {file}: {str(e)}") from e
|
|
165
329
|
except Exception as e:
|
|
166
|
-
raise Exception(f"
|
|
330
|
+
raise Exception(f"Unexpected error loading file {file}: {str(e)}") from e
|
|
331
|
+
|
|
167
332
|
|
|
333
|
+
def save(data: dict, file: str, pretty: bool = True, lock: bool = False) -> None:
|
|
334
|
+
"""Save data as JSON to a file with optional pretty printing and locking.
|
|
168
335
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
336
|
+
Args:
|
|
337
|
+
data: Dictionary data to save
|
|
338
|
+
file: Path to the output JSON file
|
|
339
|
+
pretty: If True, format JSON with indentation
|
|
340
|
+
lock: Whether to use file locking
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
IOError: If file access fails
|
|
344
|
+
TypeError: If data contains non-serializable objects
|
|
345
|
+
"""
|
|
346
|
+
try:
|
|
347
|
+
os.makedirs(os.path.dirname(file), exist_ok=True)
|
|
348
|
+
with open(file, "w") as outfile:
|
|
349
|
+
if lock:
|
|
350
|
+
_lock_with_retry(outfile, False)
|
|
351
|
+
json.dump(data, outfile, indent="\t" if pretty else None, default=vars)
|
|
352
|
+
except IOError as e:
|
|
353
|
+
raise Exception(f"Error writing to file {file}: {str(e)}") from e
|
|
354
|
+
except TypeError as e:
|
|
355
|
+
raise Exception(f"Error serializing data to JSON: {str(e)}") from e
|
|
356
|
+
except Exception as e:
|
|
357
|
+
raise Exception(f"Unexpected error saving file {file}: {str(e)}") from e
|
hcs_core/ctxp/logger.py
CHANGED
|
@@ -13,14 +13,17 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import logging
|
|
16
17
|
import os
|
|
17
18
|
import re
|
|
18
|
-
import logging
|
|
19
|
-
import coloredlogs
|
|
20
19
|
from logging.handlers import RotatingFileHandler
|
|
21
20
|
|
|
21
|
+
import coloredlogs
|
|
22
|
+
|
|
22
23
|
LOG_FORMAT_SIMPLE = "%(levelname).4s %(asctime)s %(name)-16s %(message)s"
|
|
23
|
-
LOG_FORMAT_LONG =
|
|
24
|
+
LOG_FORMAT_LONG = (
|
|
25
|
+
"%(color_on)s%(asctime)s.%(msecs)03d [%(process)-5d:%(threadName)-12s] %(levelname)-7s [%(name)-16s] %(message)s%(color_off)s"
|
|
26
|
+
)
|
|
24
27
|
DATE_FORMAT_SIMPLE = "%H:%M:%S"
|
|
25
28
|
|
|
26
29
|
|
|
@@ -132,9 +135,7 @@ def setup(
|
|
|
132
135
|
)
|
|
133
136
|
|
|
134
137
|
|
|
135
|
-
def setup_console_output(
|
|
136
|
-
logger, console_log_output, console_log_level, console_log_color, console_log_mask, log_line_template, date_fmt
|
|
137
|
-
):
|
|
138
|
+
def setup_console_output(logger, console_log_output, console_log_level, console_log_color, console_log_mask, log_line_template, date_fmt):
|
|
138
139
|
if not date_fmt:
|
|
139
140
|
date_fmt = coloredlogs.DEFAULT_DATE_FORMAT
|
|
140
141
|
coloredlogs.install(
|
hcs_core/ctxp/profile.py
CHANGED
|
@@ -13,15 +13,16 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
import os
|
|
17
17
|
import pathlib
|
|
18
18
|
import shutil
|
|
19
|
-
import
|
|
19
|
+
from copy import deepcopy
|
|
20
20
|
from typing import Any
|
|
21
|
-
|
|
22
|
-
from .jsondot import dotdict, dotify
|
|
21
|
+
|
|
23
22
|
from . import state
|
|
24
|
-
from .data_util import load_data_file, save_data_file
|
|
23
|
+
from .data_util import deep_set_attr, load_data_file, save_data_file
|
|
24
|
+
from .jsondot import dotdict, dotify
|
|
25
|
+
from .util import CtxpException, panic
|
|
25
26
|
|
|
26
27
|
_repo_path: str = None
|
|
27
28
|
_active_profile_name: str = None
|
|
@@ -31,6 +32,8 @@ _auth_cache: dict = None
|
|
|
31
32
|
|
|
32
33
|
def _set_active_profile_name(name):
|
|
33
34
|
global _active_profile_name
|
|
35
|
+
if _active_profile_name == name:
|
|
36
|
+
return
|
|
34
37
|
_active_profile_name = name
|
|
35
38
|
state.set("active_profile", name)
|
|
36
39
|
|
|
@@ -50,8 +53,8 @@ def path(profile_name: str = None) -> str:
|
|
|
50
53
|
return os.path.join(_repo_path, profile_name)
|
|
51
54
|
|
|
52
55
|
|
|
53
|
-
def create(name: str, data: dict, auto_use: bool = True):
|
|
54
|
-
if exists(name):
|
|
56
|
+
def create(name: str, data: dict, auto_use: bool = True, overwrite: bool = False):
|
|
57
|
+
if not overwrite and exists(name):
|
|
55
58
|
if auto_use:
|
|
56
59
|
use(name)
|
|
57
60
|
raise CtxpException("Profile already exists: " + name)
|
|
@@ -60,7 +63,7 @@ def create(name: str, data: dict, auto_use: bool = True):
|
|
|
60
63
|
save_data_file(data, file(name), "yaml", 0o600)
|
|
61
64
|
if auto_use:
|
|
62
65
|
use(name)
|
|
63
|
-
return get(name)
|
|
66
|
+
return get(name, reload=True)
|
|
64
67
|
|
|
65
68
|
|
|
66
69
|
def copy(from_name: str, to_name: str, overwrite: bool = True):
|
|
@@ -72,15 +75,14 @@ def copy(from_name: str, to_name: str, overwrite: bool = True):
|
|
|
72
75
|
create(to_name, deepcopy(data), auto_use=False)
|
|
73
76
|
|
|
74
77
|
|
|
75
|
-
def current(reload: bool = False, exit_on_failure=True, exclude_secret: bool = False) -> dict:
|
|
78
|
+
def current(reload: bool = False, exit_on_failure: bool = True, exclude_secret: bool = False) -> dict:
|
|
76
79
|
"""Get content of the current active profile"""
|
|
80
|
+
|
|
77
81
|
profile_name = name()
|
|
78
82
|
data = get(profile_name, reload)
|
|
79
83
|
|
|
80
84
|
if data is None and exit_on_failure:
|
|
81
|
-
panic(
|
|
82
|
-
"Profile not set. Use 'hcs profile use [profile-name]' to choose one, or use 'hcs profile init' to create default profiles."
|
|
83
|
-
)
|
|
85
|
+
panic("Profile not set. Use 'hcs profile use [profile-name]' to choose one, or use 'hcs profile init' to create default profiles.")
|
|
84
86
|
|
|
85
87
|
if exclude_secret:
|
|
86
88
|
data = dotdict(dict(data))
|
|
@@ -94,7 +96,6 @@ def current(reload: bool = False, exit_on_failure=True, exclude_secret: bool = F
|
|
|
94
96
|
|
|
95
97
|
def save():
|
|
96
98
|
"""Save the current profile"""
|
|
97
|
-
global _data
|
|
98
99
|
if _data is None:
|
|
99
100
|
return
|
|
100
101
|
write(name(), _data)
|
|
@@ -123,6 +124,8 @@ def use(name: str) -> str:
|
|
|
123
124
|
return
|
|
124
125
|
|
|
125
126
|
_set_active_profile_name(name)
|
|
127
|
+
global _data
|
|
128
|
+
_data = _load_data_file_with_env_overrides(name, None)
|
|
126
129
|
return name
|
|
127
130
|
|
|
128
131
|
|
|
@@ -138,6 +141,7 @@ def delete(profile_name: str = None) -> None:
|
|
|
138
141
|
|
|
139
142
|
if profile_name is None:
|
|
140
143
|
profile_name = name()
|
|
144
|
+
|
|
141
145
|
global _active_profile_name, _data
|
|
142
146
|
if _active_profile_name == profile_name:
|
|
143
147
|
_active_profile_name = "default"
|
|
@@ -152,14 +156,39 @@ def get(profile_name: str, reload: bool = False, default=None) -> dotdict:
|
|
|
152
156
|
global _data
|
|
153
157
|
if _data is not None and not reload:
|
|
154
158
|
return _data
|
|
155
|
-
data =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
data = _load_data_file_with_env_overrides(profile_name, default)
|
|
160
|
+
_data = data
|
|
161
|
+
else:
|
|
162
|
+
data = _load_data_file_with_env_overrides(profile_name, default)
|
|
163
|
+
return data
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _load_data_file_with_env_overrides(profile_name: str, default: dict) -> dict:
|
|
167
|
+
|
|
168
|
+
from ._init import app_name
|
|
169
|
+
|
|
170
|
+
my_name = app_name()
|
|
171
|
+
|
|
172
|
+
# Apply environment variables as defaults
|
|
173
|
+
env_prefix = my_name + "_profile_"
|
|
174
|
+
if os.environ.get(env_prefix + "mode") == "ENV":
|
|
175
|
+
if default:
|
|
176
|
+
data = dict(default)
|
|
177
|
+
else:
|
|
178
|
+
data = {}
|
|
159
179
|
else:
|
|
160
180
|
data = load_data_file(file(profile_name), default=default)
|
|
161
|
-
if data is
|
|
162
|
-
|
|
181
|
+
if data is None:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
for key, value in os.environ.items():
|
|
185
|
+
if key.startswith(env_prefix):
|
|
186
|
+
# Strip prefix to get profile key
|
|
187
|
+
profile_key = key[len(env_prefix) :]
|
|
188
|
+
profile_key = profile_key.replace("_", ".")
|
|
189
|
+
deep_set_attr(data, profile_key, value, raise_on_not_found=False)
|
|
190
|
+
|
|
191
|
+
data = dotify(data)
|
|
163
192
|
return data
|
|
164
193
|
|
|
165
194
|
|
|
@@ -194,8 +223,11 @@ class auth:
|
|
|
194
223
|
if json.dumps(data) == json.dumps(_auth_cache):
|
|
195
224
|
return
|
|
196
225
|
_auth_cache = deepcopy(data)
|
|
197
|
-
|
|
198
|
-
|
|
226
|
+
if os.environ.get("CTXP_AUTH_PERSIST", "true").lower() == "false":
|
|
227
|
+
pass
|
|
228
|
+
else:
|
|
229
|
+
file_name = auth._file_name()
|
|
230
|
+
save_data_file(data, file_name, "yaml", 0o600)
|
|
199
231
|
|
|
200
232
|
@staticmethod
|
|
201
233
|
def delete() -> None:
|
hcs_core/ctxp/profile_store.py
CHANGED
hcs_core/ctxp/recent.py
CHANGED
|
@@ -31,7 +31,7 @@ def all():
|
|
|
31
31
|
return _recent
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def require(
|
|
34
|
+
def require(k: str, provided):
|
|
35
35
|
if not k:
|
|
36
36
|
raise Exception("Missing 'key' in recent.require(current, key).")
|
|
37
37
|
|
|
@@ -79,9 +79,9 @@ def of(k: str):
|
|
|
79
79
|
|
|
80
80
|
class helper:
|
|
81
81
|
@staticmethod
|
|
82
|
-
def default_list(array: list, name: str):
|
|
82
|
+
def default_list(array: list, name: str, field_name: str = "id"):
|
|
83
83
|
with of(name) as r:
|
|
84
84
|
if len(array) == 1:
|
|
85
|
-
r.set(array[0][
|
|
85
|
+
r.set(array[0][field_name])
|
|
86
86
|
else:
|
|
87
87
|
r.unset()
|
hcs_core/ctxp/state.py
CHANGED
|
@@ -13,9 +13,10 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import os
|
|
16
17
|
import pathlib
|
|
17
18
|
from typing import Any
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
from . import jsondot
|
|
20
21
|
|
|
21
22
|
|
|
@@ -30,7 +31,7 @@ class _StateFile:
|
|
|
30
31
|
self._cache = jsondot.load(self._path, {}, lock=True)
|
|
31
32
|
return self._cache
|
|
32
33
|
|
|
33
|
-
def get(self, key: str, default: Any, reload: bool = False):
|
|
34
|
+
def get(self, key: str, default: Any = None, reload: bool = False):
|
|
34
35
|
return self._data(reload).get(key, default)
|
|
35
36
|
|
|
36
37
|
def set(self, key: str, value: Any):
|
|
@@ -46,7 +47,7 @@ def init(store_path: str, name: str):
|
|
|
46
47
|
_file = _StateFile(os.path.join(store_path, name))
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
def get(key: str, default: Any, reload: bool = False):
|
|
50
|
+
def get(key: str, default: Any = None, reload: bool = False):
|
|
50
51
|
return _file.get(key=key, default=default, reload=reload)
|
|
51
52
|
|
|
52
53
|
|