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/__init__.py CHANGED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.316"
hcs_core/ctxp/__init__.py CHANGED
@@ -13,7 +13,15 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
- from ._init import init
17
- from .util import panic, error, CtxpException, launch_text_editor, choose
18
- from . import profile, config, var_template, state
19
- from .profile_store import profile_store
16
+ from . import config as config
17
+ from . import profile as profile
18
+ from . import state as state
19
+ from . import var_template as var_template
20
+ from ._init import init as init
21
+ from ._init import init_cli as init_cli
22
+ from .profile_store import profile_store as profile_store
23
+ from .util import CtxpException as CtxpException
24
+ from .util import choose as choose
25
+ from .util import error as error
26
+ from .util import launch_text_editor as launch_text_editor
27
+ from .util import panic as panic
hcs_core/ctxp/_init.py CHANGED
@@ -13,27 +13,99 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
- import click
16
+ import os
17
17
  from os import path
18
18
  from pathlib import Path
19
- from . import profile
20
- from . import state
21
- from . import config
22
- from . import cli_processor
23
-
24
- user_home = str(Path.home())
25
-
26
-
27
- def init(
28
- cli_name: str,
29
- main_cli: click.Group,
30
- commands_dir: str = "./cmds",
31
- store_path: str = user_home,
32
- config_path="./config",
33
- ):
34
- real_store_path = path.join(store_path, "." + cli_name)
35
- state.init(real_store_path, ".state")
36
- profile.init(real_store_path)
37
- config.init(config_path)
38
-
39
- return cli_processor.init(main_cli, commands_dir)
19
+
20
+ import click
21
+
22
+ from . import cli_processor, config, profile, state, telemetry
23
+
24
+
25
+ def _get_store_path():
26
+ if os.name == "nt": # Windows OS
27
+ return str(Path.home())
28
+ uid = os.getuid()
29
+ if uid == 0 or uid == 1000:
30
+ return "/tmp"
31
+ if os.path.exists("/.dockerenv"):
32
+ return "/tmp"
33
+ return str(Path.home())
34
+
35
+
36
+ user_home = _get_store_path()
37
+
38
+
39
+ _initialized_app_name = None
40
+
41
+
42
+ def init(app_name: str, store_path: str = user_home, config_path: str = "./config"):
43
+ global _initialized_app_name
44
+ if _initialized_app_name == app_name:
45
+ return
46
+
47
+ if _initialized_app_name is not None:
48
+ raise ValueError(f"App {app_name} already initialized with {_initialized_app_name}")
49
+ _initialized_app_name = app_name
50
+ try:
51
+ real_store_path = path.join(store_path, "." + app_name)
52
+ state.init(real_store_path, ".state")
53
+ profile.init(real_store_path)
54
+ config.init(config_path)
55
+
56
+ except Exception as e:
57
+ # critical errors, must print stack trace.
58
+ if _need_stack_trace(e):
59
+ raise e
60
+ else:
61
+ # Other errors, no stack.
62
+ from .util import panic
63
+
64
+ panic(e)
65
+
66
+
67
+ # init default with env if configured
68
+ _app_name = os.environ.get("CTXP_APP_NAME")
69
+ if _app_name:
70
+ init(_app_name)
71
+
72
+
73
+ def app_name():
74
+ dir_name = path.dirname(state._file._path)
75
+ name = dir_name[dir_name.rindex(os.sep) + 1 :]
76
+ if name.startswith("."):
77
+ return name[1:]
78
+ raise ValueError("Unable to determine app name: " + dir_name)
79
+
80
+
81
+ def init_cli(main_cli: click.Group, commands_dir: str = "./cmds"):
82
+ try:
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)
89
+ if _need_stack_trace(e):
90
+ raise e
91
+ else:
92
+ from .util import panic
93
+
94
+ panic(e)
95
+
96
+
97
+ def _need_stack_trace(e) -> bool:
98
+ program_errors = [
99
+ LookupError,
100
+ TypeError,
101
+ ValueError,
102
+ ArithmeticError,
103
+ NameError,
104
+ SyntaxError,
105
+ KeyError,
106
+ AttributeError,
107
+ ]
108
+ for t in program_errors:
109
+ if isinstance(e, t):
110
+ return True
111
+ return False
@@ -14,6 +14,7 @@ limitations under the License.
14
14
  """
15
15
 
16
16
  import click
17
+
17
18
  import hcs_core.ctxp as ctxp
18
19
  import hcs_core.ctxp.cli_options as cli
19
20
  import hcs_core.ctxp.util as util
@@ -43,11 +44,11 @@ def echo_obj(opt: str, id: str):
43
44
 
44
45
  @cli_test.command()
45
46
  @click.option("--opt")
46
- @click.argument("ids", nargs=-1)
47
+ @click.argument("obj_ids", nargs=-1)
47
48
  @cli.formatter(_format_custom_table)
48
- def echo_obj_list(opt: str, ids: tuple[str]):
49
+ def echo_obj_list(opt: str, obj_ids: tuple[str]):
49
50
  ret = []
50
- for i in ids:
51
+ for i in obj_ids:
51
52
  ret.append({"id": i, "opt": opt})
52
53
  return ret
53
54
 
@@ -14,6 +14,7 @@ limitations under the License.
14
14
  """
