appngizer.client — appngizer 1.14.0 documentation

# Source code for appngizer.client

    # -*- coding: utf-8 -*-
    '''

        This module contains network clients which are used to communicate
        with an appNGizer instance.

        Currently there is only an implementation of a :class:`XMLClient`.

    '''
    import logging
    import requests
    import re
    import urllib, urlparse

    from lxml import etree
    from lxml.html import soupparser

    import appngizer.errors

    log = logging.getLogger(__name__)

    class Singleton(type):
        '''
            Singleton class pattern to be used as metaclass for the :class:`Client`.
        '''
        _instances = {}
        def __call__(self, *args, **kwargs):
            if self not in self._instances:
                self._instances[self] = super(Singleton, self).__call__(*args, **kwargs)
            else:
                if len(args) > 0:
                    if not self._instances[self].base_url.startswith(args[0]):
                        del self._instances[self]
                        self._instances[self] = super(Singleton, self).__call__(*args, **kwargs)
            return self._instances[self]

    class Client(object):
        '''
            Abstract class of an appNGizer client
            All further appNGizer clients will inherits from this class
        '''
        __metaclass__ = Singleton

        # : regex to match a valid http|s:// url
        REGEX_URL = re.compile(
            r'^(?:http)s?://'  # http:// or https://
            r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # domain...
            r'localhost|'  # localhost...
            r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'  # ...or ip
            r'(?::\d+)?'  # optional port
            r'(?:/?|[/?]\S+)$', re.IGNORECASE)

        def __init__(self, url, sharedsecret):
            '''
            :param str url: url to an appNGizer instance
            :param str sharedsecret: sharedsecret to be used
            '''
            self.base_url = self.validate_url(url)
            self.net = ClientNetwork()
            self.content_type = 'text/plain'
            self.sharedsecret = sharedsecret
            self.authenticated = self._auth()
            self.response = None
            self.response_transf = None

        def _auth(self):
            '''Authenticates against an appNGizer instance

            :return: bool (True if authenticated)
            '''
            self.net.session.headers.update({'Content-Type': 'text/plain'})
            self.content_type = 'text/plain'
            response = self.net.request('POST', self.base_url, data=self.sharedsecret)
            self.content_type = 'application/xml'
            self.net.session.headers.update({'Content-Type': self.content_type})
            self._process_response(response)
            return True
        def _process_response(self, response=None, exception=None):
            '''Process received response

            :return: bool (True if processed)
            '''
            self._check_response(response)
            self.response = response
            if response.text != '':
                self._transform_response()
            return True
        def _check_response(self, response):
            '''Checks response

            :param requests.Response response: response object
            :return: bool (True if valid, Falise if not valid)
            '''
            response_ct = response.headers.get('Content-Type')
            if not response.ok:
                if response.status_code == 400:
                    raise appngizer.errors.HttpClientBadRequest('400 - Bad request ({})'.format(response.url))
                if response.status_code == 409:
                    raise appngizer.errors.HttpElementConflict('409 - Conflict ({})'.format(response.url))
                if response.status_code == 403:
                    raise appngizer.errors.HttpElementForbidden('403 - Forbidden ({})'.format(response.url))
                if response.status_code == 404:
                    raise appngizer.errors.HttpElementNotFound('404 - Not found ({})'.format(response.url))
                if response.status_code == 500:
                    # try to get exception message from html error page if exist
                    if response.text:
                        html_error = soupparser.fromstring(response.text)
                        pre_childs = html_error.xpath('//pre[contains(text(),"Exception")]')
                        pre_texts = []
                        for pre_text in pre_childs:
                            pre_texts.append(pre_text.text)
                        raise appngizer.errors.HttpServerError('500 - Server error ({}): {}'.format(response.url, ' '.join(pre_texts)))
                    else:
                        raise appngizer.errors.HttpServerError('500 - Server error ({})'.format(response.url))
                else:
                    raise appngizer.errors.ClientError(response.raise_for_status())
            else:
                if self.content_type != response_ct:
                    if response.status_code == 204 and response.request.method == 'DELETE':
                        return True
                    if response.status_code == 200 and response_ct == None:
                        return True
                    else:
                        raise appngizer.errors.ClientError('Unexpected response Content-Type: {0}'.format(response_ct))
            return True
        def _transform_response(self):
            '''Transforms and set response attribute
            '''
            self.response_transf = self.response

        def request(self, method, path, pdata=None):
            '''Sends request to appNGizer instance

            :param str method: HTTP method
            :param str path: url to appNGizer instance
            :param str pdata: data to send
            :return: :class:`Client` object
            '''
            url = self.validate_url(self.base_url + path)
            response = self.net.request(method, url, data=pdata)
            self._process_response(response)
            return self
        def validate_url(self, url):
            '''Validates an url

            :param str url:
            :return: url as string if valide
            '''
            if not url.endswith('/'):
                url = url + '/'
            # TODO: deactivated because this currently conflicts with url encoding
            # if self.REGEX_URL.match(url):
            #    return url
            # else:
            #    raise appngizer.errors.ClientError('Invalid url: {0}'.format(url))
            return url

    class XMLClient(Client):
        '''
            appNGizer XML Client class
        '''
        def __init__(self, url, sharedsecret):
            '''
            :param str url: url to an appNGizer instance
            :param str sharedsecret: sharedsecret to be used
            '''
            self.base_url = self.validate_url(url)
            self.net = ClientNetwork()
            self.content_type = 'application/xml'
            self.sharedsecret = sharedsecret
            self.authenticated = self._auth()
            self.response = None
            self.response_transf = None

        def _transform_response(self):
            '''Transforms and set response attribute
            '''
            if self.response.status_code == 204:
                self.response_transf = self.response.content
            else:
                self.response_transf = etree.fromstring(self.response.content)

    class ClientNetwork(object):
        '''
            appNGizer ClientNetwork class
        '''
        def __init__(self):
            self.session = requests.Session()
        def __del__(self):
            self.session.close()

        def _send_request(self, method, url, *args, **kwargs):
            '''Sends request to an appNGizer instance

            :param str method: HTTP method
            :param str path: url to appNGizer instance
            :param list *args: additional args for :class:`requests.Request` object
            :param dict *kwargs: additional kwargs for :class:`requests.Request`
            :return: :class:`requests.Response` object
            '''
            log.debug('Sending %s request to %s. args: %r, kwargs: %r', method, url, args, kwargs)
            kwargs.setdefault('headers', {})
            response = self.session.request(method, url, *args, **kwargs)
            log.debug('Received %s. Headers: %s.', response, response.headers)
            log.debug('Content: %r', response.content)
            return response

        def request(self, method, url, *args, **kwargs):
            '''Sends request to an appNGizer instance

            :param str method: HTTP method
            :param str path: url to appNGizer instance
            :param list args: additional args for :class:`requests.Request` object
            :param dict kwargs: additional kwargs for :class:`requests.Request`
            :return: :class:`requests.Response` object
            '''
            response = self._send_request(method, url, *args, **kwargs)
            return response