envstack 1.0.1__tar.gz → 1.0.3__tar.gz

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 (33) hide show
  1. {envstack-1.0.1/lib/envstack.egg-info → envstack-1.0.3}/PKG-INFO +6 -2
  2. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/__init__.py +1 -1
  3. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/encrypt.py +47 -10
  4. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/env.py +1 -1
  5. {envstack-1.0.1 → envstack-1.0.3/lib/envstack.egg-info}/PKG-INFO +6 -2
  6. envstack-1.0.3/lib/envstack.egg-info/requires.txt +7 -0
  7. {envstack-1.0.1 → envstack-1.0.3}/pyproject.toml +6 -2
  8. envstack-1.0.1/lib/envstack/wrapper.py +0 -335
  9. envstack-1.0.1/lib/envstack.egg-info/requires.txt +0 -2
  10. {envstack-1.0.1 → envstack-1.0.3}/LICENSE +0 -0
  11. {envstack-1.0.1 → envstack-1.0.3}/README.md +0 -0
  12. {envstack-1.0.1 → envstack-1.0.3}/dist.json +0 -0
  13. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/cli.py +0 -0
  14. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/config.py +0 -0
  15. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/envshell.py +0 -0
  16. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/exceptions.py +0 -0
  17. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/logger.py +0 -0
  18. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/node.py +0 -0
  19. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/path.py +0 -0
  20. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/util.py +0 -0
  21. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack.egg-info/SOURCES.txt +0 -0
  22. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack.egg-info/dependency_links.txt +0 -0
  23. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack.egg-info/entry_points.txt +0 -0
  24. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack.egg-info/not-zip-safe +0 -0
  25. {envstack-1.0.1 → envstack-1.0.3}/lib/envstack.egg-info/top_level.txt +0 -0
  26. {envstack-1.0.1 → envstack-1.0.3}/setup.cfg +0 -0
  27. {envstack-1.0.1 → envstack-1.0.3}/tests/test_cmds.py +0 -0
  28. {envstack-1.0.1 → envstack-1.0.3}/tests/test_encrypt.py +0 -0
  29. {envstack-1.0.1 → envstack-1.0.3}/tests/test_env.py +0 -0
  30. {envstack-1.0.1 → envstack-1.0.3}/tests/test_node.py +0 -0
  31. {envstack-1.0.1 → envstack-1.0.3}/tests/test_path.py +0 -0
  32. {envstack-1.0.1 → envstack-1.0.3}/tests/test_util.py +0 -0
  33. {envstack-1.0.1 → envstack-1.0.3}/tests/test_wrapper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: envstack
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Environment variable composition layer for tools and processes.
5
5
  Author-email: Ryan Galloway <ryan@rsgalloway.com>
6
6
  License: BSD 3-Clause License
@@ -20,11 +20,15 @@ Classifier: Programming Language :: Python :: 3.8
20
20
  Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: 3.10
22
22
  Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
23
26
  Requires-Python: >=3.6
24
27
  Description-Content-Type: text/markdown
25
28
  License-File: LICENSE
26
29
  Requires-Dist: PyYAML>=5.1.2
27
- Requires-Dist: cryptography>=43.0.1
30
+ Requires-Dist: cryptography<47,>=43.0.1; python_version < "3.9"
31
+ Requires-Dist: cryptography>=43.0.1; python_version >= "3.9"
28
32
 
29
33
  envstack
30
34
  ========