15
15
 
16
16
  import click
17
+
17
18
  import hcs_core.ctxp as ctxp
18
19
  import hcs_core.ctxp.util as util
19
20
 
@@ -47,7 +48,7 @@ def get(name: str, key: str):
47
48
  @click.argument("name")
48
49
  @click.argument("key_value") # 'key value pair, example: k1=v1'
49
50
  def set(name: str, key_value: str):
50
- """Set a context object by name."""
51
+ """Set a context property by name."""
51
52
  parts = key_value.split("=")
52
53
  if len(parts) != 2:
53
54
  ctxp.panic("Invalid KEY_VALUE format. Valid example: key1=value1")
@@ -57,6 +58,20 @@ def set(name: str, key_value: str):
57
58
  ctxp.context.set(name, data)
58
59
 
59
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
+
60
75
  @context.command()
61
76
  @click.argument("name")
62
77
  def delete(name: str):
@@ -13,11 +13,13 @@ 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 json
17
+ import sys
18
+
19
+ import click
19
20
  import questionary
20
- from hcs_core.ctxp import util, panic, profile, cli_processor
21
+
22
+ from hcs_core.ctxp import cli_options, cli_processor, panic, profile, util
21
23
 
22
24
 
23
25
  @click.group(name="profile", cls=cli_processor.LazyGroup)
@@ -37,8 +39,10 @@ def use(name: str):
37
39
  """Switch to a specific profile. If no name specified, launch interactive list to choose profile."""
38
40
 
39
41
  if name:
40
- if profile.use(name) is None:
42
+ p = profile.use(name)
43
+ if p is None:
41
44
  panic("No such profile: " + name)
45
+ return profile.get(name)
42
46
  else:
43
47
  current = profile.name()
44
48
  choices = profile.names()
@@ -49,6 +53,7 @@ def use(name: str):
49
53
  if ret:
50
54
  if profile.use(ret) is None:
51
55
  panic("No such profile: " + name)
56
+ return profile.current()
52
57
  else:
53
58
  # aborted
54
59
  return "", 1
@@ -57,7 +62,8 @@ def use(name: str):
57
62
  @profile_cmd_group.command()
58
63
  @click.option("--from-name", "-f", required=False)
59
64
  @click.option("--to-name", "-t", required=True)
60
- def copy(from_name: str, to_name: str):
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):
61
67
  """Copy profile."""
62
68
 
63
69
  if not from_name:
@@ -68,7 +74,11 @@ def copy(from_name: str, to_name: str):
68
74
  panic("No such profile: " + from_name)
69
75
  if profile.exists(to_name):
70
76
  panic("Profile already exists: " + to_name)
71
- profile.create(to_name, data, True)
77
+ data = profile.create(to_name, data, True)
78
+
79
+ if no_edit:
80
+ return data
81
+ util.launch_text_editor(profile.file(to_name))
72
82
 
73
83
 
74
84
  @profile_cmd_group.command()
@@ -78,9 +88,7 @@ def get(name: str):
78
88
  if name:
79
89
  data = profile.get(name)
80
90
  if data is None:
81
- panic(
82
- "Profile not found. Use 'hcs profile list' to show available profiles, or 'hcs profile init' to create one."
83
- )
91
+ panic("Profile not found. Use 'hcs profile list' to show available profiles, or 'hcs profile init' to create one.")
84
92
  else:
85
93
  data = profile.current()
86
94
  if data is None:
@@ -92,9 +100,20 @@ def get(name: str):
92
100
 
93
101
 
94
102
  @profile_cmd_group.command()
95
- @click.argument("name")
96
- def delete(name: str):
103
+ @cli_options.confirm
104
+ @click.argument("name", required=False)
105
+ def delete(confirm: bool, name: str):
97
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
98
117
  profile.delete(name)
99
118
 
100
119
 
@@ -13,8 +13,9 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
- import click
16
+ import os
17
17
 
18
+ import click
18
19
 
