def setUp(self):
        import recurly

        # Mock everything out unless we have an API key.
        try:
            api_key = os.environ['RECURLY_API_KEY']
        except KeyError:
            # Mock everything out.
            recurly.API_KEY = 'apikey'
            self.test_id = 'mock'
        else:
            recurly.API_KEY = api_key
            recurly.CA_CERTS_FILE = os.environ.get('RECURLY_CA_CERTS_FILE')
            self.mock_request = self.noop_mock_request
            self.mock_sleep = self.noop_mock_sleep
            self.test_id = datetime.now().strftime('%Y%m%d%H%M%S')

        # Update our endpoint if we have a different test host.
        try:
            recurly_host = os.environ['RECURLY_HOST']
        except KeyError:
            pass
        else:
            recurly.BASE_URI = 'https://%s/v2/' % recurly_host

        logging.basicConfig(level=logging.INFO)
        logging.getLogger('recurly').setLevel(logging.DEBUG)
示例#2
0
    def __invoice(self, url):
        # We must null out currency in subscriptions and adjustments
        # TODO we should deprecate and remove default currency support
        def filter_currency(resources):
            for resource in resources:
                resource.attributes = tuple(
                    [a for a in resource.attributes if a != 'currency'])

        try:
            filter_currency(self.adjustments)
        except AttributeError:
            pass
        try:
            filter_currency(self.subscriptions)
        except AttributeError:
            pass

        url = urljoin(recurly.base_uri(), url)
        response = self.http_request(url, 'POST', self)
        if response.status not in (200, 201):
            self.raise_http_error(response)
        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        elem = ElementTree.fromstring(response_xml)
        invoice_collection = InvoiceCollection.from_element(elem)
        return invoice_collection
 def raise_http_error(cls, response):
     """Raise a `ResponseError` of the appropriate subclass in
     reaction to the given `http_client.HTTPResponse`."""
     response_xml = response.read()
     logging.getLogger('recurly.http.response').debug(response_xml)
     exc_class = recurly.errors.error_class_for_http_status(response.status)
     raise exc_class(response_xml)
示例#4
0
        def actionator(*args, **kwargs):
            if kwargs:
                full_url = '%s?%s' % (url, urlencode_params(kwargs))
            else:
                full_url = url

            body = args[0] if args else None
            response = self.http_request(full_url, method, body)

            if response.status_code == 200:
                response_xml = response.content
                logging.getLogger('recurly.http.response').debug(response_xml)
                return self.update_from_element(
                    ElementTree.fromstring(response_xml))
            elif response.status_code == 201:
                response_xml = response.content
                logging.getLogger('recurly.http.response').debug(response_xml)
                elem = ElementTree.fromstring(response_xml)
                return self.value_for_element(elem)
            elif response.status_code == 204:
                pass
            elif extra_handler is not None:
                return extra_handler(response)
            else:
                self.raise_http_error(response)
示例#5
0
    def reopen(self):
        """Reopen a closed account."""
        url = urljoin(self._url, '/reopen')
        response = self.http_request(url, 'PUT')
        if response.status != 200:
            self.raise_http_error(response)

        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        self.update_from_element(ElementTree.fromstring(response_xml))
示例#6
0
    def mark_failed(self):
        url = urljoin(self._url, '/mark_failed')

        collection = InvoiceCollection()
        response = self.http_request(url, 'PUT')
        if response.status != 200:
            self.raise_http_error(response)
        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        collection.update_from_element(ElementTree.fromstring(response_xml))

        return collection
    def put(self, url):
        """Sends this `Resource` instance to the service with a
        ``PUT`` request to the given URL."""
        response = self.http_request(
            url, 'PUT', self,
            {'Content-Type': 'application/xml; charset=utf-8'})
        if response.status != 200:
            self.raise_http_error(response)

        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        self.update_from_element(ElementTree.fromstring(response_xml))
示例#8
0
    def build_invoice(self):
        """Preview an invoice for any outstanding adjustments this account has."""
        url = urljoin(self._url, '/invoices/preview')

        response = self.http_request(url, 'POST')
        if response.status != 200:
            self.raise_http_error(response)

        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        elem = ElementTree.fromstring(response_xml)

        invoice_collection = InvoiceCollection.from_element(elem)
        return invoice_collection
示例#9
0
    def element_for_url(cls, url):
        """Return the resource at the given URL, as a
        (`http_client.HTTPResponse`, `xml.etree.ElementTree.Element`) tuple
        resulting from a ``GET`` request to that URL."""
        response = cls.http_request(url)
        if response.status_code != 200:
            cls.raise_http_error(response)

        # assert response.headers.get('Content-Type').startswith('application/xml')

        response_xml = response.content
        logging.getLogger('recurly.http.response').debug(response_xml)
        response_doc = ElementTree.fromstring(response_xml)

        return response, response_doc
    def post(self, url, body=None):
        """Sends this `Resource` instance to the service with a
        ``POST`` request to the given URL. Takes an optional body"""
        response = self.http_request(
            url, 'POST', body or self,
            {'Content-Type': 'application/xml; charset=utf-8'})
        if response.status not in (200, 201, 204):
            self.raise_http_error(response)

        self._url = response.getheader('Location')

        if response.status in (200, 201):
            response_xml = response.read()
            logging.getLogger('recurly.http.response').debug(response_xml)
            self.update_from_element(ElementTree.fromstring(response_xml))
示例#11
0
    def update_billing_info(self, billing_info):
        """Change this account's billing information to the given `BillingInfo`."""
        url = urljoin(self._url, '/billing_info')
        response = billing_info.http_request(
            url, 'PUT', billing_info,
            {'Content-Type': 'application/xml; charset=utf-8'})
        if response.status == 200:
            pass
        elif response.status == 201:
            billing_info._url = response.getheader('Location')
        else:
            billing_info.raise_http_error(response)

        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        billing_info.update_from_element(ElementTree.fromstring(response_xml))
示例#12
0
    def invoice(self, **kwargs):
        """Create an invoice for any outstanding adjustments this account has."""
        url = urljoin(self._url, '/invoices')

        if kwargs:
            response = self.http_request(
                url, 'POST', Invoice(**kwargs),
                {'Content-Type': 'application/xml; charset=utf-8'})
        else:
            response = self.http_request(url, 'POST')

        if response.status != 201:
            self.raise_http_error(response)

        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        elem = ElementTree.fromstring(response_xml)

        invoice_collection = InvoiceCollection.from_element(elem)
        return invoice_collection
示例#13
0
def cache_rate_limit_headers(resp_headers):
    try:
        recurly.cached_rate_limits = {
            'cached_at':
            datetime.utcnow(),
            'limit':
            int(resp_headers['X-RateLimit-Limit']),
            'remaining':
            int(resp_headers['X-RateLimit-Remaining']),
            'resets_at':
            datetime.utcfromtimestamp(int(resp_headers['X-RateLimit-Reset']))
        }
    except:
        log = logging.getLogger('recurly.cached_rate_limits')
        log.info('Failed to parse rate limits from header')