@@ -34,7 +34,7 @@ Stacked environment variable management system.
34
34
  """
35
35
 
36
36
  __prog__ = "envstack"
37
- __version__ = "1.0.1"
37
+ __version__ = "1.0.3"
38
38
 
39
39
  from envstack.env import clear, init, revert, save # noqa: F401
40
40
  from envstack.env import load_environ, resolve_environ # noqa: F401
@@ -37,22 +37,53 @@ import base64
37
37
  import binascii
38
38
  import os
39
39
  import secrets
40
+ import warnings
40
41
  from base64 import b64decode, b64encode
42
+ from functools import wraps
41
43
 
42
44
  from envstack.logger import log
43
45
 
44
46
  # cryptography and _rust dependency may not be available everywhere
45
47
  # ImportError: DLL load failed while importing _rust: Module not found.
48
+ CRYPTOGRAPHY_AVAILABLE = False
46
49
  Fernet = None
50
+ InvalidToken = type("InvalidToken", (Exception,), {})
51
+ InvalidTag = type("InvalidTag", (Exception,), {})
52
+ padding = None
53
+ Cipher = None
54
+ algorithms = None
55
+ modes = None
47
56
  try:
48
- import cryptography.exceptions
49
- from cryptography.fernet import Fernet, InvalidToken
50
- from cryptography.hazmat.primitives import padding
51
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
57
+ # cryptography emits a Python 3.8 deprecation warning during import.
58
+ with warnings.catch_warnings():
59
+ warnings.filterwarnings(
60
+ "ignore",
61
+ message=r"Python 3\.8 is no longer supported by the Python core team.*",
62
+ category=DeprecationWarning,
63
+ module=r"cryptography(\..*)?$",
64
+ )
65
+ from cryptography.fernet import Fernet, InvalidToken
66
+ from cryptography.exceptions import InvalidTag
67
+ from cryptography.hazmat.primitives import padding
68
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
69
+
70
+ CRYPTOGRAPHY_AVAILABLE = True
52
71
  except ImportError as err:
53
72
  log.debug("cryptography module not available: %s", err)
54
73
 
55
74
 
75
+ def require_cryptography(func):
76
+ """Guard crypto-backed functions when cryptography is unavailable."""
77
+
78
+ @wraps(func)
79
+ def wrapper(*args, **kwargs):
80
+ if not CRYPTOGRAPHY_AVAILABLE:
81
+ raise RuntimeError("cryptography support is not available")
82
+ return func(*args, **kwargs)
83
+
84
+ return wrapper
85
+
86
+
56
87
  class Base64Encryptor(object):
57
88
  """Encrypt and decrypt secrets using base64 encoding."""
58
89
 
@@ -84,6 +115,7 @@ class FernetEncryptor(object):
84
115
  self.key = self.get_key(env)
85
116
 
86
117
  @classmethod
118
+ @require_cryptography
87
119
  def generate_key(csl):
88
120
  """Generate a new 256-bit encryption key."""
89
121
  if Fernet:
@@ -104,6 +136,7 @@ class FernetEncryptor(object):
104
136
  return Fernet(key)
105
137
  return key
106
138
 
139
+ @require_cryptography
107
140
  def encrypt(self, data: str):
108
141
  """Encrypt a secret using Fernet.
109
142
 
@@ -121,9 +154,9 @@ class FernetEncryptor(object):
121
154
  log.error("invalid value: %s", e)
122
155
  except Exception as e:
123
156
  log.error("unhandled error: %s", e)
124
- finally:
125
- return results
157
+ return results
126
158
 
159
+ @require_cryptography
127
160
  def decrypt(self, data: str):
128
161
  """Decrypt a secret using Fernet.
129
162
 
@@ -153,6 +186,7 @@ class AESGCMEncryptor(object):
153
186
  self.key = self.get_key(env)
154
187
 
155
188
  @classmethod
189
+ @require_cryptography
156
190
  def generate_key(csl):
157
191
  """Generate a new 256-bit encryption key."""
158
192
  key = secrets.token_bytes(32)
@@ -171,6 +205,7 @@ class AESGCMEncryptor(object):
171
205
  raise ValueError("invalid base64 encoding: %s" % e)
172
206
  return key
173
207
 
208
+ @require_cryptography
174
209
  def encrypt_data(self, secret: str):
175
210
  """Encrypt a secret using AES-GCM.
176
211
 
@@ -189,6 +224,7 @@ class AESGCMEncryptor(object):
189
224
  "tag": b64encode(encryptor.tag).decode(),
190
225
  }
191
226
 
