#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Constant settings for Cowbird application.
Constants defined with format ``COWBIRD_[VARIABLE_NAME]`` can be matched with corresponding
settings formatted as ``cowbird.[variable_name]`` in the ``cowbird.ini`` configuration file.
.. note::
Since the ``cowbird.ini`` file has to be loaded by the application to retrieve various configuration settings,
constant ``COWBIRD_INI_FILE_PATH`` (or any other `path variable` defined before it - see below) has to be defined
by environment variable if the default location is not desired (ie: if you want to provide your own configuration).
"""
import logging
import os
import re
from typing import Optional, cast
from typing_extensions import Literal
from pyramid.settings import asbool
from pyramid.threadlocal import get_current_registry
from cowbird.typedefs import AnySettingsContainer, SettingValue
[docs]
AnyLogLevel = Literal[
"NOTSET", 0,
"DEBUG", "debug", 10,
"INFO", "info", 20,
"WARNING", "warning", 30,
"ERROR", "error", 40,
"FATAL", "fatal", 50,
"CRITICAL", "critical", 50,
]
# ===========================
# path variables
# ===========================
[docs]
COWBIRD_MODULE_DIR = os.path.abspath(os.path.dirname(__file__))
[docs]
COWBIRD_ROOT = os.path.dirname(COWBIRD_MODULE_DIR)
[docs]
COWBIRD_CONFIG_DIR = os.getenv(
"COWBIRD_CONFIG_DIR", os.path.join(COWBIRD_ROOT, "config"))
[docs]
COWBIRD_CONFIG_PATH = os.getenv("COWBIRD_CONFIG_PATH") # default None, require explicit specification
[docs]
COWBIRD_INI_FILE_PATH = os.getenv(
"COWBIRD_INI_FILE_PATH", f"{COWBIRD_CONFIG_DIR}/cowbird.ini")
# UID/GID used in birdhouse. We should assign the user's resources to this owner, so that the resources can be
# accessible to the user via JupyterHub.
[docs]
DEFAULT_USER_UID = int(os.getenv("COWBIRD_FILESYSTEM_USER_UID", 1000))
[docs]
DEFAULT_USER_GID = int(os.getenv("COWBIRD_FILESYSTEM_USER_GID", 1000))
[docs]
DEFAULT_ADMIN_UID = int(os.getenv("COWBIRD_FILESYSTEM_ADMIN_UID", 0))
[docs]
DEFAULT_ADMIN_GID = int(os.getenv("COWBIRD_FILESYSTEM_ADMIN_GID", 0))
[docs]
def _get_default_log_level() -> AnyLogLevel:
"""
Get logging level from INI configuration file or fallback to default ``INFO`` if it cannot be retrieved.
"""
_default_log_lvl = "INFO"
try:
from cowbird.utils import get_settings_from_config_ini # pylint: disable=C0415 # avoid circular import error
_settings = get_settings_from_config_ini(COWBIRD_INI_FILE_PATH, section="logger_cowbird")
_default_log_lvl = _settings.get("level", _default_log_lvl)
# also considers 'ModuleNotFoundError' derived from 'ImportError', but not added to avoid Python <3.6 name error
except (AttributeError, ImportError): # noqa: W0703 # nosec: B110
pass
return cast(AnyLogLevel, _default_log_lvl)
# ===========================
# variables from cowbird.env
# ===========================
# ---------------------------
# COWBIRD
# ---------------------------
[docs]
COWBIRD_URL = os.getenv("COWBIRD_URL", None) # must be defined
[docs]
COWBIRD_LOG_LEVEL = os.getenv("COWBIRD_LOG_LEVEL", _get_default_log_level()) # log level to apply to the loggers
[docs]
COWBIRD_LOG_PRINT = asbool(os.getenv("COWBIRD_LOG_PRINT", False)) # log also forces print to the console
[docs]
COWBIRD_LOG_REQUEST = asbool(os.getenv("COWBIRD_LOG_REQUEST", True)) # log detail of every incoming request
[docs]
COWBIRD_LOG_EXCEPTION = asbool(os.getenv("COWBIRD_LOG_EXCEPTION", True)) # log detail of generated exceptions
[docs]
COWBIRD_ADMIN_PERMISSION = "admin"
# ===========================
# constants
# ===========================
# ignore matches of settings and environment variables for following cases
[docs]
COWBIRD_CONSTANTS = [
"COWBIRD_CONSTANTS",
"COWBIRD_MODULE_DIR",
"COWBIRD_ROOT",
"COWBIRD_ADMIN_PERMISSION",
# add more as needed
]
# ===========================
# utilities
# ===========================
[docs]
_REGEX_ASCII_ONLY = re.compile(r"\W|^(?=\d)")
[docs]
_SETTING_SECTION_PREFIXES = [
"cowbird",
]
[docs]
_SETTINGS_REQUIRED = [
"COWBIRD_URL",
"COWBIRD_CONFIG_PATH",
# FIXME: add others here as needed
]
[docs]
def get_constant_setting_name(name: str) -> str:
"""
Find the equivalent setting name of the provided environment variable name.
Lower-case name and replace all non-ascii chars by `_`.
Then, convert known prefixes with their dotted name.
"""
name = re.sub(_REGEX_ASCII_ONLY, "_", name.strip().lower())
for prefix in _SETTING_SECTION_PREFIXES:
known_prefix = f"{prefix}_"
dotted_prefix = f"{prefix}."
if name.startswith(known_prefix):
return name.replace(known_prefix, dotted_prefix, 1)
return name
[docs]
def get_constant(constant_name: str,
settings_container: Optional[AnySettingsContainer] = None,
settings_name: Optional[str] = None,
default_value: Optional[SettingValue] = None,
raise_missing: bool = True,
print_missing: bool = False,
raise_not_set: bool = True
) -> SettingValue:
"""
Search in order for matched value of :paramref:`constant_name`:
1. search in :py:data:`COWBIRD_CONSTANTS`
2. search in settings if specified
3. search alternative setting names (see below)
4. search in :mod:`cowbird.constants` definitions
5. search in environment variables
Parameter :paramref:`constant_name` is expected to have the format ``COWBIRD_[VARIABLE_NAME]`` although any value
can be passed to retrieve generic settings from all above-mentioned search locations.
If :paramref:`settings_name` is provided as alternative name, it is used as is to search for results if
:paramref:`constant_name` was not found. Otherwise, ``cowbird.[variable_name]`` is used for additional search when
the format ``COWBIRD_[VARIABLE_NAME]`` was used for :paramref:`constant_name`
(i.e.: ``COWBIRD_ADMIN_USER`` will also search for ``cowbird.admin_user`` and so on for corresponding constants).
:param constant_name: key to search for a value
:param settings_container: WSGI application settings container (if not provided, uses found one in current thread)
:param settings_name: alternative name for `settings` if specified
:param default_value: default value to be returned if not found anywhere, and exception raises are disabled.
:param raise_missing: raise exception if key is not found anywhere
:param print_missing: print message if key is not found anywhere, return ``None``
:param raise_not_set: raise an exception if the found key is ``None``, search until last case if others are ``None``
:returns: found value or `default_value`
:raises ValueError: if resulting value is invalid based on options (by default raise missing/``None`` value)
:raises LookupError: if no appropriate value could be found from all search locations (according to options)
"""
from cowbird.utils import get_settings, print_log, raise_log # pylint: disable=C0415 # avoid circular import error
if constant_name in COWBIRD_CONSTANTS:
return globals()[constant_name]
missing = True
cowbird_value = None
if settings_container:
settings = get_settings(settings_container)
else:
# note: this will work only after include of cowbird will have triggered configurator setup
print_log("Using settings from local thread.", level=logging.DEBUG)
settings = get_settings(get_current_registry())
if settings and constant_name in settings: # pylint: disable=E1135
missing = False
cowbird_value = settings.get(constant_name)
if cowbird_value is not None:
print_log(f"Constant found in settings with: {constant_name}", level=logging.DEBUG)
return cowbird_value
if not settings_name:
settings_name = get_constant_setting_name(constant_name)
print_log(f"Constant alternate search: {settings_name}", level=logging.DEBUG)
if settings and settings_name and settings_name in settings: # pylint: disable=E1135
missing = False
cowbird_value = settings.get(settings_name)
if cowbird_value is not None:
print_log(f"Constant found in settings with: {settings_name}", level=logging.DEBUG)
return cowbird_value
cowbird_globals = globals()
if constant_name in cowbird_globals:
missing = False
cowbird_value = cowbird_globals.get(constant_name)
if cowbird_value is not None:
print_log(f"Constant found in definitions with: {constant_name}", level=logging.DEBUG)
return cowbird_value
if constant_name in os.environ:
missing = False
cowbird_value = os.environ.get(constant_name)
if cowbird_value is not None:
print_log(f"Constant found in environment with: {constant_name}", level=logging.DEBUG)
return cowbird_value
if not missing and raise_not_set:
raise_log(f"Constant was found but was not set: {constant_name}",
level=logging.ERROR, exception=ValueError)
if missing and raise_missing:
raise_log(f"Constant could not be found: {constant_name}",
level=logging.ERROR, exception=LookupError)
if missing and print_missing:
print_log(f"Constant could not be found: {constant_name} (using default: {default_value})", level=logging.WARN)
return cowbird_value or default_value
[docs]
def validate_required(container: AnySettingsContainer) -> None:
"""
Validates that some value is provided for every mandatory configuration setting.
:raises: when any of the requirements are missing a definition.
"""
for cfg in _SETTINGS_REQUIRED:
get_constant(cfg, settings_container=container, raise_missing=True, raise_not_set=True)