# -*- coding: utf-8 -*-
"""Tools for getting OS env vars."""
import logging
import os
import pathlib
from typing import List, Optional, Tuple, Union
import dotenv
LOGGER = logging.getLogger("axonius_api_client.setup_env")
"""Logger to use"""
dotenv.main.logger = LOGGER
YES: List[str] = ["1", "true", "t", "yes", "y", "on"]
"""Values that should be considered as truthy"""
NO: List[str] = ["0", "false", "f", "no", "n", "off"]
"""Values that should be considered as falsey"""
KEY_PRE: str = "AX_"
"""Prefix for axonapi related OS env vars"""
KEY_DEFAULT_PATH: str = f"{KEY_PRE}PATH"
"""OS env to use for :attr:`DEFAULT_PATH` instead of CWD"""
KEY_ENV_FILE: str = f"{KEY_PRE}ENV_FILE"
"""OS env to use for .env file name"""
KEY_ENV_PATH: str = f"{KEY_PRE}ENV"
"""OS env to use for path to '.env' file"""
KEY_OVERRIDE: str = f"{KEY_PRE}ENV_OVERRIDE"
"""OS env to control ignoring OS env when loading .env file"""
KEY_URL: str = f"{KEY_PRE}URL"
"""OS env to get API URL from"""
KEY_KEY: str = f"{KEY_PRE}KEY"
"""OS env to get API key from"""
KEY_SECRET: str = f"{KEY_PRE}SECRET"
"""OS env to get API secret from"""
KEY_FEATURES: str = f"{KEY_PRE}FEATURES"
"""OS env to get API features to enable from"""
KEY_CERTWARN: str = f"{KEY_PRE}CERTWARN"
"""OS env to get cert warning bool from"""
KEY_CERTPATH: str = f"{KEY_PRE}CERTPATH"
"""OS env to get cert warning bool from"""
KEY_DEBUG: str = f"{KEY_PRE}DEBUG"
"""OS env to enable debug logging"""
KEY_DEBUG_PRINT: str = f"{KEY_PRE}DEBUG_PRINT"
"""OS env to use print() instead of LOGGER.debug()"""
KEY_USER_AGENT: str = f"{KEY_PRE}USER_AGENT"
"""OS env to use a custom User Agent string."""
DEFAULT_DEBUG: str = "no"
"""Default for :attr:`KEY_DEBUG`"""
DEFAULT_DEBUG_PRINT: str = "no"
"""Default for :attr:`KEY_DEBUG_PRINT`"""
DEFAULT_OVERRIDE: str = "yes"
"""Default for :attr:`KEY_OVERRIDE`"""
DEFAULT_CERTWARN: str = "yes"
"""Default for :attr:`KEY_CERTWARN`"""
DEFAULT_ENV_FILE: str = ".env"
"""Default for :attr:`KEY_ENV_FILE`"""
KEYS_HIDDEN: List[str] = [KEY_KEY, KEY_SECRET]
"""List of keys to hide in :meth:`get_env_ax`"""
HIDDEN: str = "_HIDDEN_"
"""Value to use for hidden keys in :meth:`get_env_ax`"""
[docs]def find_dotenv(
ax_env: Optional[Union[str, pathlib.Path]] = None, default: str = os.getcwd()
) -> Tuple[str, str]:
"""Find a .env file.
Args:
ax_env: manual path to look for .env file
default: default path to use if :attr:`KEY_DEFAULT_PATH` is not set
Notes:
Order of operations:
* Check for ax_env for .env (or dir with .env in it)
* Check for OS env var :attr:`KEY_ENV_PATH` for .env (or dir with .env in it)
* Check for OS env var :attr:`KEY_DEFAULT_PATH` as dir with .env in it
* use dotenv.find_dotenv() to walk tree from CWD
* use dotenv.find_dotenv() to walk tree from package root
"""
env_file = get_env_str(key=KEY_ENV_FILE, default=DEFAULT_ENV_FILE)
if ax_env:
found_env = pathlib.Path(ax_env).expanduser().resolve()
found_env = found_env / env_file if found_env.is_dir() else found_env
if found_env.is_file():
return "supplied", str(found_env)
found_env = get_env_path(key=KEY_ENV_PATH, get_dir=False)
if found_env and found_env.exists():
found_env = found_env / env_file if found_env.is_dir() else found_env
if found_env.is_file():
return "env_path", str(found_env)
found_env = get_env_path(key=KEY_DEFAULT_PATH, default=default)
if found_env and found_env.exists():
found_env = found_env / env_file if found_env.is_dir() else found_env
if found_env.is_file():
return "default_path", str(found_env)
found_env = dotenv.find_dotenv(filename=env_file, usecwd=True) or ""
if found_env and pathlib.Path(found_env).is_file():
return "find_dotenv_cwd", found_env
found_env = dotenv.find_dotenv(filename=env_file, usecwd=False) or ""
if found_env and pathlib.Path(found_env).is_file():
return "find_dotenv_pkg", found_env
return "not_found", ""
[docs]def load_dotenv(ax_env: Optional[Union[str, pathlib.Path]] = None, **kwargs) -> str:
"""Load a '.env' file as environment variables accessible to this package.
Args:
ax_env: path to .env file to load, if directory will look for '.env' in that directory
**kwargs: passed to dotenv.load_dotenv()
"""
src, ax_env = find_dotenv(ax_env=ax_env)
override = get_env_bool(key=KEY_OVERRIDE, default=DEFAULT_OVERRIDE)
DEBUG_LOG(f"Loading .env with override {override} from {src!r} {str(ax_env)!r}")
if pathlib.Path(ax_env).is_file():
DEBUG_LOG(f"{KEY_PRE}.* env vars before load dotenv: {get_env_ax()}")
dotenv.load_dotenv(dotenv_path=ax_env, verbose=DEBUG, override=override)
DEBUG_LOG(f"{KEY_PRE}.* env vars after load dotenv: {get_env_ax()}")
return ax_env
[docs]def get_env_bool(key: str, default: Optional[bool] = None) -> bool:
"""Get an OS env var and turn convert it to a boolean.
Args:
key: OS env key
default: default to use if not found
Raises:
:exc:`ValueError`: OS env var value is not able to be converted to bool
"""
value = get_env_str(key=key, default=default, lower=True)
if value in YES:
return True
if value in NO:
return False
msg = [
f"Supplied value {value!r} for OS environment variable {key!r} must be one of:",
f" For true: {', '.join(YES)}",
f" For false: {', '.join(NO)}",
]
raise ValueError("\n".join(msg))
[docs]def get_env_str(
key: str, default: Optional[str] = None, empty_ok: bool = False, lower: bool = False
) -> str:
"""Get an OS env var.
Args:
key: OS env key
default: default to use if not found
empty_ok: dont throw an exc if the key's value is empty
lower: lowercase the value
Raises:
:exc:`ValueError`: OS env var value is empty and empty_ok is False
"""
orig_value = os.environ.get(key, "").strip()
value = orig_value
if default is not None and value in [None, ""]:
value = default
if not empty_ok and value in [None, ""]:
raise ValueError(
f"OS environment variable {key!r} is empty with value {orig_value!r}\n"
f"Must specify {key!r} in .env file or in OS environment variable"
)
value = value.lower() if lower and isinstance(value, str) else value
return value
[docs]def get_env_path(
key: str, default: Optional[str] = None, get_dir: bool = True
) -> Union[pathlib.Path, str]:
"""Get a path from an OS env var.
Args:
key: OS env var to get path from
default: default path to use if OS env var not set
get_dir: return directory containing file of path is file
"""
value = get_env_str(key=key, default=default, empty_ok=True)
if value:
value = pathlib.Path(value).expanduser().resolve()
if get_dir and value.is_file():
value = value.parent
return value or ""
[docs]def get_env_csv(
key: str, default: Optional[str] = None, empty_ok: bool = False, lower: bool = False
) -> List[str]:
"""Get an OS env var as a CSV.
Args:
key: OS env key
default: default to use if not found
empty_ok: dont throw an exc if the key's value is empty
lower: lowercase the value
"""
value = get_env_str(key=key, default=default, empty_ok=empty_ok, lower=lower)
value = [y for y in [x.strip() for x in value.split(",")] if y]
return value
[docs]def get_env_user_agent(**kwargs) -> str:
"""Pass."""
load_dotenv(**kwargs)
return get_env_str(key=KEY_USER_AGENT, default="", empty_ok=True)
[docs]def get_env_connect(**kwargs) -> dict:
"""Get URL, API key, API secret, and certwarn from OS env vars.
Args:
**kwargs: passed to :meth:`load_dotenv`
"""
load_dotenv(**kwargs)
return {
"url": get_env_str(key=KEY_URL),
"key": get_env_str(key=KEY_KEY),
"secret": get_env_str(key=KEY_SECRET),
"certwarn": get_env_bool(key=KEY_CERTWARN, default=DEFAULT_CERTWARN),
}
[docs]def get_env_features(**kwargs) -> List[str]:
"""Get list of features to enable from OS env vars.
Args:
**kwargs: passed to :meth:`load_dotenv`
"""
load_dotenv(**kwargs)
value = get_env_csv(key=KEY_FEATURES, default="", empty_ok=True, lower=True)
return value
[docs]def get_env_ax():
"""Get all axonapi related OS env vars."""
value = {k: v for k, v in os.environ.items() if k.startswith(KEY_PRE)}
value = {k: HIDDEN if k in KEYS_HIDDEN else v for k, v in value.items()}
return value
[docs]def set_env(key: str, value: str, **kwargs) -> Tuple[str, Tuple[bool, str, str]]:
"""Set an environment variable in .env file."""
from . import INIT_DOTENV as ax_env
return dotenv.set_key(dotenv_path=ax_env, key_to_set=key, value_to_set=str(value))
DEBUG_PRINT: bool = get_env_bool(key=KEY_DEBUG_PRINT, default=DEFAULT_DEBUG_PRINT)
"""Use print() instead of LOGGER.debug()."""
DEBUG_USE = print if DEBUG_PRINT else LOGGER.debug
"""use print or LOGGER.debug()"""
DEBUG: bool = get_env_bool(key=KEY_DEBUG, default=DEFAULT_DEBUG)
"""Enable package wide debugging."""
DEBUG_LOG = DEBUG_USE if DEBUG else lambda x: x
"""Function to use for debug logging"""
DEFAULT_PATH: str = str(get_env_path(key=KEY_DEFAULT_PATH, default=os.getcwd()))
"""Default path to use throughout this package"""