227
+ @require_cryptography
192
228
  def decrypt_data(self, encrypted_data: dict):
193
229
  """Decrypt a secret using AES-GCM.
194
230
 
@@ -218,14 +254,13 @@ class AESGCMEncryptor(object):
218
254
  results = compact_store(encrypted_data)
219
255
  except binascii.Error as e:
220
256
  log.error("invalid base64 encoding: %s", e)
221
- except cryptography.exceptions.InvalidTag:
257
+ except InvalidTag:
222
258
  log.error("invalid encryption key")
223
259
  except ValueError as e:
224
260
  log.error("invalid value: %s", e)
225
261
  except Exception as e:
226
262
  log.error("unhandled error: %s", e)
227
- finally:
228
- return results
263
+ return results
229
264
 
230
265
  def decrypt(self, data: str):
231
266
  """Convenience function to decrypt a secret using AES-GCM.
@@ -239,7 +274,7 @@ class AESGCMEncryptor(object):
239
274
  return decrypted.decode()
240
275
  except binascii.Error as e:
241
276
  log.debug("invalid base64 encoding: %s", e)
242
- except cryptography.exceptions.InvalidTag:
277
+ except InvalidTag:
243
278
  log.debug("invalid encryption key")
244
279
  except ValueError as e:
245
280
  log.debug("invalid value: %s", e)
@@ -248,6 +283,7 @@ class AESGCMEncryptor(object):
248
283
  return data
249
284
 
250
285
 
286
+ @require_cryptography
251
287
  def pad_data(data: str):
252
288
  """Pad data to be block-aligned for AES encryption.
253
289
 
@@ -258,6 +294,7 @@ def pad_data(data: str):
258
294
  return padder.update(str(data).encode()) + padder.finalize()
259
295
 
260
296
 
297
+ @require_cryptography
261
298
  def unpad_data(data: dict):
262
299
  """Unpad data after decryption.
263
300
 
@@ -248,7 +248,7 @@ class EnvVar(string.Template, str):
248
248
  >>> v.vars()
249
249
  ['FOO', 'BAR']
