Switched to static config embedded in python code to environment based variables.
This commit is contained in:
parent
e7786593e1
commit
0fc9aee963
30 changed files with 5794 additions and 26 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
__pycache__
|
||||||
|
.env
|
||||||
5
.env.template
Normal file
5
.env.template
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
VOIPMS_ENDPOINT = os.getenv("VOIPMS_ENDPOINT", "https://voip.ms/api/v1/server.php")
|
||||||
|
VOIPMS_3CX_ENDPOINT = os.getenv("VOIPMS_3CX_ENDPOINT", "https://voip.ms/api/3cx/msg")
|
||||||
|
YEASTAR_WEBHOOK_URL = os.getenv("YEASTAR_WEBHOOK_URL", "please_set_YEASTAR_WEBHOOK_URL_in_env")
|
||||||
|
YEASTAR_SECRET = os.getenv("YEASTAR_SECRET", "secret_set_in_yeastar_channel_config")
|
||||||
|
ENDPOINT_OBSCURITY = os.getenv("ENDPOINT_OBSCURITY","some_random_string_makes_your_endpoint_harder_to_guess")
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -362,5 +362,4 @@ MigrationBackup/
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
|
||||||
config.py
|
.env
|
||||||
/config.py
|
|
||||||
|
|
|
||||||
11
config.py
Normal file
11
config.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# config.py
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
|
||||||
|
load_dotenv(dotenv_path=".env", override=False)
|
||||||
|
|
||||||
|
VOIPMS_ENDPOINT = os.getenv("VOIPMS_ENDPOINT", "https://voip.ms/api/v1/server.php")
|
||||||
|
VOIPMS_3CX_ENDPOINT = os.getenv("VOIPMS_3CX_ENDPOINT", "https://voip.ms/api/3cx/msg")
|
||||||
|
YEASTAR_WEBHOOK_URL = os.getenv("YEASTAR_WEBHOOK_URL", "please_set_YEASTAR_WEBHOOK_URL_in_env")
|
||||||
|
YEASTAR_SECRET = os.getenv("YEASTAR_SECRET", "secret_set_in_yeastar_channel_config")
|
||||||
|
ENDPOINT_OBSCURITY = os.getenv("ENDPOINT_OBSCURITY","some_random_string_makes_your_endpoint_harder_to_guess")
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
# config.template.py
|
|
||||||
|
|
||||||
# Yeastar webhook config
|
|
||||||
# Get the webhook URL from the channel configuration
|
|
||||||
# Set a secret in the channel configuration and put that here.
|
|
||||||
YEASTAR_WEBHOOK_URL = "https://your-yeastar-webhook-url"
|
|
||||||
YEASTAR_SECRET = "your-yeastar-secret"
|
|
||||||
|
|
||||||
# VOIP.ms SOAP endpoint (Farily static, update this if it changes in the future)
|
|
||||||
VOIPMS_ENDPOINT = "https://voip.ms/api/v1/server.php"
|
|
||||||
VOIPMS_3CX_ENDPOINT = "https://voip.ms/api/3cx/msg"
|
|
||||||
|
|
||||||
# Make this something semi-random and URL safe. Adds obscurity and makes your endpoint harder to guess
|
|
||||||
ENDPOINT_OBSCURITY = "abcdefghijklmnopqrstuvwxyz"
|
|
||||||
|
|
||||||
# When this is configured and you start your server up, your endpoints will be:
|
|
||||||
# http://your-ip-address:5000/{ENDPOINT_OBSCURITY}/yeastar-outbound <------- point your yeastar sms channel here
|
|
||||||
# and
|
|
||||||
# http://your-ip-address:5000/{ENDPOINT_OBSCURITY}/voipms-inbound <------- point your voip.ms DIDs to post here
|
|
||||||
|
|
||||||
# Endpoints can deliver verbose output for troubleshooting without restarting the proxy.
|
|
||||||
# Just update the channel or did and add "/verbose" to the URL.
|
|
||||||
|
|
@ -5,3 +5,5 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- .env:/app/.env
|
||||||
49
env/Lib/site-packages/dotenv/__init__.py
vendored
Normal file
49
env/Lib/site-packages/dotenv/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from .main import (dotenv_values, find_dotenv, get_key, load_dotenv, set_key,
|
||||||
|
unset_key)
|
||||||
|
|
||||||
|
|
||||||
|
def load_ipython_extension(ipython: Any) -> None:
|
||||||
|
from .ipython import load_ipython_extension
|
||||||
|
load_ipython_extension(ipython)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cli_string(
|
||||||
|
path: Optional[str] = None,
|
||||||
|
action: Optional[str] = None,
|
||||||
|
key: Optional[str] = None,
|
||||||
|
value: Optional[str] = None,
|
||||||
|
quote: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""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(f'-q {quote}')
|
||||||
|
if path:
|
||||||
|
command.append(f'-f {path}')
|
||||||
|
if action:
|
||||||
|
command.append(action)
|
||||||
|
if key:
|
||||||
|
command.append(key)
|
||||||
|
if value:
|
||||||
|
if ' ' in value:
|
||||||
|
command.append(f'"{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']
|
||||||
6
env/Lib/site-packages/dotenv/__main__.py
vendored
Normal file
6
env/Lib/site-packages/dotenv/__main__.py
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""Entry point for cli, enables execution with `python -m dotenv`"""
|
||||||
|
|
||||||
|
from .cli import cli
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
||||||
205
env/Lib/site-packages/dotenv/cli.py
vendored
Normal file
205
env/Lib/site-packages/dotenv/cli.py
vendored
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Any, Dict, IO, Iterator, List, Optional
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
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 .main import dotenv_values, set_key, unset_key
|
||||||
|
from .version import __version__
|
||||||
|
|
||||||
|
|
||||||
|
def enumerate_env() -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Return a path for the ${pwd}/.env file.
|
||||||
|
|
||||||
|
If pwd does not exist, return None.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cwd = os.getcwd()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
path = os.path.join(cwd, '.env')
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option('-f', '--file', default=enumerate_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: click.Context, file: Any, quote: Any, export: Any) -> None:
|
||||||
|
"""This script is used to set, get or unset values from a .env file."""
|
||||||
|
ctx.obj = {'QUOTE': quote, 'EXPORT': export, 'FILE': file}
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def stream_file(path: os.PathLike) -> Iterator[IO[str]]:
|
||||||
|
"""
|
||||||
|
Open a file and yield the corresponding (decoded) stream.
|
||||||
|
|
||||||
|
Exits with error code 2 if the file cannot be opened.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path) as stream:
|
||||||
|
yield stream
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"Error opening env file: {exc}", file=sys.stderr)
|
||||||
|
exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.option('--format', default='simple',
|
||||||
|
type=click.Choice(['simple', 'json', 'shell', 'export']),
|
||||||
|
help="The format in which to display the list. Default format is simple, "
|
||||||
|
"which displays name=value without quotes.")
|
||||||
|
def list(ctx: click.Context, format: bool) -> None:
|
||||||
|
"""Display all the stored key/value."""
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
|
||||||
|
with stream_file(file) as stream:
|
||||||
|
values = dotenv_values(stream=stream)
|
||||||
|
|
||||||
|
if format == 'json':
|
||||||
|
click.echo(json.dumps(values, indent=2, sort_keys=True))
|
||||||
|
else:
|
||||||
|
prefix = 'export ' if format == 'export' else ''
|
||||||
|
for k in sorted(values):
|
||||||
|
v = values[k]
|
||||||
|
if v is not None:
|
||||||
|
if format in ('export', 'shell'):
|
||||||
|
v = shlex.quote(v)
|
||||||
|
click.echo(f'{prefix}{k}={v}')
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument('key', required=True)
|
||||||
|
@click.argument('value', required=True)
|
||||||
|
def set(ctx: click.Context, key: Any, value: 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(f'{key}={value}')
|
||||||
|
else:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument('key', required=True)
|
||||||
|
def get(ctx: click.Context, key: Any) -> None:
|
||||||
|
"""Retrieve the value for the given key."""
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
|
||||||
|
with stream_file(file) as stream:
|
||||||
|
values = dotenv_values(stream=stream)
|
||||||
|
|
||||||
|
stored_value = values.get(key)
|
||||||
|
if stored_value:
|
||||||
|
click.echo(stored_value)
|
||||||
|
else:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument('key', required=True)
|
||||||
|
def unset(ctx: click.Context, key: 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(f"Successfully removed {key}")
|
||||||
|
else:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(context_settings={'ignore_unknown_options': True})
|
||||||
|
@click.pass_context
|
||||||
|
@click.option(
|
||||||
|
"--override/--no-override",
|
||||||
|
default=True,
|
||||||
|
help="Override variables from the environment file with those from the .env file.",
|
||||||
|
)
|
||||||
|
@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
|
||||||
|
def run(ctx: click.Context, override: bool, commandline: List[str]) -> None:
|
||||||
|
"""Run command with environment variables present."""
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
if not os.path.isfile(file):
|
||||||
|
raise click.BadParameter(
|
||||||
|
f'Invalid value for \'-f\' "{file}" does not exist.',
|
||||||
|
ctx=ctx
|
||||||
|
)
|
||||||
|
dotenv_as_dict = {
|
||||||
|
k: v
|
||||||
|
for (k, v) in dotenv_values(file).items()
|
||||||
|
if v is not None and (override or k not in os.environ)
|
||||||
|
}
|
||||||
|
|
||||||
|
if not commandline:
|
||||||
|
click.echo('No command given.')
|
||||||
|
exit(1)
|
||||||
|
run_command(commandline, dotenv_as_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(command: List[str], env: Dict[str, str]) -> None:
|
||||||
|
"""Replace the current process with the specified command.
|
||||||
|
|
||||||
|
Replaces the current process with the specified command and 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
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
This function does not return any value. It replaces the current process with the new one.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# copy the current environment variables and add the vales from
|
||||||
|
# `env`
|
||||||
|
cmd_env = os.environ.copy()
|
||||||
|
cmd_env.update(env)
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
# execvpe on Windows returns control immediately
|
||||||
|
# rather than once the command has finished.
|
||||||
|
p = Popen(command,
|
||||||
|
universal_newlines=True,
|
||||||
|
bufsize=0,
|
||||||
|
shell=False,
|
||||||
|
env=cmd_env)
|
||||||
|
_, _ = p.communicate()
|
||||||
|
|
||||||
|
exit(p.returncode)
|
||||||
|
else:
|
||||||
|
os.execvpe(command[0], args=command, env=cmd_env)
|
||||||
39
env/Lib/site-packages/dotenv/ipython.py
vendored
Normal file
39
env/Lib/site-packages/dotenv/ipython.py
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
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)
|
||||||
400
env/Lib/site-packages/dotenv/main.py
vendored
Normal file
400
env/Lib/site-packages/dotenv/main.py
vendored
Normal file
|
|
@ -0,0 +1,400 @@
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from collections import OrderedDict
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from .parser import Binding, parse_stream
|
||||||
|
from .variables import parse_variables
|
||||||
|
|
||||||
|
# A type alias for a string path to be used for the paths in this file.
|
||||||
|
# These paths may flow to `open()` and `shutil.move()`; `shutil.move()`
|
||||||
|
# only accepts string paths, not byte paths or file descriptors. See
|
||||||
|
# https://github.com/python/typeshed/pull/6832.
|
||||||
|
StrPath = Union[str, "os.PathLike[str]"]
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def with_warn_for_invalid_lines(mappings: 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: Optional[StrPath],
|
||||||
|
stream: Optional[IO[str]] = None,
|
||||||
|
verbose: bool = False,
|
||||||
|
encoding: Optional[str] = None,
|
||||||
|
interpolate: bool = True,
|
||||||
|
override: bool = True,
|
||||||
|
) -> None:
|
||||||
|
self.dotenv_path: Optional[StrPath] = dotenv_path
|
||||||
|
self.stream: Optional[IO[str]] = stream
|
||||||
|
self._dict: Optional[Dict[str, Optional[str]]] = None
|
||||||
|
self.verbose: bool = verbose
|
||||||
|
self.encoding: Optional[str] = encoding
|
||||||
|
self.interpolate: bool = interpolate
|
||||||
|
self.override: bool = override
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _get_stream(self) -> Iterator[IO[str]]:
|
||||||
|
if self.dotenv_path and os.path.isfile(self.dotenv_path):
|
||||||
|
with open(self.dotenv_path, encoding=self.encoding) as stream:
|
||||||
|
yield stream
|
||||||
|
elif self.stream is not None:
|
||||||
|
yield self.stream
|
||||||
|
else:
|
||||||
|
if self.verbose:
|
||||||
|
logger.info(
|
||||||
|
"python-dotenv could not find configuration file %s.",
|
||||||
|
self.dotenv_path or ".env",
|
||||||
|
)
|
||||||
|
yield io.StringIO("")
|
||||||
|
|
||||||
|
def dict(self) -> Dict[str, Optional[str]]:
|
||||||
|
"""Return dotenv as dict"""
|
||||||
|
if self._dict:
|
||||||
|
return self._dict
|
||||||
|
|
||||||
|
raw_values = self.parse()
|
||||||
|
|
||||||
|
if self.interpolate:
|
||||||
|
self._dict = OrderedDict(
|
||||||
|
resolve_variables(raw_values, override=self.override)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._dict = OrderedDict(raw_values)
|
||||||
|
|
||||||
|
return self._dict
|
||||||
|
|
||||||
|
def parse(self) -> Iterator[Tuple[str, Optional[str]]]:
|
||||||
|
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) -> bool:
|
||||||
|
"""
|
||||||
|
Load the current dotenv as system environment variable.
|
||||||
|
"""
|
||||||
|
if not self.dict():
|
||||||
|
return False
|
||||||
|
|
||||||
|
for k, v in self.dict().items():
|
||||||
|
if k in os.environ and not self.override:
|
||||||
|
continue
|
||||||
|
if v is not None:
|
||||||
|
os.environ[k] = v
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get(self, key: str) -> Optional[str]:
|
||||||
|
""" """
|
||||||
|
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: StrPath,
|
||||||
|
key_to_get: str,
|
||||||
|
encoding: Optional[str] = "utf-8",
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get the value of a given key from the given .env.
|
||||||
|
|
||||||
|
Returns `None` if the key isn't found or doesn't have a value.
|
||||||
|
"""
|
||||||
|
return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def rewrite(
|
||||||
|
path: StrPath,
|
||||||
|
encoding: Optional[str],
|
||||||
|
) -> Iterator[Tuple[IO[str], IO[str]]]:
|
||||||
|
pathlib.Path(path).touch()
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", encoding=encoding, delete=False) as dest:
|
||||||
|
error = None
|
||||||
|
try:
|
||||||
|
with open(path, encoding=encoding) as source:
|
||||||
|
yield (source, dest)
|
||||||
|
except BaseException as err:
|
||||||
|
error = err
|
||||||
|
|
||||||
|
if error is None:
|
||||||
|
shutil.move(dest.name, path)
|
||||||
|
else:
|
||||||
|
os.unlink(dest.name)
|
||||||
|
raise error from None
|
||||||
|
|
||||||
|
|
||||||
|
def set_key(
|
||||||
|
dotenv_path: StrPath,
|
||||||
|
key_to_set: str,
|
||||||
|
value_to_set: str,
|
||||||
|
quote_mode: str = "always",
|
||||||
|
export: bool = False,
|
||||||
|
encoding: Optional[str] = "utf-8",
|
||||||
|
) -> Tuple[Optional[bool], str, str]:
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
if quote_mode not in ("always", "auto", "never"):
|
||||||
|
raise ValueError(f"Unknown quote_mode: {quote_mode}")
|
||||||
|
|
||||||
|
quote = quote_mode == "always" or (
|
||||||
|
quote_mode == "auto" and not value_to_set.isalnum()
|
||||||
|
)
|
||||||
|
|
||||||
|
if quote:
|
||||||
|
value_out = "'{}'".format(value_to_set.replace("'", "\\'"))
|
||||||
|
else:
|
||||||
|
value_out = value_to_set
|
||||||
|
if export:
|
||||||
|
line_out = f"export {key_to_set}={value_out}\n"
|
||||||
|
else:
|
||||||
|
line_out = f"{key_to_set}={value_out}\n"
|
||||||
|
|
||||||
|
with rewrite(dotenv_path, encoding=encoding) as (source, dest):
|
||||||
|
replaced = False
|
||||||
|
missing_newline = 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)
|
||||||
|
missing_newline = not mapping.original.string.endswith("\n")
|
||||||
|
if not replaced:
|
||||||
|
if missing_newline:
|
||||||
|
dest.write("\n")
|
||||||
|
dest.write(line_out)
|
||||||
|
|
||||||
|
return True, key_to_set, value_to_set
|
||||||
|
|
||||||
|
|
||||||
|
def unset_key(
|
||||||
|
dotenv_path: StrPath,
|
||||||
|
key_to_unset: str,
|
||||||
|
quote_mode: str = "always",
|
||||||
|
encoding: Optional[str] = "utf-8",
|
||||||
|
) -> Tuple[Optional[bool], str]:
|
||||||
|
"""
|
||||||
|
Removes a given key from the given `.env` file.
|
||||||
|
|
||||||
|
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, encoding=encoding) 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_variables(
|
||||||
|
values: Iterable[Tuple[str, Optional[str]]],
|
||||||
|
override: bool,
|
||||||
|
) -> Mapping[str, Optional[str]]:
|
||||||
|
new_values: Dict[str, Optional[str]] = {}
|
||||||
|
|
||||||
|
for name, value in values:
|
||||||
|
if value is None:
|
||||||
|
result = None
|
||||||
|
else:
|
||||||
|
atoms = parse_variables(value)
|
||||||
|
env: Dict[str, Optional[str]] = {}
|
||||||
|
if override:
|
||||||
|
env.update(os.environ) # type: ignore
|
||||||
|
env.update(new_values)
|
||||||
|
else:
|
||||||
|
env.update(new_values)
|
||||||
|
env.update(os.environ) # type: ignore
|
||||||
|
result = "".join(atom.resolve(env) for atom in atoms)
|
||||||
|
|
||||||
|
new_values[name] = result
|
||||||
|
|
||||||
|
return new_values
|
||||||
|
|
||||||
|
|
||||||
|
def _walk_to_root(path: str) -> Iterator[str]:
|
||||||
|
"""
|
||||||
|
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: str = ".env",
|
||||||
|
raise_error_if_not_found: bool = False,
|
||||||
|
usecwd: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
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"""
|
||||||
|
if hasattr(sys, "ps1") or hasattr(sys, "ps2"):
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
main = __import__("__main__", None, None, fromlist=["__file__"])
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
return False
|
||||||
|
return not hasattr(main, "__file__")
|
||||||
|
|
||||||
|
def _is_debugger():
|
||||||
|
return sys.gettrace() is not None
|
||||||
|
|
||||||
|
if usecwd or _is_interactive() or _is_debugger() 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()
|
||||||
|
current_file = __file__
|
||||||
|
|
||||||
|
while frame.f_code.co_filename == current_file or not os.path.exists(
|
||||||
|
frame.f_code.co_filename
|
||||||
|
):
|
||||||
|
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: Optional[StrPath] = None,
|
||||||
|
stream: Optional[IO[str]] = None,
|
||||||
|
verbose: bool = False,
|
||||||
|
override: bool = False,
|
||||||
|
interpolate: bool = True,
|
||||||
|
encoding: Optional[str] = "utf-8",
|
||||||
|
) -> bool:
|
||||||
|
"""Parse a .env file and then load all the variables found as environment variables.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
dotenv_path: Absolute or relative path to .env file.
|
||||||
|
stream: Text stream (such as `io.StringIO`) with .env content, used if
|
||||||
|
`dotenv_path` is `None`.
|
||||||
|
verbose: Whether to output a warning the .env file is missing.
|
||||||
|
override: Whether to override the system environment variables with the variables
|
||||||
|
from the `.env` file.
|
||||||
|
encoding: Encoding to be used to read the file.
|
||||||
|
Returns:
|
||||||
|
Bool: True if at least one environment variable is set else False
|
||||||
|
|
||||||
|
If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
|
||||||
|
.env file with it's default parameters. If you need to change the default parameters
|
||||||
|
of `find_dotenv()`, you can explicitly call `find_dotenv()` and pass the result
|
||||||
|
to this function as `dotenv_path`.
|
||||||
|
"""
|
||||||
|
if dotenv_path is None and stream is None:
|
||||||
|
dotenv_path = find_dotenv()
|
||||||
|
|
||||||
|
dotenv = DotEnv(
|
||||||
|
dotenv_path=dotenv_path,
|
||||||
|
stream=stream,
|
||||||
|
verbose=verbose,
|
||||||
|
interpolate=interpolate,
|
||||||
|
override=override,
|
||||||
|
encoding=encoding,
|
||||||
|
)
|
||||||
|
return dotenv.set_as_environment_variables()
|
||||||
|
|
||||||
|
|
||||||
|
def dotenv_values(
|
||||||
|
dotenv_path: Optional[StrPath] = None,
|
||||||
|
stream: Optional[IO[str]] = None,
|
||||||
|
verbose: bool = False,
|
||||||
|
interpolate: bool = True,
|
||||||
|
encoding: Optional[str] = "utf-8",
|
||||||
|
) -> Dict[str, Optional[str]]:
|
||||||
|
"""
|
||||||
|
Parse a .env file and return its content as a dict.
|
||||||
|
|
||||||
|
The returned dict will have `None` values for keys without values in the .env file.
|
||||||
|
For example, `foo=bar` results in `{"foo": "bar"}` whereas `foo` alone results in
|
||||||
|
`{"foo": None}`
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
dotenv_path: Absolute or relative path to the .env file.
|
||||||
|
stream: `StringIO` object with .env content, used if `dotenv_path` is `None`.
|
||||||
|
verbose: Whether to output a warning if the .env file is missing.
|
||||||
|
encoding: Encoding to be used to read the file.
|
||||||
|
|
||||||
|
If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
|
||||||
|
.env file.
|
||||||
|
"""
|
||||||
|
if dotenv_path is None and stream is None:
|
||||||
|
dotenv_path = find_dotenv()
|
||||||
|
|
||||||
|
return DotEnv(
|
||||||
|
dotenv_path=dotenv_path,
|
||||||
|
stream=stream,
|
||||||
|
verbose=verbose,
|
||||||
|
interpolate=interpolate,
|
||||||
|
override=True,
|
||||||
|
encoding=encoding,
|
||||||
|
).dict()
|
||||||
175
env/Lib/site-packages/dotenv/parser.py
vendored
Normal file
175
env/Lib/site-packages/dotenv/parser.py
vendored
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
import codecs
|
||||||
|
import re
|
||||||
|
from typing import (IO, Iterator, Match, NamedTuple, Optional, # noqa:F401
|
||||||
|
Pattern, Sequence, Tuple)
|
||||||
|
|
||||||
|
|
||||||
|
def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]:
|
||||||
|
return re.compile(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"\\[\\']")
|
||||||
|
|
||||||
|
|
||||||
|
class Original(NamedTuple):
|
||||||
|
string: str
|
||||||
|
line: int
|
||||||
|
|
||||||
|
|
||||||
|
class Binding(NamedTuple):
|
||||||
|
key: Optional[str]
|
||||||
|
value: Optional[str]
|
||||||
|
original: Original
|
||||||
|
error: bool
|
||||||
|
|
||||||
|
|
||||||
|
class Position:
|
||||||
|
def __init__(self, chars: int, line: int) -> None:
|
||||||
|
self.chars = chars
|
||||||
|
self.line = line
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def start(cls) -> "Position":
|
||||||
|
return cls(chars=0, line=1)
|
||||||
|
|
||||||
|
def set(self, other: "Position") -> None:
|
||||||
|
self.chars = other.chars
|
||||||
|
self.line = other.line
|
||||||
|
|
||||||
|
def advance(self, string: str) -> None:
|
||||||
|
self.chars += len(string)
|
||||||
|
self.line += len(re.findall(_newline, string))
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Reader:
|
||||||
|
def __init__(self, stream: IO[str]) -> None:
|
||||||
|
self.string = stream.read()
|
||||||
|
self.position = Position.start()
|
||||||
|
self.mark = Position.start()
|
||||||
|
|
||||||
|
def has_next(self) -> bool:
|
||||||
|
return self.position.chars < len(self.string)
|
||||||
|
|
||||||
|
def set_mark(self) -> None:
|
||||||
|
self.mark.set(self.position)
|
||||||
|
|
||||||
|
def get_marked(self) -> Original:
|
||||||
|
return Original(
|
||||||
|
string=self.string[self.mark.chars:self.position.chars],
|
||||||
|
line=self.mark.line,
|
||||||
|
)
|
||||||
|
|
||||||
|
def peek(self, count: int) -> str:
|
||||||
|
return self.string[self.position.chars:self.position.chars + count]
|
||||||
|
|
||||||
|
def read(self, count: int) -> str:
|
||||||
|
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: Pattern[str]) -> Sequence[str]:
|
||||||
|
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: Pattern[str], string: str) -> str:
|
||||||
|
def decode_match(match: Match[str]) -> str:
|
||||||
|
return codecs.decode(match.group(0), 'unicode-escape') # type: ignore
|
||||||
|
|
||||||
|
return regex.sub(decode_match, string)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_key(reader: Reader) -> Optional[str]:
|
||||||
|
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: Reader) -> str:
|
||||||
|
(part,) = reader.read_regex(_unquoted_value)
|
||||||
|
return re.sub(r"\s+#.*", "", part).rstrip()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_value(reader: Reader) -> str:
|
||||||
|
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: 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: Optional[str] = parse_value(reader)
|
||||||
|
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: IO[str]) -> Iterator[Binding]:
|
||||||
|
reader = Reader(stream)
|
||||||
|
while reader.has_next():
|
||||||
|
yield parse_binding(reader)
|
||||||
1
env/Lib/site-packages/dotenv/py.typed
vendored
Normal file
1
env/Lib/site-packages/dotenv/py.typed
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Marker file for PEP 561
|
||||||
86
env/Lib/site-packages/dotenv/variables.py
vendored
Normal file
86
env/Lib/site-packages/dotenv/variables.py
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import re
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
from typing import Iterator, Mapping, Optional, Pattern
|
||||||
|
|
||||||
|
_posix_variable: Pattern[str] = re.compile(
|
||||||
|
r"""
|
||||||
|
\$\{
|
||||||
|
(?P<name>[^\}:]*)
|
||||||
|
(?::-
|
||||||
|
(?P<default>[^\}]*)
|
||||||
|
)?
|
||||||
|
\}
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Atom(metaclass=ABCMeta):
|
||||||
|
def __ne__(self, other: object) -> bool:
|
||||||
|
result = self.__eq__(other)
|
||||||
|
if result is NotImplemented:
|
||||||
|
return NotImplemented
|
||||||
|
return not result
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def resolve(self, env: Mapping[str, Optional[str]]) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
|
class Literal(Atom):
|
||||||
|
def __init__(self, value: str) -> None:
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"Literal(value={self.value})"
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, self.__class__):
|
||||||
|
return NotImplemented
|
||||||
|
return self.value == other.value
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((self.__class__, self.value))
|
||||||
|
|
||||||
|
def resolve(self, env: Mapping[str, Optional[str]]) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class Variable(Atom):
|
||||||
|
def __init__(self, name: str, default: Optional[str]) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.default = default
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"Variable(name={self.name}, default={self.default})"
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, self.__class__):
|
||||||
|
return NotImplemented
|
||||||
|
return (self.name, self.default) == (other.name, other.default)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((self.__class__, self.name, self.default))
|
||||||
|
|
||||||
|
def resolve(self, env: Mapping[str, Optional[str]]) -> str:
|
||||||
|
default = self.default if self.default is not None else ""
|
||||||
|
result = env.get(self.name, default)
|
||||||
|
return result if result is not None else ""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_variables(value: str) -> Iterator[Atom]:
|
||||||
|
cursor = 0
|
||||||
|
|
||||||
|
for match in _posix_variable.finditer(value):
|
||||||
|
(start, end) = match.span()
|
||||||
|
name = match["name"]
|
||||||
|
default = match["default"]
|
||||||
|
|
||||||
|
if start > cursor:
|
||||||
|
yield Literal(value=value[cursor:start])
|
||||||
|
|
||||||
|
yield Variable(name=name, default=default)
|
||||||
|
cursor = end
|
||||||
|
|
||||||
|
length = len(value)
|
||||||
|
if cursor < length:
|
||||||
|
yield Literal(value=value[cursor:length])
|
||||||
1
env/Lib/site-packages/dotenv/version.py
vendored
Normal file
1
env/Lib/site-packages/dotenv/version.py
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "1.1.1"
|
||||||
347
env/Lib/site-packages/flask/config.py
vendored
Normal file
347
env/Lib/site-packages/flask/config.py
vendored
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import types
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from werkzeug.utils import import_string
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigAttribute:
|
||||||
|
"""Makes an attribute forward to the config"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, get_converter: t.Callable | None = None) -> None:
|
||||||
|
self.__name__ = name
|
||||||
|
self.get_converter = get_converter
|
||||||
|
|
||||||
|
def __get__(self, obj: t.Any, owner: t.Any = None) -> t.Any:
|
||||||
|
if obj is None:
|
||||||
|
return self
|
||||||
|
rv = obj.config[self.__name__]
|
||||||
|
if self.get_converter is not None:
|
||||||
|
rv = self.get_converter(rv)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def __set__(self, obj: t.Any, value: t.Any) -> None:
|
||||||
|
obj.config[self.__name__] = value
|
||||||
|
|
||||||
|
|
||||||
|
class Config(dict):
|
||||||
|
"""Works exactly like a dict but provides ways to fill it from files
|
||||||
|
or special dictionaries. There are two common patterns to populate the
|
||||||
|
config.
|
||||||
|
|
||||||
|
Either you can fill the config from a config file::
|
||||||
|
|
||||||
|
app.config.from_pyfile('yourconfig.cfg')
|
||||||
|
|
||||||
|
Or alternatively you can define the configuration options in the
|
||||||
|
module that calls :meth:`from_object` or provide an import path to
|
||||||
|
a module that should be loaded. It is also possible to tell it to
|
||||||
|
use the same module and with that provide the configuration values
|
||||||
|
just before the call::
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
SECRET_KEY = 'development key'
|
||||||
|
app.config.from_object(__name__)
|
||||||
|
|
||||||
|
In both cases (loading from any Python file or loading from modules),
|
||||||
|
only uppercase keys are added to the config. This makes it possible to use
|
||||||
|
lowercase values in the config file for temporary values that are not added
|
||||||
|
to the config or to define the config keys in the same file that implements
|
||||||
|
the application.
|
||||||
|
|
||||||
|
Probably the most interesting way to load configurations is from an
|
||||||
|
environment variable pointing to a file::
|
||||||
|
|
||||||
|
app.config.from_envvar('YOURAPPLICATION_SETTINGS')
|
||||||
|
|
||||||
|
In this case before launching the application you have to set this
|
||||||
|
environment variable to the file you want to use. On Linux and OS X
|
||||||
|
use the export statement::
|
||||||
|
|
||||||
|
export YOURAPPLICATION_SETTINGS='/path/to/config/file'
|
||||||
|
|
||||||
|
On windows use `set` instead.
|
||||||
|
|
||||||
|
:param root_path: path to which files are read relative from. When the
|
||||||
|
config object is created by the application, this is
|
||||||
|
the application's :attr:`~flask.Flask.root_path`.
|
||||||
|
:param defaults: an optional dictionary of default values
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, root_path: str | os.PathLike, defaults: dict | None = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(defaults or {})
|
||||||
|
self.root_path = root_path
|
||||||
|
|
||||||
|
def from_envvar(self, variable_name: str, silent: bool = False) -> bool:
|
||||||
|
"""Loads a configuration from an environment variable pointing to
|
||||||
|
a configuration file. This is basically just a shortcut with nicer
|
||||||
|
error messages for this line of code::
|
||||||
|
|
||||||
|
app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS'])
|
||||||
|
|
||||||
|
:param variable_name: name of the environment variable
|
||||||
|
:param silent: set to ``True`` if you want silent failure for missing
|
||||||
|
files.
|
||||||
|
:return: ``True`` if the file was loaded successfully.
|
||||||
|
"""
|
||||||
|
rv = os.environ.get(variable_name)
|
||||||
|
if not rv:
|
||||||
|
if silent:
|
||||||
|
return False
|
||||||
|
raise RuntimeError(
|
||||||
|
f"The environment variable {variable_name!r} is not set"
|
||||||
|
" and as such configuration could not be loaded. Set"
|
||||||
|
" this variable and make it point to a configuration"
|
||||||
|
" file"
|
||||||
|
)
|
||||||
|
return self.from_pyfile(rv, silent=silent)
|
||||||
|
|
||||||
|
def from_prefixed_env(
|
||||||
|
self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads
|
||||||
|
) -> bool:
|
||||||
|
"""Load any environment variables that start with ``FLASK_``,
|
||||||
|
dropping the prefix from the env key for the config key. Values
|
||||||
|
are passed through a loading function to attempt to convert them
|
||||||
|
to more specific types than strings.
|
||||||
|
|
||||||
|
Keys are loaded in :func:`sorted` order.
|
||||||
|
|
||||||
|
The default loading function attempts to parse values as any
|
||||||
|
valid JSON type, including dicts and lists.
|
||||||
|
|
||||||
|
Specific items in nested dicts can be set by separating the
|
||||||
|
keys with double underscores (``__``). If an intermediate key
|
||||||
|
doesn't exist, it will be initialized to an empty dict.
|
||||||
|
|
||||||
|
:param prefix: Load env vars that start with this prefix,
|
||||||
|
separated with an underscore (``_``).
|
||||||
|
:param loads: Pass each string value to this function and use
|
||||||
|
the returned value as the config value. If any error is
|
||||||
|
raised it is ignored and the value remains a string. The
|
||||||
|
default is :func:`json.loads`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.1
|
||||||
|
"""
|
||||||
|
prefix = f"{prefix}_"
|
||||||
|
len_prefix = len(prefix)
|
||||||
|
|
||||||
|
for key in sorted(os.environ):
|
||||||
|
if not key.startswith(prefix):
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = os.environ[key]
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = loads(value)
|
||||||
|
except Exception:
|
||||||
|
# Keep the value as a string if loading failed.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Change to key.removeprefix(prefix) on Python >= 3.9.
|
||||||
|
key = key[len_prefix:]
|
||||||
|
|
||||||
|
if "__" not in key:
|
||||||
|
# A non-nested key, set directly.
|
||||||
|
self[key] = value
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Traverse nested dictionaries with keys separated by "__".
|
||||||
|
current = self
|
||||||
|
*parts, tail = key.split("__")
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
# If an intermediate dict does not exist, create it.
|
||||||
|
if part not in current:
|
||||||
|
current[part] = {}
|
||||||
|
|
||||||
|
current = current[part]
|
||||||
|
|
||||||
|
current[tail] = value
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def from_pyfile(self, filename: str | os.PathLike, silent: bool = False) -> bool:
|
||||||
|
"""Updates the values in the config from a Python file. This function
|
||||||
|
behaves as if the file was imported as module with the
|
||||||
|
:meth:`from_object` function.
|
||||||
|
|
||||||
|
:param filename: the filename of the config. This can either be an
|
||||||
|
absolute filename or a filename relative to the
|
||||||
|
root path.
|
||||||
|
:param silent: set to ``True`` if you want silent failure for missing
|
||||||
|
files.
|
||||||
|
:return: ``True`` if the file was loaded successfully.
|
||||||
|
|
||||||
|
.. versionadded:: 0.7
|
||||||
|
`silent` parameter.
|
||||||
|
"""
|
||||||
|
filename = os.path.join(self.root_path, filename)
|
||||||
|
d = types.ModuleType("config")
|
||||||
|
d.__file__ = filename
|
||||||
|
try:
|
||||||
|
with open(filename, mode="rb") as config_file:
|
||||||
|
exec(compile(config_file.read(), filename, "exec"), d.__dict__)
|
||||||
|
except OSError as e:
|
||||||
|
if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR):
|
||||||
|
return False
|
||||||
|
e.strerror = f"Unable to load configuration file ({e.strerror})"
|
||||||
|
raise
|
||||||
|
self.from_object(d)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def from_object(self, obj: object | str) -> None:
|
||||||
|
"""Updates the values from the given object. An object can be of one
|
||||||
|
of the following two types:
|
||||||
|
|
||||||
|
- a string: in this case the object with that name will be imported
|
||||||
|
- an actual object reference: that object is used directly
|
||||||
|
|
||||||
|
Objects are usually either modules or classes. :meth:`from_object`
|
||||||
|
loads only the uppercase attributes of the module/class. A ``dict``
|
||||||
|
object will not work with :meth:`from_object` because the keys of a
|
||||||
|
``dict`` are not attributes of the ``dict`` class.
|
||||||
|
|
||||||
|
Example of module-based configuration::
|
||||||
|
|
||||||
|
app.config.from_object('yourapplication.default_config')
|
||||||
|
from yourapplication import default_config
|
||||||
|
app.config.from_object(default_config)
|
||||||
|
|
||||||
|
Nothing is done to the object before loading. If the object is a
|
||||||
|
class and has ``@property`` attributes, it needs to be
|
||||||
|
instantiated before being passed to this method.
|
||||||
|
|
||||||
|
You should not use this function to load the actual configuration but
|
||||||
|
rather configuration defaults. The actual config should be loaded
|
||||||
|
with :meth:`from_pyfile` and ideally from a location not within the
|
||||||
|
package because the package might be installed system wide.
|
||||||
|
|
||||||
|
See :ref:`config-dev-prod` for an example of class-based configuration
|
||||||
|
using :meth:`from_object`.
|
||||||
|
|
||||||
|
:param obj: an import name or object
|
||||||
|
"""
|
||||||
|
if isinstance(obj, str):
|
||||||
|
obj = import_string(obj)
|
||||||
|
for key in dir(obj):
|
||||||
|
if key.isupper():
|
||||||
|
self[key] = getattr(obj, key)
|
||||||
|
|
||||||
|
def from_file(
|
||||||
|
self,
|
||||||
|
filename: str | os.PathLike,
|
||||||
|
load: t.Callable[[t.IO[t.Any]], t.Mapping],
|
||||||
|
silent: bool = False,
|
||||||
|
text: bool = True,
|
||||||
|
) -> bool:
|
||||||
|
"""Update the values in the config from a file that is loaded
|
||||||
|
using the ``load`` parameter. The loaded data is passed to the
|
||||||
|
:meth:`from_mapping` method.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import json
|
||||||
|
app.config.from_file("config.json", load=json.load)
|
||||||
|
|
||||||
|
import tomllib
|
||||||
|
app.config.from_file("config.toml", load=tomllib.load, text=False)
|
||||||
|
|
||||||
|
:param filename: The path to the data file. This can be an
|
||||||
|
absolute path or relative to the config root path.
|
||||||
|
:param load: A callable that takes a file handle and returns a
|
||||||
|
mapping of loaded data from the file.
|
||||||
|
:type load: ``Callable[[Reader], Mapping]`` where ``Reader``
|
||||||
|
implements a ``read`` method.
|
||||||
|
:param silent: Ignore the file if it doesn't exist.
|
||||||
|
:param text: Open the file in text or binary mode.
|
||||||
|
:return: ``True`` if the file was loaded successfully.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.3
|
||||||
|
The ``text`` parameter was added.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
filename = os.path.join(self.root_path, filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filename, "r" if text else "rb") as f:
|
||||||
|
obj = load(f)
|
||||||
|
except OSError as e:
|
||||||
|
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
|
||||||
|
return False
|
||||||
|
|
||||||
|
e.strerror = f"Unable to load configuration file ({e.strerror})"
|
||||||
|
raise
|
||||||
|
|
||||||
|
return self.from_mapping(obj)
|
||||||
|
|
||||||
|
def from_mapping(
|
||||||
|
self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any
|
||||||
|
) -> bool:
|
||||||
|
"""Updates the config like :meth:`update` ignoring items with
|
||||||
|
non-upper keys.
|
||||||
|
|
||||||
|
:return: Always returns ``True``.
|
||||||
|
|
||||||
|
.. versionadded:: 0.11
|
||||||
|
"""
|
||||||
|
mappings: dict[str, t.Any] = {}
|
||||||
|
if mapping is not None:
|
||||||
|
mappings.update(mapping)
|
||||||
|
mappings.update(kwargs)
|
||||||
|
for key, value in mappings.items():
|
||||||
|
if key.isupper():
|
||||||
|
self[key] = value
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_namespace(
|
||||||
|
self, namespace: str, lowercase: bool = True, trim_namespace: bool = True
|
||||||
|
) -> dict[str, t.Any]:
|
||||||
|
"""Returns a dictionary containing a subset of configuration options
|
||||||
|
that match the specified namespace/prefix. Example usage::
|
||||||
|
|
||||||
|
app.config['IMAGE_STORE_TYPE'] = 'fs'
|
||||||
|
app.config['IMAGE_STORE_PATH'] = '/var/app/images'
|
||||||
|
app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com'
|
||||||
|
image_store_config = app.config.get_namespace('IMAGE_STORE_')
|
||||||
|
|
||||||
|
The resulting dictionary `image_store_config` would look like::
|
||||||
|
|
||||||
|
{
|
||||||
|
'type': 'fs',
|
||||||
|
'path': '/var/app/images',
|
||||||
|
'base_url': 'http://img.website.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
This is often useful when configuration options map directly to
|
||||||
|
keyword arguments in functions or class constructors.
|
||||||
|
|
||||||
|
:param namespace: a configuration namespace
|
||||||
|
:param lowercase: a flag indicating if the keys of the resulting
|
||||||
|
dictionary should be lowercase
|
||||||
|
:param trim_namespace: a flag indicating if the keys of the resulting
|
||||||
|
dictionary should not include the namespace
|
||||||
|
|
||||||
|
.. versionadded:: 0.11
|
||||||
|
"""
|
||||||
|
rv = {}
|
||||||
|
for k, v in self.items():
|
||||||
|
if not k.startswith(namespace):
|
||||||
|
continue
|
||||||
|
if trim_namespace:
|
||||||
|
key = k[len(namespace) :]
|
||||||
|
else:
|
||||||
|
key = k
|
||||||
|
if lowercase:
|
||||||
|
key = key.lower()
|
||||||
|
rv[key] = v
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{type(self).__name__} {dict.__repr__(self)}>"
|
||||||
2442
env/Lib/site-packages/gunicorn/config.py
vendored
Normal file
2442
env/Lib/site-packages/gunicorn/config.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/INSTALLER
vendored
Normal file
1
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/INSTALLER
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pip
|
||||||
733
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/METADATA
vendored
Normal file
733
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/METADATA
vendored
Normal file
|
|
@ -0,0 +1,733 @@
|
||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: python-dotenv
|
||||||
|
Version: 1.1.1
|
||||||
|
Summary: Read key-value pairs from a .env file and set them as environment variables
|
||||||
|
Home-page: https://github.com/theskumar/python-dotenv
|
||||||
|
Author: Saurabh Kumar
|
||||||
|
Author-email: me+github@saurabh-kumar.com
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Keywords: environment variables,deployments,settings,env,dotenv,configurations,python
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Intended Audience :: System Administrators
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Topic :: System :: Systems Administration
|
||||||
|
Classifier: Topic :: Utilities
|
||||||
|
Classifier: Environment :: Web Environment
|
||||||
|
Requires-Python: >=3.9
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
License-File: LICENSE
|
||||||
|
Provides-Extra: cli
|
||||||
|
Requires-Dist: click>=5.0; extra == "cli"
|
||||||
|
Dynamic: author
|
||||||
|
Dynamic: author-email
|
||||||
|
Dynamic: classifier
|
||||||
|
Dynamic: description
|
||||||
|
Dynamic: description-content-type
|
||||||
|
Dynamic: home-page
|
||||||
|
Dynamic: keywords
|
||||||
|
Dynamic: license
|
||||||
|
Dynamic: license-file
|
||||||
|
Dynamic: provides-extra
|
||||||
|
Dynamic: requires-python
|
||||||
|
Dynamic: summary
|
||||||
|
|
||||||
|
# python-dotenv
|
||||||
|
|
||||||
|
[![Build Status][build_status_badge]][build_status_link]
|
||||||
|
[![PyPI version][pypi_badge]][pypi_link]
|
||||||
|
|
||||||
|
python-dotenv reads key-value pairs from a `.env` file and can set them as environment
|
||||||
|
variables. It helps in the development of applications following the
|
||||||
|
[12-factor](https://12factor.net/) principles.
|
||||||
|
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [Other Use Cases](#other-use-cases)
|
||||||
|
* [Load configuration without altering the environment](#load-configuration-without-altering-the-environment)
|
||||||
|
* [Parse configuration as a stream](#parse-configuration-as-a-stream)
|
||||||
|
* [Load .env files in IPython](#load-env-files-in-ipython)
|
||||||
|
- [Command-line Interface](#command-line-interface)
|
||||||
|
- [File format](#file-format)
|
||||||
|
* [Multiline values](#multiline-values)
|
||||||
|
* [Variable expansion](#variable-expansion)
|
||||||
|
- [Related Projects](#related-projects)
|
||||||
|
- [Acknowledgements](#acknowledgements)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install python-dotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
If your application takes its configuration from environment variables, like a 12-factor
|
||||||
|
application, launching it in development is not very practical because you have to set
|
||||||
|
those environment variables yourself.
|
||||||
|
|
||||||
|
To help you with that, you can add python-dotenv to your application to make it load the
|
||||||
|
configuration from a `.env` file when it is present (e.g. in development) while remaining
|
||||||
|
configurable via the environment:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv() # take environment variables
|
||||||
|
|
||||||
|
# Code of your application, which uses environment variables (e.g. from `os.environ` or
|
||||||
|
# `os.getenv`) as if they came from the actual environment.
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, `load_dotenv` doesn't override existing environment variables and looks for a `.env` file in same directory as python script or searches for it incrementally higher up.
|
||||||
|
|
||||||
|
To configure the development environment, add a `.env` in the root directory of your
|
||||||
|
project:
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── .env
|
||||||
|
└── foo.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The syntax of `.env` files supported by python-dotenv is similar to that of Bash:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development settings
|
||||||
|
DOMAIN=example.org
|
||||||
|
ADMIN_EMAIL=admin@${DOMAIN}
|
||||||
|
ROOT_URL=${DOMAIN}/app
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use variables in values, ensure they are surrounded with `{` and `}`, like
|
||||||
|
`${DOMAIN}`, as bare variables such as `$DOMAIN` are not expanded.
|
||||||
|
|
||||||
|
You will probably want to add `.env` to your `.gitignore`, especially if it contains
|
||||||
|
secrets like a password.
|
||||||
|
|
||||||
|
See the section "File format" below for more information about what you can write in a
|
||||||
|
`.env` file.
|
||||||
|
|
||||||
|
## Other Use Cases
|
||||||
|
|
||||||
|
### Load configuration without altering the environment
|
||||||
|
|
||||||
|
The function `dotenv_values` works more or less the same way as `load_dotenv`, except it
|
||||||
|
doesn't touch the environment, it just returns a `dict` with the values parsed from the
|
||||||
|
`.env` file.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dotenv import dotenv_values
|
||||||
|
|
||||||
|
config = dotenv_values(".env") # config = {"USER": "foo", "EMAIL": "foo@example.org"}
|
||||||
|
```
|
||||||
|
|
||||||
|
This notably enables advanced configuration management:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
from dotenv import dotenv_values
|
||||||
|
|
||||||
|
config = {
|
||||||
|
**dotenv_values(".env.shared"), # load shared development variables
|
||||||
|
**dotenv_values(".env.secret"), # load sensitive variables
|
||||||
|
**os.environ, # override loaded values with environment variables
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parse configuration as a stream
|
||||||
|
|
||||||
|
`load_dotenv` and `dotenv_values` accept [streams][python_streams] via their `stream`
|
||||||
|
argument. It is thus possible to load the variables from sources other than the
|
||||||
|
filesystem (e.g. the network).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
config = StringIO("USER=foo\nEMAIL=foo@example.org")
|
||||||
|
load_dotenv(stream=config)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load .env files in IPython
|
||||||
|
|
||||||
|
You can use dotenv in IPython. By default, it will use `find_dotenv` to search for a
|
||||||
|
`.env` file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
%load_ext dotenv
|
||||||
|
%dotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also specify a path:
|
||||||
|
|
||||||
|
```python
|
||||||
|
%dotenv relative/or/absolute/path/to/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional flags:
|
||||||
|
|
||||||
|
- `-o` to override existing variables.
|
||||||
|
- `-v` for increased verbosity.
|
||||||
|
|
||||||
|
## Command-line Interface
|
||||||
|
|
||||||
|
A CLI interface `dotenv` is also included, which helps you manipulate the `.env` file
|
||||||
|
without manually opening it.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ pip install "python-dotenv[cli]"
|
||||||
|
$ dotenv set USER foo
|
||||||
|
$ dotenv set EMAIL foo@example.org
|
||||||
|
$ dotenv list
|
||||||
|
USER=foo
|
||||||
|
EMAIL=foo@example.org
|
||||||
|
$ dotenv list --format=json
|
||||||
|
{
|
||||||
|
"USER": "foo",
|
||||||
|
"EMAIL": "foo@example.org"
|
||||||
|
}
|
||||||
|
$ dotenv run -- python foo.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `dotenv --help` for more information about the options and subcommands.
|
||||||
|
|
||||||
|
## File format
|
||||||
|
|
||||||
|
The format is not formally specified and still improves over time. That being said,
|
||||||
|
`.env` files should mostly look like Bash files.
|
||||||
|
|
||||||
|
Keys can be unquoted or single-quoted. Values can be unquoted, single- or double-quoted.
|
||||||
|
Spaces before and after keys, equal signs, and values are ignored. Values can be followed
|
||||||
|
by a comment. Lines can start with the `export` directive, which does not affect their
|
||||||
|
interpretation.
|
||||||
|
|
||||||
|
Allowed escape sequences:
|
||||||
|
|
||||||
|
- in single-quoted values: `\\`, `\'`
|
||||||
|
- in double-quoted values: `\\`, `\'`, `\"`, `\a`, `\b`, `\f`, `\n`, `\r`, `\t`, `\v`
|
||||||
|
|
||||||
|
### Multiline values
|
||||||
|
|
||||||
|
It is possible for single- or double-quoted values to span multiple lines. The following
|
||||||
|
examples are equivalent:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
FOO="first line
|
||||||
|
second line"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
FOO="first line\nsecond line"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variable without a value
|
||||||
|
|
||||||
|
A variable can have no value:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
FOO
|
||||||
|
```
|
||||||
|
|
||||||
|
It results in `dotenv_values` associating that variable name with the value `None` (e.g.
|
||||||
|
`{"FOO": None}`. `load_dotenv`, on the other hand, simply ignores such variables.
|
||||||
|
|
||||||
|
This shouldn't be confused with `FOO=`, in which case the variable is associated with the
|
||||||
|
empty string.
|
||||||
|
|
||||||
|
### Variable expansion
|
||||||
|
|
||||||
|
python-dotenv can interpolate variables using POSIX variable expansion.
|
||||||
|
|
||||||
|
With `load_dotenv(override=True)` or `dotenv_values()`, the value of a variable is the
|
||||||
|
first of the values defined in the following list:
|
||||||
|
|
||||||
|
- Value of that variable in the `.env` file.
|
||||||
|
- Value of that variable in the environment.
|
||||||
|
- Default value, if provided.
|
||||||
|
- Empty string.
|
||||||
|
|
||||||
|
With `load_dotenv(override=False)`, the value of a variable is the first of the values
|
||||||
|
defined in the following list:
|
||||||
|
|
||||||
|
- Value of that variable in the environment.
|
||||||
|
- Value of that variable in the `.env` file.
|
||||||
|
- Default value, if provided.
|
||||||
|
- Empty string.
|
||||||
|
|
||||||
|
## Related Projects
|
||||||
|
|
||||||
|
- [Honcho](https://github.com/nickstenning/honcho) - For managing
|
||||||
|
Procfile-based applications.
|
||||||
|
- [django-dotenv](https://github.com/jpadilla/django-dotenv)
|
||||||
|
- [django-environ](https://github.com/joke2k/django-environ)
|
||||||
|
- [django-environ-2](https://github.com/sergeyklay/django-environ-2)
|
||||||
|
- [django-configuration](https://github.com/jezdez/django-configurations)
|
||||||
|
- [dump-env](https://github.com/sobolevn/dump-env)
|
||||||
|
- [environs](https://github.com/sloria/environs)
|
||||||
|
- [dynaconf](https://github.com/rochacbruno/dynaconf)
|
||||||
|
- [parse_it](https://github.com/naorlivne/parse_it)
|
||||||
|
- [python-decouple](https://github.com/HBNetwork/python-decouple)
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
This project is currently maintained by [Saurabh Kumar](https://saurabh-kumar.com) and
|
||||||
|
[Bertrand Bonnefoy-Claudet](https://github.com/bbc2) and would not have been possible
|
||||||
|
without the support of these [awesome
|
||||||
|
people](https://github.com/theskumar/python-dotenv/graphs/contributors).
|
||||||
|
|
||||||
|
[build_status_badge]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml/badge.svg
|
||||||
|
[build_status_link]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml
|
||||||
|
[pypi_badge]: https://badge.fury.io/py/python-dotenv.svg
|
||||||
|
[pypi_link]: https://badge.fury.io/py/python-dotenv
|
||||||
|
[python_streams]: https://docs.python.org/3/library/io.html
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this
|
||||||
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.1.1] - 2025-06-24
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
* CLI: Ensure `find_dotenv` work reliably on python 3.13 by [@theskumar] in [#563](https://github.com/theskumar/python-dotenv/pull/563)
|
||||||
|
* CLI: revert the use of execvpe on Windows by [@wrongontheinternet] in [#566](https://github.com/theskumar/python-dotenv/pull/566)
|
||||||
|
|
||||||
|
|
||||||
|
## [1.1.0] - 2025-03-25
|
||||||
|
|
||||||
|
**Feature**
|
||||||
|
|
||||||
|
- Add support for python 3.13
|
||||||
|
- Enhance `dotenv run`, switch to `execvpe` for better resource management and signal handling ([#523]) by [@eekstunt]
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- `find_dotenv` and `load_dotenv` now correctly looks up at the current directory when running in debugger or pdb ([#553] by [@randomseed42])
|
||||||
|
|
||||||
|
**Misc**
|
||||||
|
|
||||||
|
- Drop support for Python 3.8
|
||||||
|
|
||||||
|
## [1.0.1] - 2024-01-23
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
* Gracefully handle code which has been imported from a zipfile ([#456] by [@samwyma])
|
||||||
|
* Allow modules using `load_dotenv` to be reloaded when launched in a separate thread ([#497] by [@freddyaboulton])
|
||||||
|
* Fix file not closed after deletion, handle error in the rewrite function ([#469] by [@Qwerty-133])
|
||||||
|
|
||||||
|
**Misc**
|
||||||
|
* Use pathlib.Path in tests ([#466] by [@eumiro])
|
||||||
|
* Fix year in release date in changelog.md ([#454] by [@jankislinger])
|
||||||
|
* Use https in README links ([#474] by [@Nicals])
|
||||||
|
|
||||||
|
## [1.0.0] - 2023-02-24
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
* Drop support for python 3.7, add python 3.12-dev (#449 by [@theskumar])
|
||||||
|
* Handle situations where the cwd does not exist. (#446 by [@jctanner])
|
||||||
|
|
||||||
|
## [0.21.1] - 2023-01-21
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
* Use Python 3.11 non-beta in CI (#438 by [@bbc2])
|
||||||
|
* Modernize variables code (#434 by [@Nougat-Waffle])
|
||||||
|
* Modernize main.py and parser.py code (#435 by [@Nougat-Waffle])
|
||||||
|
* Improve conciseness of cli.py and __init__.py (#439 by [@Nougat-Waffle])
|
||||||
|
* Improve error message for `get` and `list` commands when env file can't be opened (#441 by [@bbc2])
|
||||||
|
* Updated License to align with BSD OSI template (#433 by [@lsmith77])
|
||||||
|
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
* Fix Out-of-scope error when "dest" variable is undefined (#413 by [@theGOTOguy])
|
||||||
|
* Fix IPython test warning about deprecated `magic` (#440 by [@bbc2])
|
||||||
|
* Fix type hint for dotenv_path var, add StrPath alias (#432 by [@eaf])
|
||||||
|
|
||||||
|
## [0.21.0] - 2022-09-03
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
* CLI: add support for invocations via 'python -m'. (#395 by [@theskumar])
|
||||||
|
* `load_dotenv` function now returns `False`. (#388 by [@larsks])
|
||||||
|
* CLI: add --format= option to list command. (#407 by [@sammck])
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
* Drop Python 3.5 and 3.6 and upgrade GA (#393 by [@eggplants])
|
||||||
|
* Use `open` instead of `io.open`. (#389 by [@rabinadk1])
|
||||||
|
* Improve documentation for variables without a value (#390 by [@bbc2])
|
||||||
|
* Add `parse_it` to Related Projects (#410 by [@naorlivne])
|
||||||
|
* Update README.md (#415 by [@harveer07])
|
||||||
|
* Improve documentation with direct use of MkDocs (#398 by [@bbc2])
|
||||||
|
|
||||||
|
## [0.20.0] - 2022-03-24
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- Add `encoding` (`Optional[str]`) parameter to `get_key`, `set_key` and `unset_key`.
|
||||||
|
(#379 by [@bbc2])
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- Use dict to specify the `entry_points` parameter of `setuptools.setup` (#376 by
|
||||||
|
[@mgorny]).
|
||||||
|
- Don't build universal wheels (#387 by [@bbc2]).
|
||||||
|
|
||||||
|
## [0.19.2] - 2021-11-11
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- In `set_key`, add missing newline character before new entry if necessary. (#361 by
|
||||||
|
[@bbc2])
|
||||||
|
|
||||||
|
## [0.19.1] - 2021-08-09
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- Add support for Python 3.10. (#359 by [@theskumar])
|
||||||
|
|
||||||
|
## [0.19.0] - 2021-07-24
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- Require Python 3.5 or a later version. Python 2 and 3.4 are no longer supported. (#341
|
||||||
|
by [@bbc2]).
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- The `dotenv_path` argument of `set_key` and `unset_key` now has a type of `Union[str,
|
||||||
|
os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]).
|
||||||
|
- The `stream` argument of `load_dotenv` and `dotenv_values` can now be a text stream
|
||||||
|
(`IO[str]`), which includes values like `io.StringIO("foo")` and `open("file.env",
|
||||||
|
"r")` (#348 by [@bbc2]).
|
||||||
|
|
||||||
|
## [0.18.0] - 2021-06-20
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- Raise `ValueError` if `quote_mode` isn't one of `always`, `auto` or `never` in
|
||||||
|
`set_key` (#330 by [@bbc2]).
|
||||||
|
- When writing a value to a .env file with `set_key` or `dotenv set <key> <value>` (#330
|
||||||
|
by [@bbc2]):
|
||||||
|
- Use single quotes instead of double quotes.
|
||||||
|
- Don't strip surrounding quotes.
|
||||||
|
- In `auto` mode, don't add quotes if the value is only made of alphanumeric characters
|
||||||
|
(as determined by `string.isalnum`).
|
||||||
|
|
||||||
|
## [0.17.1] - 2021-04-29
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- Fixed tests for build environments relying on `PYTHONPATH` (#318 by [@befeleme]).
|
||||||
|
|
||||||
|
## [0.17.0] - 2021-04-02
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- Make `dotenv get <key>` only show the value, not `key=value` (#313 by [@bbc2]).
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- Add `--override`/`--no-override` option to `dotenv run` (#312 by [@zueve] and [@bbc2]).
|
||||||
|
|
||||||
|
## [0.16.0] - 2021-03-27
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- The default value of the `encoding` parameter for `load_dotenv` and `dotenv_values` is
|
||||||
|
now `"utf-8"` instead of `None` (#306 by [@bbc2]).
|
||||||
|
- Fix resolution order in variable expansion with `override=False` (#287 by [@bbc2]).
|
||||||
|
|
||||||
|
## [0.15.0] - 2020-10-28
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- Add `--export` option to `set` to make it prepend the binding with `export` (#270 by
|
||||||
|
[@jadutter]).
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- Make `set` command create the `.env` file in the current directory if no `.env` file was
|
||||||
|
found (#270 by [@jadutter]).
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- Fix potentially empty expanded value for duplicate key (#260 by [@bbc2]).
|
||||||
|
- Fix import error on Python 3.5.0 and 3.5.1 (#267 by [@gongqingkui]).
|
||||||
|
- Fix parsing of unquoted values containing several adjacent space or tab characters
|
||||||
|
(#277 by [@bbc2], review by [@x-yuri]).
|
||||||
|
|
||||||
|
## [0.14.0] - 2020-07-03
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- Privilege definition in file over the environment in variable expansion (#256 by
|
||||||
|
[@elbehery95]).
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- Improve error message for when file isn't found (#245 by [@snobu]).
|
||||||
|
- Use HTTPS URL in package meta data (#251 by [@ekohl]).
|
||||||
|
|
||||||
|
## [0.13.0] - 2020-04-16
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- Add support for a Bash-like default value in variable expansion (#248 by [@bbc2]).
|
||||||
|
|
||||||
|
## [0.12.0] - 2020-02-28
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- Use current working directory to find `.env` when bundled by PyInstaller (#213 by
|
||||||
|
[@gergelyk]).
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- Fix escaping of quoted values written by `set_key` (#236 by [@bbc2]).
|
||||||
|
- Fix `dotenv run` crashing on environment variables without values (#237 by [@yannham]).
|
||||||
|
- Remove warning when last line is empty (#238 by [@bbc2]).
|
||||||
|
|
||||||
|
## [0.11.0] - 2020-02-07
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- Add `interpolate` argument to `load_dotenv` and `dotenv_values` to disable interpolation
|
||||||
|
(#232 by [@ulyssessouza]).
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- Use logging instead of warnings (#231 by [@bbc2]).
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- Fix installation in non-UTF-8 environments (#225 by [@altendky]).
|
||||||
|
- Fix PyPI classifiers (#228 by [@bbc2]).
|
||||||
|
|
||||||
|
## [0.10.5] - 2020-01-19
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- Fix handling of malformed lines and lines without a value (#222 by [@bbc2]):
|
||||||
|
- Don't print warning when key has no value.
|
||||||
|
- Reject more malformed lines (e.g. "A: B", "a='b',c").
|
||||||
|
- Fix handling of lines with just a comment (#224 by [@bbc2]).
|
||||||
|
|
||||||
|
## [0.10.4] - 2020-01-17
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- Make typing optional (#179 by [@techalchemy]).
|
||||||
|
- Print a warning on malformed line (#211 by [@bbc2]).
|
||||||
|
- Support keys without a value (#220 by [@ulyssessouza]).
|
||||||
|
|
||||||
|
## 0.10.3
|
||||||
|
|
||||||
|
- Improve interactive mode detection ([@andrewsmith])([#183]).
|
||||||
|
- Refactor parser to fix parsing inconsistencies ([@bbc2])([#170]).
|
||||||
|
- Interpret escapes as control characters only in double-quoted strings.
|
||||||
|
- Interpret `#` as start of comment only if preceded by whitespace.
|
||||||
|
|
||||||
|
## 0.10.2
|
||||||
|
|
||||||
|
- Add type hints and expose them to users ([@qnighy])([#172])
|
||||||
|
- `load_dotenv` and `dotenv_values` now accept an `encoding` parameter, defaults to `None`
|
||||||
|
([@theskumar])([@earlbread])([#161])
|
||||||
|
- Fix `str`/`unicode` inconsistency in Python 2: values are always `str` now. ([@bbc2])([#121])
|
||||||
|
- Fix Unicode error in Python 2, introduced in 0.10.0. ([@bbc2])([#176])
|
||||||
|
|
||||||
|
## 0.10.1
|
||||||
|
- Fix parsing of variable without a value ([@asyncee])([@bbc2])([#158])
|
||||||
|
|
||||||
|
## 0.10.0
|
||||||
|
|
||||||
|
- Add support for UTF-8 in unquoted values ([@bbc2])([#148])
|
||||||
|
- Add support for trailing comments ([@bbc2])([#148])
|
||||||
|
- Add backslashes support in values ([@bbc2])([#148])
|
||||||
|
- Add support for newlines in values ([@bbc2])([#148])
|
||||||
|
- Force environment variables to str with Python2 on Windows ([@greyli])
|
||||||
|
- Drop Python 3.3 support ([@greyli])
|
||||||
|
- Fix stderr/-out/-in redirection ([@venthur])
|
||||||
|
|
||||||
|
|
||||||
|
## 0.9.0
|
||||||
|
|
||||||
|
- Add `--version` parameter to cli ([@venthur])
|
||||||
|
- Enable loading from current directory ([@cjauvin])
|
||||||
|
- Add 'dotenv run' command for calling arbitrary shell script with .env ([@venthur])
|
||||||
|
|
||||||
|
## 0.8.1
|
||||||
|
|
||||||
|
- Add tests for docs ([@Flimm])
|
||||||
|
- Make 'cli' support optional. Use `pip install python-dotenv[cli]`. ([@theskumar])
|
||||||
|
|
||||||
|
## 0.8.0
|
||||||
|
|
||||||
|
- `set_key` and `unset_key` only modified the affected file instead of
|
||||||
|
parsing and re-writing file, this causes comments and other file
|
||||||
|
entact as it is.
|
||||||
|
- Add support for `export` prefix in the line.
|
||||||
|
- Internal refractoring ([@theskumar])
|
||||||
|
- Allow `load_dotenv` and `dotenv_values` to work with `StringIO())` ([@alanjds])([@theskumar])([#78])
|
||||||
|
|
||||||
|
## 0.7.1
|
||||||
|
|
||||||
|
- Remove hard dependency on iPython ([@theskumar])
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
- Add support to override system environment variable via .env.
|
||||||
|
([@milonimrod](https://github.com/milonimrod))
|
||||||
|
([\#63](https://github.com/theskumar/python-dotenv/issues/63))
|
||||||
|
- Disable ".env not found" warning by default
|
||||||
|
([@maxkoryukov](https://github.com/maxkoryukov))
|
||||||
|
([\#57](https://github.com/theskumar/python-dotenv/issues/57))
|
||||||
|
|
||||||
|
## 0.6.5
|
||||||
|
|
||||||
|
- Add support for special characters `\`.
|
||||||
|
([@pjona](https://github.com/pjona))
|
||||||
|
([\#60](https://github.com/theskumar/python-dotenv/issues/60))
|
||||||
|
|
||||||
|
## 0.6.4
|
||||||
|
|
||||||
|
- Fix issue with single quotes ([@Flimm])
|
||||||
|
([\#52](https://github.com/theskumar/python-dotenv/issues/52))
|
||||||
|
|
||||||
|
## 0.6.3
|
||||||
|
|
||||||
|
- Handle unicode exception in setup.py
|
||||||
|
([\#46](https://github.com/theskumar/python-dotenv/issues/46))
|
||||||
|
|
||||||
|
## 0.6.2
|
||||||
|
|
||||||
|
- Fix dotenv list command ([@ticosax](https://github.com/ticosax))
|
||||||
|
- Add iPython Support
|
||||||
|
([@tillahoffmann](https://github.com/tillahoffmann))
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
- Drop support for Python 2.6
|
||||||
|
- Handle escaped characters and newlines in quoted values. (Thanks
|
||||||
|
[@iameugenejo](https://github.com/iameugenejo))
|
||||||
|
- Remove any spaces around unquoted key/value. (Thanks
|
||||||
|
[@paulochf](https://github.com/paulochf))
|
||||||
|
- Added POSIX variable expansion. (Thanks
|
||||||
|
[@hugochinchilla](https://github.com/hugochinchilla))
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
- Fix `find_dotenv` - it now start search from the file where this
|
||||||
|
function is called from.
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
- Add `find_dotenv` method that will try to find a `.env` file.
|
||||||
|
(Thanks [@isms](https://github.com/isms))
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
- cli: Added `-q/--quote` option to control the behaviour of quotes
|
||||||
|
around values in `.env`. (Thanks
|
||||||
|
[@hugochinchilla](https://github.com/hugochinchilla)).
|
||||||
|
- Improved test coverage.
|
||||||
|
|
||||||
|
[#78]: https://github.com/theskumar/python-dotenv/issues/78
|
||||||
|
[#121]: https://github.com/theskumar/python-dotenv/issues/121
|
||||||
|
[#148]: https://github.com/theskumar/python-dotenv/issues/148
|
||||||
|
[#158]: https://github.com/theskumar/python-dotenv/issues/158
|
||||||
|
[#170]: https://github.com/theskumar/python-dotenv/issues/170
|
||||||
|
[#172]: https://github.com/theskumar/python-dotenv/issues/172
|
||||||
|
[#176]: https://github.com/theskumar/python-dotenv/issues/176
|
||||||
|
[#183]: https://github.com/theskumar/python-dotenv/issues/183
|
||||||
|
[#359]: https://github.com/theskumar/python-dotenv/issues/359
|
||||||
|
[#469]: https://github.com/theskumar/python-dotenv/issues/469
|
||||||
|
[#456]: https://github.com/theskumar/python-dotenv/issues/456
|
||||||
|
[#466]: https://github.com/theskumar/python-dotenv/issues/466
|
||||||
|
[#454]: https://github.com/theskumar/python-dotenv/issues/454
|
||||||
|
[#474]: https://github.com/theskumar/python-dotenv/issues/474
|
||||||
|
[#523]: https://github.com/theskumar/python-dotenv/issues/523
|
||||||
|
[#553]: https://github.com/theskumar/python-dotenv/issues/553
|
||||||
|
|
||||||
|
[@alanjds]: https://github.com/alanjds
|
||||||
|
[@altendky]: https://github.com/altendky
|
||||||
|
[@andrewsmith]: https://github.com/andrewsmith
|
||||||
|
[@asyncee]: https://github.com/asyncee
|
||||||
|
[@bbc2]: https://github.com/bbc2
|
||||||
|
[@befeleme]: https://github.com/befeleme
|
||||||
|
[@cjauvin]: https://github.com/cjauvin
|
||||||
|
[@eaf]: https://github.com/eaf
|
||||||
|
[@earlbread]: https://github.com/earlbread
|
||||||
|
[@eekstunt]: https://github.com/eekstunt
|
||||||
|
[@eggplants]: https://github.com/@eggplants
|
||||||
|
[@ekohl]: https://github.com/ekohl
|
||||||
|
[@elbehery95]: https://github.com/elbehery95
|
||||||
|
[@eumiro]: https://github.com/eumiro
|
||||||
|
[@Flimm]: https://github.com/Flimm
|
||||||
|
[@freddyaboulton]: https://github.com/freddyaboulton
|
||||||
|
[@gergelyk]: https://github.com/gergelyk
|
||||||
|
[@gongqingkui]: https://github.com/gongqingkui
|
||||||
|
[@greyli]: https://github.com/greyli
|
||||||
|
[@harveer07]: https://github.com/@harveer07
|
||||||
|
[@jadutter]: https://github.com/jadutter
|
||||||
|
[@jankislinger]: https://github.com/jankislinger
|
||||||
|
[@jctanner]: https://github.com/jctanner
|
||||||
|
[@larsks]: https://github.com/@larsks
|
||||||
|
[@lsmith77]: https://github.com/lsmith77
|
||||||
|
[@mgorny]: https://github.com/mgorny
|
||||||
|
[@naorlivne]: https://github.com/@naorlivne
|
||||||
|
[@Nicals]: https://github.com/Nicals
|
||||||
|
[@Nougat-Waffle]: https://github.com/Nougat-Waffle
|
||||||
|
[@qnighy]: https://github.com/qnighy
|
||||||
|
[@Qwerty-133]: https://github.com/Qwerty-133
|
||||||
|
[@rabinadk1]: https://github.com/@rabinadk1
|
||||||
|
[@sammck]: https://github.com/@sammck
|
||||||
|
[@samwyma]: https://github.com/samwyma
|
||||||
|
[@snobu]: https://github.com/snobu
|
||||||
|
[@techalchemy]: https://github.com/techalchemy
|
||||||
|
[@theGOTOguy]: https://github.com/theGOTOguy
|
||||||
|
[@theskumar]: https://github.com/theskumar
|
||||||
|
[@ulyssessouza]: https://github.com/ulyssessouza
|
||||||
|
[@venthur]: https://github.com/venthur
|
||||||
|
[@x-yuri]: https://github.com/x-yuri
|
||||||
|
[@yannham]: https://github.com/yannham
|
||||||
|
[@zueve]: https://github.com/zueve
|
||||||
|
[@randomseed42]: https://github.com/zueve
|
||||||
|
[@wrongontheinternet]: https://github.com/wrongontheinternet
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.1.1...HEAD
|
||||||
|
[1.1.1]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...1.1.1
|
||||||
|
[1.1.0]: https://github.com/theskumar/python-dotenv/compare/v1.0.1...v1.1.0
|
||||||
|
[1.0.1]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1
|
||||||
|
[1.0.0]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v1.0.0
|
||||||
|
[0.21.1]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v0.21.1
|
||||||
|
[0.21.0]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...v0.21.0
|
||||||
|
[0.20.0]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0
|
||||||
|
[0.19.2]: https://github.com/theskumar/python-dotenv/compare/v0.19.1...v0.19.2
|
||||||
|
[0.19.1]: https://github.com/theskumar/python-dotenv/compare/v0.19.0...v0.19.1
|
||||||
|
[0.19.0]: https://github.com/theskumar/python-dotenv/compare/v0.18.0...v0.19.0
|
||||||
|
[0.18.0]: https://github.com/theskumar/python-dotenv/compare/v0.17.1...v0.18.0
|
||||||
|
[0.17.1]: https://github.com/theskumar/python-dotenv/compare/v0.17.0...v0.17.1
|
||||||
|
[0.17.0]: https://github.com/theskumar/python-dotenv/compare/v0.16.0...v0.17.0
|
||||||
|
[0.16.0]: https://github.com/theskumar/python-dotenv/compare/v0.15.0...v0.16.0
|
||||||
|
[0.15.0]: https://github.com/theskumar/python-dotenv/compare/v0.14.0...v0.15.0
|
||||||
|
[0.14.0]: https://github.com/theskumar/python-dotenv/compare/v0.13.0...v0.14.0
|
||||||
|
[0.13.0]: https://github.com/theskumar/python-dotenv/compare/v0.12.0...v0.13.0
|
||||||
|
[0.12.0]: https://github.com/theskumar/python-dotenv/compare/v0.11.0...v0.12.0
|
||||||
|
[0.11.0]: https://github.com/theskumar/python-dotenv/compare/v0.10.5...v0.11.0
|
||||||
|
[0.10.5]: https://github.com/theskumar/python-dotenv/compare/v0.10.4...v0.10.5
|
||||||
|
[0.10.4]: https://github.com/theskumar/python-dotenv/compare/v0.10.3...v0.10.4
|
||||||
25
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/RECORD
vendored
Normal file
25
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/RECORD
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
../../Scripts/dotenv.exe,sha256=ak9kM_lqvTUCPNB4dP0GrtGdq3CSiNw7knf3DFrWar8,106363
|
||||||
|
dotenv/__init__.py,sha256=WBU5SfSiKAhS3hzu17ykNuuwbuwyDCX91Szv4vUeOuM,1292
|
||||||
|
dotenv/__main__.py,sha256=N0RhLG7nHIqtlJHwwepIo-zbJPNx9sewCCRGY528h_4,129
|
||||||
|
dotenv/__pycache__/__init__.cpython-39.pyc,,
|
||||||
|
dotenv/__pycache__/__main__.cpython-39.pyc,,
|
||||||
|
dotenv/__pycache__/cli.cpython-39.pyc,,
|
||||||
|
dotenv/__pycache__/ipython.cpython-39.pyc,,
|
||||||
|
dotenv/__pycache__/main.cpython-39.pyc,,
|
||||||
|
dotenv/__pycache__/parser.cpython-39.pyc,,
|
||||||
|
dotenv/__pycache__/variables.cpython-39.pyc,,
|
||||||
|
dotenv/__pycache__/version.cpython-39.pyc,,
|
||||||
|
dotenv/cli.py,sha256=ut83SItbWcmEahAkSOzkHqvRKhqhj0tA53vcXpyleOM,6197
|
||||||
|
dotenv/ipython.py,sha256=avI6aez_RxnBptYgchIquF2TSgKI-GOhY3ppiu3VuWE,1303
|
||||||
|
dotenv/main.py,sha256=HJgkS0XZcd0f2VZaVGxlUcrOEhqBcmQ6Lz9hQrMfaus,12467
|
||||||
|
dotenv/parser.py,sha256=QgU5HwMwM2wMqt0vz6dHTJ4nzPmwqRqvi4MSyeVifgU,5186
|
||||||
|
dotenv/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
|
||||||
|
dotenv/variables.py,sha256=CD0qXOvvpB3q5RpBQMD9qX6vHX7SyW-SuiwGMFSlt08,2348
|
||||||
|
dotenv/version.py,sha256=q8_5C0f-8mHWNb6mMw02zlYPnEGXBqvOmP3z0CEwZKM,22
|
||||||
|
python_dotenv-1.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
python_dotenv-1.1.1.dist-info/METADATA,sha256=dELvSKXwZ-NbQKAe-k-uJM8khmVN8ZM92B5tyY801yY,24628
|
||||||
|
python_dotenv-1.1.1.dist-info/RECORD,,
|
||||||
|
python_dotenv-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
||||||
|
python_dotenv-1.1.1.dist-info/entry_points.txt,sha256=yRl1rCbswb1nQTQ_gZRlCw5QfabztUGnfGWLhlXFNdI,47
|
||||||
|
python_dotenv-1.1.1.dist-info/licenses/LICENSE,sha256=gGGbcEnwjIFoOtDgHwjyV6hAZS3XHugxRtNmWMfSwrk,1556
|
||||||
|
python_dotenv-1.1.1.dist-info/top_level.txt,sha256=eyqUH4SHJNr6ahOYlxIunTr4XinE8Z5ajWLdrK3r0D8,7
|
||||||
5
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/WHEEL
vendored
Normal file
5
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/WHEEL
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: setuptools (80.9.0)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
||||||
2
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/entry_points.txt
vendored
Normal file
2
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/entry_points.txt
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[console_scripts]
|
||||||
|
dotenv = dotenv.__main__:cli
|
||||||
27
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/licenses/LICENSE
vendored
Normal file
27
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/licenses/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dotenv-rw), 2013, Jacob Kaplan-Moss (django-dotenv)
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
- Neither the name of django-dotenv nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
1
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/top_level.txt
vendored
Normal file
1
env/Lib/site-packages/python_dotenv-1.1.1.dist-info/top_level.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
dotenv
|
||||||
344
env/Lib/site-packages/setuptools/_distutils/command/config.py
vendored
Normal file
344
env/Lib/site-packages/setuptools/_distutils/command/config.py
vendored
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
"""distutils.command.config
|
||||||
|
|
||||||
|
Implements the Distutils 'config' command, a (mostly) empty command class
|
||||||
|
that exists mainly to be sub-classed by specific module distributions and
|
||||||
|
applications. The idea is that while every "config" command is different,
|
||||||
|
at least they're all named the same, and users always see "config" in the
|
||||||
|
list of standard commands. Also, this is a good place to put common
|
||||||
|
configure-like tasks: "try to compile this C code", or "figure out where
|
||||||
|
this header file lives".
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os, re
|
||||||
|
|
||||||
|
from distutils.core import Command
|
||||||
|
from distutils.errors import DistutilsExecError
|
||||||
|
from distutils.sysconfig import customize_compiler
|
||||||
|
from distutils import log
|
||||||
|
|
||||||
|
LANG_EXT = {"c": ".c", "c++": ".cxx"}
|
||||||
|
|
||||||
|
class config(Command):
|
||||||
|
|
||||||
|
description = "prepare to build"
|
||||||
|
|
||||||
|
user_options = [
|
||||||
|
('compiler=', None,
|
||||||
|
"specify the compiler type"),
|
||||||
|
('cc=', None,
|
||||||
|
"specify the compiler executable"),
|
||||||
|
('include-dirs=', 'I',
|
||||||
|
"list of directories to search for header files"),
|
||||||
|
('define=', 'D',
|
||||||
|
"C preprocessor macros to define"),
|
||||||
|
('undef=', 'U',
|
||||||
|
"C preprocessor macros to undefine"),
|
||||||
|
('libraries=', 'l',
|
||||||
|
"external C libraries to link with"),
|
||||||
|
('library-dirs=', 'L',
|
||||||
|
"directories to search for external C libraries"),
|
||||||
|
|
||||||
|
('noisy', None,
|
||||||
|
"show every action (compile, link, run, ...) taken"),
|
||||||
|
('dump-source', None,
|
||||||
|
"dump generated source files before attempting to compile them"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# The three standard command methods: since the "config" command
|
||||||
|
# does nothing by default, these are empty.
|
||||||
|
|
||||||
|
def initialize_options(self):
|
||||||
|
self.compiler = None
|
||||||
|
self.cc = None
|
||||||
|
self.include_dirs = None
|
||||||
|
self.libraries = None
|
||||||
|
self.library_dirs = None
|
||||||
|
|
||||||
|
# maximal output for now
|
||||||
|
self.noisy = 1
|
||||||
|
self.dump_source = 1
|
||||||
|
|
||||||
|
# list of temporary files generated along-the-way that we have
|
||||||
|
# to clean at some point
|
||||||
|
self.temp_files = []
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
if self.include_dirs is None:
|
||||||
|
self.include_dirs = self.distribution.include_dirs or []
|
||||||
|
elif isinstance(self.include_dirs, str):
|
||||||
|
self.include_dirs = self.include_dirs.split(os.pathsep)
|
||||||
|
|
||||||
|
if self.libraries is None:
|
||||||
|
self.libraries = []
|
||||||
|
elif isinstance(self.libraries, str):
|
||||||
|
self.libraries = [self.libraries]
|
||||||
|
|
||||||
|
if self.library_dirs is None:
|
||||||
|
self.library_dirs = []
|
||||||
|
elif isinstance(self.library_dirs, str):
|
||||||
|
self.library_dirs = self.library_dirs.split(os.pathsep)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Utility methods for actual "config" commands. The interfaces are
|
||||||
|
# loosely based on Autoconf macros of similar names. Sub-classes
|
||||||
|
# may use these freely.
|
||||||
|
|
||||||
|
def _check_compiler(self):
|
||||||
|
"""Check that 'self.compiler' really is a CCompiler object;
|
||||||
|
if not, make it one.
|
||||||
|
"""
|
||||||
|
# We do this late, and only on-demand, because this is an expensive
|
||||||
|
# import.
|
||||||
|
from distutils.ccompiler import CCompiler, new_compiler
|
||||||
|
if not isinstance(self.compiler, CCompiler):
|
||||||
|
self.compiler = new_compiler(compiler=self.compiler,
|
||||||
|
dry_run=self.dry_run, force=1)
|
||||||
|
customize_compiler(self.compiler)
|
||||||
|
if self.include_dirs:
|
||||||
|
self.compiler.set_include_dirs(self.include_dirs)
|
||||||
|
if self.libraries:
|
||||||
|
self.compiler.set_libraries(self.libraries)
|
||||||
|
if self.library_dirs:
|
||||||
|
self.compiler.set_library_dirs(self.library_dirs)
|
||||||
|
|
||||||
|
def _gen_temp_sourcefile(self, body, headers, lang):
|
||||||
|
filename = "_configtest" + LANG_EXT[lang]
|
||||||
|
with open(filename, "w") as file:
|
||||||
|
if headers:
|
||||||
|
for header in headers:
|
||||||
|
file.write("#include <%s>\n" % header)
|
||||||
|
file.write("\n")
|
||||||
|
file.write(body)
|
||||||
|
if body[-1] != "\n":
|
||||||
|
file.write("\n")
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def _preprocess(self, body, headers, include_dirs, lang):
|
||||||
|
src = self._gen_temp_sourcefile(body, headers, lang)
|
||||||
|
out = "_configtest.i"
|
||||||
|
self.temp_files.extend([src, out])
|
||||||
|
self.compiler.preprocess(src, out, include_dirs=include_dirs)
|
||||||
|
return (src, out)
|
||||||
|
|
||||||
|
def _compile(self, body, headers, include_dirs, lang):
|
||||||
|
src = self._gen_temp_sourcefile(body, headers, lang)
|
||||||
|
if self.dump_source:
|
||||||
|
dump_file(src, "compiling '%s':" % src)
|
||||||
|
(obj,) = self.compiler.object_filenames([src])
|
||||||
|
self.temp_files.extend([src, obj])
|
||||||
|
self.compiler.compile([src], include_dirs=include_dirs)
|
||||||
|
return (src, obj)
|
||||||
|
|
||||||
|
def _link(self, body, headers, include_dirs, libraries, library_dirs,
|
||||||
|
lang):
|
||||||
|
(src, obj) = self._compile(body, headers, include_dirs, lang)
|
||||||
|
prog = os.path.splitext(os.path.basename(src))[0]
|
||||||
|
self.compiler.link_executable([obj], prog,
|
||||||
|
libraries=libraries,
|
||||||
|
library_dirs=library_dirs,
|
||||||
|
target_lang=lang)
|
||||||
|
|
||||||
|
if self.compiler.exe_extension is not None:
|
||||||
|
prog = prog + self.compiler.exe_extension
|
||||||
|
self.temp_files.append(prog)
|
||||||
|
|
||||||
|
return (src, obj, prog)
|
||||||
|
|
||||||
|
def _clean(self, *filenames):
|
||||||
|
if not filenames:
|
||||||
|
filenames = self.temp_files
|
||||||
|
self.temp_files = []
|
||||||
|
log.info("removing: %s", ' '.join(filenames))
|
||||||
|
for filename in filenames:
|
||||||
|
try:
|
||||||
|
os.remove(filename)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# XXX these ignore the dry-run flag: what to do, what to do? even if
|
||||||
|
# you want a dry-run build, you still need some sort of configuration
|
||||||
|
# info. My inclination is to make it up to the real config command to
|
||||||
|
# consult 'dry_run', and assume a default (minimal) configuration if
|
||||||
|
# true. The problem with trying to do it here is that you'd have to
|
||||||
|
# return either true or false from all the 'try' methods, neither of
|
||||||
|
# which is correct.
|
||||||
|
|
||||||
|
# XXX need access to the header search path and maybe default macros.
|
||||||
|
|
||||||
|
def try_cpp(self, body=None, headers=None, include_dirs=None, lang="c"):
|
||||||
|
"""Construct a source file from 'body' (a string containing lines
|
||||||
|
of C/C++ code) and 'headers' (a list of header files to include)
|
||||||
|
and run it through the preprocessor. Return true if the
|
||||||
|
preprocessor succeeded, false if there were any errors.
|
||||||
|
('body' probably isn't of much use, but what the heck.)
|
||||||
|
"""
|
||||||
|
from distutils.ccompiler import CompileError
|
||||||
|
self._check_compiler()
|
||||||
|
ok = True
|
||||||
|
try:
|
||||||
|
self._preprocess(body, headers, include_dirs, lang)
|
||||||
|
except CompileError:
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
self._clean()
|
||||||
|
return ok
|
||||||
|
|
||||||
|
def search_cpp(self, pattern, body=None, headers=None, include_dirs=None,
|
||||||
|
lang="c"):
|
||||||
|
"""Construct a source file (just like 'try_cpp()'), run it through
|
||||||
|
the preprocessor, and return true if any line of the output matches
|
||||||
|
'pattern'. 'pattern' should either be a compiled regex object or a
|
||||||
|
string containing a regex. If both 'body' and 'headers' are None,
|
||||||
|
preprocesses an empty file -- which can be useful to determine the
|
||||||
|
symbols the preprocessor and compiler set by default.
|
||||||
|
"""
|
||||||
|
self._check_compiler()
|
||||||
|
src, out = self._preprocess(body, headers, include_dirs, lang)
|
||||||
|
|
||||||
|
if isinstance(pattern, str):
|
||||||
|
pattern = re.compile(pattern)
|
||||||
|
|
||||||
|
with open(out) as file:
|
||||||
|
match = False
|
||||||
|
while True:
|
||||||
|
line = file.readline()
|
||||||
|
if line == '':
|
||||||
|
break
|
||||||
|
if pattern.search(line):
|
||||||
|
match = True
|
||||||
|
break
|
||||||
|
|
||||||
|
self._clean()
|
||||||
|
return match
|
||||||
|
|
||||||
|
def try_compile(self, body, headers=None, include_dirs=None, lang="c"):
|
||||||
|
"""Try to compile a source file built from 'body' and 'headers'.
|
||||||
|
Return true on success, false otherwise.
|
||||||
|
"""
|
||||||
|
from distutils.ccompiler import CompileError
|
||||||
|
self._check_compiler()
|
||||||
|
try:
|
||||||
|
self._compile(body, headers, include_dirs, lang)
|
||||||
|
ok = True
|
||||||
|
except CompileError:
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
log.info(ok and "success!" or "failure.")
|
||||||
|
self._clean()
|
||||||
|
return ok
|
||||||
|
|
||||||
|
def try_link(self, body, headers=None, include_dirs=None, libraries=None,
|
||||||
|
library_dirs=None, lang="c"):
|
||||||
|
"""Try to compile and link a source file, built from 'body' and
|
||||||
|
'headers', to executable form. Return true on success, false
|
||||||
|
otherwise.
|
||||||
|
"""
|
||||||
|
from distutils.ccompiler import CompileError, LinkError
|
||||||
|
self._check_compiler()
|
||||||
|
try:
|
||||||
|
self._link(body, headers, include_dirs,
|
||||||
|
libraries, library_dirs, lang)
|
||||||
|
ok = True
|
||||||
|
except (CompileError, LinkError):
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
log.info(ok and "success!" or "failure.")
|
||||||
|
self._clean()
|
||||||
|
return ok
|
||||||
|
|
||||||
|
def try_run(self, body, headers=None, include_dirs=None, libraries=None,
|
||||||
|
library_dirs=None, lang="c"):
|
||||||
|
"""Try to compile, link to an executable, and run a program
|
||||||
|
built from 'body' and 'headers'. Return true on success, false
|
||||||
|
otherwise.
|
||||||
|
"""
|
||||||
|
from distutils.ccompiler import CompileError, LinkError
|
||||||
|
self._check_compiler()
|
||||||
|
try:
|
||||||
|
src, obj, exe = self._link(body, headers, include_dirs,
|
||||||
|
libraries, library_dirs, lang)
|
||||||
|
self.spawn([exe])
|
||||||
|
ok = True
|
||||||
|
except (CompileError, LinkError, DistutilsExecError):
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
log.info(ok and "success!" or "failure.")
|
||||||
|
self._clean()
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
# -- High-level methods --------------------------------------------
|
||||||
|
# (these are the ones that are actually likely to be useful
|
||||||
|
# when implementing a real-world config command!)
|
||||||
|
|
||||||
|
def check_func(self, func, headers=None, include_dirs=None,
|
||||||
|
libraries=None, library_dirs=None, decl=0, call=0):
|
||||||
|
"""Determine if function 'func' is available by constructing a
|
||||||
|
source file that refers to 'func', and compiles and links it.
|
||||||
|
If everything succeeds, returns true; otherwise returns false.
|
||||||
|
|
||||||
|
The constructed source file starts out by including the header
|
||||||
|
files listed in 'headers'. If 'decl' is true, it then declares
|
||||||
|
'func' (as "int func()"); you probably shouldn't supply 'headers'
|
||||||
|
and set 'decl' true in the same call, or you might get errors about
|
||||||
|
a conflicting declarations for 'func'. Finally, the constructed
|
||||||
|
'main()' function either references 'func' or (if 'call' is true)
|
||||||
|
calls it. 'libraries' and 'library_dirs' are used when
|
||||||
|
linking.
|
||||||
|
"""
|
||||||
|
self._check_compiler()
|
||||||
|
body = []
|
||||||
|
if decl:
|
||||||
|
body.append("int %s ();" % func)
|
||||||
|
body.append("int main () {")
|
||||||
|
if call:
|
||||||
|
body.append(" %s();" % func)
|
||||||
|
else:
|
||||||
|
body.append(" %s;" % func)
|
||||||
|
body.append("}")
|
||||||
|
body = "\n".join(body) + "\n"
|
||||||
|
|
||||||
|
return self.try_link(body, headers, include_dirs,
|
||||||
|
libraries, library_dirs)
|
||||||
|
|
||||||
|
def check_lib(self, library, library_dirs=None, headers=None,
|
||||||
|
include_dirs=None, other_libraries=[]):
|
||||||
|
"""Determine if 'library' is available to be linked against,
|
||||||
|
without actually checking that any particular symbols are provided
|
||||||
|
by it. 'headers' will be used in constructing the source file to
|
||||||
|
be compiled, but the only effect of this is to check if all the
|
||||||
|
header files listed are available. Any libraries listed in
|
||||||
|
'other_libraries' will be included in the link, in case 'library'
|
||||||
|
has symbols that depend on other libraries.
|
||||||
|
"""
|
||||||
|
self._check_compiler()
|
||||||
|
return self.try_link("int main (void) { }", headers, include_dirs,
|
||||||
|
[library] + other_libraries, library_dirs)
|
||||||
|
|
||||||
|
def check_header(self, header, include_dirs=None, library_dirs=None,
|
||||||
|
lang="c"):
|
||||||
|
"""Determine if the system header file named by 'header_file'
|
||||||
|
exists and can be found by the preprocessor; return true if so,
|
||||||
|
false otherwise.
|
||||||
|
"""
|
||||||
|
return self.try_cpp(body="/* No body */", headers=[header],
|
||||||
|
include_dirs=include_dirs)
|
||||||
|
|
||||||
|
def dump_file(filename, head=None):
|
||||||
|
"""Dumps a file content into log.info.
|
||||||
|
|
||||||
|
If head is not None, will be dumped before the file content.
|
||||||
|
"""
|
||||||
|
if head is None:
|
||||||
|
log.info('%s', filename)
|
||||||
|
else:
|
||||||
|
log.info(head)
|
||||||
|
file = open(filename)
|
||||||
|
try:
|
||||||
|
log.info(file.read())
|
||||||
|
finally:
|
||||||
|
file.close()
|
||||||
130
env/Lib/site-packages/setuptools/_distutils/config.py
vendored
Normal file
130
env/Lib/site-packages/setuptools/_distutils/config.py
vendored
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
"""distutils.pypirc
|
||||||
|
|
||||||
|
Provides the PyPIRCCommand class, the base class for the command classes
|
||||||
|
that uses .pypirc in the distutils.command package.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from configparser import RawConfigParser
|
||||||
|
|
||||||
|
from distutils.cmd import Command
|
||||||
|
|
||||||
|
DEFAULT_PYPIRC = """\
|
||||||
|
[distutils]
|
||||||
|
index-servers =
|
||||||
|
pypi
|
||||||
|
|
||||||
|
[pypi]
|
||||||
|
username:%s
|
||||||
|
password:%s
|
||||||
|
"""
|
||||||
|
|
||||||
|
class PyPIRCCommand(Command):
|
||||||
|
"""Base command that knows how to handle the .pypirc file
|
||||||
|
"""
|
||||||
|
DEFAULT_REPOSITORY = 'https://upload.pypi.org/legacy/'
|
||||||
|
DEFAULT_REALM = 'pypi'
|
||||||
|
repository = None
|
||||||
|
realm = None
|
||||||
|
|
||||||
|
user_options = [
|
||||||
|
('repository=', 'r',
|
||||||
|
"url of repository [default: %s]" % \
|
||||||
|
DEFAULT_REPOSITORY),
|
||||||
|
('show-response', None,
|
||||||
|
'display full response text from server')]
|
||||||
|
|
||||||
|
boolean_options = ['show-response']
|
||||||
|
|
||||||
|
def _get_rc_file(self):
|
||||||
|
"""Returns rc file path."""
|
||||||
|
return os.path.join(os.path.expanduser('~'), '.pypirc')
|
||||||
|
|
||||||
|
def _store_pypirc(self, username, password):
|
||||||
|
"""Creates a default .pypirc file."""
|
||||||
|
rc = self._get_rc_file()
|
||||||
|
with os.fdopen(os.open(rc, os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f:
|
||||||
|
f.write(DEFAULT_PYPIRC % (username, password))
|
||||||
|
|
||||||
|
def _read_pypirc(self):
|
||||||
|
"""Reads the .pypirc file."""
|
||||||
|
rc = self._get_rc_file()
|
||||||
|
if os.path.exists(rc):
|
||||||
|
self.announce('Using PyPI login from %s' % rc)
|
||||||
|
repository = self.repository or self.DEFAULT_REPOSITORY
|
||||||
|
|
||||||
|
config = RawConfigParser()
|
||||||
|
config.read(rc)
|
||||||
|
sections = config.sections()
|
||||||
|
if 'distutils' in sections:
|
||||||
|
# let's get the list of servers
|
||||||
|
index_servers = config.get('distutils', 'index-servers')
|
||||||
|
_servers = [server.strip() for server in
|
||||||
|
index_servers.split('\n')
|
||||||
|
if server.strip() != '']
|
||||||
|
if _servers == []:
|
||||||
|
# nothing set, let's try to get the default pypi
|
||||||
|
if 'pypi' in sections:
|
||||||
|
_servers = ['pypi']
|
||||||
|
else:
|
||||||
|
# the file is not properly defined, returning
|
||||||
|
# an empty dict
|
||||||
|
return {}
|
||||||
|
for server in _servers:
|
||||||
|
current = {'server': server}
|
||||||
|
current['username'] = config.get(server, 'username')
|
||||||
|
|
||||||
|
# optional params
|
||||||
|
for key, default in (('repository',
|
||||||
|
self.DEFAULT_REPOSITORY),
|
||||||
|
('realm', self.DEFAULT_REALM),
|
||||||
|
('password', None)):
|
||||||
|
if config.has_option(server, key):
|
||||||
|
current[key] = config.get(server, key)
|
||||||
|
else:
|
||||||
|
current[key] = default
|
||||||
|
|
||||||
|
# work around people having "repository" for the "pypi"
|
||||||
|
# section of their config set to the HTTP (rather than
|
||||||
|
# HTTPS) URL
|
||||||
|
if (server == 'pypi' and
|
||||||
|
repository in (self.DEFAULT_REPOSITORY, 'pypi')):
|
||||||
|
current['repository'] = self.DEFAULT_REPOSITORY
|
||||||
|
return current
|
||||||
|
|
||||||
|
if (current['server'] == repository or
|
||||||
|
current['repository'] == repository):
|
||||||
|
return current
|
||||||
|
elif 'server-login' in sections:
|
||||||
|
# old format
|
||||||
|
server = 'server-login'
|
||||||
|
if config.has_option(server, 'repository'):
|
||||||
|
repository = config.get(server, 'repository')
|
||||||
|
else:
|
||||||
|
repository = self.DEFAULT_REPOSITORY
|
||||||
|
return {'username': config.get(server, 'username'),
|
||||||
|
'password': config.get(server, 'password'),
|
||||||
|
'repository': repository,
|
||||||
|
'server': server,
|
||||||
|
'realm': self.DEFAULT_REALM}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _read_pypi_response(self, response):
|
||||||
|
"""Read and decode a PyPI HTTP response."""
|
||||||
|
import cgi
|
||||||
|
content_type = response.getheader('content-type', 'text/plain')
|
||||||
|
encoding = cgi.parse_header(content_type)[1].get('charset', 'ascii')
|
||||||
|
return response.read().decode(encoding)
|
||||||
|
|
||||||
|
def initialize_options(self):
|
||||||
|
"""Initialize options."""
|
||||||
|
self.repository = None
|
||||||
|
self.realm = None
|
||||||
|
self.show_response = 0
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
"""Finalizes options."""
|
||||||
|
if self.repository is None:
|
||||||
|
self.repository = self.DEFAULT_REPOSITORY
|
||||||
|
if self.realm is None:
|
||||||
|
self.realm = self.DEFAULT_REALM
|
||||||
749
env/Lib/site-packages/setuptools/config.py
vendored
Normal file
749
env/Lib/site-packages/setuptools/config.py
vendored
Normal file
|
|
@ -0,0 +1,749 @@
|
||||||
|
import ast
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
import functools
|
||||||
|
import importlib
|
||||||
|
from collections import defaultdict
|
||||||
|
from functools import partial
|
||||||
|
from functools import wraps
|
||||||
|
from glob import iglob
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
from distutils.errors import DistutilsOptionError, DistutilsFileError
|
||||||
|
from setuptools.extern.packaging.version import LegacyVersion, parse
|
||||||
|
from setuptools.extern.packaging.specifiers import SpecifierSet
|
||||||
|
|
||||||
|
|
||||||
|
class StaticModule:
|
||||||
|
"""
|
||||||
|
Attempt to load the module by the name
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
spec = importlib.util.find_spec(name)
|
||||||
|
with open(spec.origin) as strm:
|
||||||
|
src = strm.read()
|
||||||
|
module = ast.parse(src)
|
||||||
|
vars(self).update(locals())
|
||||||
|
del self.self
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
try:
|
||||||
|
return next(
|
||||||
|
ast.literal_eval(statement.value)
|
||||||
|
for statement in self.module.body
|
||||||
|
if isinstance(statement, ast.Assign)
|
||||||
|
for target in statement.targets
|
||||||
|
if isinstance(target, ast.Name) and target.id == attr
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise AttributeError(
|
||||||
|
"{self.name} has no attribute {attr}".format(**locals())
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def patch_path(path):
|
||||||
|
"""
|
||||||
|
Add path to front of sys.path for the duration of the context.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
sys.path.insert(0, path)
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
sys.path.remove(path)
|
||||||
|
|
||||||
|
|
||||||
|
def read_configuration(filepath, find_others=False, ignore_option_errors=False):
|
||||||
|
"""Read given configuration file and returns options from it as a dict.
|
||||||
|
|
||||||
|
:param str|unicode filepath: Path to configuration file
|
||||||
|
to get options from.
|
||||||
|
|
||||||
|
:param bool find_others: Whether to search for other configuration files
|
||||||
|
which could be on in various places.
|
||||||
|
|
||||||
|
:param bool ignore_option_errors: Whether to silently ignore
|
||||||
|
options, values of which could not be resolved (e.g. due to exceptions
|
||||||
|
in directives such as file:, attr:, etc.).
|
||||||
|
If False exceptions are propagated as expected.
|
||||||
|
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
from setuptools.dist import Distribution, _Distribution
|
||||||
|
|
||||||
|
filepath = os.path.abspath(filepath)
|
||||||
|
|
||||||
|
if not os.path.isfile(filepath):
|
||||||
|
raise DistutilsFileError('Configuration file %s does not exist.' % filepath)
|
||||||
|
|
||||||
|
current_directory = os.getcwd()
|
||||||
|
os.chdir(os.path.dirname(filepath))
|
||||||
|
|
||||||
|
try:
|
||||||
|
dist = Distribution()
|
||||||
|
|
||||||
|
filenames = dist.find_config_files() if find_others else []
|
||||||
|
if filepath not in filenames:
|
||||||
|
filenames.append(filepath)
|
||||||
|
|
||||||
|
_Distribution.parse_config_files(dist, filenames=filenames)
|
||||||
|
|
||||||
|
handlers = parse_configuration(
|
||||||
|
dist, dist.command_options, ignore_option_errors=ignore_option_errors
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.chdir(current_directory)
|
||||||
|
|
||||||
|
return configuration_to_dict(handlers)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_option(target_obj, key):
|
||||||
|
"""
|
||||||
|
Given a target object and option key, get that option from
|
||||||
|
the target object, either through a get_{key} method or
|
||||||
|
from an attribute directly.
|
||||||
|
"""
|
||||||
|
getter_name = 'get_{key}'.format(**locals())
|
||||||
|
by_attribute = functools.partial(getattr, target_obj, key)
|
||||||
|
getter = getattr(target_obj, getter_name, by_attribute)
|
||||||
|
return getter()
|
||||||
|
|
||||||
|
|
||||||
|
def configuration_to_dict(handlers):
|
||||||
|
"""Returns configuration data gathered by given handlers as a dict.
|
||||||
|
|
||||||
|
:param list[ConfigHandler] handlers: Handlers list,
|
||||||
|
usually from parse_configuration()
|
||||||
|
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
config_dict = defaultdict(dict)
|
||||||
|
|
||||||
|
for handler in handlers:
|
||||||
|
for option in handler.set_options:
|
||||||
|
value = _get_option(handler.target_obj, option)
|
||||||
|
config_dict[handler.section_prefix][option] = value
|
||||||
|
|
||||||
|
return config_dict
|
||||||
|
|
||||||
|
|
||||||
|
def parse_configuration(distribution, command_options, ignore_option_errors=False):
|
||||||
|
"""Performs additional parsing of configuration options
|
||||||
|
for a distribution.
|
||||||
|
|
||||||
|
Returns a list of used option handlers.
|
||||||
|
|
||||||
|
:param Distribution distribution:
|
||||||
|
:param dict command_options:
|
||||||
|
:param bool ignore_option_errors: Whether to silently ignore
|
||||||
|
options, values of which could not be resolved (e.g. due to exceptions
|
||||||
|
in directives such as file:, attr:, etc.).
|
||||||
|
If False exceptions are propagated as expected.
|
||||||
|
:rtype: list
|
||||||
|
"""
|
||||||
|
options = ConfigOptionsHandler(distribution, command_options, ignore_option_errors)
|
||||||
|
options.parse()
|
||||||
|
|
||||||
|
meta = ConfigMetadataHandler(
|
||||||
|
distribution.metadata,
|
||||||
|
command_options,
|
||||||
|
ignore_option_errors,
|
||||||
|
distribution.package_dir,
|
||||||
|
)
|
||||||
|
meta.parse()
|
||||||
|
|
||||||
|
return meta, options
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigHandler:
|
||||||
|
"""Handles metadata supplied in configuration files."""
|
||||||
|
|
||||||
|
section_prefix = None
|
||||||
|
"""Prefix for config sections handled by this handler.
|
||||||
|
Must be provided by class heirs.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
aliases = {}
|
||||||
|
"""Options aliases.
|
||||||
|
For compatibility with various packages. E.g.: d2to1 and pbr.
|
||||||
|
Note: `-` in keys is replaced with `_` by config parser.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, target_obj, options, ignore_option_errors=False):
|
||||||
|
sections = {}
|
||||||
|
|
||||||
|
section_prefix = self.section_prefix
|
||||||
|
for section_name, section_options in options.items():
|
||||||
|
if not section_name.startswith(section_prefix):
|
||||||
|
continue
|
||||||
|
|
||||||
|
section_name = section_name.replace(section_prefix, '').strip('.')
|
||||||
|
sections[section_name] = section_options
|
||||||
|
|
||||||
|
self.ignore_option_errors = ignore_option_errors
|
||||||
|
self.target_obj = target_obj
|
||||||
|
self.sections = sections
|
||||||
|
self.set_options = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parsers(self):
|
||||||
|
"""Metadata item name to parser function mapping."""
|
||||||
|
raise NotImplementedError(
|
||||||
|
'%s must provide .parsers property' % self.__class__.__name__
|
||||||
|
)
|
||||||
|
|
||||||
|
def __setitem__(self, option_name, value):
|
||||||
|
unknown = tuple()
|
||||||
|
target_obj = self.target_obj
|
||||||
|
|
||||||
|
# Translate alias into real name.
|
||||||
|
option_name = self.aliases.get(option_name, option_name)
|
||||||
|
|
||||||
|
current_value = getattr(target_obj, option_name, unknown)
|
||||||
|
|
||||||
|
if current_value is unknown:
|
||||||
|
raise KeyError(option_name)
|
||||||
|
|
||||||
|
if current_value:
|
||||||
|
# Already inhabited. Skipping.
|
||||||
|
return
|
||||||
|
|
||||||
|
skip_option = False
|
||||||
|
parser = self.parsers.get(option_name)
|
||||||
|
if parser:
|
||||||
|
try:
|
||||||
|
value = parser(value)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
skip_option = True
|
||||||
|
if not self.ignore_option_errors:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if skip_option:
|
||||||
|
return
|
||||||
|
|
||||||
|
setter = getattr(target_obj, 'set_%s' % option_name, None)
|
||||||
|
if setter is None:
|
||||||
|
setattr(target_obj, option_name, value)
|
||||||
|
else:
|
||||||
|
setter(value)
|
||||||
|
|
||||||
|
self.set_options.append(option_name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_list(cls, value, separator=','):
|
||||||
|
"""Represents value as a list.
|
||||||
|
|
||||||
|
Value is split either by separator (defaults to comma) or by lines.
|
||||||
|
|
||||||
|
:param value:
|
||||||
|
:param separator: List items separator character.
|
||||||
|
:rtype: list
|
||||||
|
"""
|
||||||
|
if isinstance(value, list): # _get_parser_compound case
|
||||||
|
return value
|
||||||
|
|
||||||
|
if '\n' in value:
|
||||||
|
value = value.splitlines()
|
||||||
|
else:
|
||||||
|
value = value.split(separator)
|
||||||
|
|
||||||
|
return [chunk.strip() for chunk in value if chunk.strip()]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_list_glob(cls, value, separator=','):
|
||||||
|
"""Equivalent to _parse_list() but expands any glob patterns using glob().
|
||||||
|
|
||||||
|
However, unlike with glob() calls, the results remain relative paths.
|
||||||
|
|
||||||
|
:param value:
|
||||||
|
:param separator: List items separator character.
|
||||||
|
:rtype: list
|
||||||
|
"""
|
||||||
|
glob_characters = ('*', '?', '[', ']', '{', '}')
|
||||||
|
values = cls._parse_list(value, separator=separator)
|
||||||
|
expanded_values = []
|
||||||
|
for value in values:
|
||||||
|
|
||||||
|
# Has globby characters?
|
||||||
|
if any(char in value for char in glob_characters):
|
||||||
|
# then expand the glob pattern while keeping paths *relative*:
|
||||||
|
expanded_values.extend(sorted(
|
||||||
|
os.path.relpath(path, os.getcwd())
|
||||||
|
for path in iglob(os.path.abspath(value))))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# take the value as-is:
|
||||||
|
expanded_values.append(value)
|
||||||
|
|
||||||
|
return expanded_values
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_dict(cls, value):
|
||||||
|
"""Represents value as a dict.
|
||||||
|
|
||||||
|
:param value:
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
separator = '='
|
||||||
|
result = {}
|
||||||
|
for line in cls._parse_list(value):
|
||||||
|
key, sep, val = line.partition(separator)
|
||||||
|
if sep != separator:
|
||||||
|
raise DistutilsOptionError(
|
||||||
|
'Unable to parse option value to dict: %s' % value
|
||||||
|
)
|
||||||
|
result[key.strip()] = val.strip()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_bool(cls, value):
|
||||||
|
"""Represents value as boolean.
|
||||||
|
|
||||||
|
:param value:
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
value = value.lower()
|
||||||
|
return value in ('1', 'true', 'yes')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _exclude_files_parser(cls, key):
|
||||||
|
"""Returns a parser function to make sure field inputs
|
||||||
|
are not files.
|
||||||
|
|
||||||
|
Parses a value after getting the key so error messages are
|
||||||
|
more informative.
|
||||||
|
|
||||||
|
:param key:
|
||||||
|
:rtype: callable
|
||||||
|
"""
|
||||||
|
|
||||||
|
def parser(value):
|
||||||
|
exclude_directive = 'file:'
|
||||||
|
if value.startswith(exclude_directive):
|
||||||
|
raise ValueError(
|
||||||
|
'Only strings are accepted for the {0} field, '
|
||||||
|
'files are not accepted'.format(key)
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_file(cls, value):
|
||||||
|
"""Represents value as a string, allowing including text
|
||||||
|
from nearest files using `file:` directive.
|
||||||
|
|
||||||
|
Directive is sandboxed and won't reach anything outside
|
||||||
|
directory with setup.py.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
file: README.rst, CHANGELOG.md, src/file.txt
|
||||||
|
|
||||||
|
:param str value:
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
include_directive = 'file:'
|
||||||
|
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return value
|
||||||
|
|
||||||
|
if not value.startswith(include_directive):
|
||||||
|
return value
|
||||||
|
|
||||||
|
spec = value[len(include_directive) :]
|
||||||
|
filepaths = (os.path.abspath(path.strip()) for path in spec.split(','))
|
||||||
|
return '\n'.join(
|
||||||
|
cls._read_file(path)
|
||||||
|
for path in filepaths
|
||||||
|
if (cls._assert_local(path) or True) and os.path.isfile(path)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _assert_local(filepath):
|
||||||
|
if not filepath.startswith(os.getcwd()):
|
||||||
|
raise DistutilsOptionError('`file:` directive can not access %s' % filepath)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _read_file(filepath):
|
||||||
|
with io.open(filepath, encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_attr(cls, value, package_dir=None):
|
||||||
|
"""Represents value as a module attribute.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
attr: package.attr
|
||||||
|
attr: package.module.attr
|
||||||
|
|
||||||
|
:param str value:
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
attr_directive = 'attr:'
|
||||||
|
if not value.startswith(attr_directive):
|
||||||
|
return value
|
||||||
|
|
||||||
|
attrs_path = value.replace(attr_directive, '').strip().split('.')
|
||||||
|
attr_name = attrs_path.pop()
|
||||||
|
|
||||||
|
module_name = '.'.join(attrs_path)
|
||||||
|
module_name = module_name or '__init__'
|
||||||
|
|
||||||
|
parent_path = os.getcwd()
|
||||||
|
if package_dir:
|
||||||
|
if attrs_path[0] in package_dir:
|
||||||
|
# A custom path was specified for the module we want to import
|
||||||
|
custom_path = package_dir[attrs_path[0]]
|
||||||
|
parts = custom_path.rsplit('/', 1)
|
||||||
|
if len(parts) > 1:
|
||||||
|
parent_path = os.path.join(os.getcwd(), parts[0])
|
||||||
|
module_name = parts[1]
|
||||||
|
else:
|
||||||
|
module_name = custom_path
|
||||||
|
elif '' in package_dir:
|
||||||
|
# A custom parent directory was specified for all root modules
|
||||||
|
parent_path = os.path.join(os.getcwd(), package_dir[''])
|
||||||
|
|
||||||
|
with patch_path(parent_path):
|
||||||
|
try:
|
||||||
|
# attempt to load value statically
|
||||||
|
return getattr(StaticModule(module_name), attr_name)
|
||||||
|
except Exception:
|
||||||
|
# fallback to simple import
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
|
return getattr(module, attr_name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_parser_compound(cls, *parse_methods):
|
||||||
|
"""Returns parser function to represents value as a list.
|
||||||
|
|
||||||
|
Parses a value applying given methods one after another.
|
||||||
|
|
||||||
|
:param parse_methods:
|
||||||
|
:rtype: callable
|
||||||
|
"""
|
||||||
|
|
||||||
|
def parse(value):
|
||||||
|
parsed = value
|
||||||
|
|
||||||
|
for method in parse_methods:
|
||||||
|
parsed = method(parsed)
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
return parse
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_section_to_dict(cls, section_options, values_parser=None):
|
||||||
|
"""Parses section options into a dictionary.
|
||||||
|
|
||||||
|
Optionally applies a given parser to values.
|
||||||
|
|
||||||
|
:param dict section_options:
|
||||||
|
:param callable values_parser:
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
value = {}
|
||||||
|
values_parser = values_parser or (lambda val: val)
|
||||||
|
for key, (_, val) in section_options.items():
|
||||||
|
value[key] = values_parser(val)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def parse_section(self, section_options):
|
||||||
|
"""Parses configuration file section.
|
||||||
|
|
||||||
|
:param dict section_options:
|
||||||
|
"""
|
||||||
|
for (name, (_, value)) in section_options.items():
|
||||||
|
try:
|
||||||
|
self[name] = value
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
pass # Keep silent for a new option may appear anytime.
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
"""Parses configuration file items from one
|
||||||
|
or more related sections.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for section_name, section_options in self.sections.items():
|
||||||
|
|
||||||
|
method_postfix = ''
|
||||||
|
if section_name: # [section.option] variant
|
||||||
|
method_postfix = '_%s' % section_name
|
||||||
|
|
||||||
|
section_parser_method = getattr(
|
||||||
|
self,
|
||||||
|
# Dots in section names are translated into dunderscores.
|
||||||
|
('parse_section%s' % method_postfix).replace('.', '__'),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if section_parser_method is None:
|
||||||
|
raise DistutilsOptionError(
|
||||||
|
'Unsupported distribution option section: [%s.%s]'
|
||||||
|
% (self.section_prefix, section_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
section_parser_method(section_options)
|
||||||
|
|
||||||
|
def _deprecated_config_handler(self, func, msg, warning_class):
|
||||||
|
"""this function will wrap around parameters that are deprecated
|
||||||
|
|
||||||
|
:param msg: deprecation message
|
||||||
|
:param warning_class: class of warning exception to be raised
|
||||||
|
:param func: function to be wrapped around
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def config_handler(*args, **kwargs):
|
||||||
|
warnings.warn(msg, warning_class)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return config_handler
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigMetadataHandler(ConfigHandler):
|
||||||
|
|
||||||
|
section_prefix = 'metadata'
|
||||||
|
|
||||||
|
aliases = {
|
||||||
|
'home_page': 'url',
|
||||||
|
'summary': 'description',
|
||||||
|
'classifier': 'classifiers',
|
||||||
|
'platform': 'platforms',
|
||||||
|
}
|
||||||
|
|
||||||
|
strict_mode = False
|
||||||
|
"""We need to keep it loose, to be partially compatible with
|
||||||
|
`pbr` and `d2to1` packages which also uses `metadata` section.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, target_obj, options, ignore_option_errors=False, package_dir=None
|
||||||
|
):
|
||||||
|
super(ConfigMetadataHandler, self).__init__(
|
||||||
|
target_obj, options, ignore_option_errors
|
||||||
|
)
|
||||||
|
self.package_dir = package_dir
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parsers(self):
|
||||||
|
"""Metadata item name to parser function mapping."""
|
||||||
|
parse_list = self._parse_list
|
||||||
|
parse_file = self._parse_file
|
||||||
|
parse_dict = self._parse_dict
|
||||||
|
exclude_files_parser = self._exclude_files_parser
|
||||||
|
|
||||||
|
return {
|
||||||
|
'platforms': parse_list,
|
||||||
|
'keywords': parse_list,
|
||||||
|
'provides': parse_list,
|
||||||
|
'requires': self._deprecated_config_handler(
|
||||||
|
parse_list,
|
||||||
|
"The requires parameter is deprecated, please use "
|
||||||
|
"install_requires for runtime dependencies.",
|
||||||
|
DeprecationWarning,
|
||||||
|
),
|
||||||
|
'obsoletes': parse_list,
|
||||||
|
'classifiers': self._get_parser_compound(parse_file, parse_list),
|
||||||
|
'license': exclude_files_parser('license'),
|
||||||
|
'license_file': self._deprecated_config_handler(
|
||||||
|
exclude_files_parser('license_file'),
|
||||||
|
"The license_file parameter is deprecated, "
|
||||||
|
"use license_files instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
),
|
||||||
|
'license_files': parse_list,
|
||||||
|
'description': parse_file,
|
||||||
|
'long_description': parse_file,
|
||||||
|
'version': self._parse_version,
|
||||||
|
'project_urls': parse_dict,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _parse_version(self, value):
|
||||||
|
"""Parses `version` option value.
|
||||||
|
|
||||||
|
:param value:
|
||||||
|
:rtype: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
version = self._parse_file(value)
|
||||||
|
|
||||||
|
if version != value:
|
||||||
|
version = version.strip()
|
||||||
|
# Be strict about versions loaded from file because it's easy to
|
||||||
|
# accidentally include newlines and other unintended content
|
||||||
|
if isinstance(parse(version), LegacyVersion):
|
||||||
|
tmpl = (
|
||||||
|
'Version loaded from {value} does not '
|
||||||
|
'comply with PEP 440: {version}'
|
||||||
|
)
|
||||||
|
raise DistutilsOptionError(tmpl.format(**locals()))
|
||||||
|
|
||||||
|
return version
|
||||||
|
|
||||||
|
version = self._parse_attr(value, self.package_dir)
|
||||||
|
|
||||||
|
if callable(version):
|
||||||
|
version = version()
|
||||||
|
|
||||||
|
if not isinstance(version, str):
|
||||||
|
if hasattr(version, '__iter__'):
|
||||||
|
version = '.'.join(map(str, version))
|
||||||
|
else:
|
||||||
|
version = '%s' % version
|
||||||
|
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigOptionsHandler(ConfigHandler):
|
||||||
|
|
||||||
|
section_prefix = 'options'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parsers(self):
|
||||||
|
"""Metadata item name to parser function mapping."""
|
||||||
|
parse_list = self._parse_list
|
||||||
|
parse_list_semicolon = partial(self._parse_list, separator=';')
|
||||||
|
parse_bool = self._parse_bool
|
||||||
|
parse_dict = self._parse_dict
|
||||||
|
parse_cmdclass = self._parse_cmdclass
|
||||||
|
|
||||||
|
return {
|
||||||
|
'zip_safe': parse_bool,
|
||||||
|
'include_package_data': parse_bool,
|
||||||
|
'package_dir': parse_dict,
|
||||||
|
'scripts': parse_list,
|
||||||
|
'eager_resources': parse_list,
|
||||||
|
'dependency_links': parse_list,
|
||||||
|
'namespace_packages': parse_list,
|
||||||
|
'install_requires': parse_list_semicolon,
|
||||||
|
'setup_requires': parse_list_semicolon,
|
||||||
|
'tests_require': parse_list_semicolon,
|
||||||
|
'packages': self._parse_packages,
|
||||||
|
'entry_points': self._parse_file,
|
||||||
|
'py_modules': parse_list,
|
||||||
|
'python_requires': SpecifierSet,
|
||||||
|
'cmdclass': parse_cmdclass,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _parse_cmdclass(self, value):
|
||||||
|
def resolve_class(qualified_class_name):
|
||||||
|
idx = qualified_class_name.rfind('.')
|
||||||
|
class_name = qualified_class_name[idx + 1 :]
|
||||||
|
pkg_name = qualified_class_name[:idx]
|
||||||
|
|
||||||
|
module = __import__(pkg_name)
|
||||||
|
|
||||||
|
return getattr(module, class_name)
|
||||||
|
|
||||||
|
return {k: resolve_class(v) for k, v in self._parse_dict(value).items()}
|
||||||
|
|
||||||
|
def _parse_packages(self, value):
|
||||||
|
"""Parses `packages` option value.
|
||||||
|
|
||||||
|
:param value:
|
||||||
|
:rtype: list
|
||||||
|
"""
|
||||||
|
find_directives = ['find:', 'find_namespace:']
|
||||||
|
trimmed_value = value.strip()
|
||||||
|
|
||||||
|
if trimmed_value not in find_directives:
|
||||||
|
return self._parse_list(value)
|
||||||
|
|
||||||
|
findns = trimmed_value == find_directives[1]
|
||||||
|
|
||||||
|
# Read function arguments from a dedicated section.
|
||||||
|
find_kwargs = self.parse_section_packages__find(
|
||||||
|
self.sections.get('packages.find', {})
|
||||||
|
)
|
||||||
|
|
||||||
|
if findns:
|
||||||
|
from setuptools import find_namespace_packages as find_packages
|
||||||
|
else:
|
||||||
|
from setuptools import find_packages
|
||||||
|
|
||||||
|
return find_packages(**find_kwargs)
|
||||||
|
|
||||||
|
def parse_section_packages__find(self, section_options):
|
||||||
|
"""Parses `packages.find` configuration file section.
|
||||||
|
|
||||||
|
To be used in conjunction with _parse_packages().
|
||||||
|
|
||||||
|
:param dict section_options:
|
||||||
|
"""
|
||||||
|
section_data = self._parse_section_to_dict(section_options, self._parse_list)
|
||||||
|
|
||||||
|
valid_keys = ['where', 'include', 'exclude']
|
||||||
|
|
||||||
|
find_kwargs = dict(
|
||||||
|
[(k, v) for k, v in section_data.items() if k in valid_keys and v]
|
||||||
|
)
|
||||||
|
|
||||||
|
where = find_kwargs.get('where')
|
||||||
|
if where is not None:
|
||||||
|
find_kwargs['where'] = where[0] # cast list to single val
|
||||||
|
|
||||||
|
return find_kwargs
|
||||||
|
|
||||||
|
def parse_section_entry_points(self, section_options):
|
||||||
|
"""Parses `entry_points` configuration file section.
|
||||||
|
|
||||||
|
:param dict section_options:
|
||||||
|
"""
|
||||||
|
parsed = self._parse_section_to_dict(section_options, self._parse_list)
|
||||||
|
self['entry_points'] = parsed
|
||||||
|
|
||||||
|
def _parse_package_data(self, section_options):
|
||||||
|
parsed = self._parse_section_to_dict(section_options, self._parse_list)
|
||||||
|
|
||||||
|
root = parsed.get('*')
|
||||||
|
if root:
|
||||||
|
parsed[''] = root
|
||||||
|
del parsed['*']
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
def parse_section_package_data(self, section_options):
|
||||||
|
"""Parses `package_data` configuration file section.
|
||||||
|
|
||||||
|
:param dict section_options:
|
||||||
|
"""
|
||||||
|
self['package_data'] = self._parse_package_data(section_options)
|
||||||
|
|
||||||
|
def parse_section_exclude_package_data(self, section_options):
|
||||||
|
"""Parses `exclude_package_data` configuration file section.
|
||||||
|
|
||||||
|
:param dict section_options:
|
||||||
|
"""
|
||||||
|
self['exclude_package_data'] = self._parse_package_data(section_options)
|
||||||
|
|
||||||
|
def parse_section_extras_require(self, section_options):
|
||||||
|
"""Parses `extras_require` configuration file section.
|
||||||
|
|
||||||
|
:param dict section_options:
|
||||||
|
"""
|
||||||
|
parse_list = partial(self._parse_list, separator=';')
|
||||||
|
self['extras_require'] = self._parse_section_to_dict(
|
||||||
|
section_options, parse_list
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_section_data_files(self, section_options):
|
||||||
|
"""Parses `data_files` configuration file section.
|
||||||
|
|
||||||
|
:param dict section_options:
|
||||||
|
"""
|
||||||
|
parsed = self._parse_section_to_dict(section_options, self._parse_list_glob)
|
||||||
|
self['data_files'] = [(k, v) for k, v in parsed.items()]
|
||||||
BIN
env/Scripts/dotenv.exe
vendored
Normal file
BIN
env/Scripts/dotenv.exe
vendored
Normal file
Binary file not shown.
|
|
@ -1,4 +1,5 @@
|
||||||
Flask==3.0.0
|
Flask==3.0.0
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
phonenumbers==8.13.28
|
phonenumbers==8.13.28
|
||||||
|
python-dotenv
|
||||||
gunicorn
|
gunicorn
|
||||||
|
|
@ -19,6 +19,9 @@
|
||||||
<VisualStudioVersion Condition=" '$(VisualStudioVersion)' == '' ">10.0</VisualStudioVersion>
|
<VisualStudioVersion Condition=" '$(VisualStudioVersion)' == '' ">10.0</VisualStudioVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Content Include=".dockerignore" />
|
||||||
|
<Content Include=".env" />
|
||||||
|
<Content Include=".env.template" />
|
||||||
<Content Include="docker-compose.yml" />
|
<Content Include="docker-compose.yml" />
|
||||||
<Content Include="Dockerfile" />
|
<Content Include="Dockerfile" />
|
||||||
<Content Include="requirements.txt" />
|
<Content Include="requirements.txt" />
|
||||||
|
|
@ -26,7 +29,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="app.py" />
|
<Compile Include="app.py" />
|
||||||
<Compile Include="config.py" />
|
<Compile Include="config.py" />
|
||||||
<Compile Include="config.template.py" />
|
|
||||||
<Compile Include="routes\threecx.py" />
|
<Compile Include="routes\threecx.py" />
|
||||||
<Compile Include="routes\voipms.py" />
|
<Compile Include="routes\voipms.py" />
|
||||||
<Compile Include="routes\yeastar.py" />
|
<Compile Include="routes\yeastar.py" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue