envstack 0.8.2__tar.gz → 0.8.4__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-0.8.2/lib/envstack.egg-info → envstack-0.8.4}/PKG-INFO +53 -2
- {envstack-0.8.2 → envstack-0.8.4}/README.md +52 -1
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack/__init__.py +1 -1
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack/cli.py +47 -22
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack/env.py +96 -46
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack/util.py +26 -34
- {envstack-0.8.2 → envstack-0.8.4/lib/envstack.egg-info}/PKG-INFO +53 -2
- {envstack-0.8.2 → envstack-0.8.4}/setup.py +1 -1
- {envstack-0.8.2 → envstack-0.8.4}/tests/test_cmds.py +126 -5
- {envstack-0.8.2 → envstack-0.8.4}/tests/test_env.py +234 -22
- {envstack-0.8.2 → envstack-0.8.4}/tests/test_util.py +71 -8
- {envstack-0.8.2 → envstack-0.8.4}/LICENSE +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/dist.json +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack/config.py +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack/encrypt.py +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack/exceptions.py +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack/logger.py +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack/node.py +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack/path.py +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack/wrapper.py +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack.egg-info/SOURCES.txt +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack.egg-info/dependency_links.txt +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack.egg-info/entry_points.txt +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack.egg-info/not-zip-safe +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack.egg-info/requires.txt +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/lib/envstack.egg-info/top_level.txt +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/setup.cfg +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/tests/test_encrypt.py +0 -0
- {envstack-0.8.2 → envstack-0.8.4}/tests/test_node.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: envstack
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.4
|
|
4
4
|
Summary: Stacked environment variable management system
|
|
5
5
|
Home-page: http://github.com/rsgalloway/envstack
|
|
6
6
|
Author: Ryan Galloway
|
|
@@ -169,8 +169,8 @@ allows you to override the value:
|
|
|
169
169
|
| Value | Description |
|
|
170
170
|
|---------------------|-------------|
|
|
171
171
|
| value | 'value' |
|
|
172
|
-
| ${VAR:-default} | os.environ.get('VAR', 'default') |
|
|
173
172
|
| ${VAR:=default} | VAR = VAR or 'default' |
|
|
173
|
+
| ${VAR:-default} | os.environ.get('VAR', 'default') |
|
|
174
174
|
| ${VAR:?error message} | if not VAR: raise ValueError() |
|
|
175
175
|
|
|
176
176
|
Without the expansion modifier, values are set and do not change (but can be
|
|
@@ -208,6 +208,37 @@ $ HELLO=goodbye envstack -- echo {HELLO}
|
|
|
208
208
|
goodbye
|
|
209
209
|
```
|
|
210
210
|
|
|
211
|
+
#### Using the command-line
|
|
212
|
+
|
|
213
|
+
Here we can set values using the `envstack` command:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
$ envstack --set HELLO:world
|
|
217
|
+
HELLO: world
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
We can also encrypt the values automatically (base64 by default):
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
$ envstack -s HELLO:world -e
|
|
224
|
+
HELLO: d29ybGQ=
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Add more variables (note that `$` needs to be escaped in bash or else it will
|
|
228
|
+
be evaluated immediately):
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
$ envstack -s HELLO:world VAR:\${HELLO}
|
|
232
|
+
HELLO: world
|
|
233
|
+
VAR: ${HELLO}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
To write out to a file use the `-o` option:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
$ envstack -s HELLO:world -o hello.env
|
|
240
|
+
```
|
|
241
|
+
|
|
211
242
|
## Creating Stacks
|
|
212
243
|
|
|
213
244
|
Several example or starter stacks are available in the [env folder of the
|
|
@@ -235,6 +266,13 @@ windows:
|
|
|
235
266
|
<<: *all
|
|
236
267
|
```
|
|
237
268
|
|
|
269
|
+
Or using Python:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
>>> env = Env({"FOO": "bar", "BAR": "${FOO}"})
|
|
273
|
+
>>> env.write("foobar.env")
|
|
274
|
+
```
|
|
275
|
+
|
|
238
276
|
Get the resolved environment for the `foobar` stack:
|
|
239
277
|
|
|
240
278
|
```bash
|
|
@@ -277,6 +315,19 @@ BIZ=foo
|
|
|
277
315
|
FOO=foo
|
|
278
316
|
```
|
|
279
317
|
|
|
318
|
+
Here is an example using nested variable expansion:
|
|
319
|
+
|
|
320
|
+
```yaml
|
|
321
|
+
FOO: ${BIZ:=${BAR:=${BAZ:=baz}}}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Resolves to:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
$ envstack -r
|
|
328
|
+
FOO=baz
|
|
329
|
+
```
|
|
330
|
+
|
|
280
331
|
#### Includes
|
|
281
332
|
|
|
282
333
|
Environment stack files can include other namespaced environments (you should
|
|
@@ -145,8 +145,8 @@ allows you to override the value:
|
|
|
145
145
|
| Value | Description |
|
|
146
146
|
|---------------------|-------------|
|
|
147
147
|
| value | 'value' |
|
|
148
|
-
| ${VAR:-default} | os.environ.get('VAR', 'default') |
|
|
149
148
|
| ${VAR:=default} | VAR = VAR or 'default' |
|
|
149
|
+
| ${VAR:-default} | os.environ.get('VAR', 'default') |
|
|
150
150
|
| ${VAR:?error message} | if not VAR: raise ValueError() |
|
|
151
151
|
|
|
152
152
|
Without the expansion modifier, values are set and do not change (but can be
|
|
@@ -184,6 +184,37 @@ $ HELLO=goodbye envstack -- echo {HELLO}
|
|
|
184
184
|
goodbye
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
+
#### Using the command-line
|
|
188
|
+
|
|
189
|
+
Here we can set values using the `envstack` command:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
$ envstack --set HELLO:world
|
|
193
|
+
HELLO: world
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
We can also encrypt the values automatically (base64 by default):
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
$ envstack -s HELLO:world -e
|
|
200
|
+
HELLO: d29ybGQ=
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Add more variables (note that `$` needs to be escaped in bash or else it will
|
|
204
|
+
be evaluated immediately):
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
$ envstack -s HELLO:world VAR:\${HELLO}
|
|
208
|
+
HELLO: world
|
|
209
|
+
VAR: ${HELLO}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
To write out to a file use the `-o` option:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
$ envstack -s HELLO:world -o hello.env
|
|
216
|
+
```
|
|
217
|
+
|
|
187
218
|
## Creating Stacks
|
|
188
219
|
|
|
189
220
|
Several example or starter stacks are available in the [env folder of the
|
|
@@ -211,6 +242,13 @@ windows:
|
|
|
211
242
|
<<: *all
|
|
212
243
|
```
|
|
213
244
|
|
|
245
|
+
Or using Python:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
>>> env = Env({"FOO": "bar", "BAR": "${FOO}"})
|
|
249
|
+
>>> env.write("foobar.env")
|
|
250
|
+
```
|
|
251
|
+
|
|
214
252
|
Get the resolved environment for the `foobar` stack:
|
|
215
253
|
|
|
216
254
|
```bash
|
|
@@ -253,6 +291,19 @@ BIZ=foo
|
|
|
253
291
|
FOO=foo
|
|
254
292
|
```
|
|
255
293
|
|
|
294
|
+
Here is an example using nested variable expansion:
|
|
295
|
+
|
|
296
|
+
```yaml
|
|
297
|
+
FOO: ${BIZ:=${BAR:=${BAZ:=baz}}}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Resolves to:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
$ envstack -r
|
|
304
|
+
FOO=baz
|
|
305
|
+
```
|
|
306
|
+
|
|
256
307
|
#### Includes
|
|
257
308
|
|
|
258
309
|
Environment stack files can include other namespaced environments (you should
|
|
@@ -40,7 +40,9 @@ import traceback
|
|
|
40
40
|
from envstack import __version__, config
|
|
41
41
|
from envstack.env import (
|
|
42
42
|
bake_environ,
|
|
43
|
-
|
|
43
|
+
Env,
|
|
44
|
+
encrypt_environ,
|
|
45
|
+
export_env_to_shell,
|
|
44
46
|
export,
|
|
45
47
|
load_environ,
|
|
46
48
|
resolve_environ,
|
|
@@ -83,12 +85,25 @@ def parse_args():
|
|
|
83
85
|
default=[config.DEFAULT_NAMESPACE],
|
|
84
86
|
help="the environment stacks to use (default '%s')" % config.DEFAULT_NAMESPACE,
|
|
85
87
|
)
|
|
88
|
+
encrypt_group = parser.add_argument_group("encryption options")
|
|
89
|
+
encrypt_group.add_argument(
|
|
90
|
+
"-e",
|
|
91
|
+
"--encrypt",
|
|
92
|
+
action="store_true",
|
|
93
|
+
help="encrypt environment values",
|
|
94
|
+
)
|
|
95
|
+
encrypt_group.add_argument(
|
|
96
|
+
"--keygen",
|
|
97
|
+
action="store_true",
|
|
98
|
+
help="generate encryption keys",
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument_group(encrypt_group)
|
|
86
101
|
bake_group = parser.add_argument_group("bake options")
|
|
87
102
|
bake_group.add_argument(
|
|
88
103
|
"-o",
|
|
89
104
|
"--out",
|
|
90
105
|
metavar="FILENAME",
|
|
91
|
-
help="
|
|
106
|
+
help="save the environment to an env file",
|
|
92
107
|
)
|
|
93
108
|
bake_group.add_argument(
|
|
94
109
|
"--depth",
|
|
@@ -96,27 +111,19 @@ def parse_args():
|
|
|
96
111
|
default=0,
|
|
97
112
|
help="depth of environment stack to bake",
|
|
98
113
|
)
|
|
99
|
-
bake_group.add_argument(
|
|
100
|
-
"--keygen",
|
|
101
|
-
action="store_true",
|
|
102
|
-
help="generate encryption keys",
|
|
103
|
-
)
|
|
104
|
-
bake_group.add_argument(
|
|
105
|
-
"--encrypt",
|
|
106
|
-
action="store_true",
|
|
107
|
-
help="encrypt the baked environment values",
|
|
108
|
-
)
|
|
109
114
|
parser.add_argument_group(bake_group)
|
|
110
|
-
parser.
|
|
115
|
+
export_group = parser.add_argument_group("export options")
|
|
116
|
+
export_group.add_argument(
|
|
111
117
|
"--clear",
|
|
112
118
|
action="store_true",
|
|
113
119
|
help="generate unset commands for %s" % config.SHELL,
|
|
114
120
|
)
|
|
115
|
-
|
|
121
|
+
export_group.add_argument(
|
|
116
122
|
"--export",
|
|
117
123
|
action="store_true",
|
|
118
124
|
help="generate export commands for %s" % config.SHELL,
|
|
119
125
|
)
|
|
126
|
+
parser.add_argument_group(export_group)
|
|
120
127
|
parser.add_argument(
|
|
121
128
|
"-p",
|
|
122
129
|
"--platform",
|
|
@@ -124,6 +131,13 @@ def parse_args():
|
|
|
124
131
|
metavar="PLATFORM",
|
|
125
132
|
help="platform to resolve variables for (linux, darwin, windows)",
|
|
126
133
|
)
|
|
134
|
+
parser.add_argument(
|
|
135
|
+
"-s",
|
|
136
|
+
"--set",
|
|
137
|
+
nargs="*",
|
|
138
|
+
metavar="VAR:VALUE",
|
|
139
|
+
help="set a key:value pair in the environment",
|
|
140
|
+
)
|
|
127
141
|
parser.add_argument(
|
|
128
142
|
"--scope",
|
|
129
143
|
metavar="SCOPE",
|
|
@@ -179,19 +193,28 @@ def main():
|
|
|
179
193
|
elif args.keygen:
|
|
180
194
|
from envstack.encrypt import generate_keys
|
|
181
195
|
|
|
182
|
-
|
|
196
|
+
data = generate_keys()
|
|
183
197
|
|
|
184
198
|
if args.export:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
print(export_env_to_shell(keys))
|
|
199
|
+
print(export_env_to_shell(data))
|
|
188
200
|
elif args.out:
|
|
189
|
-
|
|
201
|
+
Env(data).write(args.out)
|
|
202
|
+
else:
|
|
203
|
+
for key, value in data.items():
|
|
204
|
+
print(f"{key}={value}")
|
|
205
|
+
|
|
206
|
+
elif args.set:
|
|
207
|
+
data = dict(kv.split(":", 1) for kv in args.set)
|
|
190
208
|
|
|
191
|
-
|
|
209
|
+
if args.encrypt:
|
|
210
|
+
data = encrypt_environ(data, encrypt=(not args.out))
|
|
211
|
+
if args.export:
|
|
212
|
+
print(export_env_to_shell(data))
|
|
213
|
+
elif args.out:
|
|
214
|
+
Env(data).write(args.out)
|
|
192
215
|
else:
|
|
193
|
-
for key, value in
|
|
194
|
-
print(f"{key}
|
|
216
|
+
for key, value in data.items():
|
|
217
|
+
print(f"{key}={value}")
|
|
195
218
|
|
|
196
219
|
elif args.out:
|
|
197
220
|
bake_environ(
|
|
@@ -223,6 +246,8 @@ def main():
|
|
|
223
246
|
print(source.path)
|
|
224
247
|
|
|
225
248
|
elif args.clear:
|
|
249
|
+
from envstack.env import clear
|
|
250
|
+
|
|
226
251
|
print(clear(args.namespace, config.SHELL))
|
|
227
252
|
|
|
228
253
|
elif args.export:
|
|
@@ -89,7 +89,7 @@ class Source(object):
|
|
|
89
89
|
return hash(self.__repr__())
|
|
90
90
|
|
|
91
91
|
def __repr__(self):
|
|
92
|
-
return f
|
|
92
|
+
return f"<Source '{self.path}'>"
|
|
93
93
|
|
|
94
94
|
def __str__(self):
|
|
95
95
|
return str(self.path)
|
|
@@ -294,6 +294,88 @@ class Env(dict):
|
|
|
294
294
|
"""
|
|
295
295
|
self.scope = path
|
|
296
296
|
|
|
297
|
+
def bake(self, filename: str = None, depth: int = 0, encrypt: bool = False):
|
|
298
|
+
"""Bakes an environment with multiple sources into a single environment
|
|
299
|
+
and writes to a new env file.
|
|
300
|
+
|
|
301
|
+
>>> env = load_environ(stack_name)
|
|
302
|
+
>>> env.bake("baked.env")
|
|
303
|
+
|
|
304
|
+
:param filename: path to save the baked environment.
|
|
305
|
+
:param depth: depth of source files to incldue (default: all).
|
|
306
|
+
:param encrypt: encrypt the values.
|
|
307
|
+
:returns: baked environment.
|
|
308
|
+
"""
|
|
309
|
+
# get the sources for the given environment
|
|
310
|
+
sources = self.sources
|
|
311
|
+
|
|
312
|
+
# look for encryption keys in the environment
|
|
313
|
+
os.environ.update(get_keys_from_env(self))
|
|
314
|
+
|
|
315
|
+
# create a baked source
|
|
316
|
+
baked = Source(filename)
|
|
317
|
+
|
|
318
|
+
def get_node_class(value):
|
|
319
|
+
"""Returns the node class to use for a given value."""
|
|
320
|
+
if encrypt:
|
|
321
|
+
if type(value) in custom_node_types:
|
|
322
|
+
return value.__class__
|
|
323
|
+
else:
|
|
324
|
+
return EncryptedNode
|
|
325
|
+
return value.__class__
|
|
326
|
+
|
|
327
|
+
# merge the sources into the outfile
|
|
328
|
+
for source in sources[-depth:]:
|
|
329
|
+
for key, value in source.data.items():
|
|
330
|
+
if isinstance(value, dict):
|
|
331
|
+
for k, v in value.items():
|
|
332
|
+
node_class = get_node_class(v)
|
|
333
|
+
baked.data.setdefault(key, {})[k] = node_class(v)
|
|
334
|
+
else:
|
|
335
|
+
node_class = get_node_class(value)
|
|
336
|
+
baked.data[key] = node_class(value)
|
|
337
|
+
|
|
338
|
+
# clear includes if environment stack is fully baked
|
|
339
|
+
if depth <= 0:
|
|
340
|
+
baked.data["include"] = []
|
|
341
|
+
|
|
342
|
+
# write the baked environment to the file
|
|
343
|
+
if filename:
|
|
344
|
+
baked.write()
|
|
345
|
+
|
|
346
|
+
# create the baked environment from the baked source
|
|
347
|
+
baked_env = Env()
|
|
348
|
+
baked_env.load_source(baked)
|
|
349
|
+
|
|
350
|
+
return baked_env
|
|
351
|
+
|
|
352
|
+
def write(self, filename: str = None):
|
|
353
|
+
"""Writes the environment to an env file.
|
|
354
|
+
|
|
355
|
+
>>> env = Env({"FOO": "${BAR}", "BAR": "bar"})
|
|
356
|
+
>>> env.write("foo.env")
|
|
357
|
+
|
|
358
|
+
To encrypt values, use EncryptedNode:
|
|
359
|
+
|
|
360
|
+
>>> env = Env({"FOO": "${BAR}", "BAR": EncryptedNode("bar")})
|
|
361
|
+
>>> env.write("encrypted.env")
|
|
362
|
+
|
|
363
|
+
:param filename: path to save the baked environment.
|
|
364
|
+
:returns: Source object.
|
|
365
|
+
"""
|
|
366
|
+
# the environment was loaded from one or more sources
|
|
367
|
+
if self.sources:
|
|
368
|
+
baked = self.bake(filename)
|
|
369
|
+
return baked.sources[0]
|
|
370
|
+
|
|
371
|
+
# the environment was created from scratch
|
|
372
|
+
else:
|
|
373
|
+
source = Source(filename)
|
|
374
|
+
for k, v in self.items():
|
|
375
|
+
source.data[k] = v
|
|
376
|
+
source.write()
|
|
377
|
+
return source
|
|
378
|
+
|
|
297
379
|
|
|
298
380
|
def clear_file_cache():
|
|
299
381
|
"""Clears global file cache."""
|
|
@@ -664,55 +746,21 @@ def bake_environ(
|
|
|
664
746
|
:param encrypt: encrypt the values.
|
|
665
747
|
:returns: baked environment.
|
|
666
748
|
"""
|
|
667
|
-
|
|
668
|
-
env = load_environ(name, scope=scope)
|
|
669
|
-
sources = env.sources
|
|
670
|
-
|
|
671
|
-
# resolve internal environment so that encryption keys are found
|
|
672
|
-
# (encryptors looks in os.environ by default)
|
|
673
|
-
os.environ.update(util.encode(resolve_environ(env)))
|
|
749
|
+
return load_environ(name, scope=scope).bake(filename, depth, encrypt)
|
|
674
750
|
|
|
675
|
-
# create a baked source
|
|
676
|
-
baked = Source(filename)
|
|
677
751
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
else:
|
|
684
|
-
return EncryptedNode
|
|
685
|
-
return value.__class__
|
|
686
|
-
|
|
687
|
-
# merge the sources into the outfile
|
|
688
|
-
for source in sources[-depth:]:
|
|
689
|
-
for key, value in source.data.items():
|
|
690
|
-
if isinstance(value, dict):
|
|
691
|
-
for k, v in value.items():
|
|
692
|
-
node_class = get_node_class(v)
|
|
693
|
-
baked.data.setdefault(key, {})[k] = node_class(v)
|
|
694
|
-
else:
|
|
695
|
-
node_class = get_node_class(value)
|
|
696
|
-
baked.data[key] = node_class(value)
|
|
697
|
-
|
|
698
|
-
# clear includes if environment stack is fully baked
|
|
699
|
-
if depth <= 0:
|
|
700
|
-
baked.data["include"] = []
|
|
701
|
-
|
|
702
|
-
# write the baked environment to the file
|
|
703
|
-
if filename:
|
|
704
|
-
baked.write()
|
|
705
|
-
|
|
706
|
-
# create the baked environment
|
|
707
|
-
baked_env = Env()
|
|
708
|
-
baked_env.update(baked.load())
|
|
752
|
+
def encrypt_environ(
|
|
753
|
+
env: dict, node_class: BaseNode = EncryptedNode, encrypt: bool = True
|
|
754
|
+
):
|
|
755
|
+
"""Encrypts all values in a given environment, returning a new environment.
|
|
756
|
+
Looks for encryption keys in the environment.
|
|
709
757
|
|
|
710
|
-
|
|
758
|
+
Python:
|
|
711
759
|
|
|
760
|
+
>>> env = {"FOO": "bar"}
|
|
761
|
+
>>> env = envstack.encrypt_environ(env)
|
|
712
762
|
|
|
713
|
-
|
|
714
|
-
"""Encrypts all values in a given environment, returning a new environment.
|
|
715
|
-
Looks for encryption keys in the environment.
|
|
763
|
+
Command line:
|
|
716
764
|
|
|
717
765
|
$ envstack [STACK] --encrypt
|
|
718
766
|
|
|
@@ -720,6 +768,7 @@ def encrypt_environ(env: dict, node_class: BaseNode = EncryptedNode):
|
|
|
720
768
|
:param node_class: node class to use for encryption.
|
|
721
769
|
Defaults to EncryptedNode, which looks for encryption keys in the
|
|
722
770
|
environment to determine the encryption method.
|
|
771
|
+
:param encrypt: pre-encrypt the values.
|
|
723
772
|
:returns: encrypted environment.
|
|
724
773
|
"""
|
|
725
774
|
# stores the encrypted environment
|
|
@@ -736,7 +785,8 @@ def encrypt_environ(env: dict, node_class: BaseNode = EncryptedNode):
|
|
|
736
785
|
if type(v) not in custom_node_types:
|
|
737
786
|
# TODO: use to_yaml() method to serialize instead?
|
|
738
787
|
node = node_class(v)
|
|
739
|
-
|
|
788
|
+
if encrypt:
|
|
789
|
+
node.value = node.encryptor(env=resolved_env).encrypt(str(v))
|
|
740
790
|
encrypted_env[k] = node
|
|
741
791
|
else:
|
|
742
792
|
encrypted_env[k] = v
|
|
@@ -49,14 +49,14 @@ from envstack.node import AESGCMNode, Base64Node, EncryptedNode, FernetNode
|
|
|
49
49
|
# value for unresolvable variables
|
|
50
50
|
null = ""
|
|
51
51
|
|
|
52
|
+
# regular expression pattern for matching windows drive letters
|
|
53
|
+
drive_letter_pattern = re.compile(r"(?P<sep>[:;])?(?P<drive>[A-Z]:[/\\])")
|
|
54
|
+
|
|
52
55
|
# regular expression pattern for bash-like variable expansion
|
|
53
56
|
variable_pattern = re.compile(
|
|
54
|
-
r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([
|
|
57
|
+
r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([-=?])((?:\$\{[^}]+\}|[^}])*))?\}"
|
|
55
58
|
)
|
|
56
59
|
|
|
57
|
-
# regular expression pattern for matching windows drive letters
|
|
58
|
-
drive_letter_pattern = re.compile(r"(?P<sep>[:;])?(?P<drive>[A-Z]:[/\\])")
|
|
59
|
-
|
|
60
60
|
|
|
61
61
|
def clear_sys_path(var: str = "PYTHONPATH"):
|
|
62
62
|
"""
|
|
@@ -72,9 +72,9 @@ def clear_sys_path(var: str = "PYTHONPATH"):
|
|
|
72
72
|
def decode_value(value: str):
|
|
73
73
|
"""Returns a decoded value that's been encoded by a wrapper.
|
|
74
74
|
|
|
75
|
-
Decoding encoded environments can be tricky. For example, it must account
|
|
76
|
-
templates that include curly braces, e.g. path templates string
|
|
77
|
-
preserved:
|
|
75
|
+
Decoding encoded environments can be tricky. For example, it must account
|
|
76
|
+
for path templates that include curly braces, e.g. path templates string
|
|
77
|
+
like this must be preserved:
|
|
78
78
|
|
|
79
79
|
'/path/with/{variable}'
|
|
80
80
|
|
|
@@ -270,6 +270,16 @@ def evaluate_modifiers(expression: str, environ: dict = os.environ):
|
|
|
270
270
|
error message.
|
|
271
271
|
"""
|
|
272
272
|
|
|
273
|
+
def sanitize_value(value):
|
|
274
|
+
"""Sanitize the value before returning it."""
|
|
275
|
+
# HACK: remove trailing curly braces if they exist
|
|
276
|
+
if type(value) is str and value.endswith("}") and not value.startswith("${"):
|
|
277
|
+
return value.rstrip("}")
|
|
278
|
+
# sanitize path-like values
|
|
279
|
+
elif type(value) is str and ":" in value and ("/" in value or "\\" in value):
|
|
280
|
+
return dedupe_paths(value)
|
|
281
|
+
return value
|
|
282
|
+
|
|
273
283
|
def substitute_variable(match):
|
|
274
284
|
"""Substitute a variable match with its value."""
|
|
275
285
|
var_name = match.group(1)
|
|
@@ -288,17 +298,20 @@ def evaluate_modifiers(expression: str, environ: dict = os.environ):
|
|
|
288
298
|
else:
|
|
289
299
|
value = value.replace(varstr, "")
|
|
290
300
|
|
|
291
|
-
|
|
301
|
+
# ${VAR:=default} or ${VAR:-default}
|
|
302
|
+
if operator in ("=", "-"):
|
|
292
303
|
if override:
|
|
293
304
|
value = override
|
|
294
305
|
elif variable_pattern.search(value) or value is None:
|
|
295
306
|
value = evaluate_modifiers(argument, environ)
|
|
296
307
|
else:
|
|
297
308
|
value = value or argument
|
|
309
|
+
# ${VAR:?error message}
|
|
298
310
|
elif operator == "?":
|
|
299
311
|
if not value:
|
|
300
312
|
error_message = argument if argument else f"{var_name} is not set"
|
|
301
313
|
raise ValueError(error_message)
|
|
314
|
+
# handle recursive references
|
|
302
315
|
elif variable_pattern.search(value):
|
|
303
316
|
value = evaluate_modifiers(value, environ)
|
|
304
317
|
# handle simple ${VAR} substitution
|
|
@@ -311,19 +324,10 @@ def evaluate_modifiers(expression: str, environ: dict = os.environ):
|
|
|
311
324
|
# substitute all matches in the expression
|
|
312
325
|
result = variable_pattern.sub(substitute_variable, expression)
|
|
313
326
|
|
|
314
|
-
# HACK: remove trailing curly braces if they exist
|
|
315
|
-
if result.endswith("}") and not result.startswith("${"):
|
|
316
|
-
result = result.rstrip("}")
|
|
317
|
-
|
|
318
327
|
# evaluate any remaining modifiers, eg. ${VAR:=${FOO:=bar}}
|
|
319
328
|
if variable_pattern.search(result):
|
|
320
329
|
result = evaluate_modifiers(result, environ)
|
|
321
330
|
|
|
322
|
-
# dedupe path-like values and resolve separators
|
|
323
|
-
# TODO: replace with regex pattern to detect path-like strings
|
|
324
|
-
elif ":" in result and ("/" in result or "\\" in result):
|
|
325
|
-
result = dedupe_paths(result)
|
|
326
|
-
|
|
327
331
|
# detect recursion errors
|
|
328
332
|
except RecursionError:
|
|
329
333
|
raise CyclicalReference(f"Cyclical reference detected in {expression}")
|
|
@@ -340,27 +344,15 @@ def evaluate_modifiers(expression: str, environ: dict = os.environ):
|
|
|
340
344
|
elif isinstance(expression, FernetNode):
|
|
341
345
|
result = expression.resolve(env=environ)
|
|
342
346
|
elif isinstance(expression, list):
|
|
343
|
-
result = [
|
|
344
|
-
(
|
|
345
|
-
variable_pattern.sub(substitute_variable, str(v))
|
|
346
|
-
if isinstance(v, str)
|
|
347
|
-
else v
|
|
348
|
-
)
|
|
349
|
-
for v in expression
|
|
350
|
-
]
|
|
347
|
+
result = [(evaluate_modifiers(v, environ)) for v in expression]
|
|
351
348
|
elif isinstance(expression, dict):
|
|
352
349
|
result = {
|
|
353
|
-
k: (
|
|
354
|
-
variable_pattern.sub(substitute_variable, str(v))
|
|
355
|
-
if isinstance(v, str)
|
|
356
|
-
else v
|
|
357
|
-
)
|
|
358
|
-
for k, v in expression.items()
|
|
350
|
+
k: (evaluate_modifiers(v, environ)) for k, v in expression.items()
|
|
359
351
|
}
|
|
360
352
|
else:
|
|
361
353
|
result = expression
|
|
362
354
|
|
|
363
|
-
return result
|
|
355
|
+
return sanitize_value(result)
|
|
364
356
|
|
|
365
357
|
|
|
366
358
|
def load_sys_path(
|
|
@@ -571,9 +563,9 @@ def partition_platform_data(data: dict):
|
|
|
571
563
|
:param data: dictionary to partition.
|
|
572
564
|
:returns: platform partitioned dictionary.
|
|
573
565
|
"""
|
|
574
|
-
# ensure all is present
|
|
566
|
+
# ensure "all" key is present
|
|
575
567
|
if "all" not in data:
|
|
576
|
-
data["all"] =
|
|
568
|
+
data["all"] = dict((k, v) for k, v in data.items() if k != "include")
|
|
577
569
|
|
|
578
570
|
# platforms of interest (darwin, linux, windows)
|
|
579
571
|
platforms = ["darwin", "linux", "windows"]
|