envstack 1.0.2__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.2/lib/envstack.egg-info → envstack-1.0.3}/PKG-INFO +6 -2
  2. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/__init__.py +1 -1
  3. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/encrypt.py +14 -4
  4. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/env.py +1 -1
  5. {envstack-1.0.2 → 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.2 → envstack-1.0.3}/pyproject.toml +6 -2
  8. envstack-1.0.2/lib/envstack/wrapper.py +0 -335
  9. envstack-1.0.2/lib/envstack.egg-info/requires.txt +0 -2
  10. {envstack-1.0.2 → envstack-1.0.3}/LICENSE +0 -0
  11. {envstack-1.0.2 → envstack-1.0.3}/README.md +0 -0
  12. {envstack-1.0.2 → envstack-1.0.3}/dist.json +0 -0
  13. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/cli.py +0 -0
  14. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/config.py +0 -0
  15. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/envshell.py +0 -0
  16. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/exceptions.py +0 -0
  17. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/logger.py +0 -0
  18. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/node.py +0 -0
  19. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/path.py +0 -0
  20. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/util.py +0 -0
  21. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack.egg-info/SOURCES.txt +0 -0
  22. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack.egg-info/dependency_links.txt +0 -0
  23. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack.egg-info/entry_points.txt +0 -0
  24. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack.egg-info/not-zip-safe +0 -0
  25. {envstack-1.0.2 → envstack-1.0.3}/lib/envstack.egg-info/top_level.txt +0 -0
  26. {envstack-1.0.2 → envstack-1.0.3}/setup.cfg +0 -0
  27. {envstack-1.0.2 → envstack-1.0.3}/tests/test_cmds.py +0 -0
  28. {envstack-1.0.2 → envstack-1.0.3}/tests/test_encrypt.py +0 -0
  29. {envstack-1.0.2 → envstack-1.0.3}/tests/test_env.py +0 -0
  30. {envstack-1.0.2 → envstack-1.0.3}/tests/test_node.py +0 -0
  31. {envstack-1.0.2 → envstack-1.0.3}/tests/test_path.py +0 -0
  32. {envstack-1.0.2 → envstack-1.0.3}/tests/test_util.py +0 -0
  33. {envstack-1.0.2 → 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.2
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.2"
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,6 +37,7 @@ import base64
37
37
  import binascii
38
38
  import os
39
39
  import secrets
40
+ import warnings
40
41
  from base64 import b64decode, b64encode
41
42
  from functools import wraps
42
43
 
@@ -53,10 +54,19 @@ Cipher = None
53
54
  algorithms = None
54
55
  modes = None
55
56
  try:
56
- from cryptography.fernet import Fernet, InvalidToken
57
- from cryptography.exceptions import InvalidTag
58
- from cryptography.hazmat.primitives import padding
59
- 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
+
60
70
  CRYPTOGRAPHY_AVAILABLE = True
61
71
  except ImportError as err:
62
72
  log.debug("cryptography module not available: %s", err)
@@ -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.2
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.2"
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