Source code for lnk.errors

#!/usr/bin/env python
#! -*- coding: utf-8 -*-

"""
Custom error classes + error handling and warning mechanisms.

All non-standard errors raised by lnk should be defined here.
"""

import click
import ecstasy
import googleapiclient.errors
import re
import requests
import sys

from collections import namedtuple

Message = namedtuple('Message', ['what', 'level'])

[docs]class Error(Exception): """ Exception base class for all errors. Implements a system of multiple verbosity levels such that error output can be controlled according to the verbosity setting specified by the user (how many -v are passed). Attributes: what (str): A plain string message specifying what happened. levels (list): A list of strings for each verbosity level. """ def __init__(self, what, **additional): self.what = what or 'Something bad happened.' #\a is the bell character (makes a 'beep' sound) additional['Error'] = Message('\a{0}'.format(self.what), 0) additional['Type'] = Message(type(self).__name__, 2) self.levels = self.get_levels(additional) super(Error, self).__init__(self.what)
[docs] def level(self, verbosity): """ Fetches a list of all non-empty levels for a given verbosity. Arguments: verbosity (int): The verbosity level (e.g. 0 for 'what'). Returns: All levels for the given verbosity, but filtering out empty ones. """ levels = self.levels[:verbosity + 1] return [i for i in levels if i]
@staticmethod
[docs] def get_levels(additional): """ Transforms and formats error levels. Each additional level passed to the constructor of Error is either the error string, in which case its level is 1 or a Error.Message tuple consisting of that same error string and additionally an integer specifying the level. This information is parsed here, such that the result is a list of level strings. Moreover, each key/value pair of each error message is formatted with ecstasy to make it look pretty. Arguments: additional (dict): The additional error messages. Returns: A list of strings for each level of verbosity. """ # Each nested list corresponds to one further level of verbosity levels = [[], [], [], []] for key, value in additional.items(): if key and value: level = 1 # default if isinstance(value, Message): level = value.level value = value.what line = '<{0}>: {1}'.format(key, value) line = ecstasy.beautify(line, ecstasy.Color.Red) levels[level].append(line) return ['\n'.join(level) if level else None for level in levels]
[docs]class HTTPError(Error): """ Raised in case of a faulty HTTP request. Usually because the data provided by the user was ill-formed (such as an expanded url where a shortened one is expected). Additionally may have a code and status information. """ def __init__(self, what, code=None, status=None, **additional): super(HTTPError, self).__init__(what, Code=code, Status=status, **additional)
[docs]class APIError(Error): """ Raised if there was a problem communicating with the API. Meaning if the problem was not ill-formed data as is the case for an HTTPError, but something specific to the API (possibly associated with its key/access-token). """ def __init__(self, what, message=None, **additional): super(APIError, self).__init__(what, Message=message, **additional)
[docs]class UsageError(Error): """ Raised for invalid command-line usage. A UsageError is raised by lnk if some cli-related badness is not handled by click (such as misisng urls). If invalid command-line usage is handled by click, this is detected and click's exception is then re-raised as a UsageError (i.e. any faulty cli input is always reported as an error of this type). """ def __init__(self, what, **additional): super(UsageError, self).__init__(what, **additional)
[docs]class InvalidKeyError(Error): """ Raised when an invalid key is passed to the config command. """ def __init__(self, what, **additional): super(InvalidKeyError, self).__init__(what, **additional)
[docs]class ConnectionError(Error): """ Raised if there was a problem connecting with the API. (Usually if the user is not connected to the interwebs). """ def __init__(self, what, hint=None, **additional): hint = hint or 'Are you connected to the webs?' super(ConnectionError, self).__init__(what, Hint=hint, **additional)
[docs]class AuthorizationError(Error): """ Raised in case of an authorization error. This is mostly the case initially, when the user first runs lnk and has noet yet authenticated himself and authorized lnk to access private information (such as user data for bitly). """ def __init__(self, service, what='Missing authorization code!', **additional): logo = ecstasy.beautify('<lnk>', ecstasy.Style.Bold) details = 'You have not yet authorized {0} to '.format(logo) details += 'access your private {0} information. '.format(service) hint = "Run 'lnk {0} key --generate'.".format(service) super(AuthorizationError, self).__init__(what, Details=details, Hint=hint, **additional)
[docs]class InternalError(Error): """ Raised when something went wrong internally. Will be thrown only within methods that are non-accessible via the API but are used forinternal features or processing. Basically get mad at the project creator. """ def __init__(self, what, **additional): """ Initializes the Error super-class. Arguments: what (str): A descriptive string regarding the cause of the error. """ super(InternalError, self).__init__(what, **additional)
[docs]class Catch(object): """ Exception-handling class. Catch is essentially a interface to its main method 'catch', but adds the ability to add a usage string that is displayed optionally in case of a UsageError, and also the service being used while the exception is thrown. The information about the url-shortening service is of use for ConnectionErrors. Attributes: verbosity (int): The verbosity level (defaults to 0). usage (str): Optionally, a usage string to display for UsageErrors. service (str): The url-shortening service currently in use. """ def __init__(self, verbosity=0, usage=None, service=None): self.verbosity = verbosity self.usage = usage self.service = '{0} '.format(service) if service else ''
[docs] def catch(self, function, *args, **kwargs): """ Executes a function and handles any potential exceptions. Arguments: function (func): The function to execute. args (variadic): The positional arguments to pass to the function call. kwargs (variadic): The keyword arguments to pass to the function call. """ try: try: function(*args, **kwargs) # Re-raise as an error we can handle (and format) except click.ClickException: error = self.get_error() raise UsageError(error.message) except googleapiclient.errors.HttpError: self.handle_google_error() except requests.exceptions.ConnectionError: error = self.get_error() raise ConnectionError('Could not establish connection ' 'to {0}server!'.format(self.service)) except click.exceptions.Abort: click.echo() # Just the newline except Error: self.handle_error()
[docs] def handle_error(self): """ Handles an Error. Takes care of retrieveing the error message string from the Error that is appropriate to the verbosity level passed to the constructor of Catch. This message string is then printed to stdout. If the Error was a UsageError, the usage string is printed too. """ error = self.get_error() click.echo('\n'.join(error.level(self.verbosity))) if isinstance(error, UsageError) and self.usage: click.echo(self.usage)
@staticmethod
[docs] def handle_google_error(): """ Handles exceptions raised by the goo.gl api client. Exceptions raised by the goo.gl api client do not make any error clean message available, but only a long, formatted string ready for output. This method takes care of retrieving the actualy 'reason' for the exception via regular expressions. This reason is then re-raised as an HTTPError that can be handled by Catch. """ error = Catch.get_error() match = re.search(r'<HttpError.+returned "([\w\s]+)">$', str(error)) what = '{0}.'.format(match.group(1)) if what == 'Required.': what = 'Invalid link.' raise HTTPError(what)
@staticmethod
[docs] def get_error(): """Retrieves the last error raised in the system.""" # type, value, traceback _, error, _ = sys.exc_info() return error
[docs]def catch(function, *args, **kwargs): """ Convenience function for a Catch object with default settings. Arguments: function (func): The function to execute. args (variadic): The positional arguments to pass to the function call. kwargs (variadic): The keyword arguments to pass to the function call. """ Catch().catch(function, *args, **kwargs)
[docs]def warn(what): """ Outputs and formats a warning. Arguments: what (str): The warning string to output. """ what = '\a<Warning>: {0}'.format(what) formatted = ecstasy.beautify(what, ecstasy.Color.Yellow) click.echo(formatted)