This commit is contained in:
Untriex Programming
2021-03-17 08:57:57 +01:00
parent 339be0ccd8
commit ed6afdb5c9
3074 changed files with 423348 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
from .compat import IS_TYPE_CHECKING
from .main import load_dotenv, get_key, set_key, unset_key, find_dotenv, dotenv_values
if IS_TYPE_CHECKING:
from typing import Any, Optional
def load_ipython_extension(ipython):
# type: (Any) -> None
from .ipython import load_ipython_extension
load_ipython_extension(ipython)
def get_cli_string(path=None, action=None, key=None, value=None, quote=None):
# type: (Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]) -> str
"""Returns a string suitable for running as a shell script.
Useful for converting a arguments passed to a fabric task
to be passed to a `local` or `run` command.
"""
command = ['dotenv']
if quote:
command.append('-q %s' % quote)
if path:
command.append('-f %s' % path)
if action:
command.append(action)
if key:
command.append(key)
if value:
if ' ' in value:
command.append('"%s"' % value)
else:
command.append(value)
return ' '.join(command).strip()
__all__ = ['get_cli_string',
'load_dotenv',
'dotenv_values',
'get_key',
'set_key',
'unset_key',
'find_dotenv',
'load_ipython_extension']

View File

@@ -0,0 +1,165 @@
import os
import sys
from subprocess import Popen
try:
import click
except ImportError:
sys.stderr.write('It seems python-dotenv is not installed with cli option. \n'
'Run pip install "python-dotenv[cli]" to fix this.')
sys.exit(1)
from .compat import IS_TYPE_CHECKING, to_env
from .main import dotenv_values, get_key, set_key, unset_key
from .version import __version__
if IS_TYPE_CHECKING:
from typing import Any, List, Dict
@click.group()
@click.option('-f', '--file', default=os.path.join(os.getcwd(), '.env'),
type=click.Path(file_okay=True),
help="Location of the .env file, defaults to .env file in current working directory.")
@click.option('-q', '--quote', default='always',
type=click.Choice(['always', 'never', 'auto']),
help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.")
@click.option('-e', '--export', default=False,
type=click.BOOL,
help="Whether to write the dot file as an executable bash script.")
@click.version_option(version=__version__)
@click.pass_context
def cli(ctx, file, quote, export):
# type: (click.Context, Any, Any, Any) -> None
'''This script is used to set, get or unset values from a .env file.'''
ctx.obj = {}
ctx.obj['QUOTE'] = quote
ctx.obj['EXPORT'] = export
ctx.obj['FILE'] = file
@cli.command()
@click.pass_context
def list(ctx):
# type: (click.Context) -> None
'''Display all the stored key/value.'''
file = ctx.obj['FILE']
if not os.path.isfile(file):
raise click.BadParameter(
'Path "%s" does not exist.' % (file),
ctx=ctx
)
dotenv_as_dict = dotenv_values(file)
for k, v in dotenv_as_dict.items():
click.echo('%s=%s' % (k, v))
@cli.command()
@click.pass_context
@click.argument('key', required=True)
@click.argument('value', required=True)
def set(ctx, key, value):
# type: (click.Context, Any, Any) -> None
'''Store the given key/value.'''
file = ctx.obj['FILE']
quote = ctx.obj['QUOTE']
export = ctx.obj['EXPORT']
success, key, value = set_key(file, key, value, quote, export)
if success:
click.echo('%s=%s' % (key, value))
else:
exit(1)
@cli.command()
@click.pass_context
@click.argument('key', required=True)
def get(ctx, key):
# type: (click.Context, Any) -> None
'''Retrieve the value for the given key.'''
file = ctx.obj['FILE']
if not os.path.isfile(file):
raise click.BadParameter(
'Path "%s" does not exist.' % (file),
ctx=ctx
)
stored_value = get_key(file, key)
if stored_value:
click.echo('%s=%s' % (key, stored_value))
else:
exit(1)
@cli.command()
@click.pass_context
@click.argument('key', required=True)
def unset(ctx, key):
# type: (click.Context, Any) -> None
'''Removes the given key.'''
file = ctx.obj['FILE']
quote = ctx.obj['QUOTE']
success, key = unset_key(file, key, quote)
if success:
click.echo("Successfully removed %s" % key)
else:
exit(1)
@cli.command(context_settings={'ignore_unknown_options': True})
@click.pass_context
@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
def run(ctx, commandline):
# type: (click.Context, List[str]) -> None
"""Run command with environment variables present."""
file = ctx.obj['FILE']
if not os.path.isfile(file):
raise click.BadParameter(
'Invalid value for \'-f\' "%s" does not exist.' % (file),
ctx=ctx
)
dotenv_as_dict = {to_env(k): to_env(v) for (k, v) in dotenv_values(file).items() if v is not None}
if not commandline:
click.echo('No command given.')
exit(1)
ret = run_command(commandline, dotenv_as_dict)
exit(ret)
def run_command(command, env):
# type: (List[str], Dict[str, str]) -> int
"""Run command in sub process.
Runs the command in a sub process with the variables from `env`
added in the current environment variables.
Parameters
----------
command: List[str]
The command and it's parameters
env: Dict
The additional environment variables
Returns
-------
int
The return code of the command
"""
# copy the current environment variables and add the vales from
# `env`
cmd_env = os.environ.copy()
cmd_env.update(env)
p = Popen(command,
universal_newlines=True,
bufsize=0,
shell=False,
env=cmd_env)
_, _ = p.communicate()
return p.returncode
if __name__ == "__main__":
cli()

View File

@@ -0,0 +1,49 @@
import sys
PY2 = sys.version_info[0] == 2 # type: bool
if PY2:
from StringIO import StringIO # noqa
else:
from io import StringIO # noqa
def is_type_checking():
# type: () -> bool
try:
from typing import TYPE_CHECKING
except ImportError:
return False
return TYPE_CHECKING
IS_TYPE_CHECKING = is_type_checking()
if IS_TYPE_CHECKING:
from typing import Text
def to_env(text):
# type: (Text) -> str
"""
Encode a string the same way whether it comes from the environment or a `.env` file.
"""
if PY2:
return text.encode(sys.getfilesystemencoding() or "utf-8")
else:
return text
def to_text(string):
# type: (str) -> Text
"""
Make a string Unicode if it isn't already.
This is useful for defining raw unicode strings because `ur"foo"` isn't valid in
Python 3.
"""
if PY2:
return string.decode("utf-8")
else:
return string

View File

@@ -0,0 +1,41 @@
from __future__ import print_function
from IPython.core.magic import Magics, line_magic, magics_class # type: ignore
from IPython.core.magic_arguments import (argument, magic_arguments, # type: ignore
parse_argstring) # type: ignore
from .main import find_dotenv, load_dotenv
@magics_class
class IPythonDotEnv(Magics):
@magic_arguments()
@argument(
'-o', '--override', action='store_true',
help="Indicate to override existing variables"
)
@argument(
'-v', '--verbose', action='store_true',
help="Indicate function calls to be verbose"
)
@argument('dotenv_path', nargs='?', type=str, default='.env',
help='Search in increasingly higher folders for the `dotenv_path`')
@line_magic
def dotenv(self, line):
args = parse_argstring(self.dotenv, line)
# Locate the .env file
dotenv_path = args.dotenv_path
try:
dotenv_path = find_dotenv(dotenv_path, True, True)
except IOError:
print("cannot find .env file")
return
# Load the .env file
load_dotenv(dotenv_path, verbose=args.verbose, override=args.override)
def load_ipython_extension(ipython):
"""Register the %dotenv magic."""
ipython.register_magics(IPythonDotEnv)

View File

@@ -0,0 +1,325 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
import io
import logging
import os
import re
import shutil
import sys
import tempfile
from collections import OrderedDict
from contextlib import contextmanager
from .compat import IS_TYPE_CHECKING, PY2, StringIO, to_env
from .parser import Binding, parse_stream
logger = logging.getLogger(__name__)
if IS_TYPE_CHECKING:
from typing import (
Dict, Iterable, Iterator, Match, Optional, Pattern, Union, Text, IO, Tuple
)
if sys.version_info >= (3, 6):
_PathLike = os.PathLike
else:
_PathLike = Text
if sys.version_info >= (3, 0):
_StringIO = StringIO
else:
_StringIO = StringIO[Text]
__posix_variable = re.compile(
r"""
\$\{
(?P<name>[^\}:]*)
(?::-
(?P<default>[^\}]*)
)?
\}
""",
re.VERBOSE,
) # type: Pattern[Text]
def with_warn_for_invalid_lines(mappings):
# type: (Iterator[Binding]) -> Iterator[Binding]
for mapping in mappings:
if mapping.error:
logger.warning(
"Python-dotenv could not parse statement starting at line %s",
mapping.original.line,
)
yield mapping
class DotEnv():
def __init__(self, dotenv_path, verbose=False, encoding=None, interpolate=True):
# type: (Union[Text, _PathLike, _StringIO], bool, Union[None, Text], bool) -> None
self.dotenv_path = dotenv_path # type: Union[Text,_PathLike, _StringIO]
self._dict = None # type: Optional[Dict[Text, Optional[Text]]]
self.verbose = verbose # type: bool
self.encoding = encoding # type: Union[None, Text]
self.interpolate = interpolate # type: bool
@contextmanager
def _get_stream(self):
# type: () -> Iterator[IO[Text]]
if isinstance(self.dotenv_path, StringIO):
yield self.dotenv_path
elif os.path.isfile(self.dotenv_path):
with io.open(self.dotenv_path, encoding=self.encoding) as stream:
yield stream
else:
if self.verbose:
logger.info("Python-dotenv could not find configuration file %s.", self.dotenv_path or '.env')
yield StringIO('')
def dict(self):
# type: () -> Dict[Text, Optional[Text]]
"""Return dotenv as dict"""
if self._dict:
return self._dict
if self.interpolate:
values = resolve_nested_variables(self.parse())
else:
values = OrderedDict(self.parse())
self._dict = values
return values
def parse(self):
# type: () -> Iterator[Tuple[Text, Optional[Text]]]
with self._get_stream() as stream:
for mapping in with_warn_for_invalid_lines(parse_stream(stream)):
if mapping.key is not None:
yield mapping.key, mapping.value
def set_as_environment_variables(self, override=False):
# type: (bool) -> bool
"""
Load the current dotenv as system environemt variable.
"""
for k, v in self.dict().items():
if k in os.environ and not override:
continue
if v is not None:
os.environ[to_env(k)] = to_env(v)
return True
def get(self, key):
# type: (Text) -> Optional[Text]
"""
"""
data = self.dict()
if key in data:
return data[key]
if self.verbose:
logger.warning("Key %s not found in %s.", key, self.dotenv_path)
return None
def get_key(dotenv_path, key_to_get):
# type: (Union[Text, _PathLike], Text) -> Optional[Text]
"""
Gets the value of a given key from the given .env
If the .env path given doesn't exist, fails
"""
return DotEnv(dotenv_path, verbose=True).get(key_to_get)
@contextmanager
def rewrite(path):
# type: (_PathLike) -> Iterator[Tuple[IO[Text], IO[Text]]]
try:
if not os.path.isfile(path):
with io.open(path, "w+") as source:
source.write("")
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as dest:
with io.open(path) as source:
yield (source, dest) # type: ignore
except BaseException:
if os.path.isfile(dest.name):
os.unlink(dest.name)
raise
else:
shutil.move(dest.name, path)
def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always", export=False):
# type: (_PathLike, Text, Text, Text, bool) -> Tuple[Optional[bool], Text, Text]
"""
Adds or Updates a key/value to the given .env
If the .env path given doesn't exist, fails instead of risking creating
an orphan .env somewhere in the filesystem
"""
value_to_set = value_to_set.strip("'").strip('"')
if " " in value_to_set:
quote_mode = "always"
if quote_mode == "always":
value_out = '"{}"'.format(value_to_set.replace('"', '\\"'))
else:
value_out = value_to_set
if export:
line_out = 'export {}={}\n'.format(key_to_set, value_out)
else:
line_out = "{}={}\n".format(key_to_set, value_out)
with rewrite(dotenv_path) as (source, dest):
replaced = False
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
if mapping.key == key_to_set:
dest.write(line_out)
replaced = True
else:
dest.write(mapping.original.string)
if not replaced:
dest.write(line_out)
return True, key_to_set, value_to_set
def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
# type: (_PathLike, Text, Text) -> Tuple[Optional[bool], Text]
"""
Removes a given key from the given .env
If the .env path given doesn't exist, fails
If the given key doesn't exist in the .env, fails
"""
if not os.path.exists(dotenv_path):
logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path)
return None, key_to_unset
removed = False
with rewrite(dotenv_path) as (source, dest):
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
if mapping.key == key_to_unset:
removed = True
else:
dest.write(mapping.original.string)
if not removed:
logger.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path)
return None, key_to_unset
return removed, key_to_unset
def resolve_nested_variables(values):
# type: (Iterable[Tuple[Text, Optional[Text]]]) -> Dict[Text, Optional[Text]]
def _replacement(name, default):
# type: (Text, Optional[Text]) -> Text
default = default if default is not None else ""
ret = new_values.get(name, os.getenv(name, default))
return ret # type: ignore
def _re_sub_callback(match):
# type: (Match[Text]) -> Text
"""
From a match object gets the variable name and returns
the correct replacement
"""
matches = match.groupdict()
return _replacement(name=matches["name"], default=matches["default"]) # type: ignore
new_values = {}
for (k, v) in values:
new_values[k] = __posix_variable.sub(_re_sub_callback, v) if v is not None else None
return new_values
def _walk_to_root(path):
# type: (Text) -> Iterator[Text]
"""
Yield directories starting from the given directory up to the root
"""
if not os.path.exists(path):
raise IOError('Starting path not found')
if os.path.isfile(path):
path = os.path.dirname(path)
last_dir = None
current_dir = os.path.abspath(path)
while last_dir != current_dir:
yield current_dir
parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
last_dir, current_dir = current_dir, parent_dir
def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
# type: (Text, bool, bool) -> Text
"""
Search in increasingly higher folders for the given file
Returns path to the file if found, or an empty string otherwise
"""
def _is_interactive():
""" Decide whether this is running in a REPL or IPython notebook """
main = __import__('__main__', None, None, fromlist=['__file__'])
return not hasattr(main, '__file__')
if usecwd or _is_interactive() or getattr(sys, 'frozen', False):
# Should work without __file__, e.g. in REPL or IPython notebook.
path = os.getcwd()
else:
# will work for .py files
frame = sys._getframe()
# find first frame that is outside of this file
if PY2 and not __file__.endswith('.py'):
# in Python2 __file__ extension could be .pyc or .pyo (this doesn't account
# for edge case of Python compiled for non-standard extension)
current_file = __file__.rsplit('.', 1)[0] + '.py'
else:
current_file = __file__
while frame.f_code.co_filename == current_file:
assert frame.f_back is not None
frame = frame.f_back
frame_filename = frame.f_code.co_filename
path = os.path.dirname(os.path.abspath(frame_filename))
for dirname in _walk_to_root(path):
check_path = os.path.join(dirname, filename)
if os.path.isfile(check_path):
return check_path
if raise_error_if_not_found:
raise IOError('File not found')
return ''
def load_dotenv(dotenv_path=None, stream=None, verbose=False, override=False, interpolate=True, **kwargs):
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, bool, Union[None, Text]) -> bool
"""Parse a .env file and then load all the variables found as environment variables.
- *dotenv_path*: absolute or relative path to .env file.
- *stream*: `StringIO` object with .env content.
- *verbose*: whether to output the warnings related to missing .env file etc. Defaults to `False`.
- *override*: where to override the system environment variables with the variables in `.env` file.
Defaults to `False`.
"""
f = dotenv_path or stream or find_dotenv()
return DotEnv(f, verbose=verbose, interpolate=interpolate, **kwargs).set_as_environment_variables(override=override)
def dotenv_values(dotenv_path=None, stream=None, verbose=False, interpolate=True, **kwargs):
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, Union[None, Text]) -> Dict[Text, Optional[Text]] # noqa: E501
f = dotenv_path or stream or find_dotenv()
return DotEnv(f, verbose=verbose, interpolate=interpolate, **kwargs).dict()

