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.
- {envstack-1.0.1/lib/envstack.egg-info → envstack-1.0.3}/PKG-INFO +6 -2
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/__init__.py +1 -1
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/encrypt.py +47 -10
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/env.py +1 -1
- {envstack-1.0.1 → envstack-1.0.3/lib/envstack.egg-info}/PKG-INFO +6 -2
- envstack-1.0.3/lib/envstack.egg-info/requires.txt +7 -0
- {envstack-1.0.1 → envstack-1.0.3}/pyproject.toml +6 -2
- envstack-1.0.1/lib/envstack/wrapper.py +0 -335
- envstack-1.0.1/lib/envstack.egg-info/requires.txt +0 -2
- {envstack-1.0.1 → envstack-1.0.3}/LICENSE +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/README.md +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/dist.json +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/cli.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/config.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/envshell.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/exceptions.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/logger.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/node.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/path.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack/util.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack.egg-info/SOURCES.txt +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack.egg-info/dependency_links.txt +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack.egg-info/entry_points.txt +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack.egg-info/not-zip-safe +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/lib/envstack.egg-info/top_level.txt +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/setup.cfg +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/tests/test_cmds.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/tests/test_encrypt.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/tests/test_env.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/tests/test_node.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/tests/test_path.py +0 -0
- {envstack-1.0.1 → envstack-1.0.3}/tests/test_util.py +0 -0
- {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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
========
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "envstack"
|
|
7
|
-
version = "1.0.
|
|
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
|
-
|
|
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()
|
|
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
|
|
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
|