"""A library level configuration for githubcap."""
import contextlib
import logging
import os
import typing
import yaml
import attr
from .exceptions import ConfigNotFound
from .exceptions import ConfigurationError
_LOG = logging.getLogger(__name__)
_CONFIGURATION_FILE_HEADER = """# Configuration file for githubcap in YAML language.
#
# It is *NOT* recommended to store password in a plain text - please use
# a token that is preferred authentication method, which also works with
# two factor authentication.
#
# Refer to githubcap documentation for more configuration info:
# https://githubcap.readthedocs.org/en/latest/configuration.html
#
---
"""
[docs]class ConfigurationDefaults: # pylint: disable=too-few-public-methods
"""Default values for configuration."""
CONFIG_FILE_PATH = os.path.join(os.getenv('HOME'), '.config', 'githubcap', 'config.yaml')
HEADERS = {}
USER = None
PASSWORD = None
TOKEN = None
PER_PAGE_LISTING = 100
GITHUB_API = os.getenv('GITHUB_API', 'https://api.github.com')
OMIT_RATE_LIMITING = False
PAGINATION = True
VALIDATE_SCHEMAS = True
@attr.s(slots=True)
class _ConfigurationSingleton(object):
"""A library level singleton for storing configuration options."""
# It's ok not to have factories here, this is a singleton.
headers = attr.ib(default=ConfigurationDefaults.HEADERS, type=dict)
user = attr.ib(default=ConfigurationDefaults.USER, type=str)
password = attr.ib(default=ConfigurationDefaults.PASSWORD, type=str)
token = attr.ib(default=ConfigurationDefaults.TOKEN, type=str)
per_page_listing = attr.ib(default=ConfigurationDefaults.PER_PAGE_LISTING, type=int)
github_api = attr.ib(default=ConfigurationDefaults.GITHUB_API, type=str)
omit_rate_limiting = attr.ib(default=ConfigurationDefaults.OMIT_RATE_LIMITING, type=bool)
pagination = attr.ib(default=ConfigurationDefaults.PAGINATION, type=bool)
validate_schemas = attr.ib(default=ConfigurationDefaults.VALIDATE_SCHEMAS, type=bool)
@per_page_listing.validator
def per_page_listing_validator(self, _, value): # pylint: disable=no-self-use
"""Validate supplied per page configuration option."""
if not 1 <= value <= 100:
raise ConfigurationError("Page listing has to be between 1 and 100.")
@contextlib.contextmanager
def temporary_change(self, **adjusted_options): # pylint: disable=no-self-use
"""Temporary change configuration options - old configuration options are yield.
>>> from githubcap import Configuration
>>> from githubcap.resources import IssueHandler
>>> with Configuration().temporary_change(pagination=10, validate_schemas=False):
>>> IssueHandler.by_number(organization='selinon', project='selinon', number=1)
"""
option_backup = {}
config = Configuration()
for option, value in adjusted_options.items():
option_backup[option] = getattr(config, option)
setattr(Configuration(), option, value)
yield option_backup
for option, value in option_backup.items():
setattr(config, option, value)
@classmethod
def from_config_file(cls, config_file_path: typing.Optional[str] = None) -> None:
"""Initialize configuration from a configuration file (YAML format)."""
config_file_path = config_file_path or ConfigurationDefaults.CONFIG_FILE_PATH
try:
with open(config_file_path) as config_file:
configuration = yaml.safe_load(config_file)
except FileNotFoundError as exc:
raise ConfigNotFound("No configuration present in {!s}".format(config_file_path)) from exc
except Exception as exc:
raise ConfigurationError("Unable to open configuration: {!s}".format(str(exc))) from exc
try:
instance = cls(**configuration)
_LOG.debug("Configuration successfully loaded from file %r", config_file_path)
return instance
except TypeError as exc:
raise ConfigurationError("Unknown configuration option: {!s}".format(str(exc))) from exc
except Exception as exc:
raise ConfigurationError("Failed to initialize configuration: {!s}".format(str(exc))) from exc
def to_dict(self) -> dict:
"""Represent configuration in a dict."""
return attr.asdict(self)
def write2file(self, file_path: typing.Optional[str] = None, overwrite: typing.Optional[bool] = False) -> None:
"""Write configuration to a YAML file."""
file_path = file_path or ConfigurationDefaults.CONFIG_FILE_PATH
if not file_path.endswith(('.yaml', '.yml')):
file_path += '.yaml'
if os.path.isfile(file_path) and not overwrite:
raise ConfigurationError("Configuration file already present (overwrite flag was not set)")
# Create directory structure first.
dir_name = os.path.dirname(file_path)
os.makedirs(dir_name, exist_ok=True)
with open(file_path, 'w') as config_file:
config_file.write(_CONFIGURATION_FILE_HEADER)
yaml.dump(self.to_dict(), config_file)
_LOG.info("Configuration file written to %r", file_path)
@classmethod
def get_configuration(cls, config_file_path: typing.Optional[str] = None):
"""Get configuration instance (used only in :class:githubcap.configuration.Configuration)."""
try:
return cls.from_config_file(config_file_path)
except ConfigNotFound as exc:
if config_file_path is not None:
raise
_LOG.debug("Fallback to default configuration: %s", exc)
return cls()
[docs]class Configuration(object): # pylint: disable=too-few-public-methods
"""A library level configuration."""
_instance = None
def __init__(self, config_file=None, **kwargs):
"""Initialize configuration if not done so already."""
if Configuration._instance is None:
Configuration._instance = _ConfigurationSingleton.get_configuration(config_file)
elif config_file is not None:
# Prevent from potentially weird behaviour.
raise ConfigurationError("Configuration already instantiated, initialize configuration from a "
"custom file before first configuration access.")
for key, value in kwargs.items():
setattr(Configuration._instance, key, value)
def __str__(self):
"""Represent Configuration as a string - wraps singleton."""
return str(Configuration._instance)
def __repr__(self):
"""Represent Configuration - wraps singleton."""
return repr(Configuration._instance)
def __getattr__(self, item):
"""Override access so items are retrieved from singleton."""
if item == 'instance':
return self._instance
try:
return getattr(Configuration._instance, item)
except AttributeError as exc:
raise ConfigurationError("Unknown configuration option '{!s}'".format(item)) from exc
def __setattr__(self, key, value):
"""Override assigning items so items assigned in singleton."""
if key == '_instance':
return super().__setattr__(key, value)
try:
return setattr(Configuration._instance, key, value)
except AttributeError as exc:
raise ConfigurationError("Unknown configuration option '{!s}'".format(key)) from exc