250
250
  """
251
- matches = super(EnvVar, self).pattern.findall(str(self.template))
251
+ matches = type(self).pattern.findall(str(self.template))
252
252
  return [key for match in matches for key in match if key]
253
253
 
254
254
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: envstack
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Environment variable composition layer for tools and processes.
5
5
  Author-email: Ryan Galloway <ryan@rsgalloway.com>
6
6
  License: BSD 3-Clause License
@@ -20,11 +20,15 @@ Classifier: Programming Language :: Python :: 3.8
20
20
  Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: 3.10
22
22
  Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
23
26
  Requires-Python: >=3.6
24
27
  Description-Content-Type: text/markdown
25
28
  License-File: LICENSE
26
29
  Requires-Dist: PyYAML>=5.1.2
27
- Requires-Dist: cryptography>=43.0.1
30
+ Requires-Dist: cryptography<47,>=43.0.1; python_version < "3.9"
31
+ Requires-Dist: cryptography>=43.0.1; python_version >= "3.9"
28
32
 
29
33
  envstack
30
34
  ========
@@ -0,0 +1,7 @@
1
+ PyYAML>=5.1.2
2
+
3
+ [:python_version < "3.9"]
4
+ cryptography<47,>=43.0.1
5
+
6
+ [:python_version >= "3.9"]
7
+ cryptography>=43.0.1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "envstack"
7
- version = "1.0.1"
7
+ version = "1.0.3"
8
8
  description = "Environment variable composition layer for tools and processes."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.6"
@@ -12,7 +12,8 @@ license = { text = "BSD 3-Clause License" }
12
12
  authors = [{ name = "Ryan Galloway", email = "ryan@rsgalloway.com" }]
13
13
  dependencies = [
14
14
  "PyYAML>=5.1.2",
15
- "cryptography>=43.0.1",
15
+ 'cryptography>=43.0.1,<47; python_version < "3.9"',
16
+ 'cryptography>=43.0.1; python_version >= "3.9"',
16
17
  ]
17
18
  classifiers = [
18
19
  "Development Status :: 4 - Beta",
@@ -25,6 +26,9 @@ classifiers = [
25
26
  "Programming Language :: Python :: 3.9",
26
27
  "Programming Language :: Python :: 3.10",
27
28
  "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Programming Language :: Python :: 3.14",
28
32
  ]
29
33
  keywords = [
30
34
  "environment",
@@ -1,335 +0,0 @@
1
- #!/usr/bin/env python3
2
- #
3
- # Copyright (c) 2024-2026, Ryan Galloway (ryan@rsgalloway.com)
4
- #
5
- # Redistribution and use in source and binary forms, with or without
6
- # modification, are permitted provided that the following conditions are met:
7
- #
8
- # - Redistributions of source code must retain the above copyright notice,
9
- # this list of conditions and the following disclaimer.
10
- #
11
- # - Redistributions in binary form must reproduce the above copyright notice,
12
- # this list of conditions and the following disclaimer in the documentation
13
- # and/or other materials provided with the distribution.
14
- #
15
- # - Neither the name of the software nor the names of its contributors
16
- # may be used to endorse or promote products derived from this software
17
- # without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
30
- #
31
-
32
- __doc__ = """
33
- Contains executable wrapper classes and functions.
34
- """
35
-
36
- import os
37
- import re
38
- import shlex
39
- import subprocess
40
- import traceback
41
-
42
- from envstack import config, logger
43
- from envstack.env import load_environ, resolve_environ
44
- from envstack.util import encode, evaluate_modifiers
45
-
46
-
47
- def to_args(cmd: str):
48
- """
49
- Converts a command line string to an arg list to be passed to
50
- subprocess.Popen that preserves args with quotes.
51
-
52
- :param cmd: command line string.
53
- """
54
- return shlex.split(cmd)
55
-
56
-
57
- def shell_join(args):
58
- """
59
- Joins a list of arguments into a single quoted shell string.
60
-
61
- :param args: list of arguments.
62
- :returns: shell string.
63
- """
64
- argstr = ".".join(args)
65
- if '"' in argstr or "'" in argstr:
66
- try:
67
- return shlex.join(args)
68
- except AttributeError:
69
- return " ".join(shlex.quote(arg) for arg in args)
70
- else:
71
- return " ".join(args)
72
-
73
-
74
- class Wrapper(object):
75
- """Wrapper class for executables. Subprocessed with preconfigured environment.
76
-
77
- Subclasses should override executable():
78
-
79
- class ToolWrapper(Wrapper):
80
- def __init__(self, namespace, args):
81
- super(ToolWrapper, self).__init__(namespace, args)
82
- def executable(self):
83
- return '$TOOL_ROOT/bin/tool'
84
-
85
- To launch 'tool', create an instance of the wrapper, and pass as the first
86
- argument the namespace continaining config options, then call launch():
87
-
88
- tool = ToolWrapper('tool', sys.argv[1:])
89
- tool.launch()
90
-
91
- The log attribute can be set to a custom logger:
92
-
93
- tool.log = MyLogger()
94
- """
95
-
96
- shell: bool = False
97
-
98
- def __init__(self, namespace, args=[]):
99
- """Initializes the wrapper with the given namespace and args.
100
-
101
- :param namespace: environment stack namespace.
102
- :param args: command line arguments.
103
- """
104
- super(Wrapper, self).__init__()
105
- self.args = args
106
- self.name = namespace
107
- self.shell = self.shell
108
- self.log = logger.log
109
- self.env = load_environ(namespace)
110
-
111
- def executable(self):
112
- """Returns the path to the executable."""
113
- raise NotImplementedError
114
-
115
- def launch(self):
116
- """Launches the wrapped tool in a subprocess with env."""
117
- env = self.get_subprocess_env()
118
- command = self.get_subprocess_command(env)
119
-
120
- try:
121
- proc = subprocess.run(
122
- command,
123
- env=encode(env),
124
- shell=self.shell,
125
- check=False,
126
- )
127
- return proc.returncode
128
- except Exception:
129
- traceback.print_exc()
130
- return 1
131
-
132
- def get_subprocess_args(self, cmd):
133
- """Returns the arguments to be passed to the subprocess."""
134
- return self.args
135
-
136
- def get_subprocess_command(self, env):
137
- """Returns argv (preferred) or a command string if shell=True."""
138
- cmd = evaluate_modifiers(self.executable(), env)
139
- args = self.get_subprocess_args(cmd)
140
-
141
- if self.shell:
142
- return " ".join([cmd] + args)
143
-
144
- return [cmd] + args
145
-
146
- def get_subprocess_env(self):
147
- """
148
- Returns the environment that gets passed to the subprocess when launch()
149
- is called on the wrapper.
150
- """
151
- env = os.environ.copy()
152
- env.update(resolve_environ(self.env))
153
- return env
154
-
155
-
156
- class CommandWrapper(Wrapper):
157
- """Wrapper class for running wrapped commands from the command-line."""
158
-
159
- def __init__(self, namespace=config.DEFAULT_NAMESPACE, args=[]):
160
- """
161
- Initializes the command wrapper with the given namespace and args, e.g.:
162
-
163
- >>> cmd = CommandWrapper(stack, ['ls', '-al'])
164
- >>> print(cmd.executable())
165
- ls
166
- >>> print(cmd.args)
167
- ['-al']
168
-
169
- :param namespace: environment stack name (default: 'default').
170
- :param args: command and arguments as a list.
171
- """
172
- super(CommandWrapper, self).__init__(namespace, args)
173
- self.cmd = list(args)
174
-
175
- def executable(self):
176
- """Returns the command to run."""
177
- return self.cmd[0]
178
-
179
- def get_subprocess_args(self, cmd):
180
- return self.cmd[1:]
181
-
182
-
183
- class ShellWrapper(Wrapper):
184
- """
185
- Runs a *string expression* under the user's shell using -c.
186
- Useful for wrapping shell expressions that need variable expansion:
187
-
188
- >>> shell = ShellWrapper('default', 'echo {HOME}')
189
- """
190
-
191
- def __init__(self, namespace=config.DEFAULT_NAMESPACE, expr: str = ""):
192
- super().__init__(namespace, args=[])
193
- self.expr = expr
194
-
195
- def get_interactive(self, env: dict = os.environ):
196
- override = env.get("INTERACTIVE")
197
- if override is not None:
198
- return str(override).lower() in {"1", "true", "yes", "on"}
199
- return False # default safe
200
-
201
- def get_subprocess_command(self, env: dict = os.environ):
202
- interactive = self.get_interactive(env)
203
- if interactive:
204
- return [config.SHELL, "-i", "-c", self.expr]
205
- return [config.SHELL, "-c", self.expr]
206
-
207
- def executable(self):
208
- """Returns the shell command to run the original command."""
209
- return self.cmd
210
-
211
-
212
- class CmdWrapper(CommandWrapper):
213
- """Wrapper class for running wrapped commands in Windows cmd.exe."""
214
-
215
- def __init__(self, namespace=config.DEFAULT_NAMESPACE, args=[]):
216
- super().__init__(namespace, args)
217
-
218
- # Always run through cmd.exe explicitly
219
- self.shell = False
220
- self._cmd_exe = config.SHELL # expected: "cmd" or "cmd.exe"
221
-
222
- # Join the intended argv into one command-line string for /c
223
- cmdline = shell_join(self.cmd)
224
-
225
- # cmd.exe /c <command>
226
- self._subprocess_argv = [self._cmd_exe, "/c", cmdline]
227
-
228
- def executable(self):
229
- return self._cmd_exe
230
-
231
- def get_subprocess_args(self, cmd):
232
- # Not used (we override get_subprocess_command)
233
- return []
234
-
235
- def get_subprocess_command(self, env):
236
- return list(self._subprocess_argv)
237
-
238
-
239
- def capture_output(
240
- command: str,
241
- namespace: str = config.DEFAULT_NAMESPACE,
242
- timeout: int = config.COMMAND_TIMEOUT,
243
- ):
244
- """
245
- Runs a command (string or argv) with the given stack namespace and captures stdout/stderr.
246
-
247
- Returns: (returncode, stdout, stderr)
248
- """
249
- import errno
250
-
251
- shellname = os.path.basename(config.SHELL).lower()
252
- argv = list(command) if isinstance(command, (list, tuple)) else to_args(command)
253
-
254
- # build env exactly like Wrapper.launch()
255
- env = os.environ.copy()
256
- env.update(resolve_environ(load_environ(namespace)))
257
-
258
- # prefer argv execution where possible
259
- if shellname in ["bash", "sh", "zsh"]:
260
- needs_shell = any(re.search(r"\{(\w+)\}", a) for a in argv)
261
- if needs_shell:
262
- expr_argv = [re.sub(r"\{(\w+)\}", r"${\1}", a) for a in argv]
263
- expr = shell_join(expr_argv)
264
- cmd = [config.SHELL, "-c", expr]
265
- else:
266
- cmd = argv
267
-
268
- # for cmd always use original command string
269
- elif shellname in ["cmd"]:
270
- cmd = command
271
-
272
- else:
273
- cmd = argv
274
-
275
- try:
276
- proc = subprocess.run(
277
- cmd,
278
- env=encode(env),
279
- shell=False,
280
- check=False,
281
- capture_output=True,
282
- text=True,
283
- timeout=timeout,
284
- )
285
- return proc.returncode, proc.stdout, proc.stderr
286
- except FileNotFoundError as e:
287
- # no process ran; synthesize a bash-like error and code
288
- # 127 is the conventional "command not found" code in shells
289
- missing = e.filename or (
290
- cmd[0] if isinstance(cmd, list) and cmd else str(command)
291
- )
292
- return 127, "", f"{missing}: command not found"
293
- except OSError as e:
294
- # Other OS-level execution errors (permission, exec format, etc.)
295
- rc = 126 if getattr(e, "errno", None) in (errno.EACCES,) else 1
296
- return rc, "", str(e)
297
- except subprocess.TimeoutExpired as e:
298
- return 124, "", f"Command timed out after {timeout} seconds"
299
-
300
-
301
- def run_command(command: str, namespace: str = config.DEFAULT_NAMESPACE):
302
- """
303
- Runs a given command with the given stack namespace.
304
-
305
- >>> run_command(['ls', '-l'])
306
-
307
- Or to run in a specific stack namespace:
308
-
309
- >>> run_command(['ls', '-l'], 'my-stack')
310
-
311
- - Automatically detects the shell to use.
312
- - Converts {VAR} to $VAR for bash, sh, zsh, and %VAR% for cmd.
313
-
314
- :param command: command to run as a list of arguments.
315
- :param namespace: environment stack name (default: 'default').
316
- :returns: command exit code
317
- """
318
- logger.setup_stream_handler()
319
- shellname = os.path.basename(config.SHELL).lower()
320
- argv = list(command) if isinstance(command, (list, tuple)) else to_args(command)
321
-
322
- if shellname in ["bash", "sh", "zsh"]:
323
- needs_shell = any(re.search(r"\{(\w+)\}", a) for a in argv)
324
- if needs_shell:
325
- expr_argv = [re.sub(r"\{(\w+)\}", r"${\1}", a) for a in argv]
326
- expr = shell_join(expr_argv)
327
- return ShellWrapper(namespace, expr).launch()
328
-
329
- return CommandWrapper(namespace, argv).launch()
330
-
331
- if shellname in ["cmd"]:
332
- expr = [re.sub(r"\{(\w+)\}", r"%\1%", a) for a in argv]
333
- return CmdWrapper(namespace, expr).launch()
334
-
335
- return CommandWrapper(namespace, argv).launch()
@@ -1,2 +0,0 @@
1
- PyYAML>=5.1.2
2
- cryptography>=43.0.1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes