envstack 0.9.2__tar.gz → 0.9.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.
Files changed (31) hide show
  1. {envstack-0.9.2/lib/envstack.egg-info → envstack-0.9.4}/PKG-INFO +10 -7
  2. {envstack-0.9.2 → envstack-0.9.4}/README.md +9 -6
  3. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack/__init__.py +2 -2
  4. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack/cli.py +26 -6
  5. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack/config.py +1 -1
  6. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack/encrypt.py +8 -8
  7. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack/env.py +7 -3
  8. envstack-0.9.4/lib/envstack/envshell.py +143 -0
  9. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack/exceptions.py +1 -1
  10. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack/logger.py +1 -1
  11. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack/node.py +2 -2
  12. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack/path.py +1 -1
  13. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack/util.py +1 -6
  14. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack/wrapper.py +62 -57
  15. {envstack-0.9.2 → envstack-0.9.4/lib/envstack.egg-info}/PKG-INFO +10 -7
  16. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack.egg-info/SOURCES.txt +3 -1
  17. {envstack-0.9.2 → envstack-0.9.4}/setup.py +2 -2
  18. {envstack-0.9.2 → envstack-0.9.4}/tests/test_cmds.py +53 -88
  19. {envstack-0.9.2 → envstack-0.9.4}/tests/test_encrypt.py +1 -1
  20. {envstack-0.9.2 → envstack-0.9.4}/tests/test_env.py +10 -10
  21. {envstack-0.9.2 → envstack-0.9.4}/tests/test_node.py +2 -2
  22. {envstack-0.9.2 → envstack-0.9.4}/tests/test_util.py +1 -1
  23. envstack-0.9.4/tests/test_wrapper.py +141 -0
  24. {envstack-0.9.2 → envstack-0.9.4}/LICENSE +0 -0
  25. {envstack-0.9.2 → envstack-0.9.4}/dist.json +0 -0
  26. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack.egg-info/dependency_links.txt +0 -0
  27. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack.egg-info/entry_points.txt +0 -0
  28. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack.egg-info/not-zip-safe +0 -0
  29. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack.egg-info/requires.txt +0 -0
  30. {envstack-0.9.2 → envstack-0.9.4}/lib/envstack.egg-info/top_level.txt +0 -0
  31. {envstack-0.9.2 → envstack-0.9.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: envstack
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: Stacked environment variable management system
5
5
  Home-page: http://github.com/rsgalloway/envstack
6
6
  Author: Ryan Galloway
@@ -112,7 +112,9 @@ STACK=default
112
112
  If you are not seeing the above output, make sure the `default.env` stack file
113
113
  is in `${ENVPATH}` or the current working directory.
114
114
 
115
- > NOTE: The name of the current stack will always be stored in `${STACK}`
115
+ > NOTE: The name of the current stack will always be stored in `${STACK}`.
116
+
117
+ ENV is the tier, STACK is the namespace.
116
118
 
117
119
  Environments can be combined, or stacked, in order of priority (variables
118
120
  defined in stacks flow from higher scope to lower scope, left to right):
@@ -371,21 +373,21 @@ Then use `keys.env` to encrypt any other environment files:
371
373
  $ ./keys.env -- envstack -eo encrypted.env
372
374
  ```
373
375
 
374
- To decrypt, add `keys` to the env stack:
376
+ To decrypt, run the command inside the `keys` environment again:
375
377
 
376
378
  ```bash
377
- $ envstack keys encrypted -r HELLO
379
+ $ ./keys.env -- envstack encrypted -r HELLO
378
380
  HELLO=world
379
381
  ```
380
382
 
381
- Or run the command inside the `keys` environment like this:
383
+ Or add `keys` to the env stack:
382
384
 
383
385
  ```bash
384
- $ ./keys.env -- envsatck encrypted -r HELLO
386
+ $ envstack keys encrypted -r HELLO
385
387
  HELLO=world
386
388
  ```
387
389
 
388
- Or include `keys` in environments to automatically decrypt:
390
+ Or automatically include `keys`:
389
391
 
390
392
  ```yaml
391
393
  include: [keys]
@@ -614,6 +616,7 @@ The following environment variables are used to help manage functionality:
614
616
  | DEFAULT_ENV_STACK | Name of the default environment stack (default) |
615
617
  | ENVPATH | Colon-separated paths to search for stack files |
616
618
  | IGNORE_MISSING | Ignore missing stack files when resolving environments |
619
+ | INTERACTIVE | Run one-off commands in an interactive shell |
617
620
  | STACK | Stores the name of the current environment stack |
618
621
 
619
622
  # Tests
@@ -88,7 +88,9 @@ STACK=default
88
88
  If you are not seeing the above output, make sure the `default.env` stack file
89
89
  is in `${ENVPATH}` or the current working directory.
90
90
 
91
- > NOTE: The name of the current stack will always be stored in `${STACK}`
91
+ > NOTE: The name of the current stack will always be stored in `${STACK}`.
92
+
93
+ ENV is the tier, STACK is the namespace.
92
94
 
93
95
  Environments can be combined, or stacked, in order of priority (variables
94
96
  defined in stacks flow from higher scope to lower scope, left to right):
@@ -347,21 +349,21 @@ Then use `keys.env` to encrypt any other environment files:
347
349
  $ ./keys.env -- envstack -eo encrypted.env
348
350
  ```
349
351
 
350
- To decrypt, add `keys` to the env stack:
352
+ To decrypt, run the command inside the `keys` environment again:
351
353
 
352
354
  ```bash
353
- $ envstack keys encrypted -r HELLO
355
+ $ ./keys.env -- envstack encrypted -r HELLO
354
356
  HELLO=world
355
357
  ```
356
358
 
357
- Or run the command inside the `keys` environment like this:
359
+ Or add `keys` to the env stack:
358
360
 
359
361
  ```bash
360
- $ ./keys.env -- envsatck encrypted -r HELLO
362
+ $ envstack keys encrypted -r HELLO
361
363
  HELLO=world
362
364
  ```
363
365
 
364
- Or include `keys` in environments to automatically decrypt:
366
+ Or automatically include `keys`:
365
367
 
366
368
  ```yaml
367
369
  include: [keys]
@@ -590,6 +592,7 @@ The following environment variables are used to help manage functionality:
590
592
  | DEFAULT_ENV_STACK | Name of the default environment stack (default) |
591
593
  | ENVPATH | Colon-separated paths to search for stack files |
592
594
  | IGNORE_MISSING | Ignore missing stack files when resolving environments |
595
+ | INTERACTIVE | Run one-off commands in an interactive shell |
593
596
  | STACK | Stores the name of the current environment stack |
594
597
 
595
598
  # Tests
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python
1
+ #!/usr/bin/env python3
2
2
  #
3
3
  # Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
4
4
  #
@@ -34,6 +34,6 @@ Stacked environment variable management system.
34
34
  """
35
35
 
36
36
  __prog__ = "envstack"
37
- __version__ = "0.9.2"
37
+ __version__ = "0.9.4"
38
38
 
39
39
  from envstack.env import clear, init, revert, save # noqa: F401
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python
1
+ #!/usr/bin/env python3
2
2
  #
3
3
  # Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
4
4
  #
@@ -37,6 +37,7 @@ import argparse
37
37
  import re
38
38
  import sys
39
39
  import traceback
40
+ from typing import List
40
41
 
41
42
  from envstack import __version__, config
42
43
  from envstack.env import (
@@ -147,15 +148,14 @@ def parse_args():
147
148
  action="version",
148
149
  version=f"envstack {__version__}",
149
150
  )
150
- group = parser.add_mutually_exclusive_group(required=False)
151
- group.add_argument(
151
+ parser.add_argument(
152
152
  "namespace",
153
153
  metavar="STACK",
154
154
  nargs="*",
155
155
  default=[config.DEFAULT_NAMESPACE],
156
156
  help="the environment stacks to use",
157
157
  )
158
- group.add_argument(
158
+ parser.add_argument(
159
159
  "-b",
160
160
  "--bare",
161
161
  action="store_true",
@@ -221,6 +221,12 @@ def parse_args():
221
221
  metavar="SCOPE",
222
222
  help="search scope for environment stack files",
223
223
  )
224
+ parser.add_argument(
225
+ "-u",
226
+ "--unresolved",
227
+ action="store_true",
228
+ help="dump unresolved environment variables to stdout",
229
+ )
224
230
  parser.add_argument(
225
231
  "-r",
226
232
  "--resolve",
@@ -254,9 +260,20 @@ def parse_args():
254
260
  return args, args_after_dash
255
261
 
256
262
 
263
+ def envshell(namespace: List[str] = None):
264
+ """Run a shell in the given environment stack."""
265
+ from .envshell import EnvshellWrapper
266
+
267
+ print("\U0001F680 Launching envstack shell... CTRL+D to exit")
268
+
269
+ name = (namespace or [config.DEFAULT_NAMESPACE])[:]
270
+ shell = EnvshellWrapper(name)
271
+ return shell.launch()
272
+
273
+
257
274
  def whichenv():
258
275
  """Entry point for the whichenv command line tool. Finds {VAR}s."""
259
- from envstack.util import findenv
276
+ from .util import findenv
260
277
 
261
278
  if len(sys.argv) != 2:
262
279
  print("Usage: whichenv [VAR]")
@@ -426,13 +443,16 @@ def main():
426
443
  for source in env.sources:
427
444
  print(source.path)
428
445
 
429
- else:
446
+ elif args.unresolved:
430
447
  env = load_environ(
431
448
  args.namespace, platform=args.platform, encrypt=args.encrypt
432
449
  )
433
450
  for k, v in sorted(env.items(), key=lambda x: str(x[0])):
434
451
  print(f"{k}={v}")
435
452
 
453
+ else:
454
+ return envshell(args.namespace)
455
+
436
456
  except KeyboardInterrupt:
437
457
  print("Stopping...")
438
458
  return 2
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python
1
+ #!/usr/bin/env python3
2
2
  #
3
3
  # Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
4
4
  #
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python
1
+ #!/usr/bin/env python3
2
2
  #
3
3
  # Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
4
4
  #
@@ -43,6 +43,7 @@ from envstack.logger import log
43
43
 
44
44
  # cryptography and _rust dependency may not be available everywhere
45
45
  # ImportError: DLL load failed while importing _rust: Module not found.
46
+ Fernet = None
46
47
  try:
47
48
  import cryptography.exceptions
48
49
  from cryptography.fernet import Fernet, InvalidToken
@@ -50,7 +51,6 @@ try:
50
51
  from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
51
52
  except ImportError as err:
52
53
  log.debug("cryptography module not available: %s", err)
53
- Fernet = None
54
54
 
55
55
 
56
56
  class Base64Encryptor(object):
@@ -78,7 +78,7 @@ class FernetEncryptor(object):
78
78
  KEY_VAR_NAME = "ENVSTACK_FERNET_KEY"
79
79
 
80
80
  def __init__(self, key: str = None, env: dict = os.environ):
81
- if key:
81
+ if key and Fernet:
82
82
  self.key = Fernet(key)
83
83
  else:
84
84
  self.key = self.get_key(env)
@@ -90,7 +90,8 @@ class FernetEncryptor(object):
90
90
  key = Fernet.generate_key()
91
91
  return key.decode()
92
92
  else:
93
- log.error("Fernet encryption not available")
93
+ log.debug("Fernet encryption not available")
94
+ return ""
94
95
 
95
96
  def get_key(self, env: dict = os.environ):
96
97
  """Load the encryption key from the environment `env`.
@@ -99,7 +100,7 @@ class FernetEncryptor(object):
99
100
  :return: encryption key.
100
101
  """
101
102
  key = env.get(self.KEY_VAR_NAME)
102
- if key:
103
+ if key and Fernet:
103
104
  return Fernet(key)
104
105
  return key
105
106
 
@@ -313,12 +314,11 @@ def generate_keys():
313
314
 
314
315
  :returns: Dictionary containing Fernet and AES-GCM keys.
315
316
  """
316
- from envstack.node import Base64Node
317
317
 
318
318
  symmetric_key = AESGCMEncryptor.generate_key()
319
319
  fernet_key = FernetEncryptor.generate_key()
320
320
 
321
321
  return {
322
- AESGCMEncryptor.KEY_VAR_NAME: Base64Node(symmetric_key),
323
- FernetEncryptor.KEY_VAR_NAME: Base64Node(fernet_key),
322
+ AESGCMEncryptor.KEY_VAR_NAME: symmetric_key,
323
+ FernetEncryptor.KEY_VAR_NAME: fernet_key,
324
324
  }
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python
1
+ #!/usr/bin/env python3
2
2
  #
3
3
  # Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
4
4
  #
@@ -36,6 +36,7 @@ Contains functions and classes for processing scoped .env files.
36
36
  import os
37
37
  import re
38
38
  import string
39
+ from collections import defaultdict
39
40
  from pathlib import Path
40
41
 
41
42
  import yaml # noqa
@@ -77,7 +78,7 @@ class Source(object):
77
78
  :param path: path to .env file.
78
79
  """
79
80
  self.path = path
80
- self.data = {}
81
+ self.data = defaultdict(dict)
81
82
 
82
83
  def __eq__(self, other):
83
84
  if not isinstance(other, Source):
@@ -126,7 +127,10 @@ class Source(object):
126
127
 
127
128
  def write(self, filepath: str = None):
128
129
  """Writes the source data to the .env file."""
129
- util.dump_yaml(filepath or self.path, self.data)
130
+ try:
131
+ util.dump_yaml(filepath or self.path, self.data)
132
+ except Exception as err:
133
+ logger.log.exception("Failed to write %s: %s", filepath or self.path, err)
130
134
 
131
135
 
132
136
  class EnvVar(string.Template, str):
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # Copyright (c) 2024-2025, 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
+ """
33
+ Contains envshell wrapper class.
34
+
35
+ Goal: drop the user into an interactive shell *without* sourcing their usual
36
+ shell rc files, so prompt vars (PS1/PROMPT) set by envstack can survive.
37
+
38
+ Notes:
39
+ - You can override the detected shell with ENVSTACK_SHELL.
40
+ Examples:
41
+ ENVSTACK_SHELL=/bin/zsh envstack --shell
42
+ ENVSTACK_SHELL=pwsh envstack --shell
43
+ """
44
+
45
+ import os
46
+ from pathlib import Path
47
+ from typing import List
48
+
49
+ from . import config
50
+ from .wrapper import Wrapper
51
+
52
+
53
+ def _basename(p: str) -> str:
54
+ """Return the lowercase basename of a path, robustly."""
55
+ try:
56
+ return Path(p).name.lower()
57
+ except Exception:
58
+ return os.path.basename(p).lower()
59
+
60
+
61
+ def _detect_shell_argv() -> List[str]:
62
+ """
63
+ Return argv list for a *clean* interactive shell.
64
+ """
65
+ shell = os.environ.get("ENVSTACK_SHELL", config.SHELL)
66
+
67
+ if os.name == "nt":
68
+ # Prefer COMSPEC if it looks like cmd.exe; otherwise allow pwsh/powershell.
69
+ comspec = os.environ.get("COMSPEC", "cmd.exe")
70
+
71
+ # If user explicitly asked for pwsh/powershell, honor it.
72
+ base = _basename(shell)
73
+ if base in ("pwsh", "pwsh.exe"):
74
+ return [shell, "-NoExit", "-NoProfile"]
75
+ if base in ("powershell", "powershell.exe"):
76
+ return [shell, "-NoExit", "-NoProfile"]
77
+
78
+ # Otherwise use cmd.exe, with /K to keep it open.
79
+ # (Even if config.SHELL returned "cmd", use COMSPEC so we get the real path.)
80
+ return [comspec, "/K"]
81
+
82
+ # POSIX shells
83
+ base = _basename(shell)
84
+
85
+ # bash: skip /etc/profile, ~/.bash_profile, ~/.bashrc, but stay interactive
86
+ if base == "bash":
87
+ return [shell, "--noprofile", "--norc", "-i"]
88
+
89
+ # zsh: -f skips zshrcs; -i for interactive
90
+ if base == "zsh":
91
+ return [shell, "-f", "-i"]
92
+
93
+ # tcsh/csh: -f skips rc; interactive by default when attached to a tty
94
+ if base in ("tcsh", "csh"):
95
+ return [shell, "-f"]
96
+
97
+ # fish: --no-config skips config.fish; interactive by default
98
+ if base == "fish":
99
+ return [shell, "--no-config"]
100
+
101
+ # Fallback: try interactive flag if common; otherwise just exec the shell
102
+ # (Most shells become interactive when connected to a tty anyway.)
103
+ return [shell, "-i"]
104
+
105
+
106
+ class EnvshellWrapper(Wrapper):
107
+ """A wrapper that spawns an interactive shell with the environment set."""
108
+
109
+ def __init__(self, *args, **kwargs):
110
+ super(EnvshellWrapper, self).__init__(*args, **kwargs)
111
+ self.shell = False # exec the shell directly
112
+
113
+ def executable(self):
114
+ """
115
+ Kept for interface compatibility. The actual argv is produced in
116
+ get_subprocess_command().
117
+ """
118
+ return ""
119
+
120
+ def get_subprocess_command(self, env):
121
+ """
122
+ Override to return argv list for subprocess.Popen(..., shell=False).
123
+ """
124
+ return _detect_shell_argv()
125
+
126
+ def get_shell_prompt(self) -> str:
127
+ """
128
+ Return the environment variable that controls the shell prompt and its
129
+ desired value.
130
+ """
131
+ if os.name == "nt":
132
+ return ("PROMPT", "$E[32m(${ENV:=${STACK}})$E[0m $P$G ")
133
+ else:
134
+ return ("PS1", "\[\e[32m\](${ENV:=${STACK}})\[\e[0m\] \w\$ ")
135
+
136
+ def get_subprocess_env(self):
137
+ """
138
+ Override to inject PS1/PROMPT if not already set.
139
+ """
140
+ prompt_env, prompt_value = self.get_shell_prompt()
141
+ if prompt_env not in self.env:
142
+ self.env[prompt_env] = prompt_value
143
+ return super().get_subprocess_env()
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python
1
+ #!/usr/bin/env python3
2
2
  #
3
3
  # Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
4
4
  #
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python
1
+ #!/usr/bin/env python3
2
2
  #
3
3
  # Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
4
4
  #
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python
1
+ #!/usr/bin/env python3
2
2
  #
3
3
  # Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
4
4
  #
@@ -328,7 +328,7 @@ class CustomDumper(yaml.SafeDumper):
328
328
  node.style = '"%s"' % node.value
329
329
  elif node.value and node.value[-1] == ":":
330
330
  node.style = '"%s"' % node.value
331
- elif ": " in node.value:
331
+ elif ": " in str(node.value):
332
332
  node.style = '"%s"' % node.value
333
333
 
334
334
  def quote_vars(self, node: yaml.Node):
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python
1
+ #!/usr/bin/env python3
2
2
  #
3
3
  # Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
4
4
  #
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python
1
+ #!/usr/bin/env python3
2
2
  #
3
3
  # Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
4
4
  #
@@ -604,17 +604,12 @@ def validate_yaml(file_path: str):
604
604
 
605
605
  :param file_path: Path to the YAML file to validate.
606
606
  """
607
- required_keys = {"all", "darwin", "linux", "windows"}
608
-
609
607
  try:
610
608
  with open(file_path, "r") as stream:
611
609
  data = yaml.safe_load(stream.read())
612
610
  # data = yaml.load(stream.read(), Loader=CustomLoader)
613
611
  if not isinstance(data, dict):
614
612
  raise yaml.YAMLError("invalid data structure")
615
- missing_keys = required_keys - data.keys()
616
- if missing_keys:
617
- raise yaml.YAMLError(f"missing keys: {', '.join(sorted(missing_keys))}")
618
613
  return data
619
614
  except OSError as e:
620
615
  print(e)