hcs-core 0.1.283__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.283"
1
+ __version__ = "0.1.316"
hcs_core/ctxp/_init.py CHANGED
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
 
20
20
  import click
21
21
 
22
- from . import cli_processor, config, profile, state
22
+ from . import cli_processor, config, profile, state, telemetry
23
23
 
24
24
 
25
25
  def _get_store_path():
@@ -43,6 +43,7 @@ def init(app_name: str, store_path: str = user_home, config_path: str = "./confi
43
43
  global _initialized_app_name
44
44
  if _initialized_app_name == app_name:
45
45
  return
46
+
46
47
  if _initialized_app_name is not None:
47
48
  raise ValueError(f"App {app_name} already initialized with {_initialized_app_name}")
48
49
  _initialized_app_name = app_name
@@ -79,8 +80,12 @@ def app_name():
79
80
 
80
81
  def init_cli(main_cli: click.Group, commands_dir: str = "./cmds"):
81
82
  try:
82
- return cli_processor.init(main_cli, commands_dir)
83
- except Exception as e:
83
+ telemetry.start(_initialized_app_name)
84
+ ret = cli_processor.init(main_cli, commands_dir)
85
+ telemetry.end()
86
+ return ret
87
+ except BaseException as e:
88
+ telemetry.end(error=e)
84
89
  if _need_stack_trace(e):
85
90
  raise e
86
91
  else:
@@ -48,7 +48,7 @@ def get(name: str, key: str):
48
48
  @click.argument("name")
49
49
  @click.argument("key_value") # 'key value pair, example: k1=v1'
50
50
  def set(name: str, key_value: str):
51
- """Set a context object by name."""
51
+ """Set a context property by name."""
52
52
  parts = key_value.split("=")
53
53
  if len(parts) != 2:
54
54
  ctxp.panic("Invalid KEY_VALUE format. Valid example: key1=value1")
@@ -58,6 +58,20 @@ def set(name: str, key_value: str):
58
58
  ctxp.context.set(name, data)
59
59
 
60
60
 
61
+ @context.command()
62
+ @click.argument("name")
63
+ @click.argument("key")
64
+ def unset(name: str, key: str):
65
+ """Unset a context property by name."""
66
+ data = ctxp.context.get(name, default=None)
67
+ if data is None:
68
+ return
69
+ data.pop(key, None)
70
+ if len(data) == 0:
71
+ return ctxp.context.delete(name)
72
+ return ctxp.context.set(name, data)
73
+
74
+
61
75
  @context.command()
62
76
  @click.argument("name")
63
77
  def delete(name: str):
@@ -19,7 +19,7 @@ import sys
19
19
  import click
20
20
  import questionary
21
21
 
22
- from hcs_core.ctxp import cli_processor, panic, profile, util
22
+ from hcs_core.ctxp import cli_options, cli_processor, panic, profile, util
23
23
 
24
24
 
25
25
  @click.group(name="profile", cls=cli_processor.LazyGroup)
@@ -53,7 +53,7 @@ def use(name: str):
53
53
  if ret:
54
54
  if profile.use(ret) is None:
55
55
  panic("No such profile: " + name)
56
- return ret
56
+ return profile.current()
57
57
  else:
58
58
  # aborted
59
59
  return "", 1
@@ -62,8 +62,8 @@ def use(name: str):
62
62
  @profile_cmd_group.command()
63
63
  @click.option("--from-name", "-f", required=False)
64
64
  @click.option("--to-name", "-t", required=True)
65
- @click.option("--edit", "-e", required=False, is_flag=True, help="Edit the profile after copying.")
66
- def copy(from_name: str, to_name: str, edit: bool):
65
+ @click.option("--no-edit", "-e", required=False, is_flag=True, help="Edit the profile after copying.")
66
+ def copy(from_name: str, to_name: str, no_edit: bool):
67
67
  """Copy profile."""
68
68
 
69
69
  if not from_name:
@@ -76,10 +76,9 @@ def copy(from_name: str, to_name: str, edit: bool):
76
76
  panic("Profile already exists: " + to_name)
77
77
  data = profile.create(to_name, data, True)
78
78
 
79
- if edit:
80
- util.launch_text_editor(profile.file(to_name))
81
- else:
79
+ if no_edit:
82
80
  return data
81
+ util.launch_text_editor(profile.file(to_name))
83
82
 
84
83
 
85
84
  @profile_cmd_group.command()
@@ -89,9 +88,7 @@ def get(name: str):
89
88
  if name:
90
89
  data = profile.get(name)
91
90
  if data is None:
92
- panic(
93
- "Profile not found. Use 'hcs profile list' to show available profiles, or 'hcs profile init' to create one."
94
- )
91
+ panic("Profile not found. Use 'hcs profile list' to show available profiles, or 'hcs profile init' to create one.")
95
92
  else:
96
93
  data = profile.current()
97
94
  if data is None:
@@ -103,9 +100,20 @@ def get(name: str):
103
100
 
104
101
 
105
102
  @profile_cmd_group.command()
106
- @click.argument("name")
107
- def delete(name: str):
103
+ @cli_options.confirm
104
+ @click.argument("name", required=False)
105
+ def delete(confirm: bool, name: str):
108
106
  """Delete a profile by name."""
107
+ if not name:
108
+ name = profile.name()
109
+
110
+ if not profile.exists(name):
111
+ return
112
+ if not confirm:
113
+ question = f"Are you sure to delete profile '{name}'?"
114
+ if not questionary.confirm(question, default=False).ask():
115
+ click.echo("Aborted")
116
+ return
109
117
  profile.delete(name)
110
118
 
111
119
 
@@ -13,6 +13,8 @@ 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
18
  import click
17
19
 
18
20
  verbose = click.option(
@@ -27,7 +29,7 @@ verbose = click.option(
27
29
  output = click.option(
28
30
  "--output",
29
31
  "-o",
30
- type=click.Choice(["json", "json-compact", "yaml", "text", "table"]),
32
+ type=click.Choice(["json", "json-compact", "yaml", "yml", "table", "t", "text"], case_sensitive=False),
31
33
  default=None,
32
34
  hidden=True,
33
35
  help="Specify output format",
@@ -41,6 +43,14 @@ field = click.option(
41
43
  help="Specify fields to output, in comma separated field names.",
42
44
  )
43
45
 
46
+ exclude_field = click.option(
47
+ "--exclude-field",
48
+ type=str,
49
+ required=False,
50
+ hidden=True,
51
+ help="Specify fields to exclude from output, in comma separated field names.",
52
+ )
53
+
44
54
  wait = click.option(
45
55
  "--wait",
46
56
  "-w",
@@ -66,9 +76,7 @@ sort = click.option(
66
76
  help="Ascending/Descending. Format is property,{asc|desc} and default is ascending",
67
77
  )
68
78
 
69
- limit = click.option(
70
- "--limit", "-l", type=int, required=False, default=100, help="Optionally, specify the number of records to fetch."
71
- )
79
+ limit = click.option("--limit", "-l", type=int, required=False, default=100, help="Optionally, specify the number of records to fetch.")
72
80
 
73
81
  ids = click.option(
74
82
  "--ids",
@@ -93,14 +101,26 @@ first = click.option(
93
101
 
94
102
  force = click.option("--force/--grace", type=bool, default=True, help="Specify deletion mode: forceful, or graceful.")
95
103
 
96
- confirm = click.option(
97
- "--confirm/--prompt", "-y", type=bool, default=False, help="Confirm the operation without prompt."
104
+ confirm = click.option("--confirm/--prompt", "-y", type=bool, default=False, help="Confirm the operation without prompt.")
105
+
106
+ env = click.option(
107
+ "--env",
108
+ multiple=True,
109
+ type=str,
110
+ required=False,
111
+ help="Alternative explicit in-line environment variable override in KEY=VALUE format.",
98
112
  )
99
113
 
100
114
 
101
- def formatter(custom_fn):
115
+ def apply_env(envs):
116
+ for kv in envs:
117
+ k, v = kv.split("=", 1)
118
+ os.environ[k.strip()] = v.strip()
119
+
120
+
121
+ def formatter(formatter=None, columns=None):
102
122
  def decorator(f):
103
- f.formatter = custom_fn
123
+ f.formatter = formatter if formatter else columns
104
124
  return f
105
125
 
106
126
  return decorator
@@ -24,8 +24,7 @@ from pathlib import Path
24
24
  import click
25
25
  from click.core import Group
26
26
 
27
- from .extension import ensure_extension
28
- from .util import avoid_trace_for_ctrl_c, print_error, print_output, validate_error_return
27
+ from .util import avoid_trace_for_ctrl_c, default_table_formatter, print_error, print_output, validate_error_return
29
28
 
30
29
  _eager_loading = os.environ.get("_CTXP_EAGER_LOAD")
31
30
  if _eager_loading:
@@ -61,6 +60,8 @@ class LazyGroup(click.Group):
61
60
 
62
61
  def _ensure_extension(self):
63
62
  if self._extension:
63
+ from .extension import ensure_extension
64
+
64
65
  ensure_extension(self._extension)
65
66
 
66
67
 
@@ -75,6 +76,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
75
76
  meta = _read_group_meta(mod_path)
76
77
  help = meta.get("help")
77
78
  extension = meta.get("extension")
79
+ hidden = meta.get("hidden", False)
78
80
 
79
81
  subgroup = current.commands.get(name)
80
82
  if subgroup and isinstance(subgroup, Group):
@@ -82,7 +84,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
82
84
  subgroup.mod_path = mod_path
83
85
  return subgroup
84
86
 
85
- subgroup = LazyGroup(extension=extension, name=name, help=help, mod_path=mod_path)
87
+ subgroup = LazyGroup(extension=extension, name=name, help=help, mod_path=mod_path, hidden=hidden)
86
88
  current.add_command(subgroup)
87
89
  return subgroup
88
90
 
@@ -166,32 +168,41 @@ def _import_cmd_file(mod_path: Path, parent: click.core.Group):
166
168
  # Create a new decorator that combines the individual decorators
167
169
  def _default_io(cmd: click.Command):
168
170
 
169
- from .cli_options import field, first, ids, output
171
+ from .cli_options import exclude_field, field, first, ids, output
170
172
 
171
173
  cmd = output(cmd)
172
174
  # cmd = cli_options.verbose(cmd)
173
175
  cmd = field(cmd)
174
176
  cmd = ids(cmd)
175
177
  cmd = first(cmd)
178
+ cmd = exclude_field(cmd)
176
179
  callback = cmd.callback
177
180
 
178
181
  def inner(*args, **kwargs):
179
182
  io_args = {
180
183
  "output": kwargs.pop("output"),
181
- #'verbose': kwargs.pop('verbose'),
184
+ # 'verbose': kwargs.pop('verbose'),
182
185
  "field": kwargs.pop("field"),
183
186
  "ids": kwargs.pop("ids"),
184
187
  "first": kwargs.pop("first"),
188
+ "exclude_field": kwargs.pop("exclude_field"),
185
189
  }
186
- if io_args["output"] == "table":
190
+ ctx = click.get_current_context()
191
+ from .telemetry import update as telemetry_update
187
192
 
188
- def _format(data):
189
- if not hasattr(callback, "formatter"):
190
- from .util import CtxpException
193
+ telemetry_update(ctx.command_path, ctx.params)
191
194
 
192
- raise CtxpException("Table output is specified, but no custom formatter specified on the command.")
193
- formatter = callback.__getattribute__("formatter")
194
- return formatter(data)
195
+ if io_args["output"] == "table" or io_args["output"] == "t":
196
+
197
+ def _format(data):
198
+ if hasattr(callback, "formatter"):
199
+ formatter = callback.__getattribute__("formatter")
200
+ else:
201
+ formatter = default_table_formatter
202
+ if isinstance(formatter, dict):
203
+ return default_table_formatter(data, formatter)
204
+ else:
205
+ return formatter(data)
195
206
 
196
207
  io_args["format"] = _format
197
208
  ret = callback(*args, **kwargs)
hcs_core/ctxp/context.py CHANGED
@@ -34,6 +34,8 @@ def get(name: str, reload: bool = False, default=None) -> jsondot.dotdict:
34
34
 
35
35
 
36
36
  def set(name: str, data: dict):
37
+ if data is None or len(data) == 0:
38
+ return _store().delete(name)
37
39
  return _store().save(name, data)
38
40
 
39
41
 
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import os
2
3
  import re
3
4
  from io import TextIOWrapper
4
5
  from os import chmod, path
@@ -99,9 +100,7 @@ def strict_dict_to_class(data: dict, class_type):
99
100
  value = strict_dict_to_class(value, field_type)
100
101
  setattr(inst, field_name, value)
101
102
  continue
102
- raise ValueError(
103
- f"Field '{class_type.__name__}.{field_name}' has an incorrect type. Declared: {field_type}, actual: {type(value)}"
104
- )
103
+ raise ValueError(f"Field '{class_type.__name__}.{field_name}' has an incorrect type. Declared: {field_type}, actual: {type(value)}")
105
104
  return inst
106
105
 
107
106
 
@@ -260,22 +259,26 @@ def process_variables(obj: dict, fn_get_var=None, use_env: bool = True):
260
259
  fn_get_var = _fn_get_var
261
260
 
262
261
  if use_env:
263
- import os
264
262
 
265
263
  prev_fn_get_var = fn_get_var
266
264
 
267
265
  def _fn_get_var_from_env(name: str):
268
266
  if not name.startswith("env."):
269
267
  return prev_fn_get_var(name)
270
- v = None
271
268
  actual_name = name[4:]
272
- v = os.environ.get(actual_name, None)
273
- if v:
274
- return v, True
275
- v, found = prev_fn_get_var(name)
276
- if found:
277
- return v
278
- raise CtxpException(f"Environment variable '{actual_name}' is used in template, but not found. ")
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
279
282
 
280
283
  fn_get_var = _fn_get_var_from_env
281
284
 
@@ -336,9 +339,7 @@ def resolve_expression(expr, fn_get_value, referencing_attr_path) -> Tuple[Any,
336
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}"
337
340
  )
338
341
  if not mapped_value.startswith(tmp_var_name + "."):
339
- raise CtxpException(
340
- f"Unsupported expression. attr_path={referencing_attr_path}, src_var_name={src_var_name}"
341
- )
342
+ raise CtxpException(f"Unsupported expression. attr_path={referencing_attr_path}, src_var_name={src_var_name}")
342
343
  new_attr_path = mapped_value[len(tmp_var_name) + 1 :]
343
344
  ret = []
344
345
  for i in target_value:
@@ -391,6 +392,25 @@ def get_common_items(iter1, iter2):
391
392
  return common_items
392
393
 
393
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
+
394
414
  def _evaluate(value, smart_search):
395
415
  # If we found an exact match, return 2 immediately
396
416
  if value == smart_search:
hcs_core/ctxp/duration.py CHANGED
@@ -2,9 +2,7 @@
2
2
 
3
3
  import re
4
4
 
5
- PATTERN = (
6
- "([-+]?)P(?:([-+]?[0-9]+)D)?(T(?:([-+]?[0-9]+)H)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)(?:[.,]([0-9]{0,9}))?S)?)?"
7
- )
5
+ PATTERN = "([-+]?)P(?:([-+]?[0-9]+)D)?(T(?:([-+]?[0-9]+)H)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)(?:[.,]([0-9]{0,9}))?S)?)?"
8
6
 
9
7
  # Examples
10
8
 
hcs_core/ctxp/fstore.py CHANGED
@@ -199,7 +199,7 @@ class fstore:
199
199
  outfile.write(str(data))
200
200
  elif format == "json-compact":
201
201
  jsondot.save(data=data, file=file_path, pretty=False, lock=True)
202
- elif format == "yaml":
202
+ elif format == "yaml" or format == "yml":
203
203
  import yaml
204
204
 
205
205
  with open(file_path, "w", encoding="utf-8") as outfile:
hcs_core/ctxp/jsondot.py CHANGED
@@ -40,7 +40,7 @@ import json
40
40
  import os.path
41
41
  import random
42
42
  import time
43
- from typing import Any, Dict, List, Optional, Union
43
+ from typing import Any, Dict, Optional, Union
44
44
 
45
45
 
46
46
  class dotdict(dict):
hcs_core/ctxp/logger.py CHANGED
@@ -21,7 +21,9 @@ from logging.handlers import RotatingFileHandler
21
21
  import coloredlogs
22
22
 
23
23
  LOG_FORMAT_SIMPLE = "%(levelname).4s %(asctime)s %(name)-16s %(message)s"
24
- 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
+ )
25
27
  DATE_FORMAT_SIMPLE = "%H:%M:%S"
26
28
 
27
29
 
@@ -133,9 +135,7 @@ def setup(
133
135
  )
134
136
 
135
137
 
136
- def setup_console_output(
137
- logger, console_log_output, console_log_level, console_log_color, console_log_mask, log_line_template, date_fmt
138
- ):
138
+ def setup_console_output(logger, console_log_output, console_log_level, console_log_color, console_log_mask, log_line_template, date_fmt):
139
139
  if not date_fmt:
140
140
  date_fmt = coloredlogs.DEFAULT_DATE_FORMAT
141
141
  coloredlogs.install(
hcs_core/ctxp/profile.py CHANGED
@@ -20,7 +20,7 @@ from copy import deepcopy
20
20
  from typing import Any
21
21
 
22
22
  from . import state
23
- from .data_util import deep_get_attr, deep_set_attr, load_data_file, save_data_file
23
+ from .data_util import deep_set_attr, load_data_file, save_data_file
24
24
  from .jsondot import dotdict, dotify
25
25
  from .util import CtxpException, panic
26
26
 
@@ -63,7 +63,7 @@ def create(name: str, data: dict, auto_use: bool = True, overwrite: bool = False
63
63
  save_data_file(data, file(name), "yaml", 0o600)
64
64
  if auto_use:
65
65
  use(name)
66
- return get(name)
66
+ return get(name, reload=True)
67
67
 
68
68
 
69
69
  def copy(from_name: str, to_name: str, overwrite: bool = True):
@@ -82,9 +82,7 @@ def current(reload: bool = False, exit_on_failure: bool = True, exclude_secret:
82
82
  data = get(profile_name, reload)
83
83
 
84
84
  if data is None and exit_on_failure:
85
- panic(
86
- "Profile not set. Use 'hcs profile use [profile-name]' to choose one, or use 'hcs profile init' to create default profiles."
87
- )
85
+ panic("Profile not set. Use 'hcs profile use [profile-name]' to choose one, or use 'hcs profile init' to create default profiles.")
88
86
 
89
87
  if exclude_secret:
90
88
  data = dotdict(dict(data))
@@ -98,7 +96,6 @@ def current(reload: bool = False, exit_on_failure: bool = True, exclude_secret:
98
96
 
99
97
  def save():
100
98
  """Save the current profile"""
101
- global _data
102
99
  if _data is None:
103
100
  return
104
101
  write(name(), _data)
@@ -127,6 +124,8 @@ def use(name: str) -> str:
127
124
  return
128
125
 
129
126
  _set_active_profile_name(name)
127
+ global _data
128
+ _data = _load_data_file_with_env_overrides(name, None)
130
129
  return name
131
130
 
132
131
 
@@ -224,10 +223,10 @@ class auth:
224
223
  if json.dumps(data) == json.dumps(_auth_cache):
225
224
  return
226
225
  _auth_cache = deepcopy(data)
227
- file_name = auth._file_name()
228
226
  if os.environ.get("CTXP_AUTH_PERSIST", "true").lower() == "false":
229
227
  pass
230
228
  else:
229
+ file_name = auth._file_name()
231
230
  save_data_file(data, file_name, "yaml", 0o600)
232
231
 
233
232
  @staticmethod
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
@@ -31,7 +31,7 @@ class _StateFile:
31
31
  self._cache = jsondot.load(self._path, {}, lock=True)
32
32
  return self._cache
33
33
 
34
- def get(self, key: str, default: Any, reload: bool = False):
34
+ def get(self, key: str, default: Any = None, reload: bool = False):
35
35
  return self._data(reload).get(key, default)
36
36
 
37
37
  def set(self, key: str, value: Any):
@@ -47,7 +47,7 @@ def init(store_path: str, name: str):
47
47
  _file = _StateFile(os.path.join(store_path, name))
48
48
 
49
49
 
50
- def get(key: str, default: Any, reload: bool = False):
50
+ def get(key: str, default: Any = None, reload: bool = False):
51
51
  return _file.get(key=key, default=default, reload=reload)
52
52
 
53
53
 
@@ -111,7 +111,6 @@ def statistics(reset_cycle: bool = False):
111
111
 
112
112
  def _daemon_worker():
113
113
  log.info("task scheduler daemon thread start")
114
- global _g_flag_stop_daemon
115
114
  while not _g_flag_stop_daemon.is_set():
116
115
  if _g_flag_running:
117
116
  schedule.run_pending()
@@ -149,7 +148,6 @@ def resume():
149
148
 
150
149
 
151
150
  def stop_daemon():
152
- global _g_flag_stop_daemon
153
151
  global _g_worker_thread
154
152
  if _g_worker_thread:
155
153
  _g_flag_stop_daemon.set()