19
20
  verbose = click.option(
20
21
  "--verbose",
@@ -28,7 +29,7 @@ verbose = click.option(
28
29
  output = click.option(
29
30
  "--output",
30
31
  "-o",
31
- type=click.Choice(["json", "json-compact", "yaml", "text", "table"]),
32
+ type=click.Choice(["json", "json-compact", "yaml", "yml", "table", "t", "text"], case_sensitive=False),
32
33
  default=None,
33
34
  hidden=True,
34
35
  help="Specify output format",
@@ -42,13 +43,22 @@ field = click.option(
42
43
  help="Specify fields to output, in comma separated field names.",
43
44
  )
44
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
+
45
54
  wait = click.option(
46
55
  "--wait",
47
56
  "-w",
48
57
  type=str,
49
- required=False,
58
+ is_flag=False,
59
+ flag_value="1m",
50
60
  default="0",
51
- help="Wait time. E.g. '30s', or '5m'. Default: 10m. Specify '0' to disable waiting and return immediately.",
61
+ help="Wait time. E.g. '30s', or '5m'. Default: 1m.",
52
62
  )
53
63
 
54
64
  search = click.option(
@@ -66,12 +76,11 @@ 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=20, 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
- id_only = click.option(
74
- "--id-only",
81
+ ids = click.option(
82
+ "--ids",
83
+ "--id-only", # to be removed.
75
84
  is_flag=True,
76
85
  type=bool,
77
86
  required=False,
@@ -92,14 +101,26 @@ first = click.option(
92
101
 
93
102
  force = click.option("--force/--grace", type=bool, default=True, help="Specify deletion mode: forceful, or graceful.")
94
103
 
95
- confirm = click.option(
96
- "--confirm/--dry-run", "-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.",
97
112
  )
98
113
 
99
114
 
100
- 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):
101
122
  def decorator(f):
102
- f.formatter = custom_fn
123
+ f.formatter = formatter if formatter else columns
103
124
  return f
104
125
 
105
126
  return decorator
@@ -13,17 +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
- from click.core import Group
19
- import os.path as path
20
- import os
21
- import re
22
16
  import importlib
23
17
  import importlib.util
18
+ import os
19
+ import os.path as path
20
+ import re
21
+ import sys
24
22
  from pathlib import Path
25
- from .util import print_output, print_error, validate_error_return, avoid_trace_for_ctrl_c
26
- from .extension import ensure_extension
23
+
24
+ import click
25
+ from click.core import Group
26
+
27
+ from .util import avoid_trace_for_ctrl_c, default_table_formatter, print_error, print_output, validate_error_return
27
28
 
28
29
  _eager_loading = os.environ.get("_CTXP_EAGER_LOAD")
29
30
  if _eager_loading:
@@ -59,6 +60,8 @@ class LazyGroup(click.Group):
59
60
 
60
61
  def _ensure_extension(self):
61
62
  if self._extension:
63
+ from .extension import ensure_extension
64
+
62
65
  ensure_extension(self._extension)
63
66
 
64
67
 
@@ -73,6 +76,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
73
76
  meta = _read_group_meta(mod_path)
74
77
  help = meta.get("help")
75
78
  extension = meta.get("extension")
79
+ hidden = meta.get("hidden", False)
76
80
 
77
81
  subgroup = current.commands.get(name)
78
82
  if subgroup and isinstance(subgroup, Group):
@@ -80,7 +84,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
80
84
  subgroup.mod_path = mod_path
81
85
  return subgroup
82
86
 
83
- 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)
84
88
  current.add_command(subgroup)
85
89
  return subgroup
86
90
 
@@ -164,32 +168,41 @@ def _import_cmd_file(mod_path: Path, parent: click.core.Group):
164
168
  # Create a new decorator that combines the individual decorators
165
169
  def _default_io(cmd: click.Command):
166
170
 
167
- from .cli_options import output, field, id_only, first
171
+ from .cli_options import exclude_field, field, first, ids, output
168
172
 
169
173
  cmd = output(cmd)
170
174
  # cmd = cli_options.verbose(cmd)
171
175
  cmd = field(cmd)
172
- cmd = id_only(cmd)
176
+ cmd = ids(cmd)
173
177
  cmd = first(cmd)
178
+ cmd = exclude_field(cmd)
174
179
  callback = cmd.callback
175
180
 
176
181
  def inner(*args, **kwargs):
177
182
  io_args = {
178
183
  "output": kwargs.pop("output"),
179
- #'verbose': kwargs.pop('verbose'),
184
+ # 'verbose': kwargs.pop('verbose'),
180
185
  "field": kwargs.pop("field"),
181
- "id_only": kwargs.pop("id_only"),
186
+ "ids": kwargs.pop("ids"),
182
187
  "first": kwargs.pop("first"),
188
+ "exclude_field": kwargs.pop("exclude_field"),
183
189
  }
184
- if io_args["output"] == "table":
190
+ ctx = click.get_current_context()
191
+ from .telemetry import update as telemetry_update
185
192
 
186
- def _format(data):
187
- if not hasattr(callback, "formatter"):
188
- from .util import CtxpException
193
+ telemetry_update(ctx.command_path, ctx.params)
189
194
 
190
- raise CtxpException("Table output is specified, but no custom formatter specified on the command.")
191
- formatter = callback.__getattribute__("formatter")
192
- 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)
193
206
 
194
207
  io_args["format"] = _format
195
208
  ret = callback(*args, **kwargs)
@@ -0,0 +1,87 @@
1
+ import logging
2
+ import os
3
+ import os.path
4
+ import subprocess
5
+
6
+ log = logging.getLogger(__name__)
7
+
8
+
9
+ def run_cmd(cmd, shell: bool = True):
10
+ cmd_text = " ".join(cmd)
11
+ log.info(f"Running command: {cmd_text}")
12
+ result = {}
13
+ result["command"] = cmd_text
14
+ # proc = subprocess.run(cmd, shell=shell, check=False, capture_output=True, timeout=60, encoding="utf-8")
15
+ proc = subprocess.run(cmd, shell=shell, check=False, timeout=60, **subprocess_args())
16
+ result["stdout"] = proc.stdout.decode("utf-8")
17
+ result["stderr"] = proc.stderr.decode("utf-8")
18
+ result["returnCode"] = proc.returncode
19
+ return result
20
+
21
+
22
+ # Create a set of arguments which make a ``subprocess.Popen`` (and
23
+ # variants) call work with or without Pyinstaller, ``--noconsole`` or
24
+ # not, on Windows and Linux. Typical use::
25
+ #
26
+ # subprocess.call(['program_to_run', 'arg_1'], **subprocess_args())
27
+ #
28
+ # When calling ``check_output``::
29
+ #
30
+ # subprocess.check_output(['program_to_run', 'arg_1'],
31
+ # **subprocess_args(False))
32
+ def subprocess_args(include_stdout=True):
33
+ # The following is true only on Windows.
34
+ if hasattr(subprocess, "STARTUPINFO"):
35
+ # On Windows, subprocess calls will pop up a command window by default
36
+ # when run from Pyinstaller with the ``--noconsole`` option. Avoid this
37
+ # distraction.
38
+ si = subprocess.STARTUPINFO()
39
+ si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
40
+ # Windows doesn't search the path by default. Pass it an environment so
41
+ # it will.
42
+ env = os.environ
43
+ else:
44
+ si = None
45
+ env = None
46
+
47
+ # ``subprocess.check_output`` doesn't allow specifying ``stdout``::
48
+ #
49
+ # Traceback (most recent call last):
50
+ # File "test_subprocess.py", line 58, in <module>
51
+ # **subprocess_args(stdout=None))
52
+ # File "C:\Python27\lib\subprocess.py", line 567, in check_output
53
+ # raise ValueError('stdout argument not allowed, it will be overridden.')
54
+ # ValueError: stdout argument not allowed, it will be overridden.
55
+ #
56
+ # So, add it only if it's needed.
57
+ if include_stdout:
58
+ ret = {"stdout": subprocess.PIPE}
59
+ else:
60
+ ret = {}
61
+
62
+ # On Windows, running this from the binary produced by Pyinstaller
63
+ # with the ``--noconsole`` option requires redirecting everything
64
+ # (stdin, stdout, stderr) to avoid an OSError exception
65
+ # "[Error 6] the handle is invalid."
66
+ ret.update(
67
+ {
68
+ "stdin": subprocess.PIPE,
69
+ "stderr": subprocess.PIPE,
70
+ "startupinfo": si,
71
+ "env": env,
72
+ }
73
+ )
74
+ return ret
75
+
76
+
77
+ # A simple test routine. Compare this output when run by Python, Pyinstaller,
78
+ # and Pyinstaller ``--noconsole``.
79
+ if __name__ == "__main__":
80
+ # Save the output from invoking Python to a text file. This is the only
81
+ # option when running with ``--noconsole``.
82
+ with open("out.txt", "w") as f:
83
+ try:
84
+ txt = subprocess.check_output(["python", "--help"], **subprocess_args(False))
85
+ f.write(txt)
86
+ except OSError as e:
87
+ f.write("Failed: " + str(e))
hcs_core/ctxp/config.py CHANGED
@@ -13,8 +13,8 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
- from .jsondot import dotdict
17
16
  from .fstore import fstore
17
+ from .jsondot import dotdict
18
18
 
19
19
  _store_impl: fstore = None
20
20