siddharth24m commited on
Commit
cf14780
·
verified ·
1 Parent(s): b39229b

Upload 17 files

Browse files
dotenv/__init__.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Optional
2
+
3
+ from .main import dotenv_values, find_dotenv, get_key, load_dotenv, set_key, unset_key
4
+
5
+
6
+ def load_ipython_extension(ipython: Any) -> None:
7
+ from .ipython import load_ipython_extension
8
+
9
+ load_ipython_extension(ipython)
10
+
11
+
12
+ def get_cli_string(
13
+ path: Optional[str] = None,
14
+ action: Optional[str] = None,
15
+ key: Optional[str] = None,
16
+ value: Optional[str] = None,
17
+ quote: Optional[str] = None,
18
+ ):
19
+ """Returns a string suitable for running as a shell script.
20
+
21
+ Useful for converting a arguments passed to a fabric task
22
+ to be passed to a `local` or `run` command.
23
+ """
24
+ command = ["dotenv"]
25
+ if quote:
26
+ command.append(f"-q {quote}")
27
+ if path:
28
+ command.append(f"-f {path}")
29
+ if action:
30
+ command.append(action)
31
+ if key:
32
+ command.append(key)
33
+ if value:
34
+ if " " in value:
35
+ command.append(f'"{value}"')
36
+ else:
37
+ command.append(value)
38
+
39
+ return " ".join(command).strip()
40
+
41
+
42
+ __all__ = [
43
+ "get_cli_string",
44
+ "load_dotenv",
45
+ "dotenv_values",
46
+ "get_key",
47
+ "set_key",
48
+ "unset_key",
49
+ "find_dotenv",
50
+ "load_ipython_extension",
51
+ ]
dotenv/__main__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Entry point for cli, enables execution with `python -m dotenv`"""
2
+
3
+ from .cli import cli
4
+
5
+ if __name__ == "__main__":
6
+ cli()
dotenv/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (2.09 kB). View file
 
dotenv/__pycache__/__main__.cpython-314.pyc ADDED
Binary file (346 Bytes). View file
 
dotenv/__pycache__/cli.cpython-314.pyc ADDED
Binary file (12.4 kB). View file
 
dotenv/__pycache__/ipython.cpython-314.pyc ADDED
Binary file (2.06 kB). View file
 
dotenv/__pycache__/main.cpython-314.pyc ADDED
Binary file (23.8 kB). View file
 
dotenv/__pycache__/parser.cpython-314.pyc ADDED
Binary file (13.6 kB). View file
 
dotenv/__pycache__/variables.cpython-314.pyc ADDED
Binary file (7.36 kB). View file
 
dotenv/__pycache__/version.cpython-314.pyc ADDED
Binary file (197 Bytes). View file
 
dotenv/cli.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import shlex
4
+ import sys
5
+ from contextlib import contextmanager
6
+ from typing import IO, Any, Dict, Iterator, List, Optional
7
+
8
+ if sys.platform == "win32":
9
+ from subprocess import Popen
10
+
11
+ try:
12
+ import click
13
+ except ImportError:
14
+ sys.stderr.write(
15
+ "It seems python-dotenv is not installed with cli option. \n"
16
+ 'Run pip install "python-dotenv[cli]" to fix this.'
17
+ )
18
+ sys.exit(1)
19
+
20
+ from .main import dotenv_values, set_key, unset_key
21
+ from .version import __version__
22
+
23
+
24
+ def enumerate_env() -> Optional[str]:
25
+ """
26
+ Return a path for the ${pwd}/.env file.
27
+
28
+ If pwd does not exist, return None.
29
+ """
30
+ try:
31
+ cwd = os.getcwd()
32
+ except FileNotFoundError:
33
+ return None
34
+ path = os.path.join(cwd, ".env")
35
+ return path
36
+
37
+
38
+ @click.group()
39
+ @click.option(
40
+ "-f",
41
+ "--file",
42
+ default=enumerate_env(),
43
+ type=click.Path(file_okay=True),
44
+ help="Location of the .env file, defaults to .env file in current working directory.",
45
+ )
46
+ @click.option(
47
+ "-q",
48
+ "--quote",
49
+ default="always",
50
+ type=click.Choice(["always", "never", "auto"]),
51
+ help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.",
52
+ )
53
+ @click.option(
54
+ "-e",
55
+ "--export",
56
+ default=False,
57
+ type=click.BOOL,
58
+ help="Whether to write the dot file as an executable bash script.",
59
+ )
60
+ @click.version_option(version=__version__)
61
+ @click.pass_context
62
+ def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None:
63
+ """This script is used to set, get or unset values from a .env file."""
64
+ ctx.obj = {"QUOTE": quote, "EXPORT": export, "FILE": file}
65
+
66
+
67
+ @contextmanager
68
+ def stream_file(path: os.PathLike) -> Iterator[IO[str]]:
69
+ """
70
+ Open a file and yield the corresponding (decoded) stream.
71
+
72
+ Exits with error code 2 if the file cannot be opened.
73
+ """
74
+
75
+ try:
76
+ with open(path) as stream:
77
+ yield stream
78
+ except OSError as exc:
79
+ print(f"Error opening env file: {exc}", file=sys.stderr)
80
+ sys.exit(2)
81
+
82
+
83
+ @cli.command(name="list")
84
+ @click.pass_context
85
+ @click.option(
86
+ "--format",
87
+ "output_format",
88
+ default="simple",
89
+ type=click.Choice(["simple", "json", "shell", "export"]),
90
+ help="The format in which to display the list. Default format is simple, "
91
+ "which displays name=value without quotes.",
92
+ )
93
+ def list_values(ctx: click.Context, output_format: str) -> None:
94
+ """Display all the stored key/value."""
95
+ file = ctx.obj["FILE"]
96
+
97
+ with stream_file(file) as stream:
98
+ values = dotenv_values(stream=stream)
99
+
100
+ if output_format == "json":
101
+ click.echo(json.dumps(values, indent=2, sort_keys=True))
102
+ else:
103
+ prefix = "export " if output_format == "export" else ""
104
+ for k in sorted(values):
105
+ v = values[k]
106
+ if v is not None:
107
+ if output_format in ("export", "shell"):
108
+ v = shlex.quote(v)
109
+ click.echo(f"{prefix}{k}={v}")
110
+
111
+
112
+ @cli.command(name="set")
113
+ @click.pass_context
114
+ @click.argument("key", required=True)
115
+ @click.argument("value", required=True)
116
+ def set_value(ctx: click.Context, key: Any, value: Any) -> None:
117
+ """
118
+ Store the given key/value.
119
+
120
+ This doesn't follow symlinks, to avoid accidentally modifying a file at a
121
+ potentially untrusted path.
122
+ """
123
+
124
+ file = ctx.obj["FILE"]
125
+ quote = ctx.obj["QUOTE"]
126
+ export = ctx.obj["EXPORT"]
127
+ success, key, value = set_key(file, key, value, quote, export)
128
+ if success:
129
+ click.echo(f"{key}={value}")
130
+ else:
131
+ sys.exit(1)
132
+
133
+
134
+ @cli.command()
135
+ @click.pass_context
136
+ @click.argument("key", required=True)
137
+ def get(ctx: click.Context, key: Any) -> None:
138
+ """Retrieve the value for the given key."""
139
+ file = ctx.obj["FILE"]
140
+
141
+ with stream_file(file) as stream:
142
+ values = dotenv_values(stream=stream)
143
+
144
+ stored_value = values.get(key)
145
+ if stored_value:
146
+ click.echo(stored_value)
147
+ else:
148
+ sys.exit(1)
149
+
150
+
151
+ @cli.command()
152
+ @click.pass_context
153
+ @click.argument("key", required=True)
154
+ def unset(ctx: click.Context, key: Any) -> None:
155
+ """
156
+ Removes the given key.
157
+
158
+ This doesn't follow symlinks, to avoid accidentally modifying a file at a
159
+ potentially untrusted path.
160
+ """
161
+ file = ctx.obj["FILE"]
162
+ quote = ctx.obj["QUOTE"]
163
+ success, key = unset_key(file, key, quote)
164
+ if success:
165
+ click.echo(f"Successfully removed {key}")
166
+ else:
167
+ sys.exit(1)
168
+
169
+
170
+ @cli.command(
171
+ context_settings={
172
+ "allow_extra_args": True,
173
+ "allow_interspersed_args": False,
174
+ "ignore_unknown_options": True,
175
+ }
176
+ )
177
+ @click.pass_context
178
+ @click.option(
179
+ "--override/--no-override",
180
+ default=True,
181
+ help="Override variables from the environment file with those from the .env file.",
182
+ )
183
+ @click.argument("commandline", nargs=-1, type=click.UNPROCESSED)
184
+ def run(ctx: click.Context, override: bool, commandline: tuple[str, ...]) -> None:
185
+ """Run command with environment variables present."""
186
+ file = ctx.obj["FILE"]
187
+ if not os.path.isfile(file):
188
+ raise click.BadParameter(
189
+ f"Invalid value for '-f' \"{file}\" does not exist.", ctx=ctx
190
+ )
191
+ dotenv_as_dict = {
192
+ k: v
193
+ for (k, v) in dotenv_values(file).items()
194
+ if v is not None and (override or k not in os.environ)
195
+ }
196
+
197
+ if not commandline:
198
+ click.echo("No command given.")
199
+ sys.exit(1)
200
+
201
+ run_command([*commandline, *ctx.args], dotenv_as_dict)
202
+
203
+
204
+ def run_command(command: List[str], env: Dict[str, str]) -> None:
205
+ """Replace the current process with the specified command.
206
+
207
+ Replaces the current process with the specified command and the variables from `env`
208
+ added in the current environment variables.
209
+
210
+ Parameters
211
+ ----------
212
+ command: List[str]
213
+ The command and it's parameters
214
+ env: Dict
215
+ The additional environment variables
216
+
217
+ Returns
218
+ -------
219
+ None
220
+ This function does not return any value. It replaces the current process with the new one.
221
+
222
+ """
223
+ # copy the current environment variables and add the vales from
224
+ # `env`
225
+ cmd_env = os.environ.copy()
226
+ cmd_env.update(env)
227
+
228
+ if sys.platform == "win32":
229
+ # execvpe on Windows returns control immediately
230
+ # rather than once the command has finished.
231
+ p = Popen(command, universal_newlines=True, bufsize=0, shell=False, env=cmd_env)
232
+ _, _ = p.communicate()
233
+
234
+ sys.exit(p.returncode)
235
+ else:
236
+ os.execvpe(command[0], args=command, env=cmd_env)
dotenv/ipython.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from IPython.core.magic import Magics, line_magic, magics_class # type: ignore
2
+ from IPython.core.magic_arguments import (
3
+ argument,
4
+ magic_arguments,
5
+ parse_argstring,
6
+ ) # type: ignore
7
+
8
+ from .main import find_dotenv, load_dotenv
9
+
10
+
11
+ @magics_class
12
+ class IPythonDotEnv(Magics):
13
+ @magic_arguments()
14
+ @argument(
15
+ "-o",
16
+ "--override",
17
+ action="store_true",
18
+ help="Indicate to override existing variables",
19
+ )
20
+ @argument(
21
+ "-v",
22
+ "--verbose",
23
+ action="store_true",
24
+ help="Indicate function calls to be verbose",
25
+ )
26
+ @argument(
27
+ "dotenv_path",
28
+ nargs="?",
29
+ type=str,
30
+ default=".env",
31
+ help="Search in increasingly higher folders for the `dotenv_path`",
32
+ )
33
+ @line_magic
34
+ def dotenv(self, line):
35
+ args = parse_argstring(self.dotenv, line)
36
+ # Locate the .env file
37
+ dotenv_path = args.dotenv_path
38
+ try:
39
+ dotenv_path = find_dotenv(dotenv_path, True, True)
40
+ except IOError:
41
+ print("cannot find .env file")
42
+ return
43
+
44
+ # Load the .env file
45
+ load_dotenv(dotenv_path, verbose=args.verbose, override=args.override)
46
+
47
+
48
+ def load_ipython_extension(ipython):
49
+ """Register the %dotenv magic."""
50
+ ipython.register_magics(IPythonDotEnv)
dotenv/main.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import logging
3
+ import os
4
+ import pathlib
5
+ import stat
6
+ import sys
7
+ import tempfile
8
+ from collections import OrderedDict
9
+ from contextlib import contextmanager
10
+ from typing import IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, Union
11
+
12
+ from .parser import Binding, parse_stream
13
+ from .variables import parse_variables
14
+
15
+ # A type alias for a string path to be used for the paths in this file.
16
+ # These paths may flow to `open()` and `os.replace()`.
17
+ StrPath = Union[str, "os.PathLike[str]"]
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def _load_dotenv_disabled() -> bool:
23
+ """
24
+ Determine if dotenv loading has been disabled.
25
+ """
26
+ if "PYTHON_DOTENV_DISABLED" not in os.environ:
27
+ return False
28
+ value = os.environ["PYTHON_DOTENV_DISABLED"].casefold()
29
+ return value in {"1", "true", "t", "yes", "y"}
30
+
31
+
32
+ def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding]:
33
+ for mapping in mappings:
34
+ if mapping.error:
35
+ logger.warning(
36
+ "python-dotenv could not parse statement starting at line %s",
37
+ mapping.original.line,
38
+ )
39
+ yield mapping
40
+
41
+
42
+ class DotEnv:
43
+ def __init__(
44
+ self,
45
+ dotenv_path: Optional[StrPath],
46
+ stream: Optional[IO[str]] = None,
47
+ verbose: bool = False,
48
+ encoding: Optional[str] = None,
49
+ interpolate: bool = True,
50
+ override: bool = True,
51
+ ) -> None:
52
+ self.dotenv_path: Optional[StrPath] = dotenv_path
53
+ self.stream: Optional[IO[str]] = stream
54
+ self._dict: Optional[Dict[str, Optional[str]]] = None
55
+ self.verbose: bool = verbose
56
+ self.encoding: Optional[str] = encoding
57
+ self.interpolate: bool = interpolate
58
+ self.override: bool = override
59
+
60
+ @contextmanager
61
+ def _get_stream(self) -> Iterator[IO[str]]:
62
+ if self.dotenv_path and _is_file_or_fifo(self.dotenv_path):
63
+ with open(self.dotenv_path, encoding=self.encoding) as stream:
64
+ yield stream
65
+ elif self.stream is not None:
66
+ yield self.stream
67
+ else:
68
+ if self.verbose:
69
+ logger.info(
70
+ "python-dotenv could not find configuration file %s.",
71
+ self.dotenv_path or ".env",
72
+ )
73
+ yield io.StringIO("")
74
+
75
+ def dict(self) -> Dict[str, Optional[str]]:
76
+ """Return dotenv as dict"""
77
+ if self._dict:
78
+ return self._dict
79
+
80
+ raw_values = self.parse()
81
+
82
+ if self.interpolate:
83
+ self._dict = OrderedDict(
84
+ resolve_variables(raw_values, override=self.override)
85
+ )
86
+ else:
87
+ self._dict = OrderedDict(raw_values)
88
+
89
+ return self._dict
90
+
91
+ def parse(self) -> Iterator[Tuple[str, Optional[str]]]:
92
+ with self._get_stream() as stream:
93
+ for mapping in with_warn_for_invalid_lines(parse_stream(stream)):
94
+ if mapping.key is not None:
95
+ yield mapping.key, mapping.value
96
+
97
+ def set_as_environment_variables(self) -> bool:
98
+ """
99
+ Load the current dotenv as system environment variable.
100
+ """
101
+ if not self.dict():
102
+ return False
103
+
104
+ for k, v in self.dict().items():
105
+ if k in os.environ and not self.override:
106
+ continue
107
+ if v is not None:
108
+ os.environ[k] = v
109
+
110
+ return True
111
+
112
+ def get(self, key: str) -> Optional[str]:
113
+ """ """
114
+ data = self.dict()
115
+
116
+ if key in data:
117
+ return data[key]
118
+
119
+ if self.verbose:
120
+ logger.warning("Key %s not found in %s.", key, self.dotenv_path)
121
+
122
+ return None
123
+
124
+
125
+ def get_key(
126
+ dotenv_path: StrPath,
127
+ key_to_get: str,
128
+ encoding: Optional[str] = "utf-8",
129
+ ) -> Optional[str]:
130
+ """
131
+ Get the value of a given key from the given .env.
132
+
133
+ Returns `None` if the key isn't found or doesn't have a value.
134
+ """
135
+ return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get)
136
+
137
+
138
+ @contextmanager
139
+ def rewrite(
140
+ path: StrPath,
141
+ encoding: Optional[str],
142
+ follow_symlinks: bool = False,
143
+ ) -> Iterator[Tuple[IO[str], IO[str]]]:
144
+ if follow_symlinks:
145
+ path = os.path.realpath(path)
146
+
147
+ try:
148
+ source: IO[str] = open(path, encoding=encoding)
149
+ try:
150
+ path_stat = os.lstat(path)
151
+ original_mode: Optional[int] = (
152
+ stat.S_IMODE(path_stat.st_mode)
153
+ if stat.S_ISREG(path_stat.st_mode)
154
+ else None
155
+ )
156
+ except BaseException:
157
+ source.close()
158
+ raise
159
+ except FileNotFoundError:
160
+ source = io.StringIO("")
161
+ original_mode = None
162
+
163
+ with tempfile.NamedTemporaryFile(
164
+ mode="w",
165
+ encoding=encoding,
166
+ delete=False,
167
+ prefix=".tmp_",
168
+ dir=os.path.dirname(os.path.abspath(path)),
169
+ ) as dest:
170
+ dest_path = pathlib.Path(dest.name)
171
+ error = None
172
+
173
+ try:
174
+ with source:
175
+ yield (source, dest)
176
+ except BaseException as err:
177
+ error = err
178
+
179
+ if error is None:
180
+ try:
181
+ if original_mode is not None:
182
+ os.chmod(dest_path, original_mode)
183
+
184
+ os.replace(dest_path, path)
185
+ except BaseException:
186
+ dest_path.unlink(missing_ok=True)
187
+ raise
188
+ else:
189
+ dest_path.unlink(missing_ok=True)
190
+ raise error from None
191
+
192
+
193
+ def set_key(
194
+ dotenv_path: StrPath,
195
+ key_to_set: str,
196
+ value_to_set: str,
197
+ quote_mode: str = "always",
198
+ export: bool = False,
199
+ encoding: Optional[str] = "utf-8",
200
+ follow_symlinks: bool = False,
201
+ ) -> Tuple[Optional[bool], str, str]:
202
+ """
203
+ Adds or Updates a key/value to the given .env
204
+
205
+ The target .env file is created if it doesn't exist.
206
+
207
+ This function doesn't follow symlinks by default, to avoid accidentally
208
+ modifying a file at a potentially untrusted path. If you don't need this
209
+ protection and need symlinks to be followed, use `follow_symlinks`.
210
+ """
211
+ if quote_mode not in ("always", "auto", "never"):
212
+ raise ValueError(f"Unknown quote_mode: {quote_mode}")
213
+
214
+ quote = quote_mode == "always" or (
215
+ quote_mode == "auto" and not value_to_set.isalnum()
216
+ )
217
+
218
+ if quote:
219
+ value_out = "'{}'".format(value_to_set.replace("'", "\\'"))
220
+ else:
221
+ value_out = value_to_set
222
+ if export:
223
+ line_out = f"export {key_to_set}={value_out}\n"
224
+ else:
225
+ line_out = f"{key_to_set}={value_out}\n"
226
+
227
+ with rewrite(dotenv_path, encoding=encoding, follow_symlinks=follow_symlinks) as (
228
+ source,
229
+ dest,
230
+ ):
231
+ replaced = False
232
+ missing_newline = False
233
+ for mapping in with_warn_for_invalid_lines(parse_stream(source)):
234
+ if mapping.key == key_to_set:
235
+ dest.write(line_out)
236
+ replaced = True
237
+ else:
238
+ dest.write(mapping.original.string)
239
+ missing_newline = not mapping.original.string.endswith("\n")
240
+ if not replaced:
241
+ if missing_newline:
242
+ dest.write("\n")
243
+ dest.write(line_out)
244
+
245
+ return True, key_to_set, value_to_set
246
+
247
+
248
+ def unset_key(
249
+ dotenv_path: StrPath,
250
+ key_to_unset: str,
251
+ quote_mode: str = "always",
252
+ encoding: Optional[str] = "utf-8",
253
+ follow_symlinks: bool = False,
254
+ ) -> Tuple[Optional[bool], str]:
255
+ """
256
+ Removes a given key from the given `.env` file.
257
+
258
+ If the .env path given doesn't exist, fails.
259
+ If the given key doesn't exist in the .env, fails.
260
+
261
+ This function doesn't follow symlinks by default, to avoid accidentally
262
+ modifying a file at a potentially untrusted path. If you don't need this
263
+ protection and need symlinks to be followed, use `follow_symlinks`.
264
+ """
265
+ if not os.path.exists(dotenv_path):
266
+ logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path)
267
+ return None, key_to_unset
268
+
269
+ removed = False
270
+ with rewrite(dotenv_path, encoding=encoding, follow_symlinks=follow_symlinks) as (
271
+ source,
272
+ dest,
273
+ ):
274
+ for mapping in with_warn_for_invalid_lines(parse_stream(source)):
275
+ if mapping.key == key_to_unset:
276
+ removed = True
277
+ else:
278
+ dest.write(mapping.original.string)
279
+
280
+ if not removed:
281
+ logger.warning(
282
+ "Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path
283
+ )
284
+ return None, key_to_unset
285
+
286
+ return removed, key_to_unset
287
+
288
+
289
+ def resolve_variables(
290
+ values: Iterable[Tuple[str, Optional[str]]],
291
+ override: bool,
292
+ ) -> Mapping[str, Optional[str]]:
293
+ new_values: Dict[str, Optional[str]] = {}
294
+
295
+ for name, value in values:
296
+ if value is None:
297
+ result = None
298
+ else:
299
+ atoms = parse_variables(value)
300
+ env: Dict[str, Optional[str]] = {}
301
+ if override:
302
+ env.update(os.environ) # type: ignore
303
+ env.update(new_values)
304
+ else:
305
+ env.update(new_values)
306
+ env.update(os.environ) # type: ignore
307
+ result = "".join(atom.resolve(env) for atom in atoms)
308
+
309
+ new_values[name] = result
310
+
311
+ return new_values
312
+
313
+
314
+ def _walk_to_root(path: str) -> Iterator[str]:
315
+ """
316
+ Yield directories starting from the given directory up to the root
317
+ """
318
+ if not os.path.exists(path):
319
+ raise IOError("Starting path not found")
320
+
321
+ if os.path.isfile(path):
322
+ path = os.path.dirname(path)
323
+
324
+ last_dir = None
325
+ current_dir = os.path.abspath(path)
326
+ while last_dir != current_dir:
327
+ yield current_dir
328
+ parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
329
+ last_dir, current_dir = current_dir, parent_dir
330
+
331
+
332
+ def find_dotenv(
333
+ filename: str = ".env",
334
+ raise_error_if_not_found: bool = False,
335
+ usecwd: bool = False,
336
+ ) -> str:
337
+ """
338
+ Search in increasingly higher folders for the given file
339
+
340
+ Returns path to the file if found, or an empty string otherwise
341
+ """
342
+
343
+ def _is_interactive():
344
+ """Decide whether this is running in a REPL or IPython notebook"""
345
+ if hasattr(sys, "ps1") or hasattr(sys, "ps2"):
346
+ return True
347
+ try:
348
+ main = __import__("__main__", None, None, fromlist=["__file__"])
349
+ except ModuleNotFoundError:
350
+ return False
351
+ return not hasattr(main, "__file__")
352
+
353
+ def _is_debugger():
354
+ return sys.gettrace() is not None
355
+
356
+ if usecwd or _is_interactive() or _is_debugger() or getattr(sys, "frozen", False):
357
+ # Should work without __file__, e.g. in REPL or IPython notebook.
358
+ path = os.getcwd()
359
+ else:
360
+ # will work for .py files
361
+ frame = sys._getframe()
362
+ current_file = __file__
363
+
364
+ while frame.f_code.co_filename == current_file or not os.path.exists(
365
+ frame.f_code.co_filename
366
+ ):
367
+ assert frame.f_back is not None
368
+ frame = frame.f_back
369
+ frame_filename = frame.f_code.co_filename
370
+ path = os.path.dirname(os.path.abspath(frame_filename))
371
+
372
+ for dirname in _walk_to_root(path):
373
+ check_path = os.path.join(dirname, filename)
374
+ if _is_file_or_fifo(check_path):
375
+ return check_path
376
+
377
+ if raise_error_if_not_found:
378
+ raise IOError("File not found")
379
+
380
+ return ""
381
+
382
+
383
+ def load_dotenv(
384
+ dotenv_path: Optional[StrPath] = None,
385
+ stream: Optional[IO[str]] = None,
386
+ verbose: bool = False,
387
+ override: bool = False,
388
+ interpolate: bool = True,
389
+ encoding: Optional[str] = "utf-8",
390
+ ) -> bool:
391
+ """Parse a .env file and then load all the variables found as environment variables.
392
+
393
+ Parameters:
394
+ dotenv_path: Absolute or relative path to .env file.
395
+ stream: Text stream (such as `io.StringIO`) with .env content, used if
396
+ `dotenv_path` is `None`.
397
+ verbose: Whether to output a warning the .env file is missing.
398
+ override: Whether to override the system environment variables with the variables
399
+ from the `.env` file.
400
+ encoding: Encoding to be used to read the file.
401
+ Returns:
402
+ Bool: True if at least one environment variable is set else False
403
+
404
+ If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
405
+ .env file with it's default parameters. If you need to change the default parameters
406
+ of `find_dotenv()`, you can explicitly call `find_dotenv()` and pass the result
407
+ to this function as `dotenv_path`.
408
+
409
+ If the environment variable `PYTHON_DOTENV_DISABLED` is set to a truthy value,
410
+ .env loading is disabled.
411
+ """
412
+ if _load_dotenv_disabled():
413
+ logger.debug(
414
+ "python-dotenv: .env loading disabled by PYTHON_DOTENV_DISABLED environment variable"
415
+ )
416
+ return False
417
+
418
+ if dotenv_path is None and stream is None:
419
+ dotenv_path = find_dotenv()
420
+
421
+ dotenv = DotEnv(
422
+ dotenv_path=dotenv_path,
423
+ stream=stream,
424
+ verbose=verbose,
425
+ interpolate=interpolate,
426
+ override=override,
427
+ encoding=encoding,
428
+ )
429
+ return dotenv.set_as_environment_variables()
430
+
431
+
432
+ def dotenv_values(
433
+ dotenv_path: Optional[StrPath] = None,
434
+ stream: Optional[IO[str]] = None,
435
+ verbose: bool = False,
436
+ interpolate: bool = True,
437
+ encoding: Optional[str] = "utf-8",
438
+ ) -> Dict[str, Optional[str]]:
439
+ """
440
+ Parse a .env file and return its content as a dict.
441
+
442
+ The returned dict will have `None` values for keys without values in the .env file.
443
+ For example, `foo=bar` results in `{"foo": "bar"}` whereas `foo` alone results in
444
+ `{"foo": None}`
445
+
446
+ Parameters:
447
+ dotenv_path: Absolute or relative path to the .env file.
448
+ stream: `StringIO` object with .env content, used if `dotenv_path` is `None`.
449
+ verbose: Whether to output a warning if the .env file is missing.
450
+ encoding: Encoding to be used to read the file.
451
+
452
+ If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
453
+ .env file.
454
+ """
455
+ if dotenv_path is None and stream is None:
456
+ dotenv_path = find_dotenv()
457
+
458
+ return DotEnv(
459
+ dotenv_path=dotenv_path,
460
+ stream=stream,
461
+ verbose=verbose,
462
+ interpolate=interpolate,
463
+ override=True,
464
+ encoding=encoding,
465
+ ).dict()
466
+
467
+
468
+ def _is_file_or_fifo(path: StrPath) -> bool:
469
+ """
470
+ Return True if `path` exists and is either a regular file or a FIFO.
471
+ """
472
+ if os.path.isfile(path):
473
+ return True
474
+
475
+ try:
476
+ st = os.stat(path)
477
+ except (FileNotFoundError, OSError):
478
+ return False
479
+
480
+ return stat.S_ISFIFO(st.st_mode)
dotenv/parser.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import codecs
2
+ import re
3
+ from typing import (
4
+ IO,
5
+ Iterator,
6
+ Match,
7
+ NamedTuple,
8
+ Optional,
9
+ Pattern,
10
+ Sequence,
11
+ )
12
+
13
+
14
+ def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]:
15
+ return re.compile(string, re.UNICODE | extra_flags)
16
+
17
+
18
+ _newline = make_regex(r"(\r\n|\n|\r)")
19
+ _multiline_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE)
20
+ _whitespace = make_regex(r"[^\S\r\n]*")
21
+ _export = make_regex(r"(?:export[^\S\r\n]+)?")
22
+ _single_quoted_key = make_regex(r"'([^']+)'")
23
+ _unquoted_key = make_regex(r"([^=\#\s]+)")
24
+ _equal_sign = make_regex(r"(=[^\S\r\n]*)")
25
+ _single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'")
26
+ _double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"')
27
+ _unquoted_value = make_regex(r"([^\r\n]*)")
28
+ _comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?")
29
+ _end_of_line = make_regex(r"[^\S\r\n]*(?:\r\n|\n|\r|$)")
30
+ _rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?")
31
+ _double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]")
32
+ _single_quote_escapes = make_regex(r"\\[\\']")
33
+
34
+
35
+ class Original(NamedTuple):
36
+ string: str
37
+ line: int
38
+
39
+
40
+ class Binding(NamedTuple):
41
+ key: Optional[str]
42
+ value: Optional[str]
43
+ original: Original
44
+ error: bool
45
+
46
+
47
+ class Position:
48
+ def __init__(self, chars: int, line: int) -> None:
49
+ self.chars = chars
50
+ self.line = line
51
+
52
+ @classmethod
53
+ def start(cls) -> "Position":
54
+ return cls(chars=0, line=1)
55
+
56
+ def set(self, other: "Position") -> None:
57
+ self.chars = other.chars
58
+ self.line = other.line
59
+
60
+ def advance(self, string: str) -> None:
61
+ self.chars += len(string)
62
+ self.line += len(re.findall(_newline, string))
63
+
64
+
65
+ class Error(Exception):
66
+ pass
67
+
68
+
69
+ class Reader:
70
+ def __init__(self, stream: IO[str]) -> None:
71
+ self.string = stream.read()
72
+ self.position = Position.start()
73
+ self.mark = Position.start()
74
+
75
+ def has_next(self) -> bool:
76
+ return self.position.chars < len(self.string)
77
+
78
+ def set_mark(self) -> None:
79
+ self.mark.set(self.position)
80
+
81
+ def get_marked(self) -> Original:
82
+ return Original(
83
+ string=self.string[self.mark.chars : self.position.chars],
84
+ line=self.mark.line,
85
+ )
86
+
87
+ def peek(self, count: int) -> str:
88
+ return self.string[self.position.chars : self.position.chars + count]
89
+
90
+ def read(self, count: int) -> str:
91
+ result = self.string[self.position.chars : self.position.chars + count]
92
+ if len(result) < count:
93
+ raise Error("read: End of string")
94
+ self.position.advance(result)
95
+ return result
96
+
97
+ def read_regex(self, regex: Pattern[str]) -> Sequence[str]:
98
+ match = regex.match(self.string, self.position.chars)
99
+ if match is None:
100
+ raise Error("read_regex: Pattern not found")
101
+ self.position.advance(self.string[match.start() : match.end()])
102
+ return match.groups()
103
+
104
+
105
+ def decode_escapes(regex: Pattern[str], string: str) -> str:
106
+ def decode_match(match: Match[str]) -> str:
107
+ return codecs.decode(match.group(0), "unicode-escape") # type: ignore
108
+
109
+ return regex.sub(decode_match, string)
110
+
111
+
112
+ def parse_key(reader: Reader) -> Optional[str]:
113
+ char = reader.peek(1)
114
+ if char == "#":
115
+ return None
116
+ elif char == "'":
117
+ (key,) = reader.read_regex(_single_quoted_key)
118
+ else:
119
+ (key,) = reader.read_regex(_unquoted_key)
120
+ return key
121
+
122
+
123
+ def parse_unquoted_value(reader: Reader) -> str:
124
+ (part,) = reader.read_regex(_unquoted_value)
125
+ return re.sub(r"\s+#.*", "", part).rstrip()
126
+
127
+
128
+ def parse_value(reader: Reader) -> str:
129
+ char = reader.peek(1)
130
+ if char == "'":
131
+ (value,) = reader.read_regex(_single_quoted_value)
132
+ return decode_escapes(_single_quote_escapes, value)
133
+ elif char == '"':
134
+ (value,) = reader.read_regex(_double_quoted_value)
135
+ return decode_escapes(_double_quote_escapes, value)
136
+ elif char in ("", "\n", "\r"):
137
+ return ""
138
+ else:
139
+ return parse_unquoted_value(reader)
140
+
141
+
142
+ def parse_binding(reader: Reader) -> Binding:
143
+ reader.set_mark()
144
+ try:
145
+ reader.read_regex(_multiline_whitespace)
146
+ if not reader.has_next():
147
+ return Binding(
148
+ key=None,
149
+ value=None,
150
+ original=reader.get_marked(),
151
+ error=False,
152
+ )
153
+ reader.read_regex(_export)
154
+ key = parse_key(reader)
155
+ reader.read_regex(_whitespace)
156
+ if reader.peek(1) == "=":
157
+ reader.read_regex(_equal_sign)
158
+ value: Optional[str] = parse_value(reader)
159
+ else:
160
+ value = None
161
+ reader.read_regex(_comment)
162
+ reader.read_regex(_end_of_line)
163
+ return Binding(
164
+ key=key,
165
+ value=value,
166
+ original=reader.get_marked(),
167
+ error=False,
168
+ )
169
+ except Error:
170
+ reader.read_regex(_rest_of_line)
171
+ return Binding(
172
+ key=None,
173
+ value=None,
174
+ original=reader.get_marked(),
175
+ error=True,
176
+ )
177
+
178
+
179
+ def parse_stream(stream: IO[str]) -> Iterator[Binding]:
180
+ reader = Reader(stream)
181
+ while reader.has_next():
182
+ yield parse_binding(reader)
dotenv/py.typed ADDED
@@ -0,0 +1 @@
 
 
1
+ # Marker file for PEP 561
dotenv/variables.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from abc import ABCMeta, abstractmethod
3
+ from typing import Iterator, Mapping, Optional, Pattern
4
+
5
+ _posix_variable: Pattern[str] = re.compile(
6
+ r"""
7
+ \$\{
8
+ (?P<name>[^\}:]*)
9
+ (?::-
10
+ (?P<default>[^\}]*)
11
+ )?
12
+ \}
13
+ """,
14
+ re.VERBOSE,
15
+ )
16
+
17
+
18
+ class Atom(metaclass=ABCMeta):
19
+ def __ne__(self, other: object) -> bool:
20
+ result = self.__eq__(other)
21
+ if result is NotImplemented:
22
+ return NotImplemented
23
+ return not result
24
+
25
+ @abstractmethod
26
+ def resolve(self, env: Mapping[str, Optional[str]]) -> str: ...
27
+
28
+
29
+ class Literal(Atom):
30
+ def __init__(self, value: str) -> None:
31
+ self.value = value
32
+
33
+ def __repr__(self) -> str:
34
+ return f"Literal(value={self.value})"
35
+
36
+ def __eq__(self, other: object) -> bool:
37
+ if not isinstance(other, self.__class__):
38
+ return NotImplemented
39
+ return self.value == other.value
40
+
41
+ def __hash__(self) -> int:
42
+ return hash((self.__class__, self.value))
43
+
44
+ def resolve(self, env: Mapping[str, Optional[str]]) -> str:
45
+ return self.value
46
+
47
+
48
+ class Variable(Atom):
49
+ def __init__(self, name: str, default: Optional[str]) -> None:
50
+ self.name = name
51
+ self.default = default
52
+
53
+ def __repr__(self) -> str:
54
+ return f"Variable(name={self.name}, default={self.default})"
55
+
56
+ def __eq__(self, other: object) -> bool:
57
+ if not isinstance(other, self.__class__):
58
+ return NotImplemented
59
+ return (self.name, self.default) == (other.name, other.default)
60
+
61
+ def __hash__(self) -> int:
62
+ return hash((self.__class__, self.name, self.default))
63
+
64
+ def resolve(self, env: Mapping[str, Optional[str]]) -> str:
65
+ default = self.default if self.default is not None else ""
66
+ result = env.get(self.name, default)
67
+ return result if result is not None else ""
68
+
69
+
70
+ def parse_variables(value: str) -> Iterator[Atom]:
71
+ cursor = 0
72
+
73
+ for match in _posix_variable.finditer(value):
74
+ (start, end) = match.span()
75
+ name = match["name"]
76
+ default = match["default"]
77
+
78
+ if start > cursor:
79
+ yield Literal(value=value[cursor:start])
80
+
81
+ yield Variable(name=name, default=default)
82
+ cursor = end
83
+
84
+ length = len(value)
85
+ if cursor < length:
86
+ yield Literal(value=value[cursor:length])
dotenv/version.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __version__ = "1.2.2"