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.
Files changed (61) hide show
  1. hcs_core/__init__.py +1 -0
  2. hcs_core/ctxp/__init__.py +12 -4
  3. hcs_core/ctxp/_init.py +94 -22
  4. hcs_core/ctxp/built_in_cmds/_ut.py +4 -3
  5. hcs_core/ctxp/built_in_cmds/context.py +16 -1
  6. hcs_core/ctxp/built_in_cmds/profile.py +30 -11
  7. hcs_core/ctxp/cli_options.py +34 -13
  8. hcs_core/ctxp/cli_processor.py +33 -20
  9. hcs_core/ctxp/cmd_util.py +87 -0
  10. hcs_core/ctxp/config.py +1 -1
  11. hcs_core/ctxp/context.py +82 -3
  12. hcs_core/ctxp/data_util.py +56 -20
  13. hcs_core/ctxp/dispatcher.py +82 -0
  14. hcs_core/ctxp/duration.py +65 -0
  15. hcs_core/ctxp/extension.py +7 -6
  16. hcs_core/ctxp/fn_util.py +57 -0
  17. hcs_core/ctxp/fstore.py +39 -22
  18. hcs_core/ctxp/jsondot.py +259 -78
  19. hcs_core/ctxp/logger.py +7 -6
  20. hcs_core/ctxp/profile.py +53 -21
  21. hcs_core/ctxp/profile_store.py +1 -0
  22. hcs_core/ctxp/recent.py +3 -3
  23. hcs_core/ctxp/state.py +4 -3
  24. hcs_core/ctxp/task_schd.py +168 -0
  25. hcs_core/ctxp/telemetry.py +145 -0
  26. hcs_core/ctxp/template_util.py +21 -0
  27. hcs_core/ctxp/timeutil.py +11 -0
  28. hcs_core/ctxp/util.py +194 -33
  29. hcs_core/ctxp/var_template.py +3 -4
  30. hcs_core/plan/__init__.py +11 -5
  31. hcs_core/plan/base_provider.py +1 -0
  32. hcs_core/plan/core.py +29 -26
  33. hcs_core/plan/dag.py +15 -12
  34. hcs_core/plan/helper.py +4 -2
  35. hcs_core/plan/kop.py +21 -8
  36. hcs_core/plan/provider/dev/dummy.py +3 -3
  37. hcs_core/sglib/auth.py +137 -95
  38. hcs_core/sglib/cli_options.py +20 -5
  39. hcs_core/sglib/client_util.py +230 -62
  40. hcs_core/sglib/csp.py +73 -6
  41. hcs_core/sglib/ez_client.py +139 -41
  42. hcs_core/sglib/hcs_client.py +3 -9
  43. hcs_core/sglib/init.py +17 -0
  44. hcs_core/sglib/login_support.py +22 -83
  45. hcs_core/sglib/payload_util.py +3 -1
  46. hcs_core/sglib/requtil.py +38 -0
  47. hcs_core/sglib/utils.py +107 -0
  48. hcs_core/util/check_license.py +0 -2
  49. hcs_core/util/duration.py +6 -3
  50. hcs_core/util/job_view.py +35 -15
  51. hcs_core/util/pki_util.py +48 -1
  52. hcs_core/util/query_util.py +54 -8
  53. hcs_core/util/scheduler.py +3 -3
  54. hcs_core/util/ssl_util.py +1 -1
  55. hcs_core/util/versions.py +15 -12
  56. hcs_core-0.1.316.dist-info/METADATA +54 -0
  57. hcs_core-0.1.316.dist-info/RECORD +69 -0
  58. {hcs_core-0.1.250.dist-info → hcs_core-0.1.316.dist-info}/WHEEL +1 -2
  59. hcs_core-0.1.250.dist-info/METADATA +0 -36
  60. hcs_core-0.1.250.dist-info/RECORD +0 -59
  61. 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
- # to support tuple-based sort. (dotdict_a < dotdict_b)
56
- return True
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 __deepcopy__(self, memo=None):
59
- # To workaround deepcopy with some python version
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
- # TODO: how to remove the lib-specific dependency from this utility class?
67
- # Register the represent_dict function with SafeDumper
68
- def _represent_dict(dumper, data):
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
- import yaml
109
+ # def __reduce_ex__(self, protocol):
110
+ # return dict, (dict(self),)
73
111
 
74
- yaml.SafeDumper.add_representer(dotdict, _represent_dict)
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
- # If already dotified, skip
81
- if isinstance(target, dotdict):
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
- # Return unchanged
93
- return target
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 dictd"""
98
- if isinstance(target, list):
99
- for i in range(len(target)):
100
- target[i] = undot(target[i])
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
- if isinstance(target, dict):
103
- ret = {}
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
- return isinstance(obj, str) or isinstance(obj, bool) or isinstance(obj, int) or isinstance(obj, float)
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
- """Deeply convert a dotdict from dict"""
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 i in range(len(target)):
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
- target[k] = plain(target[k])
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
- dict = json.loads(text)
131
- return dotify(dict)
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
- if for_read:
136
- flags = portalocker.LockFlags.SHARED
137
- else:
138
- flags = portalocker.LockFlags.EXCLUSIVE # | portalocker.LockFlags.NON_BLOCKING
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 = 0.05
284
+ backoff_seconds = initial_backoff
143
285
  while True:
144
286
  try:
145
287
  portalocker.lock(file, flags)
146
288
  return
147
- except Exception as e:
289
+ except portalocker.LockException as e:
148
290
  retry += 1
149
291
  if retry > max_retry:
150
- raise Exception(f"Fail locking file {file}") from e
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 += 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
- def load(file: str, default: Any = None, lock: bool = False) -> Any:
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"Error loading file {file}") from e
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
- def save(data: dict, file, format=True, lock: bool = False) -> None:
170
- with open(file, "w") as outfile:
171
- if lock:
172
- _lock_with_retry(outfile, False)
173
- if format:
174
- json.dump(data, outfile, indent="\t", default=vars)
175
- else:
176
- json.dump(data, outfile)
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 = "%(color_on)s%(asctime)s.%(msecs)03d [%(process)-5d:%(threadName)-12s] %(levelname)-7s [%(name)-16s] %(message)s%(color_off)s"
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
- from copy import deepcopy
16
+ import os
17
17
  import pathlib
18
18
  import shutil
19
- import os
19
+ from copy import deepcopy
20
20
  from typing import Any
21
- from .util import panic, CtxpException
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 = load_data_file(file(profile_name), default=default)
156
- if data is not None:
157
- data = dotify(data)
158
- _data = data
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 not None:
162
- data = dotify(data)
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
- file_name = auth._file_name()
198
- save_data_file(data, file_name, "yaml", 0o600)
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:
@@ -16,6 +16,7 @@ limitations under the License.
16
16
  # Profile_store provides a utility method to create profile-scoped fstore
17
17
 
18
18
  import os
19
+
19
20
  from . import profile
20
21
  from .fstore import fstore
21
22
  from .util import CtxpException
hcs_core/ctxp/recent.py CHANGED
@@ -31,7 +31,7 @@ def all():
31
31
  return _recent
32
32
 
33
33
 
34
- def require(provided, k: str):
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]["id"])
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
- import os
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