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)
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)
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)
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))
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))
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
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))
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))
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
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')
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