View File

@@ -0,0 +1,231 @@
import codecs
import re
from .compat import IS_TYPE_CHECKING, to_text
if IS_TYPE_CHECKING:
from typing import ( # noqa:F401
IO, Iterator, Match, NamedTuple, Optional, Pattern, Sequence, Text,
Tuple
)
def make_regex(string, extra_flags=0):
# type: (str, int) -> Pattern[Text]
return re.compile(to_text(string), re.UNICODE | extra_flags)
_newline = make_regex(r"(\r\n|\n|\r)")
_multiline_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE)
_whitespace = make_regex(r"[^\S\r\n]*")
_export = make_regex(r"(?:export[^\S\r\n]+)?")
_single_quoted_key = make_regex(r"'([^']+)'")
_unquoted_key = make_regex(r"([^=\#\s]+)")
_equal_sign = make_regex(r"(=[^\S\r\n]*)")
_single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'")
_double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"')
_unquoted_value = make_regex(r"([^\r\n]*)")
_comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?")
_end_of_line = make_regex(r"[^\S\r\n]*(?:\r\n|\n|\r|$)")
_rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?")
_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]")
_single_quote_escapes = make_regex(r"\\[\\']")
try:
# this is necessary because we only import these from typing
# when we are type checking, and the linter is upset if we
# re-import
import typing
Original = typing.NamedTuple(
"Original",
[
("string", typing.Text),
("line", int),
],
)
Binding = typing.NamedTuple(
"Binding",
[
("key", typing.Optional[typing.Text]),
("value", typing.Optional[typing.Text]),
("original", Original),
("error", bool),
],
)
except (ImportError, AttributeError):
from collections import namedtuple
Original = namedtuple( # type: ignore
"Original",
[
"string",
"line",
],
)
Binding = namedtuple( # type: ignore
"Binding",
[
"key",
"value",
"original",
"error",
],
)
class Position:
def __init__(self, chars, line):
# type: (int, int) -> None
self.chars = chars
self.line = line
@classmethod
def start(cls):
# type: () -> Position
return cls(chars=0, line=1)
def set(self, other):
# type: (Position) -> None
self.chars = other.chars
self.line = other.line
def advance(self, string):
# type: (Text) -> None
self.chars += len(string)
self.line += len(re.findall(_newline, string))
class Error(Exception):
pass
class Reader:
def __init__(self, stream):
# type: (IO[Text]) -> None
self.string = stream.read()
self.position = Position.start()
self.mark = Position.start()
def has_next(self):
# type: () -> bool
return self.position.chars < len(self.string)
def set_mark(self):
# type: () -> None
self.mark.set(self.position)
def get_marked(self):
# type: () -> Original
return Original(
string=self.string[self.mark.chars:self.position.chars],
line=self.mark.line,
)
def peek(self, count):
# type: (int) -> Text
return self.string[self.position.chars:self.position.chars + count]
def read(self, count):
# type: (int) -> Text
result = self.string[self.position.chars:self.position.chars + count]
if len(result) < count:
raise Error("read: End of string")
self.position.advance(result)
return result
def read_regex(self, regex):
# type: (Pattern[Text]) -> Sequence[Text]
match = regex.match(self.string, self.position.chars)
if match is None:
raise Error("read_regex: Pattern not found")
self.position.advance(self.string[match.start():match.end()])
return match.groups()
def decode_escapes(regex, string):
# type: (Pattern[Text], Text) -> Text
def decode_match(match):
# type: (Match[Text]) -> Text
return codecs.decode(match.group(0), 'unicode-escape') # type: ignore
return regex.sub(decode_match, string)
def parse_key(reader):
# type: (Reader) -> Optional[Text]
char = reader.peek(1)
if char == "#":
return None
elif char == "'":
(key,) = reader.read_regex(_single_quoted_key)
else:
(key,) = reader.read_regex(_unquoted_key)
return key
def parse_unquoted_value(reader):
# type: (Reader) -> Text
(part,) = reader.read_regex(_unquoted_value)
return re.sub(r"\s+#.*", "", part).rstrip()
def parse_value(reader):
# type: (Reader) -> Text
char = reader.peek(1)
if char == u"'":
(value,) = reader.read_regex(_single_quoted_value)
return decode_escapes(_single_quote_escapes, value)
elif char == u'"':
(value,) = reader.read_regex(_double_quoted_value)
return decode_escapes(_double_quote_escapes, value)
elif char in (u"", u"\n", u"\r"):
return u""
else:
return parse_unquoted_value(reader)
def parse_binding(reader):
# type: (Reader) -> Binding
reader.set_mark()
try:
reader.read_regex(_multiline_whitespace)
if not reader.has_next():
return Binding(
key=None,
value=None,
original=reader.get_marked(),
error=False,
)
reader.read_regex(_export)
key = parse_key(reader)
reader.read_regex(_whitespace)
if reader.peek(1) == "=":
reader.read_regex(_equal_sign)
value = parse_value(reader) # type: Optional[Text]
else:
value = None
reader.read_regex(_comment)
reader.read_regex(_end_of_line)
return Binding(
key=key,
value=value,
original=reader.get_marked(),
error=False,
)
except Error:
reader.read_regex(_rest_of_line)
return Binding(
key=None,
value=None,
original=reader.get_marked(),
error=True,
)
def parse_stream(stream):
# type: (IO[Text]) -> Iterator[Binding]
reader = Reader(stream)
while reader.has_next():
yield parse_binding(reader)

View File

@@ -0,0 +1 @@
# Marker file for PEP 561

View File

@@ -0,0 +1 @@
__version__ = "0.15.0"