示例#14
0
    def value_for_element(cls, elem):
        """Deserialize the given XML `Element` into its representative
        value.

        Depending on the content of the element, the returned value may be:
        * a string, integer, or boolean value
        * a `datetime.datetime` instance
        * a list of `Resource` instances
        * a single `Resource` instance
        * a `Money` instance
        * ``None``

        """
        log = logging.getLogger('recurly.resource')
        if elem is None:
            log.debug("Converting %r element into None value", elem)
            return

        if elem.attrib.get('nil') is not None:
            log.debug(
                "Converting %r element with nil attribute into None value",
                elem.tag)
            return

        if elem.tag.endswith(
                '_in_cents'
        ) and 'currency' not in cls.attributes and not cls.inherits_currency:
            log.debug(
                "Converting %r element in class with no matching 'currency' into a Money value",
                elem.tag)
            return Money.from_element(elem)

        attr_type = elem.attrib.get('type')
        log.debug("Converting %r element with type %r", elem.tag, attr_type)

        if attr_type == 'integer':
            return int(elem.text.strip())
        if attr_type == 'float':
            return float(elem.text.strip())
        if attr_type == 'boolean':
            return elem.text.strip() == 'true'
        if attr_type == 'datetime':
            return iso8601.parse_date(elem.text.strip())
        if attr_type == 'array':
            return [
                cls._subclass_for_nodename(sub_elem.tag).from_element(sub_elem)
                for sub_elem in elem
            ]

        # Unknown types may be the names of resource classes.
        if attr_type is not None:
            try:
                value_class = cls._subclass_for_nodename(attr_type)
            except ValueError:
                log.debug(
                    "Not converting %r element with type %r to a resource as that matches no known nodename",
                    elem.tag, attr_type)
            else:
                return value_class.from_element(elem)

        # Untyped complex elements should still be resource instances. Guess from the nodename.
        if len(elem):  # has children
            value_class = cls._subclass_for_nodename(elem.tag)
            log.debug("Converting %r tag into a %s", elem.tag,
                      value_class.__name__)
            return value_class.from_element(elem)

        value = elem.text or ''
        return value.strip()
    def http_request(cls, url, method='GET', body=None, headers=None):
        """Make an HTTP request with the given method to the given URL,
        returning the resulting `http_client.HTTPResponse` instance.

        If the `body` argument is a `Resource` instance, it is serialized
        to XML by calling its `to_element()` method before submitting it.
        Requests are authenticated per the Recurly API specification
        using the ``recurly.API_KEY`` value for the API key.

        Requests and responses are logged at the ``DEBUG`` level to the
        ``recurly.http.request`` and ``recurly.http.response`` loggers
        respectively.

        """

        if recurly.API_KEY is None:
            raise recurly.UnauthorizedError('recurly.API_KEY not set')

        url_parts = urlparse(url)
        if not any(
                url_parts.netloc.endswith(d) for d in recurly.VALID_DOMAINS):
            # TODO Exception class used for clean backport, change to
            # ConfigurationError
            raise Exception('Only a recurly domain may be called')

        is_non_ascii = lambda s: any(ord(c) >= 128 for c in s)

        if is_non_ascii(recurly.API_KEY) or is_non_ascii(recurly.SUBDOMAIN):
            raise recurly.ConfigurationError("""Setting API_KEY or SUBDOMAIN to
                    unicode strings may cause problems. Please use strings.
                    Issue described here:
                    https://gist.github.com/maximehardy/d3a0a6427d2b6791b3dc"""
                                             )

        urlparts = urlsplit(url)
        connection_options = {}
        if recurly.SOCKET_TIMEOUT_SECONDS:
            connection_options['timeout'] = recurly.SOCKET_TIMEOUT_SECONDS
        if urlparts.scheme != 'https':
            connection = http_client.HTTPConnection(urlparts.netloc,
                                                    **connection_options)
        elif recurly.CA_CERTS_FILE is None:
            connection = http_client.HTTPSConnection(urlparts.netloc,
                                                     **connection_options)
        else:
            connection_options['context'] = ssl.create_default_context(
                cafile=recurly.CA_CERTS_FILE)
            connection = http_client.HTTPSConnection(urlparts.netloc,
                                                     **connection_options)

        headers = {} if headers is None else dict(headers)
        headers.setdefault('Accept', 'application/xml')
        headers.update({'User-Agent': recurly.USER_AGENT})
        headers['X-Api-Version'] = recurly.api_version()
        headers['Authorization'] = 'Basic %s' % base64.b64encode(
            six.b('%s:' % recurly.API_KEY)).decode()

        log = logging.getLogger('recurly.http.request')
        if log.isEnabledFor(logging.DEBUG):
            log.debug("%s %s HTTP/1.1", method, url)
            for header, value in six.iteritems(headers):
                if header == 'Authorization':
                    value = '<redacted>'
                log.debug("%s: %s", header, value)
            log.debug('')
            if method in ('POST', 'PUT') and body is not None:
                if isinstance(body, Resource):
                    log.debug(body.as_log_output())
                else:
                    log.debug(body)

        if isinstance(body, Resource):
            body = ElementTree.tostring(body.to_element(), encoding='UTF-8')
            headers['Content-Type'] = 'application/xml; charset=utf-8'
        if method in ('POST', 'PUT') and body is None:
            headers['Content-Length'] = '0'
        connection.request(method, url, body, headers)
        resp = connection.getresponse()

        resp_headers = cls.headers_as_dict(resp)

        log = logging.getLogger('recurly.http.response')
        if log.isEnabledFor(logging.DEBUG):
            log.debug("HTTP/1.1 %d %s", resp.status, resp.reason)
            log.debug(resp_headers)
            log.debug('')

        recurly.cache_rate_limit_headers(resp_headers)

        return resp