"""Core service description."""
from abc import ABCMeta, abstractmethod
from collections import OrderedDict
from datetime import datetime
from datetime import timezone
import logging
from urllib.parse import urlencode
import requests
from dateutil.parser import parse
from .utils import provided_args, remove_tags, required_args
logger = logging.getLogger(__name__)
[docs]class Service(metaclass=ServiceMeta):
"""Abstract base class for services."""
ENDPOINT = None
""":py:class:`str`: The endpoint URL template for the service API."""
FRIENDLY_NAME = None
""":py:class:`str`: The friendly name of the service."""
REQUIRED = set()
""":py:class:`set`: The service's required configuration keys."""
ROOT = None
""":py:class:`str`: The root URL for the service API."""
TEMPLATE = 'undefined-section'
""":py:class:`str`: The name of the template to render."""
@abstractmethod
def __init__(self, **kwargs):
self.service_name = kwargs.get('name')
[docs] def update(self):
"""Get the current state to display on the dashboard."""
logger.debug('fetching %s project data', self.FRIENDLY_NAME)
response = requests.get(self.url, headers=self.headers)
if response.status_code == 200:
return self.format_data(response.json())
logger.error('failed to update %s project data', self.FRIENDLY_NAME)
return {}
@property
def url(self):
"""Generate the URL for the service request."""
return self.url_builder(
self.ENDPOINT,
root=getattr(self, 'root', self.ROOT),
params=self.__dict__,
url_params=self.url_params,
)
@property
def headers(self):
"""Get the headers for the service requests."""
return {}
@property
def url_params(self):
return OrderedDict()
[docs] def url_builder(self, endpoint, *, root=None, params=None, url_params=None):
"""Create a URL for the specified endpoint.
Arguments:
endpoint (:py:class:`str`): The API endpoint to access.
root: (:py:class:`str`, optional): The root URL for the
service API.
params: (:py:class:`dict`, optional): The values for format
into the created URL (defaults to ``None``).
url_params: (:py:class:`dict`, optional): Parameters to add
to the end of the URL (defaults to ``None``).
Returns:
:py:class:`str`: The resulting URL.
"""
if root is None:
root = self.ROOT
return ''.join([
root,
endpoint,
'?' + urlencode(url_params) if url_params else '',
]).format(**params or {})
[docs] @classmethod
def from_config(cls, **config):
"""Manipulate the configuration settings."""
missing = cls.REQUIRED.difference(config)
if missing:
message = 'missing required config keys ({}) from {}'
raise TypeError(message.format(
', '.join(missing),
cls.FRIENDLY_NAME or cls.__name__,
))
instance = cls(**config)
return instance
[docs] @staticmethod
def calculate_timeout(http_date):
"""Extract request timeout from e.g. ``Retry-After`` header.
Note:
Per :rfc:`2616#section-14.37`, the ``Retry-After`` header can
be either an integer number of seconds or an HTTP date. This
function can handle either.
Arguments:
http_date (:py:class:`str`): The date to parse.
Returns:
:py:class:`int`: The timeout, in seconds.
"""
try:
return int(http_date)
except ValueError:
date_after = parse(http_date)
utc_now = datetime.now(tz=timezone.utc)
return int((date_after - utc_now).total_seconds())
[docs]class ContinuousIntegrationService(Service):
"""Service subclass for common CI behaviour."""
OUTCOMES = dict()
""":py:class:`dict`: Mapping from service to Flash outcomes."""
TEMPLATE = 'ci-section'
[docs]class VersionControlService(Service):
"""Service subclass for common (D)VCS behaviour."""
TEMPLATE = 'vcs-section'
[docs]class CustomRootMixin(metaclass=MixinMeta):
"""Mix-in class for implementing custom service roots."""
ROOT = ''
def __init__(self, *, root, **kwargs):
super().__init__(**kwargs)
self.root = root
[docs] def url_builder(self, endpoint, *, root=None, params=None, url_params=None):
"""Create a URL for the specified endpoint.
Arguments:
endpoint (:py:class:`str`): The API endpoint to access.
root: (:py:class:`str`, optional): The root URL for the
service API.
params: (:py:class:`dict`, optional): The values for format
into the created URL (defaults to ``None``).
url_params: (:py:class:`dict`, optional): Parameters to add
to the end of the URL (defaults to ``None``).
Returns:
:py:class:`str`: The resulting URL.
"""
if root is None:
root = self.root
return super().url_builder(endpoint=endpoint, root=root, params=params,
url_params=url_params)
[docs]class ThresholdMixin(metaclass=MixinMeta):
"""Mix-in class for defining health thresholds.
Attributes:
NEUTRAL_THRESHOLD: The threshold beyond which the service is
considered to be in an error state.
OK_THRESHOLD: The threshold beyond which the service is
considered to be in a neutral state.
"""
NEUTRAL_THRESHOLD = None
OK_THRESHOLD = None
def __init__(self, *, ok_threshold=None, neutral_threshold=None, **kwargs):
super().__init__(**kwargs)
if ok_threshold is None:
ok_threshold = self.OK_THRESHOLD
if neutral_threshold is None:
neutral_threshold = self.NEUTRAL_THRESHOLD
self.ok_threshold = ok_threshold
self.neutral_threshold = neutral_threshold