From c2a339aaa43169c389a5a537e8fa1a96718bed44 Mon Sep 17 00:00:00 2001 From: Douglas Miller Date: Fri, 5 Aug 2022 14:39:02 -0500 Subject: [PATCH] Adding black and applying results --- recurly/__init__.py | 1867 ++++++++++++++++--------------- recurly/errors.py | 82 +- recurly/link_header.py | 34 +- recurly/recurly_logging.py | 11 +- recurly/resource.py | 270 +++-- requirements-test.txt | 3 +- scripts/check-deps | 13 - scripts/install-deps | 22 - scripts/test | 9 + setup.py | 51 +- tests/recurlytests.py | 79 +- tests/test_recurly.py | 33 +- tests/test_resources.py | 2167 +++++++++++++++++++----------------- tests/tests_errors.py | 71 +- 14 files changed, 2497 insertions(+), 2215 deletions(-) delete mode 100755 scripts/check-deps delete mode 100755 scripts/install-deps diff --git a/recurly/__init__.py b/recurly/__init__.py index 9c1e4325..d7b12986 100644 --- a/recurly/__init__.py +++ b/recurly/__init__.py @@ -22,199 +22,217 @@ """ -__version__ = '2.9.29' -__python_version__ = '.'.join(map(str, sys.version_info[:3])) +__version__ = "2.9.29" +__python_version__ = ".".join(map(str, sys.version_info[:3])) cached_rate_limits = { - 'limit': None, - 'remaining': None, - 'resets_at': None, - 'cached_at': None, - } + "limit": None, + "remaining": None, + "resets_at": None, + "cached_at": None, +} -VALID_DOMAINS = ('.recurly.com',) +VALID_DOMAINS = (".recurly.com",) """A tuple of whitelisted domains that this client can connect to.""" -USER_AGENT = 'recurly-python/%s; python %s; %s' % (recurly.__version__, recurly.__python_version__, recurly.resource.ssl.OPENSSL_VERSION) +USER_AGENT = "recurly-python/%s; python %s; %s" % ( + recurly.__version__, + recurly.__python_version__, + recurly.resource.ssl.OPENSSL_VERSION, +) -BASE_URI = 'https://%s.recurly.com/v2/' +BASE_URI = "https://%s.recurly.com/v2/" """The API endpoint to send requests to.""" -SUBDOMAIN = 'api' +SUBDOMAIN = "api" """The subdomain of the site authenticating API requests.""" API_KEY = None """The API key to use when authenticating API requests.""" -API_VERSION = '2.29' +API_VERSION = "2.29" """The API version to use when making API requests.""" CA_CERTS_FILE = None """A file contianing a set of concatenated certificate authority certs for validating the server against.""" -DEFAULT_CURRENCY = 'USD' +DEFAULT_CURRENCY = "USD" """The currency to use creating `Money` instances when one is not specified.""" SOCKET_TIMEOUT_SECONDS = None """The number of seconds after which to timeout requests to the Recurly API. If unspecified, the global default timeout is used.""" + def urljoin(url1, url2): - if url1.endswith('/'): + if url1.endswith("/"): url1 = url1[0:-1] - if not url2.startswith('/'): - url2 = '/' + url2 + if not url2.startswith("/"): + url2 = "/" + url2 return url1 + url2 + def base_uri(): if SUBDOMAIN is None: - raise ValueError('recurly.SUBDOMAIN not set') + raise ValueError("recurly.SUBDOMAIN not set") return BASE_URI % SUBDOMAIN + def api_version(): return API_VERSION + 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'])) - } + "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') + log = logging.getLogger("recurly.cached_rate_limits") + log.info("Failed to parse rate limits from header") + class Address(Resource): - nodename = 'address' + nodename = "address" attributes = ( - 'first_name', - 'last_name', - 'name_on_account', - 'company', - 'address1', - 'address2', - 'city', - 'state', - 'zip', - 'country', - 'phone', + "first_name", + "last_name", + "name_on_account", + "company", + "address1", + "address2", + "city", + "state", + "zip", + "country", + "phone", ) + class AccountBalance(Resource): """The Balance on an account""" - nodename = 'account_balance' + nodename = "account_balance" attributes = ( - 'balance_in_cents', - 'processing_prepayment_balance_in_cents', - 'past_due', + "balance_in_cents", + "processing_prepayment_balance_in_cents", + "past_due", ) + class AccountAcquisition(Resource): """Account acquisition data https://dev.recurly.com/docs/create-account-acquisition""" - nodename = 'account_acquisition' + nodename = "account_acquisition" attributes = ( - 'cost_in_cents', - 'currency', - 'channel', - 'subchannel', - 'campaign', - 'created_at', - 'updated_at', + "cost_in_cents", + "currency", + "channel", + "subchannel", + "campaign", + "created_at", + "updated_at", ) + class CustomField(Resource): """A field to store extra data on the account or subscription.""" - nodename = 'custom_field' + nodename = "custom_field" attributes = ( - 'name', - 'value', + "name", + "value", ) def to_element(self, root_name=None): # Include the field name/value pair when the value changed - if 'value' in self.__dict__: + if "value" in self.__dict__: try: - self.name = self.name # forces name into __dict__ + self.name = self.name # forces name into __dict__ except AttributeError: pass return super(CustomField, self).to_element(root_name) + class Account(Resource): """A customer account.""" - member_path = 'accounts/%s' - collection_path = 'accounts' + member_path = "accounts/%s" + collection_path = "accounts" - nodename = 'account' + nodename = "account" attributes = ( - 'account_code', - 'parent_account_code', - 'username', - 'email', - 'first_name', - 'last_name', - 'company_name', - 'vat_number', - 'tax_exempt', - 'exemption_certificate', - 'entity_use_code', - 'accept_language', - 'cc_emails', - 'account_balance', - 'created_at', - 'updated_at', - 'shipping_addresses', - 'account_acquisition', - 'has_live_subscription', - 'has_active_subscription', - 'has_future_subscription', - 'has_canceled_subscription', - 'has_paused_subscription', - 'has_past_due_invoice', - 'preferred_locale', - 'custom_fields', - 'transaction_type', - 'dunning_campaign_id', - 'invoice_template', - 'invoice_template_uuid', + "account_code", + "parent_account_code", + "username", + "email", + "first_name", + "last_name", + "company_name", + "vat_number", + "tax_exempt", + "exemption_certificate", + "entity_use_code", + "accept_language", + "cc_emails", + "account_balance", + "created_at", + "updated_at", + "shipping_addresses", + "account_acquisition", + "has_live_subscription", + "has_active_subscription", + "has_future_subscription", + "has_canceled_subscription", + "has_paused_subscription", + "has_past_due_invoice", + "preferred_locale", + "custom_fields", + "transaction_type", + "dunning_campaign_id", + "invoice_template", + "invoice_template_uuid", ) - _classes_for_nodename = { 'address': Address, 'custom_field': CustomField } + _classes_for_nodename = {"address": Address, "custom_field": CustomField} - sensitive_attributes = ('number', 'verification_value',) + sensitive_attributes = ( + "number", + "verification_value", + ) def to_element(self, root_name=None): elem = super(Account, self).to_element(root_name) # Make sure the account code is always included in a serialization. - if 'account_code' not in self.__dict__: # not already included + if "account_code" not in self.__dict__: # not already included try: account_code = self.account_code except AttributeError: pass else: - elem.append(self.element_for_value('account_code', account_code)) - if 'billing_info' in self.__dict__: + elem.append(self.element_for_value("account_code", account_code)) + if "billing_info" in self.__dict__: elem.append(self.billing_info.to_element()) - if 'address' in self.__dict__: + if "address" in self.__dict__: elem.append(self.address.to_element()) return elem @@ -226,7 +244,7 @@ def all_active(cls, **kwargs): This is a convenience method for `Account.all(state='active')`. """ - return cls.all(state='active', **kwargs) + return cls.all(state="active", **kwargs) @classmethod def all_closed(cls, **kwargs): @@ -235,7 +253,7 @@ def all_closed(cls, **kwargs): This is a convenience method for `Account.all(state='closed')`. """ - return cls.all(state='closed', **kwargs) + return cls.all(state="closed", **kwargs) @classmethod def all_past_due(cls, **kwargs): @@ -244,7 +262,7 @@ def all_past_due(cls, **kwargs): This is a convenience method for `Account.all(state='past_due'). """ - return cls.all(state='past_due', **kwargs) + return cls.all(state="past_due", **kwargs) @classmethod def all_subscribers(cls, **kwargs): @@ -253,7 +271,7 @@ def all_subscribers(cls, **kwargs): This is a convenience method for `Account.all(state='subscriber'). """ - return cls.all(state='subscriber', **kwargs) + return cls.all(state="subscriber", **kwargs) @classmethod def all_non_subscribers(cls, **kwargs): @@ -262,12 +280,12 @@ def all_non_subscribers(cls, **kwargs): This is a convenience method for `Account.all(state='non_subscriber'). """ - return cls.all(state='non_subscriber', **kwargs) + return cls.all(state="non_subscriber", **kwargs) def __getattr__(self, name): - if name == 'billing_info': + if name == "billing_info": try: - billing_info_url = self._elem.find('billing_info').attrib['href'] + billing_info_url = self._elem.find("billing_info").attrib["href"] except (AttributeError, KeyError): raise AttributeError(name) resp, elem = BillingInfo.element_for_url(billing_info_url) @@ -275,32 +293,36 @@ def __getattr__(self, name): try: return super(Account, self).__getattr__(name) except AttributeError: - if name == 'address': + if name == "address": self.address = Address() return self.address else: - raise AttributeError(name) + raise AttributeError(name) def charge(self, charge): """Charge (or credit) this account with the given `Adjustment`.""" - url = urljoin(self._url, '/adjustments') + url = urljoin(self._url, "/adjustments") return charge.post(url) def invoice(self, **kwargs): """Create an invoice for any outstanding adjustments this account has.""" - url = urljoin(self._url, '/invoices') + url = urljoin(self._url, "/invoices") if kwargs: - response = self.http_request(url, 'POST', Invoice(**kwargs), {'content-type': - 'application/xml; charset=utf-8'}) + response = self.http_request( + url, + "POST", + Invoice(**kwargs), + {"content-type": "application/xml; charset=utf-8"}, + ) else: - response = self.http_request(url, 'POST') + 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) + logging.getLogger("recurly.http.response").debug(response_xml) elem = ElementTree.fromstring(response_xml) invoice_collection = InvoiceCollection.from_element(elem) @@ -308,14 +330,14 @@ def invoice(self, **kwargs): def build_invoice(self): """Preview an invoice for any outstanding adjustments this account has.""" - url = urljoin(self._url, '/invoices/preview') + url = urljoin(self._url, "/invoices/preview") - response = self.http_request(url, 'POST') + 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) + logging.getLogger("recurly.http.response").debug(response_xml) elem = ElementTree.fromstring(response_xml) invoice_collection = InvoiceCollection.from_element(elem) @@ -323,354 +345,373 @@ def build_invoice(self): def notes(self): """Fetch Notes for this account.""" - url = urljoin(self._url, '/notes') + url = urljoin(self._url, "/notes") return Note.paginated(url) def redemption(self): - try: - return self.redemptions()[0] - except AttributeError: - raise AttributeError("redemption") + try: + return self.redemptions()[0] + except AttributeError: + raise AttributeError("redemption") def reopen(self): """Reopen a closed account.""" - url = urljoin(self._url, '/reopen') - response = self.http_request(url, 'PUT') + 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) + logging.getLogger("recurly.http.response").debug(response_xml) self.update_from_element(ElementTree.fromstring(response_xml)) def subscribe(self, subscription): """Create the given `Subscription` for this existing account.""" - url = urljoin(self._url, '/subscriptions') + url = urljoin(self._url, "/subscriptions") return subscription.post(url) # Verifies an account's billing_info # If billing_info does not exist, will result in NotFoundError - def verify(self, gateway_code = None): - url = urljoin(self._url, '/billing_info/verify') - if gateway_code: - elem = ElementTreeBuilder.Element('verify') - elem.append(Resource.element_for_value('gateway_code', gateway_code)) - body = ElementTree.tostring(elem, encoding='UTF-8') - response = self.http_request(url, 'POST', body, {'content-type':'application/xml; charset=utf-8'}) - else: - 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) - return Transaction.from_element(elem) + def verify(self, gateway_code=None): + url = urljoin(self._url, "/billing_info/verify") + if gateway_code: + elem = ElementTreeBuilder.Element("verify") + elem.append(Resource.element_for_value("gateway_code", gateway_code)) + body = ElementTree.tostring(elem, encoding="UTF-8") + response = self.http_request( + url, "POST", body, {"content-type": "application/xml; charset=utf-8"} + ) + else: + 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) + return Transaction.from_element(elem) def update_billing_info(self, billing_info): """Change this account's billing information to the given `BillingInfo`.""" # billing_info._url is only present when the site is using the wallet feature key = "_url" if key in billing_info.__dict__: - url = urljoin(self._url, '/billing_infos/{}'.format(billing_info.uuid)) + url = urljoin(self._url, "/billing_infos/{}".format(billing_info.uuid)) else: - url = urljoin(self._url, '/billing_info') - response = billing_info.http_request(url, 'PUT', billing_info, - {'content-type': 'application/xml; charset=utf-8'}) + 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') + 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) + logging.getLogger("recurly.http.response").debug(response_xml) billing_info.update_from_element(ElementTree.fromstring(response_xml)) def create_billing_info(self, billing_info): - """Create billing info to include in account's wallet.""" - url = urljoin(self._url, '/billing_infos') - return billing_info.post(url) + """Create billing info to include in account's wallet.""" + url = urljoin(self._url, "/billing_infos") + return billing_info.post(url) def get_billing_infos(self): - """Fetch all billing infos in an account's wallet.""" - url = urljoin(self._url, '/billing_infos') - return BillingInfo.paginated(url) + """Fetch all billing infos in an account's wallet.""" + url = urljoin(self._url, "/billing_infos") + return BillingInfo.paginated(url) def get_billing_info(self, billing_info_uuid): - """Fetch a billing info from account's wallet.""" - url = urljoin(self._url, '/billing_infos/{}'.format(billing_info_uuid)) - resp, elem = BillingInfo.element_for_url(url) - return BillingInfo.from_element(elem) + """Fetch a billing info from account's wallet.""" + url = urljoin(self._url, "/billing_infos/{}".format(billing_info_uuid)) + resp, elem = BillingInfo.element_for_url(url) + return BillingInfo.from_element(elem) def create_shipping_address(self, shipping_address): """Creates a shipping address on an existing account. If you are creating an account, you can embed the shipping addresses with the request""" - url = urljoin(self._url, '/shipping_addresses') + url = urljoin(self._url, "/shipping_addresses") return shipping_address.post(url) + class BillingInfoFraudInfo(recurly.Resource): - node_name = 'fraud' + node_name = "fraud" attributes = ( - 'score', - 'decision', + "score", + "decision", ) + class BillingInfo(Resource): """A set of billing information for an account.""" - nodename = 'billing_info' + nodename = "billing_info" attributes = ( - 'type', - 'name_on_account', - 'first_name', - 'last_name', - 'mandate_reference', - 'number', - 'verification_value', - 'year', - 'month', - 'start_month', - 'start_year', - 'issue_number', - 'company', - 'address1', - 'address2', - 'city', - 'state', - 'zip', - 'country', - 'phone', - 'vat_number', - 'ip_address', - 'ip_address_country', - 'card_type', - 'first_six', - 'last_four', - 'paypal_billing_agreement_id', - 'amazon_billing_agreement_id', - 'amazon_region', - 'token_id', - 'account_type', - 'routing_number', - 'account_number', - 'currency', - 'updated_at', - 'external_hpp_type', - 'gateway_token', - 'gateway_code', - 'three_d_secure_action_result_token_id', - 'transaction_type', - 'iban', - 'sort_code', - 'bsb_code', - 'tax_identifier', - 'tax_identifier_type', - 'fraud', - 'primary_payment_method', - 'backup_payment_method', - 'online_banking_payment_type' + "type", + "name_on_account", + "first_name", + "last_name", + "mandate_reference", + "number", + "verification_value", + "year", + "month", + "start_month", + "start_year", + "issue_number", + "company", + "address1", + "address2", + "city", + "state", + "zip", + "country", + "phone", + "vat_number", + "ip_address", + "ip_address_country", + "card_type", + "first_six", + "last_four", + "paypal_billing_agreement_id", + "amazon_billing_agreement_id", + "amazon_region", + "token_id", + "account_type", + "routing_number", + "account_number", + "currency", + "updated_at", + "external_hpp_type", + "gateway_token", + "gateway_code", + "three_d_secure_action_result_token_id", + "transaction_type", + "iban", + "sort_code", + "bsb_code", + "tax_identifier", + "tax_identifier_type", + "fraud", + "primary_payment_method", + "backup_payment_method", + "online_banking_payment_type", ) - sensitive_attributes = ('number', 'verification_value', 'account_number', 'iban') - xml_attribute_attributes = ('type',) + sensitive_attributes = ("number", "verification_value", "account_number", "iban") + xml_attribute_attributes = ("type",) _classes_for_nodename = { - 'fraud': BillingInfoFraudInfo, + "fraud": BillingInfoFraudInfo, } # Allows user to call verify() on billing_info object # References Account#verify - def verify(self, account_code, gateway_code = None): - recurly.Account.get(account_code).verify(gateway_code) + def verify(self, account_code, gateway_code=None): + recurly.Account.get(account_code).verify(gateway_code) + class ShippingAddress(Resource): """Shipping Address information""" - nodename = 'shipping_address' + nodename = "shipping_address" attributes = ( - 'address1', - 'address2', - 'city', - 'company', - 'country', - 'email', - 'first_name', - 'id', - 'last_name', - 'nickname', - 'phone', - 'state', - 'vat_number', - 'zip', + "address1", + "address2", + "city", + "company", + "country", + "email", + "first_name", + "id", + "last_name", + "nickname", + "phone", + "state", + "vat_number", + "zip", ) + class Delivery(Resource): """Delivery information for use with a Gift Card""" - nodename = 'delivery' + nodename = "delivery" attributes = ( - 'address', - 'deliver_at', - 'email_address', - 'first_name', - 'gifter_name', - 'last_name', - 'method', - 'personal_message', + "address", + "deliver_at", + "email_address", + "first_name", + "gifter_name", + "last_name", + "method", + "personal_message", ) + class DunningInterval(Resource): """Dunning interval""" - nodename = 'interval' + nodename = "interval" attributes = ( - 'days', - 'email_template', + "days", + "email_template", ) + class DunningCampaign(Resource): """A dunning campaign available on the site""" - member_path = 'dunning_campaigns/%s' - collection_path = 'dunning_campaigns' + member_path = "dunning_campaigns/%s" + collection_path = "dunning_campaigns" - nodename = 'dunning_campaign' + nodename = "dunning_campaign" attributes = ( - 'id', - 'code', - 'name', - 'description', - 'default_campaign', - 'dunning_cycles', - 'created_at', - 'updated_at', - 'deleted_at', + "id", + "code", + "name", + "description", + "default_campaign", + "dunning_cycles", + "created_at", + "updated_at", + "deleted_at", ) _classes_for_nodename = { - 'interval': DunningInterval, + "interval": DunningInterval, } def bulk_update(self, id, plan_codes): """Update each plan's `dunning_campaign_id`.""" - url = urljoin(base_uri(), self.member_path % (id) + '/bulk_update') + url = urljoin(base_uri(), self.member_path % (id) + "/bulk_update") elem = ElementTreeBuilder.Element(self.nodename) - plan_codes_elem = ElementTreeBuilder.Element('plan_codes') + plan_codes_elem = ElementTreeBuilder.Element("plan_codes") elem.append(plan_codes_elem) for plan_code in plan_codes: - plan_codes_elem.append(Resource.element_for_value('plan_code', plan_code)) + plan_codes_elem.append(Resource.element_for_value("plan_code", plan_code)) + + body = ElementTree.tostring(elem, encoding="UTF-8") - body = ElementTree.tostring(elem, encoding='UTF-8') + self.http_request( + url, "PUT", body, {"content-type": "application/xml; charset=utf-8"} + ) - self.http_request(url, 'PUT', body, { 'content-type': - 'application/xml; charset=utf-8' }) class DunningCycle(Resource): """A dunning cycle associated to a dunning campaign.""" - nodename = 'dunning_cycle' + nodename = "dunning_cycle" attributes = ( - 'type', - 'applies_to_manual_trial', - 'first_communication_interval', - 'send_immediately_on_hard_decline', - 'intervals', - 'expire_subscription', - 'fail_invoice', - 'total_dunning_days', - 'total_recycling_days', - 'version', - 'created_at', - 'updated_at', + "type", + "applies_to_manual_trial", + "first_communication_interval", + "send_immediately_on_hard_decline", + "intervals", + "expire_subscription", + "fail_invoice", + "total_dunning_days", + "total_recycling_days", + "version", + "created_at", + "updated_at", ) + class InvoiceTemplate(Resource): """An invoice template available on the site""" - member_path = 'invoice_templates/%s' - collection_path = 'invoice_templates' + member_path = "invoice_templates/%s" + collection_path = "invoice_templates" - nodename = 'invoice_template' + nodename = "invoice_template" attributes = ( - 'uuid', - 'code', - 'name', - 'description', - 'created_at', - 'updated_at', - 'accounts', + "uuid", + "code", + "name", + "description", + "created_at", + "updated_at", + "accounts", ) + # This is used internally for proper XML generation class _RecipientAccount(Account): - nodename = 'recipient_account' + nodename = "recipient_account" + class GiftCard(Resource): """A Gift Card for a customer to purchase or apply to a subscription or account.""" - member_path= 'gift_cards/%s' - collection_path = 'gift_cards' + member_path = "gift_cards/%s" + collection_path = "gift_cards" - nodename = 'gift_card' + nodename = "gift_card" attributes = ( - 'balance_in_cents', - 'canceled_at', - 'created_at', - 'currency', - 'delivery', - 'gifter_account', - 'id', - 'invoice', - 'product_code', - 'recipient_account', - 'redeemed_at', - 'redemption_code', - 'updated_at', - 'unit_amount_in_cents', - 'billing_info', + "balance_in_cents", + "canceled_at", + "created_at", + "currency", + "delivery", + "gifter_account", + "id", + "invoice", + "product_code", + "recipient_account", + "redeemed_at", + "redemption_code", + "updated_at", + "unit_amount_in_cents", + "billing_info", ) - _classes_for_nodename = {'recipient_account': Account,'gifter_account': - Account, 'delivery': Delivery} + _classes_for_nodename = { + "recipient_account": Account, + "gifter_account": Account, + "delivery": Delivery, + } def preview(self): """Preview the purchase of this gift card""" - if hasattr(self, '_url'): - url = self._url + '/preview' + if hasattr(self, "_url"): + url = self._url + "/preview" return self.post(url) else: - url = urljoin(recurly.base_uri(), self.collection_path + '/preview') + url = urljoin(recurly.base_uri(), self.collection_path + "/preview") return self.post(url) def redeem(self, account_code): """Redeem this gift card on the specified account code""" - redemption_path = '%s/redeem' % (self.redemption_code) + redemption_path = "%s/redeem" % (self.redemption_code) - if hasattr(self, '_url'): - url = urljoin(self._url, '/redeem') + if hasattr(self, "_url"): + url = urljoin(self._url, "/redeem") else: - url = urljoin(recurly.base_uri(), self.collection_path + '/' + redemption_path) + url = urljoin( + recurly.base_uri(), self.collection_path + "/" + redemption_path + ) recipient_account = _RecipientAccount(account_code=account_code) return self.post(url, recipient_account) @@ -679,77 +720,81 @@ def to_element(self, root_name=None): elem = super(GiftCard, self).to_element(root_name) # Make sure the redemption code is always included in a serialization. - if 'redemption_code' not in self.__dict__: # not already included + if "redemption_code" not in self.__dict__: # not already included try: redemption_code = self.redemption_code except AttributeError: pass else: - elem.append(self.element_for_value('redemption_code', - redemption_code)) + elem.append(self.element_for_value("redemption_code", redemption_code)) return elem + class Coupon(Resource): """A coupon for a customer to apply to their account.""" - member_path = 'coupons/%s' - collection_path = 'coupons' + member_path = "coupons/%s" + collection_path = "coupons" - nodename = 'coupon' + nodename = "coupon" attributes = ( - 'coupon_code', - 'name', - 'discount_type', - 'discount_percent', - 'discount_in_cents', - 'redeem_by_date', - 'invoice_description', - 'single_use', - 'applies_for_months', - 'duration', - 'temporal_unit', - 'temporal_amount', - 'max_redemptions', - 'applies_to_all_plans', - 'applies_to_all_items', - 'applies_to_non_plan_charges', - 'redemption_resource', - 'created_at', - 'updated_at', - 'deleted_at', - 'plan_codes', - 'item_codes', - 'hosted_description', - 'max_redemptions_per_account', - 'coupon_type', - 'unique_code_template', - 'unique_coupon_codes', - 'free_trial_unit', - 'free_trial_amount', - 'id', + "coupon_code", + "name", + "discount_type", + "discount_percent", + "discount_in_cents", + "redeem_by_date", + "invoice_description", + "single_use", + "applies_for_months", + "duration", + "temporal_unit", + "temporal_amount", + "max_redemptions", + "applies_to_all_plans", + "applies_to_all_items", + "applies_to_non_plan_charges", + "redemption_resource", + "created_at", + "updated_at", + "deleted_at", + "plan_codes", + "item_codes", + "hosted_description", + "max_redemptions_per_account", + "coupon_type", + "unique_code_template", + "unique_coupon_codes", + "free_trial_unit", + "free_trial_amount", + "id", ) @classmethod def value_for_element(cls, elem): - excludes = ['plan_codes', 'item_codes'] - if elem is None or elem.tag not in excludes or elem.attrib.get('type') != 'array': + excludes = ["plan_codes", "item_codes"] + if ( + elem is None + or elem.tag not in excludes + or elem.attrib.get("type") != "array" + ): return super(Coupon, cls).value_for_element(elem) return [code_elem.text for code_elem in elem] @classmethod def element_for_value(cls, attrname, value): - if attrname != 'plan_codes' and attrname != 'item_codes': + if attrname != "plan_codes" and attrname != "item_codes": return super(Coupon, cls).element_for_value(attrname, value) elem = ElementTreeBuilder.Element(attrname) - elem.attrib['type'] = 'array' + elem.attrib["type"] = "array" for code in value: - # create element from singular version of attrname - code_el = ElementTreeBuilder.Element(attrname[0 : -1]) + # create element from singular version of attrname + code_el = ElementTreeBuilder.Element(attrname[0:-1]) code_el.text = code elem.append(code_el) @@ -762,7 +807,7 @@ def all_redeemable(cls, **kwargs): This is a convenience method for `Coupon.all(state='redeemable')`. """ - return cls.all(state='redeemable', **kwargs) + return cls.all(state="redeemable", **kwargs) @classmethod def all_expired(cls, **kwargs): @@ -771,7 +816,7 @@ def all_expired(cls, **kwargs): This is a convenience method for `Coupon.all(state='expired')`. """ - return cls.all(state='expired', **kwargs) + return cls.all(state="expired", **kwargs) @classmethod def all_maxed_out(cls, **kwargs): @@ -781,129 +826,137 @@ def all_maxed_out(cls, **kwargs): This is a convenience method for `Coupon.all(state='maxed_out')`. """ - return cls.all(state='maxed_out', **kwargs) + return cls.all(state="maxed_out", **kwargs) def has_unlimited_redemptions_per_account(self): return self.max_redemptions_per_account == None def generate(self, amount): elem = ElementTreeBuilder.Element(self.nodename) - elem.append(Resource.element_for_value('number_of_unique_codes', amount)) + elem.append(Resource.element_for_value("number_of_unique_codes", amount)) - url = urljoin(self._url, '/generate') - body = ElementTree.tostring(elem, encoding='UTF-8') + url = urljoin(self._url, "/generate") + body = ElementTree.tostring(elem, encoding="UTF-8") - response = self.http_request(url, 'POST', body, { 'content-type': - 'application/xml; charset=utf-8' }) + response = self.http_request( + url, "POST", body, {"content-type": "application/xml; charset=utf-8"} + ) if response.status not in (200, 201, 204): self.raise_http_error(response) - return Page.page_for_url(response.getheader('location')) + return Page.page_for_url(response.getheader("location")) def restore(self): - url = urljoin(self._url, '/restore') + url = urljoin(self._url, "/restore") self.put(url) + class Redemption(Resource): """A particular application of a coupon to a customer account.""" - nodename = 'redemption' + nodename = "redemption" attributes = ( - 'account_code', - 'single_use', - 'total_discounted_in_cents', - 'subscription_uuid', - 'currency', - 'created_at', - 'updated_at', + "account_code", + "single_use", + "total_discounted_in_cents", + "subscription_uuid", + "currency", + "created_at", + "updated_at", ) + class TaxDetail(Resource): """A charge's tax breakdown""" - nodename = 'tax_detail' + nodename = "tax_detail" inherits_currency = True attributes = ( - 'name', - 'type', - 'level', - 'billable', - 'tax_rate', - 'tax_in_cents', - 'tax_type', - 'tax_region' + "name", + "type", + "level", + "billable", + "tax_rate", + "tax_in_cents", + "tax_type", + "tax_region", ) + class Item(Resource): """An item for a customer to apply to their account.""" - member_path = 'items/%s' - collection_path = 'items' - nodename = 'item' + member_path = "items/%s" + collection_path = "items" + nodename = "item" attributes = ( - 'item_code', - 'name', - 'description', - 'external_sku', - 'accounting_code', - 'revenue_schedule_type', - 'state', - 'created_at', - 'updated_at', - 'deleted_at', + "item_code", + "name", + "description", + "external_sku", + "accounting_code", + "revenue_schedule_type", + "state", + "created_at", + "updated_at", + "deleted_at", ) + class Adjustment(Resource): """A charge or credit applied (or to be applied) to an account's invoice.""" - nodename = 'adjustment' - member_path = 'adjustments/%s' + nodename = "adjustment" + member_path = "adjustments/%s" attributes = ( - 'uuid', - 'description', - 'accounting_code', - 'product_code', - 'item_code', - 'external_sku', - 'quantity', - 'unit_amount_in_cents', - 'discount_in_cents', - 'tax_in_cents', - 'tax_type', - 'tax_region', - 'tax_rate', - 'total_in_cents', - 'currency', - 'tax_exempt', - 'tax_inclusive', - 'tax_code', - 'tax_details', - 'start_date', - 'end_date', - 'created_at', - 'updated_at', - 'type', - 'revenue_schedule_type', - 'shipping_address', - 'shipping_address_id', + "uuid", + "description", + "accounting_code", + "product_code", + "item_code", + "external_sku", + "quantity", + "unit_amount_in_cents", + "discount_in_cents", + "tax_in_cents", + "tax_type", + "tax_region", + "tax_rate", + "total_in_cents", + "currency", + "tax_exempt", + "tax_inclusive", + "tax_code", + "tax_details", + "start_date", + "end_date", + "created_at", + "updated_at", + "type", + "revenue_schedule_type", + "shipping_address", + "shipping_address_id", ) - xml_attribute_attributes = ('type',) - _classes_for_nodename = {'tax_detail': TaxDetail, 'shipping_address': ShippingAddress} + xml_attribute_attributes = ("type",) + _classes_for_nodename = { + "tax_detail": TaxDetail, + "shipping_address": ShippingAddress, + } # This can be removed when the `original_adjustment_uuid` is moved to a link def __getattr__(self, name): - if name == 'original_adjustment': + if name == "original_adjustment": try: - uuid = super(Adjustment, self).__getattr__('original_adjustment_uuid') + uuid = super(Adjustment, self).__getattr__("original_adjustment_uuid") except (AttributeError): return super(Adjustment, self).__getattr__(name) @@ -911,61 +964,60 @@ def __getattr__(self, name): else: return super(Adjustment, self).__getattr__(name) + class Invoice(Resource): """A payable charge to an account for the customer's charges and subscriptions.""" - member_path = 'invoices/%s' - collection_path = 'invoices' + member_path = "invoices/%s" + collection_path = "invoices" - nodename = 'invoice' + nodename = "invoice" attributes = ( - 'uuid', - 'state', - 'invoice_number', - 'invoice_number_prefix', - 'po_number', - 'vat_number', - 'tax_in_cents', - 'tax_type', - 'tax_rate', - 'total_in_cents', - 'currency', - 'created_at', - 'updated_at', - 'line_items', - 'transactions', - 'terms_and_conditions', - 'customer_notes', - 'vat_reverse_charge_notes', # Only shows if reverse charge invoice - 'address', - 'closed_at', - 'collection_method', - 'net_terms', - 'attempt_next_collection_at', - 'recovery_reason', - 'balance_in_cents', - 'subtotal_before_discount_in_cents', - 'subtotal_in_cents', - 'discount_in_cents', - 'due_on', - 'type', - 'origin', - 'credit_customer_notes', - 'gateway_code', - 'billing_info', - 'billing_info_uuid', - 'dunning_campaign_id', + "uuid", + "state", + "invoice_number", + "invoice_number_prefix", + "po_number", + "vat_number", + "tax_in_cents", + "tax_type", + "tax_rate", + "total_in_cents", + "currency", + "created_at", + "updated_at", + "line_items", + "transactions", + "terms_and_conditions", + "customer_notes", + "vat_reverse_charge_notes", # Only shows if reverse charge invoice + "address", + "closed_at", + "collection_method", + "net_terms", + "attempt_next_collection_at", + "recovery_reason", + "balance_in_cents", + "subtotal_before_discount_in_cents", + "subtotal_in_cents", + "discount_in_cents", + "due_on", + "type", + "origin", + "credit_customer_notes", + "gateway_code", + "billing_info", + "billing_info_uuid", + "dunning_campaign_id", ) - blacklist_attributes = ( - 'currency', - ) + blacklist_attributes = ("currency",) def invoice_number_with_prefix(self): - return '%s%s' % (self.invoice_number_prefix, self.invoice_number) + return "%s%s" % (self.invoice_number_prefix, self.invoice_number) @classmethod def all_pending(cls, **kwargs): @@ -974,7 +1026,7 @@ def all_pending(cls, **kwargs): This is a convenience method for `Invoice.all(state='pending')`. """ - return cls.all(state='pending', **kwargs) + return cls.all(state="pending", **kwargs) @classmethod def all_paid(cls, **kwargs): @@ -983,7 +1035,7 @@ def all_paid(cls, **kwargs): This is a convenience method for `Invoice.all(state='paid')`. """ - return cls.all(state='paid', **kwargs) + return cls.all(state="paid", **kwargs) @classmethod def all_failed(cls, **kwargs): @@ -992,7 +1044,7 @@ def all_failed(cls, **kwargs): This is a convenience method for `Invoice.all(state='failed')`. """ - return cls.all(state='failed', **kwargs) + return cls.all(state="failed", **kwargs) @classmethod def all_past_due(cls, **kwargs): @@ -1001,7 +1053,7 @@ def all_past_due(cls, **kwargs): This is a convenience method for `Invoice.all(state='past_due')`. """ - return cls.all(state='past_due', **kwargs) + return cls.all(state="past_due", **kwargs) @classmethod def pdf(cls, uuid): @@ -1015,43 +1067,40 @@ def pdf(cls, uuid): """ url = urljoin(base_uri(), cls.member_path % (uuid,)) - pdf_response = cls.http_request(url, headers={'accept': 'application/pdf'}) + pdf_response = cls.http_request(url, headers={"accept": "application/pdf"}) return pdf_response.read() - def refund_amount(self, amount_in_cents, refund_options = {}): + def refund_amount(self, amount_in_cents, refund_options={}): # For backwards compatibility # TODO the consequent branch of this conditional should eventually be removed # and we should document that as a breaking change in the changelog. # The same change should be applied to the refund() method - if (isinstance(refund_options, six.string_types)): - refund_options = { 'refund_method': refund_options } + if isinstance(refund_options, six.string_types): + refund_options = {"refund_method": refund_options} else: - if 'refund_method' not in refund_options: - refund_options = { 'refund_method': 'credit_first' } + if "refund_method" not in refund_options: + refund_options = {"refund_method": "credit_first"} - amount_element = self._refund_open_amount_xml(amount_in_cents, - refund_options) + amount_element = self._refund_open_amount_xml(amount_in_cents, refund_options) return self._create_refund_invoice(amount_element) - def refund(self, adjustments, refund_options = {}): - # For backwards compatibility - # TODO the consequent branch of this conditional should eventually be removed - # and we should document that as a breaking change in the changelog. - # The same change should be applied to the refund_amount() method - if (isinstance(refund_options, six.string_types)): - refund_options = { 'refund_method': refund_options } - else: - if 'refund_method' not in refund_options: - refund_options = { 'refund_method': 'credit_first' } - - adjustments_element = self._refund_line_items_xml(adjustments, - refund_options) - return self._create_refund_invoice(adjustments_element) + def refund(self, adjustments, refund_options={}): + # For backwards compatibility + # TODO the consequent branch of this conditional should eventually be removed + # and we should document that as a breaking change in the changelog. + # The same change should be applied to the refund_amount() method + if isinstance(refund_options, six.string_types): + refund_options = {"refund_method": refund_options} + else: + if "refund_method" not in refund_options: + refund_options = {"refund_method": "credit_first"} + + adjustments_element = self._refund_line_items_xml(adjustments, refund_options) + return self._create_refund_invoice(adjustments_element) def _refund_open_amount_xml(self, amount_in_cents, refund_options): elem = ElementTreeBuilder.Element(self.nodename) - elem.append(Resource.element_for_value('amount_in_cents', - amount_in_cents)) + elem.append(Resource.element_for_value("amount_in_cents", amount_in_cents)) # Need to sort the keys for tests to pass in python 2 and 3 # Can remove `sorted` when we drop python 2 support @@ -1062,15 +1111,13 @@ def _refund_open_amount_xml(self, amount_in_cents, refund_options): def _refund_line_items_xml(self, line_items, refund_options): elem = ElementTreeBuilder.Element(self.nodename) - line_items_elem = ElementTreeBuilder.Element('line_items') + line_items_elem = ElementTreeBuilder.Element("line_items") for item in line_items: - adj_elem = ElementTreeBuilder.Element('adjustment') - adj_elem.append(Resource.element_for_value('uuid', - item['adjustment'].uuid)) - adj_elem.append(Resource.element_for_value('quantity', - item['quantity'])) - adj_elem.append(Resource.element_for_value('prorate', item['prorate'])) + adj_elem = ElementTreeBuilder.Element("adjustment") + adj_elem.append(Resource.element_for_value("uuid", item["adjustment"].uuid)) + adj_elem.append(Resource.element_for_value("quantity", item["quantity"])) + adj_elem.append(Resource.element_for_value("prorate", item["prorate"])) line_items_elem.append(adj_elem) elem.append(line_items_elem) @@ -1083,21 +1130,21 @@ def _refund_line_items_xml(self, line_items, refund_options): return elem def mark_failed(self): - url = urljoin(self._url, '/mark_failed') + url = urljoin(self._url, "/mark_failed") collection = InvoiceCollection() - response = self.http_request(url, 'PUT') + 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) + logging.getLogger("recurly.http.response").debug(response_xml) collection.update_from_element(ElementTree.fromstring(response_xml)) return collection def force_collect(self, options={}): - url = urljoin(self._url, '/collect') - response = self.http_request(url, 'PUT') + url = urljoin(self._url, "/collect") + response = self.http_request(url, "PUT") if response.status not in (200, 201): self.raise_http_error(response) response_xml = response.read() @@ -1106,8 +1153,8 @@ def force_collect(self, options={}): return invoice_collection def _create_refund_invoice(self, element): - url = urljoin(self._url, '/refund') - body = ElementTree.tostring(element, encoding='UTF-8') + url = urljoin(self._url, "/refund") + body = ElementTree.tostring(element, encoding="UTF-8") refund_invoice = Invoice() refund_invoice.post(url, body) @@ -1115,10 +1162,10 @@ def _create_refund_invoice(self, element): return refund_invoice def redemption(self): - try: - return self.redemptions()[0] - except AttributeError: - raise AttributeError("redemption") + try: + return self.redemptions()[0] + except AttributeError: + raise AttributeError("redemption") def enter_offline_payment(self, transaction): """ @@ -1130,32 +1177,34 @@ def enter_offline_payment(self, transaction): Returns: Transaction: The created transaction """ - url = urljoin(self._url, '/transactions') + url = urljoin(self._url, "/transactions") transaction.post(url) return transaction def save(self): - if hasattr(self, '_url'): + if hasattr(self, "_url"): super(Invoice, self).save() else: raise BadRequestError("New invoices cannot be created using Invoice#save") + class InvoiceCollection(Resource): """A collection of invoices resulting from some action. Includes a charge invoice and a list of credit invoices. """ - nodename = 'invoice_collection' + nodename = "invoice_collection" attributes = ( - 'charge_invoice', - 'credit_invoices', + "charge_invoice", + "credit_invoices", ) _classes_for_nodename = { - 'charge_invoice': Invoice, - 'credit_invoice': Invoice, + "charge_invoice": Invoice, + "credit_invoice": Invoice, } + class Purchase(Resource): """ @@ -1163,28 +1212,28 @@ class Purchase(Resource): adjustments, coupon codes, and gift cards all in one call. """ - collection_path = 'purchases' - nodename = 'purchase' + collection_path = "purchases" + nodename = "purchase" attributes = ( - 'account', - 'adjustments', - 'currency', - 'po_number', - 'net_terms', - 'gift_card', - 'coupon_codes', - 'subscriptions', - 'customer_notes', - 'terms_and_conditions', - 'vat_reverse_charge_notes', - 'shipping_address', - 'shipping_address_id', - 'shipping_fees', - 'gateway_code', - 'collection_method', - 'transaction_type', - 'billing_info_uuid' + "account", + "adjustments", + "currency", + "po_number", + "net_terms", + "gift_card", + "coupon_codes", + "subscriptions", + "customer_notes", + "terms_and_conditions", + "vat_reverse_charge_notes", + "shipping_address", + "shipping_address_id", + "shipping_fees", + "gateway_code", + "collection_method", + "transaction_type", + "billing_info_uuid", ) def invoice(self): @@ -1204,7 +1253,7 @@ def preview(self): Returns: InvoiceCollection: The preview of collection of invoices """ - return self.__invoice(self.collection_path + '/preview') + return self.__invoice(self.collection_path + "/preview") def authorize(self): """ @@ -1217,7 +1266,7 @@ def authorize(self): Returns: InvoiceCollection: The authorized collection of invoices """ - return self.__invoice(self.collection_path + '/authorize') + return self.__invoice(self.collection_path + "/authorize") def capture(self, transaction_uuid): """ @@ -1227,7 +1276,9 @@ def capture(self, transaction_uuid): Returns: InvoiceCollection: The captured invoice collection """ - return self.__request_invoice(self.collection_path + '/transaction-uuid-' + transaction_uuid + '/capture') + return self.__request_invoice( + self.collection_path + "/transaction-uuid-" + transaction_uuid + "/capture" + ) def pending(self): """ @@ -1237,7 +1288,7 @@ def pending(self): Returns: InvoiceCollection: The pending collection of invoices """ - return self.__invoice(self.collection_path + '/pending') + return self.__invoice(self.collection_path + "/pending") def cancel(self, transaction_uuid): """ @@ -1246,15 +1297,19 @@ def cancel(self, transaction_uuid): Returns: InvoiceCollection: The canceled invoice collection """ - return self.__request_invoice(self.collection_path + '/transaction-uuid-' + transaction_uuid + '/cancel') + return self.__request_invoice( + self.collection_path + "/transaction-uuid-" + transaction_uuid + "/cancel" + ) 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']) + resource.attributes = tuple( + [a for a in resource.attributes if a != "currency"] + ) + try: filter_currency(self.adjustments) except AttributeError: @@ -1268,126 +1323,129 @@ def filter_currency(resources): def __request_invoice(self, url, body=None): url = urljoin(recurly.base_uri(), url) - response = self.http_request(url, 'POST', body) + response = self.http_request(url, "POST", body) if response.status not in (200, 201): self.raise_http_error(response) response_xml = response.read() - logging.getLogger('recurly.http.response').debug(response_xml) + logging.getLogger("recurly.http.response").debug(response_xml) elem = ElementTree.fromstring(response_xml) invoice_collection = InvoiceCollection.from_element(elem) return invoice_collection + class ShippingFee(Resource): """A one time shipping fee on a Purchase""" - nodename = 'shipping_fee' + nodename = "shipping_fee" attributes = ( - 'shipping_method_code', - 'shipping_amount_in_cents', - 'shipping_address', - 'shipping_address_id', + "shipping_method_code", + "shipping_amount_in_cents", + "shipping_address", + "shipping_address_id", ) + class ShippingMethod(Resource): """A shipping method available on the site""" - member_path = 'shipping_methods/%s' - collection_path = 'shipping_methods' + member_path = "shipping_methods/%s" + collection_path = "shipping_methods" - nodename = 'shipping_method' + nodename = "shipping_method" attributes = ( - 'code', - 'name', - 'accounting_code', - 'tax_code', - 'created_at', - 'updated_at', + "code", + "name", + "accounting_code", + "tax_code", + "created_at", + "updated_at", ) + class Subscription(Resource): """A customer account's subscription to your service.""" - member_path = 'subscriptions/%s' - collection_path = 'subscriptions' + member_path = "subscriptions/%s" + collection_path = "subscriptions" - nodename = 'subscription' + nodename = "subscription" attributes = ( - 'uuid', - 'state', - 'plan_code', - 'coupon_code', - 'coupon_codes', - 'quantity', - 'activated_at', - 'updated_at', - 'canceled_at', - 'starts_at', - 'expires_at', - 'current_period_started_at', - 'current_period_ends_at', - 'trial_started_at', - 'trial_ends_at', - 'unit_amount_in_cents', - 'tax_in_cents', - 'tax_type', - 'tax_rate', - 'tax_inclusive', - 'total_billing_cycles', - 'remaining_billing_cycles', - 'timeframe', - 'currency', - 'subscription_add_ons', - 'account', - 'pending_subscription', - 'net_terms', - 'collection_method', - 'po_number', - 'first_renewal_date', - 'bulk', - 'terms_and_conditions', - 'customer_notes', - 'vat_reverse_charge_notes', - 'bank_account_authorized_at', - 'redemptions', - 'revenue_schedule_type', - 'gift_card', - 'shipping_address', - 'shipping_address_id', - 'shipping_method_code', - 'shipping_amount_in_cents', - 'started_with_gift', - 'converted_at', - 'no_billing_info_reason', - 'imported_trial', - 'remaining_pause_cycles', - 'paused_at', - 'auto_renew', - 'renewal_billing_cycles', - 'first_billing_date', - 'first_bill_date', - 'next_bill_date', - 'current_term_started_at', - 'current_term_ends_at', - 'custom_fields', - 'gateway_code', - 'transaction_type', - 'billing_info', - 'billing_info_uuid' + "uuid", + "state", + "plan_code", + "coupon_code", + "coupon_codes", + "quantity", + "activated_at", + "updated_at", + "canceled_at", + "starts_at", + "expires_at", + "current_period_started_at", + "current_period_ends_at", + "trial_started_at", + "trial_ends_at", + "unit_amount_in_cents", + "tax_in_cents", + "tax_type", + "tax_rate", + "tax_inclusive", + "total_billing_cycles", + "remaining_billing_cycles", + "timeframe", + "currency", + "subscription_add_ons", + "account", + "pending_subscription", + "net_terms", + "collection_method", + "po_number", + "first_renewal_date", + "bulk", + "terms_and_conditions", + "customer_notes", + "vat_reverse_charge_notes", + "bank_account_authorized_at", + "redemptions", + "revenue_schedule_type", + "gift_card", + "shipping_address", + "shipping_address_id", + "shipping_method_code", + "shipping_amount_in_cents", + "started_with_gift", + "converted_at", + "no_billing_info_reason", + "imported_trial", + "remaining_pause_cycles", + "paused_at", + "auto_renew", + "renewal_billing_cycles", + "first_billing_date", + "first_bill_date", + "next_bill_date", + "current_term_started_at", + "current_term_ends_at", + "custom_fields", + "gateway_code", + "transaction_type", + "billing_info", + "billing_info_uuid", ) - sensitive_attributes = ('number', 'verification_value', 'bulk') + sensitive_attributes = ("number", "verification_value", "bulk") def preview(self): - if hasattr(self, '_url'): - url = self._url + '/preview' + if hasattr(self, "_url"): + url = self._url + "/preview" return self.post(url) else: - url = urljoin(recurly.base_uri(), self.collection_path + '/preview') + url = urljoin(recurly.base_uri(), self.collection_path + "/preview") return self.post(url) def update_notes(self, **kwargs): @@ -1402,24 +1460,32 @@ def update_notes(self, **kwargs): """ for key, val in iteritems(kwargs): setattr(self, key, val) - url = urljoin(self._url, '/notes') + url = urljoin(self._url, "/notes") self.put(url) def postpone(self, next_bill_date, bulk=False): """Postpone a subscription""" - url = urljoin(self._url, '/postpone?next_bill_date=' + next_bill_date.isoformat() + '&bulk=' + str(bulk).lower()) + url = urljoin( + self._url, + "/postpone?next_bill_date=" + + next_bill_date.isoformat() + + "&bulk=" + + str(bulk).lower(), + ) self.put(url) def pause(self, remaining_pause_cycles): """Pause a subscription""" - url = urljoin(self._url, '/pause') + url = urljoin(self._url, "/pause") elem = ElementTreeBuilder.Element(self.nodename) - elem.append(Resource.element_for_value('remaining_pause_cycles', - remaining_pause_cycles)) - body = ElementTree.tostring(elem, encoding='UTF-8') + elem.append( + Resource.element_for_value("remaining_pause_cycles", remaining_pause_cycles) + ) + body = ElementTree.tostring(elem, encoding="UTF-8") - response = self.http_request(url, 'PUT', body, { 'content-type': - 'application/xml; charset=utf-8' }) + response = self.http_request( + url, "PUT", body, {"content-type": "application/xml; charset=utf-8"} + ) if response.status not in (200, 201, 204): self.raise_http_error(response) @@ -1428,41 +1494,45 @@ def pause(self, remaining_pause_cycles): def resume(self): """Resume a subscription""" - url = urljoin(self._url, '/resume') + url = urljoin(self._url, "/resume") self.put(url) def convert_trial_moto(self): """Convert trial to paid subscription when transaction_type == 'moto'""" - url = urljoin(self._url, '/convert_trial') + url = urljoin(self._url, "/convert_trial") - request = ElementTreeBuilder.Element('subscription') - transaction_type = ElementTreeBuilder.SubElement(request, 'transaction_type') + request = ElementTreeBuilder.Element("subscription") + transaction_type = ElementTreeBuilder.SubElement(request, "transaction_type") transaction_type.text = "moto" - body = ElementTree.tostring(request, encoding='UTF-8') + body = ElementTree.tostring(request, encoding="UTF-8") - response = self.http_request(url, 'PUT', body, { 'content-type': - 'application/xml; charset=utf-8' }) + response = self.http_request( + url, "PUT", body, {"content-type": "application/xml; charset=utf-8"} + ) if response.status not in (200, 201, 204): self.raise_http_error(response) self.update_from_element(ElementTree.fromstring(response.read())) - def convert_trial(self, three_d_secure_action_result_token_id = None): + def convert_trial(self, three_d_secure_action_result_token_id=None): """Convert trial to paid subscription""" - url = urljoin(self._url, '/convert_trial') + url = urljoin(self._url, "/convert_trial") if not three_d_secure_action_result_token_id == None: - request = ElementTreeBuilder.Element('subscription') - account = ElementTreeBuilder.SubElement(request, 'account') - billing_info = ElementTreeBuilder.SubElement(account, 'billing_info') - token = ElementTreeBuilder.SubElement(billing_info, 'three_d_secure_action_result_token_id') + request = ElementTreeBuilder.Element("subscription") + account = ElementTreeBuilder.SubElement(request, "account") + billing_info = ElementTreeBuilder.SubElement(account, "billing_info") + token = ElementTreeBuilder.SubElement( + billing_info, "three_d_secure_action_result_token_id" + ) token.text = three_d_secure_action_result_token_id - body = ElementTree.tostring(request, encoding='UTF-8') - response = self.http_request(url, 'PUT', body, { 'content-type': - 'application/xml; charset=utf-8' }) + body = ElementTree.tostring(request, encoding="UTF-8") + response = self.http_request( + url, "PUT", body, {"content-type": "application/xml; charset=utf-8"} + ) else: - response = self.http_request(url, 'PUT') + response = self.http_request(url, "PUT") if response.status not in (200, 201, 204): self.raise_http_error(response) @@ -1470,136 +1540,143 @@ def convert_trial(self, three_d_secure_action_result_token_id = None): self.update_from_element(ElementTree.fromstring(response.read())) def _update(self): - if not hasattr(self, 'timeframe'): - self.timeframe = 'now' + if not hasattr(self, "timeframe"): + self.timeframe = "now" return super(Subscription, self)._update() def __getpath__(self, name): - if name == 'plan_code': - return 'plan/plan_code' + if name == "plan_code": + return "plan/plan_code" else: return name def create_usage(self, sub_add_on, usage): """Record the usage on the given subscription add on and update the usage object with returned xml""" - url = urljoin(self._url, '/add_ons/%s/usage' % (sub_add_on.add_on_code,)) + url = urljoin(self._url, "/add_ons/%s/usage" % (sub_add_on.add_on_code,)) return usage.post(url) + class TransactionBillingInfo(recurly.Resource): - node_name = 'billing_info' + node_name = "billing_info" attributes = ( - 'first_name', - 'last_name', - 'address1', - 'address2', - 'city', - 'state', - 'country', - 'zip', - 'phone', - 'vat_number', - 'first_six', - 'last_four', - 'card_type', - 'month', - 'year', - 'transaction_uuid', + "first_name", + "last_name", + "address1", + "address2", + "city", + "state", + "country", + "zip", + "phone", + "vat_number", + "first_six", + "last_four", + "card_type", + "month", + "year", + "transaction_uuid", ) class TransactionAccount(recurly.Resource): - node_name = 'account' + node_name = "account" attributes = ( - 'first_name', - 'last_name', - 'company', - 'email', - 'account_code', + "first_name", + "last_name", + "company", + "email", + "account_code", ) - _classes_for_nodename = {'billing_info': TransactionBillingInfo} + _classes_for_nodename = {"billing_info": TransactionBillingInfo} + class TransactionDetails(recurly.Resource): - node_name = 'details' - attributes = ('account') - _classes_for_nodename = {'account': TransactionAccount} + node_name = "details" + attributes = "account" + _classes_for_nodename = {"account": TransactionAccount} class TransactionError(recurly.Resource): - node_name = 'transaction_error' + node_name = "transaction_error" attributes = ( - 'id', - 'merchant_message', - 'error_caterogy', - 'customer_message', - 'error_code', - 'gateway_error_code', + "id", + "merchant_message", + "error_caterogy", + "customer_message", + "error_code", + "gateway_error_code", ) + class TransactionFraudInfo(recurly.Resource): - node_name = 'fraud' + node_name = "fraud" attributes = ( - 'score', - 'decision', + "score", + "decision", ) + class Transaction(Resource): """An immediate one-time charge made to a customer's account.""" - member_path = 'transactions/%s' - collection_path = 'transactions' + member_path = "transactions/%s" + collection_path = "transactions" - nodename = 'transaction' + nodename = "transaction" attributes = ( - 'uuid', - 'action', - 'account', - 'currency', - 'amount_in_cents', - 'tax_in_cents', - 'status', - 'reference', - 'test', - 'voidable', - 'description', - 'refundable', - 'cvv_result', - 'avs_result', - 'avs_result_street', - 'avs_result_postal', - 'created_at', - 'updated_at', - 'details', - 'transaction_error', - 'type', - 'ip_address', - 'tax_exempt', - 'tax_code', - 'accounting_code', - 'fraud', - 'original_transaction', - 'gateway_type', - 'origin', - 'message', - 'approval_code', - 'payment_method', - 'collected_at' + "uuid", + "action", + "account", + "currency", + "amount_in_cents", + "tax_in_cents", + "status", + "reference", + "test", + "voidable", + "description", + "refundable", + "cvv_result", + "avs_result", + "avs_result_street", + "avs_result_postal", + "created_at", + "updated_at", + "details", + "transaction_error", + "type", + "ip_address", + "tax_exempt", + "tax_code", + "accounting_code", + "fraud", + "original_transaction", + "gateway_type", + "origin", + "message", + "approval_code", + "payment_method", + "collected_at", + ) + xml_attribute_attributes = ("type",) + sensitive_attributes = ( + "number", + "verification_value", ) - xml_attribute_attributes = ('type',) - sensitive_attributes = ('number', 'verification_value',) _classes_for_nodename = { - 'details': TransactionDetails, - 'fraud': TransactionFraudInfo, - 'transaction_error': TransactionError + "details": TransactionDetails, + "fraud": TransactionFraudInfo, + "transaction_error": TransactionError, } def _handle_refund_accepted(self, response): if response.status != 202: self.raise_http_error(response) - self._refund_transaction_url = response.getheader('location') + self._refund_transaction_url = response.getheader("location") return self def get_refund_transaction(self): @@ -1633,34 +1710,33 @@ def refund(self, **kwargs): try: selfnode = self._elem except AttributeError: - raise AttributeError('refund') + raise AttributeError("refund") url, method = None, None - for anchor_elem in selfnode.findall('a'): - if anchor_elem.attrib.get('name') == 'refund': - url = anchor_elem.attrib['href'] - method = anchor_elem.attrib['method'].upper() + for anchor_elem in selfnode.findall("a"): + if anchor_elem.attrib.get("name") == "refund": + url = anchor_elem.attrib["href"] + method = anchor_elem.attrib["method"].upper() if url is None or method is None: raise AttributeError("refund") # should do something more specific probably - actionator = self._make_actionator(url, method, extra_handler=self._handle_refund_accepted) + actionator = self._make_actionator( + url, method, extra_handler=self._handle_refund_accepted + ) return actionator(**kwargs) -Transaction._classes_for_nodename['transaction'] = Transaction +Transaction._classes_for_nodename["transaction"] = Transaction class PlanRampInterval(Resource): """A plan ramp - representing a price point and the billing_cycle to begin that price point + representing a price point and the billing_cycle to begin that price point """ - nodename = 'ramp_interval' - collection_path = 'ramp_intervals' + nodename = "ramp_interval" + collection_path = "ramp_intervals" - attributes = { - 'starting_billing_cycle', - 'unit_amount_in_cents' - } + attributes = {"starting_billing_cycle", "unit_amount_in_cents"} class Plan(Resource): @@ -1668,185 +1744,193 @@ class Plan(Resource): """A service level for your service to which a customer account can subscribe.""" - member_path = 'plans/%s' - collection_path = 'plans' + member_path = "plans/%s" + collection_path = "plans" - nodename = 'plan' + nodename = "plan" attributes = ( - 'plan_code', - 'name', - 'description', - 'success_url', - 'cancel_url', - 'display_donation_amounts', - 'display_quantity', - 'display_phone_number', - 'bypass_hosted_confirmation', - 'unit_name', - 'payment_page_tos_link', - 'plan_interval_length', - 'plan_interval_unit', - 'trial_interval_length', - 'trial_interval_unit', - 'accounting_code', - 'setup_fee_accounting_code', - 'created_at', - 'updated_at', - 'tax_exempt', - 'tax_code', - 'unit_amount_in_cents', - 'setup_fee_in_cents', - 'total_billing_cycles', - 'revenue_schedule_type', - 'setup_fee_revenue_schedule_type', - 'trial_requires_billing_info', - 'auto_renew', - 'allow_any_item_on_subscriptions', - 'dunning_campaign_id', - 'pricing_model', - 'ramp_intervals', + "plan_code", + "name", + "description", + "success_url", + "cancel_url", + "display_donation_amounts", + "display_quantity", + "display_phone_number", + "bypass_hosted_confirmation", + "unit_name", + "payment_page_tos_link", + "plan_interval_length", + "plan_interval_unit", + "trial_interval_length", + "trial_interval_unit", + "accounting_code", + "setup_fee_accounting_code", + "created_at", + "updated_at", + "tax_exempt", + "tax_code", + "unit_amount_in_cents", + "setup_fee_in_cents", + "total_billing_cycles", + "revenue_schedule_type", + "setup_fee_revenue_schedule_type", + "trial_requires_billing_info", + "auto_renew", + "allow_any_item_on_subscriptions", + "dunning_campaign_id", + "pricing_model", + "ramp_intervals", ) - _classes_for_nodename = {'ramp_interval': PlanRampInterval } -# + _classes_for_nodename = {"ramp_interval": PlanRampInterval} + # def get_add_on(self, add_on_code): """Return the `AddOn` for this plan with the given add-on code.""" - url = urljoin(self._url, '/add_ons/%s' % (add_on_code,)) + url = urljoin(self._url, "/add_ons/%s" % (add_on_code,)) resp, elem = AddOn.element_for_url(url) return AddOn.from_element(elem) def create_add_on(self, add_on): """Make the given `AddOn` available to subscribers on this plan.""" - url = urljoin(self._url, '/add_ons') + url = urljoin(self._url, "/add_ons") return add_on.post(url) + class Usage(Resource): """A recording of usage agains a measured unit""" - nodename = 'usage' - collection_path = 'usages' + nodename = "usage" + collection_path = "usages" attributes = ( - 'measured_unit', - 'amount', - 'merchant_tag', - 'recording_timestamp', - 'usage_timestamp', - 'usage_type', - 'unit_amount_in_cents', - 'usage_percentage', - 'billed_at', - 'created_at', - 'updated_at', + "measured_unit", + "amount", + "merchant_tag", + "recording_timestamp", + "usage_timestamp", + "usage_type", + "unit_amount_in_cents", + "usage_percentage", + "billed_at", + "created_at", + "updated_at", ) + class MeasuredUnit(Resource): """A unit of measurement for usage based billing""" - nodename = 'measured_unit' - member_path = 'measured_units/%s' - collection_path = 'measured_units' + nodename = "measured_unit" + member_path = "measured_units/%s" + collection_path = "measured_units" attributes = ( - 'id', - 'name', - 'display_name', - 'description', - 'created_at', - 'updated_at', + "id", + "name", + "display_name", + "description", + "created_at", + "updated_at", ) + class PercentageTier(Resource): - """Percentage tier associated to a set of tiers per currency + """Percentage tier associated to a set of tiers per currency in an add-on.""" - nodename = 'tier' + nodename = "tier" inherits_currency = True attributes = ( - 'ending_amount_in_cents', - 'usage_percentage', + "ending_amount_in_cents", + "usage_percentage", ) + class CurrencyPercentageTier(Resource): """Set of percetange tiers per currency in an add-on passed when usage type is percentage and tier type is tiered or volume.""" - nodename = 'percentage_tier' + nodename = "percentage_tier" attributes = ( - 'currency', - 'tiers', + "currency", + "tiers", ) _classes_for_nodename = { - 'tier': PercentageTier, + "tier": PercentageTier, } + class AddOn(Resource): """An additional benefit a customer subscribed to a particular plan can also subscribe to.""" - nodename = 'add_on' + nodename = "add_on" attributes = ( - 'add_on_code', - 'item_code', - 'item_state', - 'external_sku', - 'name', - 'display_quantity_on_hosted_page', - 'display_quantity', - 'default_quantity', - 'accounting_code', - 'unit_amount_in_cents', - 'measured_unit_id', - 'usage_type', - 'usage_timeframe', - 'usage_percentage', - 'add_on_type', - 'tax_code', - 'revenue_schedule_type', - 'optional', - 'created_at', - 'updated_at', - 'tier_type', - 'tiers', - 'percentage_tiers' + "add_on_code", + "item_code", + "item_state", + "external_sku", + "name", + "display_quantity_on_hosted_page", + "display_quantity", + "default_quantity", + "accounting_code", + "unit_amount_in_cents", + "measured_unit_id", + "usage_type", + "usage_timeframe", + "usage_percentage", + "add_on_type", + "tax_code", + "revenue_schedule_type", + "optional", + "created_at", + "updated_at", + "tier_type", + "tiers", + "percentage_tiers", ) _classes_for_nodename = { - 'percentage_tier': CurrencyPercentageTier, + "percentage_tier": CurrencyPercentageTier, } + class SubAddOnPercentageTier(Resource): """Percentage tiers associated to a subscription add-on.""" - nodename = 'percentage_tier' + nodename = "percentage_tier" inherits_currency = True attributes = ( - 'ending_amount_in_cents', - 'usage_percentage', + "ending_amount_in_cents", + "usage_percentage", ) + class Tier(Resource): """Pricing tier for plans, subscriptions and invoices""" - nodename = 'tier' + nodename = "tier" attributes = ( - 'ending_quantity', - 'unit_amount_in_cents', + "ending_quantity", + "unit_amount_in_cents", ) + class SubscriptionAddOn(Resource): """A plan add-on as added to a customer's subscription. @@ -1856,35 +1940,33 @@ class SubscriptionAddOn(Resource): """ - nodename = 'subscription_add_on' + nodename = "subscription_add_on" inherits_currency = True attributes = ( - 'add_on_code', - 'quantity', - 'unit_amount_in_cents', - 'usage_timeframe', - 'address', - 'add_on_source', - 'tiers', - 'percentage_tiers' + "add_on_code", + "quantity", + "unit_amount_in_cents", + "usage_timeframe", + "address", + "add_on_source", + "tiers", + "percentage_tiers", ) - _classes_for_nodename = { - 'percentage_tier': SubAddOnPercentageTier, - 'tier': Tier - } + _classes_for_nodename = {"percentage_tier": SubAddOnPercentageTier, "tier": Tier} + class Note(Resource): """A customer account's notes.""" - nodename = 'note' - collection_path = 'notes' + nodename = "note" + collection_path = "notes" attributes = ( - 'message', - 'created_at', + "message", + "created_at", ) @classmethod @@ -1896,33 +1978,31 @@ def from_element(cls, elem): setattr(new_note, child_el.tag, child_el.text) return new_note + class CreditPayment(Resource): """A payment from credit""" - nodename = 'credit_payment' - collection_path = 'credit_payments' - member_path = 'credit_payments/%s' + nodename = "credit_payment" + collection_path = "credit_payments" + member_path = "credit_payments/%s" attributes = ( - 'uuid', - 'unit_amount_in_cents', - 'currency', - 'action', - 'created_at', - 'updated_at', - 'voided_at', + "uuid", + "unit_amount_in_cents", + "currency", + "action", + "created_at", + "updated_at", + "voided_at", ) class ExportDate(Resource): - nodename = 'export_date' - collection_path = 'export_dates' + nodename = "export_date" + collection_path = "export_dates" - attributes = ( - 'date', - 'export_files' - ) + attributes = ("date", "export_files") def files(self, date): """ @@ -1930,20 +2010,17 @@ def files(self, date): :param date: The date to fetch the export files for :return: A list of exported files for that given date or an empty list if not file exists for that date """ - url = urljoin(recurly.base_uri() + self.collection_path, '/%s/export_files' % date) + url = urljoin( + recurly.base_uri() + self.collection_path, "/%s/export_files" % date + ) return ExportDateFile.paginated(url) class ExportDateFile(Resource): - nodename = 'export_file' - collection_path = 'export_files' + nodename = "export_file" + collection_path = "export_files" - attributes = ( - 'name', - 'md5sum', - 'expires_at', - 'download_url' - ) + attributes = ("name", "md5sum", "expires_at", "download_url") def download_information(self): """ @@ -1967,7 +2044,7 @@ def objects_for_push_notification(notification): """ notification_el = ElementTree.fromstring(notification) - objects = {'type': notification_el.tag} + objects = {"type": notification_el.tag} for child_el in notification_el: tag = child_el.tag res = Resource.value_for_element(child_el) diff --git a/recurly/errors.py b/recurly/errors.py index a7ddf28c..55173113 100644 --- a/recurly/errors.py +++ b/recurly/errors.py @@ -1,6 +1,7 @@ from defusedxml import ElementTree import six + class ResponseError(Exception): """An error received from the Recurly API in response to an HTTP @@ -13,31 +14,29 @@ def __init__(self, response_xml): def response_doc(self): """The XML document received from the service.""" try: - return self.__dict__['response_doc'] + return self.__dict__["response_doc"] except KeyError: - self.__dict__['response_doc'] = ElementTree.fromstring( - self.response_xml - ) - return self.__dict__['response_doc'] + self.__dict__["response_doc"] = ElementTree.fromstring(self.response_xml) + return self.__dict__["response_doc"] @property def symbol(self): """The machine-readable identifier for the error.""" - el = self.response_doc.find('symbol') + el = self.response_doc.find("symbol") if el is not None: return el.text @property def message(self): """The human-readable description of the error.""" - el = self.response_doc.find('description') + el = self.response_doc.find("description") if el is not None: return el.text @property def details(self): """A further human-readable elaboration on the error.""" - el = self.response_doc.find('details') + el = self.response_doc.find("details") if el is not None: return el.text @@ -45,7 +44,7 @@ def details(self): def error(self): """A fall-back error message in the event no more specific error is given.""" - el = self.response_doc.find('error') + el = self.response_doc.find("error") if el is not None: return el.text @@ -58,13 +57,14 @@ def __unicode__(self): return self.error details = self.details if details is not None: - return six.u('%s: %s %s') % (symbol, self.message, details) - return six.u('%s: %s') % (symbol, self.message) + return six.u("%s: %s %s") % (symbol, self.message, details) + return six.u("%s: %s") % (symbol, self.message) class ClientError(ResponseError): """An error resulting from a problem in the client's request (that is, an error with an HTTP ``4xx`` status code).""" + pass @@ -76,11 +76,13 @@ class BadRequestError(ClientError): Resubmitting the request will likely result in the same error. """ + pass class ConfigurationError(Exception): """An error related to a bad configuration""" + pass @@ -99,6 +101,7 @@ def __unicode__(self): class PaymentRequiredError(ClientError): """An error indicating your Recurly account is in production mode but is not in good standing (HTTP ``402 Payment Required``).""" + pass @@ -110,18 +113,21 @@ class ForbiddenError(ClientError): your login credentials are for the appropriate account. """ + pass class NotFoundError(ClientError): """An error for when the resource was not found with the given identifier (HTTP ``404 Not Found``).""" + pass class NotAcceptableError(ClientError): """An error for when the client's request could not be accepted by the remote service (HTTP ``406 Not Acceptable``).""" + pass @@ -134,12 +140,14 @@ class PreconditionFailedError(ClientError): corresponds to the HTTP ``412 Precondition Failed`` status code. """ + pass class UnsupportedMediaTypeError(ClientError): """An error resulting from the submission as an unsupported media type (HTTP ``415 Unsupported Media Type``).""" + pass @@ -153,42 +161,42 @@ def __init__(self, response_doc): @property def error_code(self): """The machine-readable identifier for the error.""" - el = self.response_doc.find('error_code') + el = self.response_doc.find("error_code") if el is not None: return el.text @property def error_category(self): """The machine-readable identifier for the error category.""" - el = self.response_doc.find('error_category') + el = self.response_doc.find("error_category") if el is not None: return el.text @property def customer_message(self): """Recommended message for the customer""" - el = self.response_doc.find('customer_message') + el = self.response_doc.find("customer_message") if el is not None: return el.text @property def merchant_message(self): """Recommended message for the merchant""" - el = self.response_doc.find('merchant_message') + el = self.response_doc.find("merchant_message") if el is not None: return el.text @property def gateway_error_code(self): """Error code from the gateway""" - el = self.response_doc.find('gateway_error_code') + el = self.response_doc.find("gateway_error_code") if el is not None: return el.text @property def three_d_secure_action_token_id(self): """3DS Action Token ID for further authentication""" - el = self.response_doc.find('three_d_secure_action_token_id') + el = self.response_doc.find("three_d_secure_action_token_id") if el is not None: return el.text @@ -201,16 +209,16 @@ class ValidationError(ClientError): @property def transaction_error(self): """The transaction error object.""" - error = self.response_doc.find('transaction_error') + error = self.response_doc.find("transaction_error") if error is not None: return TransactionError(error) @property def transaction_error_code(self): """The machine-readable error code for a transaction error.""" - error = self.response_doc.find('transaction_error') + error = self.response_doc.find("transaction_error") if error is not None: - code = error.find('error_code') + code = error.find("error_code") if code is not None: return code.text @@ -228,7 +236,7 @@ def __str__(self): return self.__unicode__() def __unicode__(self): - return six.u('%s: %s %s') % (self.symbol, self.field, self.message) + return six.u("%s: %s %s") % (self.symbol, self.field, self.message) @property def errors(self): @@ -240,14 +248,14 @@ def errors(self): """ try: - return self.__dict__['errors'] + return self.__dict__["errors"] except KeyError: pass suberrors = dict() - for err in self.response_doc.findall('error'): - field = err.attrib['field'] - symbol = err.attrib['symbol'] + for err in self.response_doc.findall("error"): + field = err.attrib["field"] + symbol = err.attrib["symbol"] message = err.text sub_err = self.Suberror(field, symbol, message) @@ -260,7 +268,7 @@ def errors(self): else: suberrors[field] = sub_err - self.__dict__['errors'] = suberrors + self.__dict__["errors"] = suberrors return suberrors def __unicode__(self): @@ -268,20 +276,25 @@ def __unicode__(self): for error_key in sorted(self.errors.keys()): error = self.errors[error_key] if isinstance(error, (tuple, list)): - all_error_strings += [six.text_type(e) for e in error] # multiple errors on field + all_error_strings += [ + six.text_type(e) for e in error + ] # multiple errors on field else: all_error_strings.append(six.text_type(error)) - return six.u('; ').join(all_error_strings) + return six.u("; ").join(all_error_strings) + class ServerError(ResponseError): """An error resulting from a problem creating the server's response to the request (that is, an error with an HTTP ``5xx`` status code).""" + pass class InternalServerError(ServerError): """An unexpected general server error (HTTP ``500 Internal Server Error``).""" + pass @@ -293,6 +306,7 @@ class BadGatewayError(ServerError): Try the request again. """ + pass @@ -303,6 +317,7 @@ class ServiceUnavailableError(ServerError): response. Try the request again. """ + pass @@ -313,6 +328,7 @@ class GatewayTimeoutError(ServerError): response. Try the request again. """ + pass @@ -335,14 +351,17 @@ class UnexpectedClientError(UnexpectedStatusError): not be used for classes mapped in the `error_classes` dict. """ + pass + class UnexpectedServerError(UnexpectedStatusError): """An error resulting from an unexpected status code in the range 500-599 of HTTP status codes. This class should not be used for classes mapped in the `error_classes` dict. """ + pass @@ -369,13 +388,16 @@ def error_class_for_http_status(status): try: return error_classes[status] except KeyError: + def new_status_error(xml_response): - if (status > 400 and status < 500): + if status > 400 and status < 500: return UnexpectedClientError(status, xml_response) - if (status > 500 and status < 600): + if status > 500 and status < 600: return UnexpectedServerError(status, xml_response) return UnexpectedStatusError(status, xml_response) + return new_status_error + other_errors = [ConfigurationError] __all__ = [x.__name__ for x in list(error_classes.values()) + other_errors] diff --git a/recurly/link_header.py b/recurly/link_header.py index 26924784..93ce31f1 100644 --- a/recurly/link_header.py +++ b/recurly/link_header.py @@ -34,43 +34,44 @@ TOKEN = r'(?:[^\(\)<>@,;:\\"/\[\]\?={} \t]+?)' QUOTED_STRING = r'(?:"(?:\\"|[^"])*")' -PARAMETER = r'(?:%(TOKEN)s(?:=(?:%(TOKEN)s|%(QUOTED_STRING)s))?)' % locals() -LINK = r'<[^>]*>\s*(?:;\s*%(PARAMETER)s?\s*)*' % locals() -COMMA = r'(?:\s*(?:,\s*)+)' -LINK_SPLIT = r'%s(?=%s|\s*$)' % (LINK, COMMA) +PARAMETER = r"(?:%(TOKEN)s(?:=(?:%(TOKEN)s|%(QUOTED_STRING)s))?)" % locals() +LINK = r"<[^>]*>\s*(?:;\s*%(PARAMETER)s?\s*)*" % locals() +COMMA = r"(?:\s*(?:,\s*)+)" +LINK_SPLIT = r"%s(?=%s|\s*$)" % (LINK, COMMA) def _unquotestring(instr): if instr[0] == instr[-1] == '"': instr = instr[1:-1] - instr = re.sub(r'\\(.)', r'\1', instr) + instr = re.sub(r"\\(.)", r"\1", instr) return instr def _splitstring(instr, item, split): if not instr: return [] - return [h.strip() for h in re.findall(r'%s(?=%s|\s*$)' % (item, split), instr)] + return [h.strip() for h in re.findall(r"%s(?=%s|\s*$)" % (item, split), instr)] + link_splitter = re.compile(LINK_SPLIT) def parse_link_value(instr): """ - Given a link-value (i.e., after separating the header-value on commas), + Given a link-value (i.e., after separating the header-value on commas), return a dictionary whose keys are link URLs and values are dictionaries of the parameters for their associated links. - - Note that internationalised parameters (e.g., title*) are + + Note that internationalised parameters (e.g., title*) are NOT percent-decoded. - + Also, only the last instance of a given parameter will be included. - - For example, - + + For example, + >>> parse_link_value('; rel="self"; title*=utf-8\'de\'letztes%20Kapitel') {'/foo': {'title*': "utf-8'de'letztes%20Kapitel", 'rel': 'self'}} - + """ out = {} if not instr: @@ -89,7 +90,8 @@ def parse_link_value(instr): return out -if __name__ == '__main__': +if __name__ == "__main__": import sys + if len(sys.argv) > 1: - print(parse_link_value(sys.argv[1])) \ No newline at end of file + print(parse_link_value(sys.argv[1])) diff --git a/recurly/recurly_logging.py b/recurly/recurly_logging.py index 37d80d1d..d444a124 100644 --- a/recurly/recurly_logging.py +++ b/recurly/recurly_logging.py @@ -1,24 +1,33 @@ import logging import os + class NullLogger(logging.Logger): def isEnabledFor(*args): return False + def debug(*args): pass + def info(*args): pass + def info(*args): pass + # Overriding the getLogger method to # return our NullLogger def getLogger(key): return NullLogger(key) + # If we have this environment variable set to 'true', we can use python's # debugger -if 'RECURLY_INSECURE_DEBUG' in os.environ and os.environ['RECURLY_INSECURE_DEBUG'] == 'true': +if ( + "RECURLY_INSECURE_DEBUG" in os.environ + and os.environ["RECURLY_INSECURE_DEBUG"] == "true" +): print("[WARNING] Recurly logger should not be enabled in production.") getLogger = logging.getLogger diff --git a/recurly/resource.py b/recurly/resource.py index 8ce4fad5..992abdac 100644 --- a/recurly/resource.py +++ b/recurly/resource.py @@ -15,6 +15,7 @@ from six.moves import http_client from six.moves.urllib.parse import urlencode, urlsplit, quote, urlparse + def urlencode_params(args): # Need to make bools lowercase for k, v in six.iteritems(args): @@ -22,19 +23,22 @@ def urlencode_params(args): args[k] = str(v).lower() return urlencode(args) + class Money(object): """An amount of money in one or more currencies.""" def __init__(self, *args, **kwargs): if args and kwargs: - raise ValueError("Money may be single currency or multi-currency but not both") + raise ValueError( + "Money may be single currency or multi-currency but not both" + ) elif kwargs: self.currencies = dict(kwargs) elif args and len(args) > 1: raise ValueError("Multi-currency Money must be instantiated with codes") elif args: - self.currencies = { recurly.DEFAULT_CURRENCY: args[0] } + self.currencies = {recurly.DEFAULT_CURRENCY: args[0]} else: self.currencies = dict() @@ -50,7 +54,7 @@ def from_element(cls, elem): def add_to_element(self, elem): for currency, amount in self.currencies.items(): currency_el = ElementTreeBuilder.Element(currency) - currency_el.attrib['type'] = 'integer' + currency_el.attrib["type"] = "integer" currency_el.text = six.text_type(amount) elem.append(currency_el) @@ -75,6 +79,7 @@ class PageError(ValueError): a series, or the first page for the first page in a series. """ + pass @@ -86,6 +91,7 @@ class Page(list): Use `Page` instances as `list` instances to access their contents. """ + def __iter__(self): if not self: return @@ -144,7 +150,7 @@ def page_for_url(cls, url): def count_for_url(cls, url): """Return the count of server side resources given a url""" headers = Resource.headers_for_url(url) - return int(headers['x-records']) + return int(headers["x-records"]) @classmethod def page_for_value(cls, resp, value): @@ -157,11 +163,11 @@ def page_for_value(cls, resp, value): """ page = cls(value) - links = parse_link_value(resp.getheader('link')) + links = parse_link_value(resp.getheader("link")) for url, data in six.iteritems(links): - if data.get('rel') == 'start': + if data.get("rel") == "start": page.start_url = url - if data.get('rel') == 'next': + if data.get("rel") == "next": page.next_url = url return page @@ -194,32 +200,34 @@ class Resource(object): its own.""" def serializable_attributes(self): - """ Attributes to be serialized in a ``POST`` or ``PUT`` request. + """Attributes to be serialized in a ``POST`` or ``PUT`` request. Returns all attributes unless a blacklist is specified """ - if hasattr(self, 'blacklist_attributes'): - return [attr for attr in self.attributes if attr not in - self.blacklist_attributes] + if hasattr(self, "blacklist_attributes"): + return [ + attr + for attr in self.attributes + if attr not in self.blacklist_attributes + ] else: return self.attributes - def __init__(self, **kwargs): try: - self.attributes.index('currency') # Test for currency attribute, - self.currency # and test if it's set. + self.attributes.index("currency") # Test for currency attribute, + self.currency # and test if it's set. except ValueError: pass except AttributeError: self.currency = recurly.DEFAULT_CURRENCY for key, value in six.iteritems(kwargs): - if key not in ('collection_path', 'member_path', 'node_name', 'attributes'): + if key not in ("collection_path", "member_path", "node_name", "attributes"): setattr(self, key, value) @classmethod - def http_request(cls, url, method='GET', body=None, headers=None): + 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. @@ -235,71 +243,81 @@ def http_request(cls, url, method='GET', body=None, headers=None): """ if recurly.API_KEY is None: - raise recurly.UnauthorizedError('recurly.API_KEY not set') + 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') + 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 + 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""") + 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) + 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) + 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) + 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') + 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 = '' + if header == "authorization": + value = "" log.debug("%s: %s", header, value) - log.debug('') - if method in ('POST', 'PUT') and body is not None: + 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 = ElementTreeBuilder.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' + body = ElementTreeBuilder.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') + 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('') + log.debug("") recurly.cache_rate_limit_headers(resp_headers) @@ -309,7 +327,7 @@ def http_request(cls, url, method='GET', body=None, headers=None): def headers_as_dict(cls, resp): """Turns an array of response headers into a dictionary""" if six.PY2: - pairs = [header.split(':', 1) for header in resp.msg.headers] + pairs = [header.split(":", 1) for header in resp.msg.headers] return dict([(k.lower(), v.strip()) for k, v in pairs]) else: return dict([(k.lower(), v.strip()) for k, v in resp.msg._headers]) @@ -325,8 +343,8 @@ def as_log_output(self): elem = self.to_element() for attrname in self.sensitive_attributes: for sensitive_el in elem.iter(attrname): - sensitive_el.text = 'XXXXXXXXXXXXXXXX' - return ElementTreeBuilder.tostring(elem, encoding='UTF-8') + sensitive_el.text = "XXXXXXXXXXXXXXXX" + return ElementTreeBuilder.tostring(elem, encoding="UTF-8") @classmethod def _learn_nodenames(cls, classes): @@ -337,7 +355,7 @@ def _learn_nodenames(cls, classes): continue if not rc_is_subclass: continue - nodename = getattr(resource_class, 'nodename', None) + nodename = getattr(resource_class, "nodename", None) if nodename is None: continue @@ -362,7 +380,7 @@ def get(cls, uuid): @classmethod def headers_for_url(cls, url): """Return the headers only for the given URL as a dict""" - response = cls.http_request(url, method='HEAD') + response = cls.http_request(url, method="HEAD") if response.status != 200: cls.raise_http_error(response) @@ -377,10 +395,10 @@ def element_for_url(cls, url): if response.status != 200: cls.raise_http_error(response) - assert response.getheader('content-type').startswith('application/xml') + assert response.getheader("content-type").startswith("application/xml") response_xml = response.read() - logging.getLogger('recurly.http.response').debug(response_xml) + logging.getLogger("recurly.http.response").debug(response_xml) response_doc = ElementTree.fromstring(response_xml) return response, response_doc @@ -390,8 +408,10 @@ def _subclass_for_nodename(cls, nodename): try: return cls._classes_for_nodename[nodename] except KeyError: - raise ValueError("Could not determine resource class for array member with tag %r" - % nodename) + raise ValueError( + "Could not determine resource class for array member with tag %r" + % nodename + ) @classmethod def value_for_element(cls, elem): @@ -407,40 +427,55 @@ def value_for_element(cls, elem): * ``None`` """ - log = logging.getLogger('recurly.resource') + 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) + 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) + 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') + attr_type = elem.attrib.get("type") log.debug("Converting %r element with type %r", elem.tag, attr_type) - if attr_type == 'integer': + if attr_type == "integer": return int(elem.text.strip()) - if attr_type == 'float': + if attr_type == "float": return float(elem.text.strip()) - if attr_type == 'boolean': - return elem.text.strip() == 'true' - if attr_type == 'datetime': + 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] + 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) + 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) @@ -454,14 +489,17 @@ def value_for_element(cls, elem): if len(elem) > 1: first_tag = elem[0].tag last_tag = elem[-1].tag - # Check if the element have and array of items + # Check if the element have and array of items # # ... # ... # ... # - if(first_tag == last_tag): - return [cls._subclass_for_nodename(sub_elem.tag).from_element(sub_elem) for sub_elem in elem] + if first_tag == last_tag: + return [ + cls._subclass_for_nodename(sub_elem.tag).from_element(sub_elem) + for sub_elem in elem + ] # De-serialize one resource # # name @@ -473,7 +511,7 @@ def value_for_element(cls, elem): log.debug("Converting %r tag into a %s", elem.tag, value_class.__name__) return value_class.from_element(elem) - value = elem.text or '' + value = elem.text or "" return value.strip() @classmethod @@ -500,22 +538,24 @@ def element_for_value(cls, attrname, value): el = ElementTreeBuilder.Element(attrname) if value is None: - el.attrib['nil'] = 'nil' + el.attrib["nil"] = "nil" elif isinstance(value, bool): - el.attrib['type'] = 'boolean' - el.text = 'true' if value else 'false' + el.attrib["type"] = "boolean" + el.text = "true" if value else "false" elif isinstance(value, int): - el.attrib['type'] = 'integer' + el.attrib["type"] = "integer" el.text = str(value) elif isinstance(value, datetime): - el.attrib['type'] = 'datetime' - el.text = value.strftime('%Y-%m-%dT%H:%M:%SZ') + el.attrib["type"] = "datetime" + el.text = value.strftime("%Y-%m-%dT%H:%M:%SZ") elif isinstance(value, list) or isinstance(value, tuple): for sub_resource in value: - if hasattr(sub_resource, 'to_element'): - el.append(sub_resource.to_element()) + if hasattr(sub_resource, "to_element"): + el.append(sub_resource.to_element()) else: - el.append(cls.element_for_value(re.sub(r"s$", "", attrname), sub_resource)) + el.append( + cls.element_for_value(re.sub(r"s$", "", attrname), sub_resource) + ) elif isinstance(value, Money): value.add_to_element(el) else: @@ -525,7 +565,7 @@ def element_for_value(cls, attrname, value): @classmethod def paginated(self, url): - """ Exposes Page.page_for_url in Resource """ + """Exposes Page.page_for_url in Resource""" return Page.page_for_url(url) @classmethod @@ -545,7 +585,7 @@ def update_from_element(self, elem): except AttributeError: pass - document_url = elem.attrib.get('href') + document_url = elem.attrib.get("href") if document_url is not None: self._url = document_url @@ -554,7 +594,7 @@ def update_from_element(self, elem): def _make_actionator(self, url, method, extra_handler=None): def actionator(*args, **kwargs): if kwargs: - full_url = '%s?%s' % (url, urlencode_params(kwargs)) + full_url = "%s?%s" % (url, urlencode_params(kwargs)) else: full_url = url @@ -563,11 +603,11 @@ def actionator(*args, **kwargs): if response.status == 200: response_xml = response.read() - logging.getLogger('recurly.http.response').debug(response_xml) + logging.getLogger("recurly.http.response").debug(response_xml) return self.update_from_element(ElementTree.fromstring(response_xml)) elif response.status == 201: response_xml = response.read() - logging.getLogger('recurly.http.response').debug(response_xml) + logging.getLogger("recurly.http.response").debug(response_xml) elem = ElementTree.fromstring(response_xml) return self.value_for_element(elem) elif response.status == 204: @@ -576,14 +616,15 @@ def actionator(*args, **kwargs): return extra_handler(response) else: self.raise_http_error(response) + return actionator - #usually the path is the same as the element name + # usually the path is the same as the element name def __getpath__(self, name): return name def __getattr__(self, name): - if name.startswith('_'): + if name.startswith("_"): raise AttributeError(name) try: @@ -601,20 +642,21 @@ def __getattr__(self, name): if elem is None: # It might be an link. - for anchor_elem in selfnode.findall('a'): - if anchor_elem.attrib.get('name') == name: - url = anchor_elem.attrib['href'] - method = anchor_elem.attrib['method'].upper() + for anchor_elem in selfnode.findall("a"): + if anchor_elem.attrib.get("name") == name: + url = anchor_elem.attrib["href"] + method = anchor_elem.attrib["method"].upper() return self._make_actionator(url, method) raise AttributeError(name) # Follow links. - if 'href' in elem.attrib: + if "href" in elem.attrib: + def make_relatitator(url): def relatitator(**kwargs): if kwargs: - full_url = '%s?%s' % (url, urlencode_params(kwargs)) + full_url = "%s?%s" % (url, urlencode_params(kwargs)) else: full_url = url @@ -624,12 +666,13 @@ def relatitator(**kwargs): if isinstance(value, list): return Page.page_for_value(resp, value) return value + return relatitator - url = elem.attrib['href'] + url = elem.attrib["href"] # has no url or has children - if url == '' or len(elem) > 0: + if url == "" or len(elem) > 0: return self.value_for_element(elem) else: return make_relatitator(url) @@ -649,7 +692,7 @@ def all(cls, **kwargs): """ url = recurly.base_uri() + cls.collection_path if kwargs: - url = '%s?%s' % (url, urlencode_params(kwargs)) + url = "%s?%s" % (url, urlencode_params(kwargs)) return Page.page_for_url(url) @classmethod @@ -659,7 +702,7 @@ def count(cls, **kwargs): """ url = recurly.base_uri() + cls.collection_path if kwargs: - url = '%s?%s' % (url, urlencode_params(kwargs)) + url = "%s?%s" % (url, urlencode_params(kwargs)) return Page.count_for_url(url) def save(self): @@ -671,7 +714,7 @@ def save(self): to its own URL. """ - if hasattr(self, '_url'): + if hasattr(self, "_url"): return self._update() return self._create() @@ -685,32 +728,39 @@ def _create(self): 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'}) + 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) + logging.getLogger("recurly.http.response").debug(response_xml) self.update_from_element(ElementTree.fromstring(response_xml)) 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'}) + 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') + self._url = response.getheader("location") if response.status in (200, 201): response_xml = response.read() - logging.getLogger('recurly.http.response').debug(response_xml) + logging.getLogger("recurly.http.response").debug(response_xml) self.update_from_element(ElementTree.fromstring(response_xml)) def delete(self): """Submits a deletion request for this `Resource` instance as a ``DELETE`` request to its URL.""" - response = self.http_request(self._url, 'DELETE') + response = self.http_request(self._url, "DELETE") if response.status != 204: self.raise_http_error(response) @@ -719,7 +769,7 @@ 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) + logging.getLogger("recurly.http.response").debug(response_xml) exc_class = recurly.errors.error_class_for_http_status(response.status) raise exc_class(response_xml) @@ -738,12 +788,12 @@ def to_element(self, root_name=None): except KeyError: continue # With one exception, type is an element xml attribute, e.g. or - # For billing_info, type property takes precedence over xml attribute when type = bacs or becs, e.g. bacs. + # For billing_info, type property takes precedence over xml attribute when type = bacs or becs, e.g. bacs. if attrname in self.xml_attribute_attributes and ( - (root_name != 'billing_info' and attrname == 'type') - or (root_name == 'billing_info' and value not in ('bacs', 'becs')) + (root_name != "billing_info" and attrname == "type") + or (root_name == "billing_info" and value not in ("bacs", "becs")) ): - elem.attrib[attrname] = six.text_type(value) + elem.attrib[attrname] = six.text_type(value) else: sub_elem = self.element_for_value(attrname, value) elem.append(sub_elem) diff --git a/requirements-test.txt b/requirements-test.txt index 5793aa61..be231c4f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,5 @@ mock six unittest2 -defusedxml \ No newline at end of file +defusedxml +black==22.3.0 \ No newline at end of file diff --git a/scripts/check-deps b/scripts/check-deps deleted file mode 100755 index 416a1f31..00000000 --- a/scripts/check-deps +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -e - -function check_for_command { - if ! hash eval "$1" 2>/dev/null; then - echo "✖ Could not find '$1'" - exit 1 - fi - echo "✓ Found $1" -} - -check_for_command "python3" -check_for_command "pip3" diff --git a/scripts/install-deps b/scripts/install-deps deleted file mode 100755 index 7ef57790..00000000 --- a/scripts/install-deps +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -e - -if ! hash brew 2>/dev/null; then - echo "✖ Homebrew not found. Please install: https://brew.sh/" - exit 1 -else - echo "✓ Found homebrew" -fi - -if ! hash python3 2>/dev/null; then - read -p "✖ 'python3' not found. Would you like to install it? [Y/n] " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy\n]$ ]] - then - brew install python3 && true - else - exit 1 - fi -else - echo "✓ Found python3" -fi diff --git a/scripts/test b/scripts/test index c5fc4955..b3a919da 100755 --- a/scripts/test +++ b/scripts/test @@ -1,4 +1,13 @@ #!/usr/bin/env bash +# First check formatting +echo "Checking style..." +if ./scripts/format --check; then + echo "Style check passed" +else + echo "Code does not conform to style guide. To autoformat, run './scripts/format'." + exit 1 +fi + set -e RECURLY_INSECURE_DEBUG=true python -m unittest discover -s tests diff --git a/setup.py b/setup.py index be1437f8..cd9eff1c 100755 --- a/setup.py +++ b/setup.py @@ -2,45 +2,46 @@ import os.path import re -with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as README: +with open(os.path.join(os.path.dirname(__file__), "README.rst")) as README: DESCRIPTION = README.read() -VERSION_RE = re.compile("^__version__ = '(.+)'$", - flags=re.MULTILINE) -with open(os.path.join(os.path.dirname(__file__), - 'recurly', '__init__.py')) as PACKAGE: +VERSION_RE = re.compile("^__version__ = '(.+)'$", flags=re.MULTILINE) +with open(os.path.join(os.path.dirname(__file__), "recurly", "__init__.py")) as PACKAGE: VERSION = VERSION_RE.search(PACKAGE.read()).group(1) more_install_requires = list() try: import ssl except ImportError: - more_install_requires.append('ssl') + more_install_requires.append("ssl") setup( - name='recurly', + name="recurly", version=VERSION, description="The official Recurly API client", long_description=DESCRIPTION, - author='Recurly', - author_email='support@recurly.com', - url='https://github.com/recurly/recurly-client-python', - license='MIT', + author="Recurly", + author_email="support@recurly.com", + url="https://github.com/recurly/recurly-client-python", + license="MIT", classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Topic :: Internet :: WWW/HTTP', + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Internet :: WWW/HTTP", ], - packages=['recurly'], - install_requires=['iso8601<1.0.0', 'backports.ssl_match_hostname', 'six>=1.4.0', 'defusedxml'] + more_install_requires, - tests_require=['mock', - 'six', - 'unittest2', - 'defusedxml'], - test_suite='unittest2.collector', + packages=["recurly"], + install_requires=[ + "iso8601<1.0.0", + "backports.ssl_match_hostname", + "six>=1.4.0", + "defusedxml", + ] + + more_install_requires, + tests_require=["mock", "six", "unittest2", "defusedxml"], + test_suite="unittest2.collector", zip_safe=True, ) diff --git a/tests/recurlytests.py b/tests/recurlytests.py index da8e4afe..6717af79 100644 --- a/tests/recurlytests.py +++ b/tests/recurlytests.py @@ -19,23 +19,24 @@ def xml(text): doc = ElementTree.fromstring(text) for el in doc.iter(): if el.text and el.text.isspace(): - el.text = '' + el.text = "" if el.tail and el.tail.isspace(): - el.tail = '' - return ElementTree.tostring(doc, encoding='UTF-8') + el.tail = "" + return ElementTree.tostring(doc, encoding="UTF-8") class MockRequestManager(object): - def __init__(self, fixture): self.fixture = fixture def __enter__(self): - self.request_context = mock.patch.object(http_client.HTTPConnection, 'request') + self.request_context = mock.patch.object(http_client.HTTPConnection, "request") self.request_context.return_value = None self.request_mock = self.request_context.__enter__() - self.fixture_file = open(join(dirname(__file__), 'fixtures', self.fixture), 'rb') + self.fixture_file = open( + join(dirname(__file__), "fixtures", self.fixture), "rb" + ) # Read through the request. preamble_line = self.fixture_file.readline().strip() @@ -44,8 +45,10 @@ def __enter__(self): self.method = self.method.decode() self.uri = self.uri.decode() except ValueError: - raise ValueError("Couldn't parse preamble line from fixture file %r; does it have a fixture in it?" - % self.fixture) + raise ValueError( + "Couldn't parse preamble line from fixture file %r; does it have a fixture in it?" + % self.fixture + ) # Read request headers def read_headers(fp): @@ -54,17 +57,22 @@ def read_headers(fp): line = fp.readline() except EOFError: return - if not line or line == six.b('\n'): + if not line or line == six.b("\n"): return yield line if six.PY2: msg = http_client.HTTPMessage(self.fixture_file, 0) - self.headers = dict((k.lower(), v.strip()) for k, v in (header.split(':', 1) for header in msg.headers)) + self.headers = dict( + (k.lower(), v.strip()) + for k, v in (header.split(":", 1) for header in msg.headers) + ) else: # http.client.HTTPMessage doesn't have importing headers from file msg = http_client.HTTPMessage() - headers = email.message_from_bytes(six.b('').join(read_headers(self.fixture_file))) + headers = email.message_from_bytes( + six.b("").join(read_headers(self.fixture_file)) + ) self.headers = dict((k.lower(), v.strip()) for k, v in headers._headers) # self.headers = {k: v for k, v in headers._headers} msg.fp = None @@ -76,15 +84,17 @@ def nextline(fp): line = fp.readline() except EOFError: return - if not line or line.startswith(six.b('\x16')): + if not line or line.startswith(six.b("\x16")): return yield line - body = six.b('').join(nextline(self.fixture_file)) # exhaust the request either way + body = six.b("").join( + nextline(self.fixture_file) + ) # exhaust the request either way self.body = None - if self.method in ('PUT', 'POST'): - if 'content-type' in self.headers: - if 'application/xml' in self.headers['content-type']: + if self.method in ("PUT", "POST"): + if "content-type" in self.headers: + if "application/xml" in self.headers["content-type"]: self.body = xml(body) else: self.body = body @@ -95,8 +105,9 @@ def nextline(fp): response = http_client.HTTPResponse(sock, method=self.method) response.begin() - - self.response_context = mock.patch.object(http_client.HTTPConnection, 'getresponse', lambda self: response) + self.response_context = mock.patch.object( + http_client.HTTPConnection, "getresponse", lambda self: response + ) self.response_mock = self.response_context.__enter__() recurly.cache_rate_limit_headers(self.headers) @@ -105,11 +116,18 @@ def nextline(fp): def assert_request(self): headers = dict(self.headers) - if 'user-agent' in headers: + if "user-agent" in headers: import recurly - headers['user-agent'] = headers['user-agent'].replace('{user-agent}', recurly.USER_AGENT) - headers['x-api-version'] = headers['x-api-version'].replace('{api-version}', recurly.API_VERSION) - self.request_mock.assert_called_once_with(self.method, self.uri, self.body, headers) + + headers["user-agent"] = headers["user-agent"].replace( + "{user-agent}", recurly.USER_AGENT + ) + headers["x-api-version"] = headers["x-api-version"].replace( + "{api-version}", recurly.API_VERSION + ) + self.request_mock.assert_called_once_with( + self.method, self.uri, self.body, headers + ) def __exit__(self, exc_type, exc_value, traceback): self.fixture_file.close() @@ -127,7 +145,6 @@ def noop_request_manager(): class RecurlyTest(unittest.TestCase): - def mock_request(self, *args, **kwargs): return MockRequestManager(*args, **kwargs) @@ -145,25 +162,25 @@ def setUp(self): # Mock everything out unless we have an API key. try: - api_key = os.environ['RECURLY_API_KEY'] + api_key = os.environ["RECURLY_API_KEY"] except KeyError: # Mock everything out. - recurly.API_KEY = 'apikey' - self.test_id = 'mock' + 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') + 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') + 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'] + recurly_host = os.environ["RECURLY_HOST"] except KeyError: pass else: - recurly.BASE_URI = 'https://%s/v2/' % recurly_host + recurly.BASE_URI = "https://%s/v2/" % recurly_host logging.basicConfig(level=logging.INFO) - logging.getLogger('recurly').setLevel(logging.DEBUG) + logging.getLogger("recurly").setLevel(logging.DEBUG) diff --git a/tests/test_recurly.py b/tests/test_recurly.py index 880665fd..a69d1e74 100644 --- a/tests/test_recurly.py +++ b/tests/test_recurly.py @@ -5,21 +5,25 @@ class TestRecurly(unittest.TestCase): - def test_hello(self): import recurly def test_xml(self): import recurly + account = recurly.Account() - account.username = 'importantbreakfast' - account_xml = ElementTree.tostring(account.to_element(), encoding='UTF-8') - self.assertEqual(account_xml, xml('importantbreakfast')) + account.username = "importantbreakfast" + account_xml = ElementTree.tostring(account.to_element(), encoding="UTF-8") + self.assertEqual( + account_xml, + xml("importantbreakfast"), + ) def test_objects_for_push_notification(self): import recurly - objs = recurly.objects_for_push_notification(""" + objs = recurly.objects_for_push_notification( + """ verena@test.com @@ -46,15 +50,16 @@ def test_objects_for_push_notification(self): 2009-11-22T13:10:38-08:00 2009-11-29T13:10:38-08:00 - """) - self.assertEqual(objs['type'], 'new_subscription_notification') - self.assertTrue('account' in objs) - self.assertTrue(isinstance(objs['account'], recurly.Account)) - self.assertEqual(objs['account'].username, 'verena') - self.assertTrue('subscription' in objs) - self.assertTrue(isinstance(objs['subscription'], recurly.Subscription)) - self.assertEqual(objs['subscription'].state, 'active') + """ + ) + self.assertEqual(objs["type"], "new_subscription_notification") + self.assertTrue("account" in objs) + self.assertTrue(isinstance(objs["account"], recurly.Account)) + self.assertEqual(objs["account"].username, "verena") + self.assertTrue("subscription" in objs) + self.assertTrue(isinstance(objs["subscription"], recurly.Subscription)) + self.assertEqual(objs["subscription"].state, "active") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_resources.py b/tests/test_resources.py index fd625ec4..54ce780f 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -6,589 +6,631 @@ from six.moves.urllib.parse import urljoin import recurly -from recurly import Account, AddOn, Address, Adjustment, BillingInfo, Coupon, Item, Plan, Redemption, Subscription, \ - SubscriptionAddOn, Transaction, MeasuredUnit, Usage, GiftCard, Delivery, ShippingAddress, AccountAcquisition, \ - Purchase, Invoice, InvoiceCollection, CreditPayment, CustomField, ExportDate, ExportDateFile, DunningCampaign, \ - DunningCycle, InvoiceTemplate, PlanRampInterval +from recurly import ( + Account, + AddOn, + Address, + Adjustment, + BillingInfo, + Coupon, + Item, + Plan, + Redemption, + Subscription, + SubscriptionAddOn, + Transaction, + MeasuredUnit, + Usage, + GiftCard, + Delivery, + ShippingAddress, + AccountAcquisition, + Purchase, + Invoice, + InvoiceCollection, + CreditPayment, + CustomField, + ExportDate, + ExportDateFile, + DunningCampaign, + DunningCycle, + InvoiceTemplate, + PlanRampInterval, +) from recurly import Money, NotFoundError, ValidationError, BadRequestError, PageError from recurly import recurly_logging as logging from recurlytests import RecurlyTest -recurly.SUBDOMAIN = 'api' +recurly.SUBDOMAIN = "api" class TestResources(RecurlyTest): - def test_authentication(self): recurly.API_KEY = None - account_code = 'test%s' % self.test_id + account_code = "test%s" % self.test_id try: Account.get(account_code) except recurly.UnauthorizedError as exc: pass else: - self.fail("Updating account with invalid email address did not raise a ValidationError") + self.fail( + "Updating account with invalid email address did not raise a ValidationError" + ) def test_config_string_types(self): - recurly.API_KEY = six.u('\xe4 unicode string') + recurly.API_KEY = six.u("\xe4 unicode string") - account_code = 'test%s' % self.test_id + account_code = "test%s" % self.test_id try: Account.get(account_code) except recurly.ConfigurationError as exc: pass else: - self.fail("Updating account with invalid email address did not raise a ValidationError") + self.fail( + "Updating account with invalid email address did not raise a ValidationError" + ) def test_cached_response_headers(self): - account_code = 'test%s' % self.test_id - with self.mock_request('account/exists-with-rate-limit-headers.xml'): + account_code = "test%s" % self.test_id + with self.mock_request("account/exists-with-rate-limit-headers.xml"): account = Account.get(account_code) - self.assertEqual(recurly.cached_rate_limits['limit'], 2000) - self.assertEqual(recurly.cached_rate_limits['remaining'], 1992) - self.assertEqual(recurly.cached_rate_limits['resets_at'], datetime(2017, 2, 2, 19, 46)) - self.assertIsInstance(recurly.cached_rate_limits['cached_at'], datetime) + self.assertEqual(recurly.cached_rate_limits["limit"], 2000) + self.assertEqual(recurly.cached_rate_limits["remaining"], 1992) + self.assertEqual( + recurly.cached_rate_limits["resets_at"], datetime(2017, 2, 2, 19, 46) + ) + self.assertIsInstance(recurly.cached_rate_limits["cached_at"], datetime) def test_credit_payment(self): - with self.mock_request('credit-payment/show.xml'): - payment = CreditPayment.get('43c29728b3482fa8727edb4cefa7c774') + with self.mock_request("credit-payment/show.xml"): + payment = CreditPayment.get("43c29728b3482fa8727edb4cefa7c774") self.assertIsInstance(payment, CreditPayment) - self.assertEquals(payment.uuid, '43c29728b3482fa8727edb4cefa7c774') + self.assertEquals(payment.uuid, "43c29728b3482fa8727edb4cefa7c774") self.assertEquals(payment.amount_in_cents, 12) - self.assertEquals(payment.currency, 'USD') - self.assertEquals(payment.action, 'payment') + self.assertEquals(payment.currency, "USD") + self.assertEquals(payment.action, "payment") def test_purchase(self): - account_code = 'test%s' % self.test_id + account_code = "test%s" % self.test_id + def create_purchase(): return Purchase( - currency = 'USD', - gateway_code = 'aBcD1234', - collection_method = 'manual', - shipping_address = ShippingAddress( - first_name = 'Verena', - last_name = 'Example', - address1 = '456 Pillow Fort Drive', - city = 'New Orleans', - state = 'LA', - zip = '70114', - country = 'US', - nickname = 'Work' + currency="USD", + gateway_code="aBcD1234", + collection_method="manual", + shipping_address=ShippingAddress( + first_name="Verena", + last_name="Example", + address1="456 Pillow Fort Drive", + city="New Orleans", + state="LA", + zip="70114", + country="US", + nickname="Work", ), - account = Account( - account_code = account_code, - billing_info = BillingInfo( - first_name = 'Verena', - last_name = 'Example', - number = '4111-1111-1111-1111', - verification_value = '123', - month = 11, - year = 2020, - address1 = '123 Main St', - city = 'New Orleans', - state = 'LA', - zip = '70114', - country = 'US', - ) + account=Account( + account_code=account_code, + billing_info=BillingInfo( + first_name="Verena", + last_name="Example", + number="4111-1111-1111-1111", + verification_value="123", + month=11, + year=2020, + address1="123 Main St", + city="New Orleans", + state="LA", + zip="70114", + country="US", + ), ), - subscriptions = [ - recurly.Subscription(plan_code = 'gold') + subscriptions=[recurly.Subscription(plan_code="gold")], + adjustments=[ + recurly.Adjustment( + unit_amount_in_cents=1000, description="Item 1", quantity=1 + ), + recurly.Adjustment( + unit_amount_in_cents=2000, description="Item 2", quantity=2 + ), ], - adjustments = [ - recurly.Adjustment(unit_amount_in_cents=1000, description='Item 1', - quantity=1), - recurly.Adjustment(unit_amount_in_cents=2000, description='Item 2', - quantity=2), - ] ) - - with self.mock_request('purchase/invoiced.xml'): + + with self.mock_request("purchase/invoiced.xml"): collection = create_purchase().invoice() self.assertIsInstance(collection, InvoiceCollection) self.assertIsInstance(collection.charge_invoice, Invoice) self.assertIsInstance(collection.credit_invoices, list) self.assertIsInstance(collection.credit_invoices[0], Invoice) - self.assertIsInstance(collection.charge_invoice.line_items[0].shipping_address, - ShippingAddress) - with self.mock_request('purchase/previewed.xml'): + self.assertIsInstance( + collection.charge_invoice.line_items[0].shipping_address, + ShippingAddress, + ) + with self.mock_request("purchase/previewed.xml"): collection = create_purchase().preview() self.assertIsInstance(collection, InvoiceCollection) self.assertIsInstance(collection.charge_invoice, Invoice) - with self.mock_request('purchase/authorized.xml'): + with self.mock_request("purchase/authorized.xml"): purchase = create_purchase() - purchase.account.email = 'benjamin.dumonde@example.com' - purchase.account.billing_info.external_hpp_type = 'adyen' + purchase.account.email = "benjamin.dumonde@example.com" + purchase.account.billing_info.external_hpp_type = "adyen" collection = purchase.authorize() self.assertIsInstance(collection, InvoiceCollection) self.assertIsInstance(collection.charge_invoice, Invoice) - with self.mock_request('purchase/captured.xml'): - captured_collection = create_purchase().capture('40625fdb0d71f87624a285476ba7d73d') + with self.mock_request("purchase/captured.xml"): + captured_collection = create_purchase().capture( + "40625fdb0d71f87624a285476ba7d73d" + ) self.assertIsInstance(captured_collection, InvoiceCollection) - self.assertEquals(captured_collection.charge_invoice.state, 'paid') - with self.mock_request('purchase/cancelled.xml'): - cancelled_collection = create_purchase().cancel('40625fdb0d71f87624a285476ba7d73d') + self.assertEquals(captured_collection.charge_invoice.state, "paid") + with self.mock_request("purchase/cancelled.xml"): + cancelled_collection = create_purchase().cancel( + "40625fdb0d71f87624a285476ba7d73d" + ) self.assertIsInstance(cancelled_collection, InvoiceCollection) - self.assertEquals(cancelled_collection.charge_invoice.state, 'failed') - with self.mock_request('purchase/pending.xml'): + self.assertEquals(cancelled_collection.charge_invoice.state, "failed") + with self.mock_request("purchase/pending.xml"): purchase = create_purchase() - purchase.account.email = 'benjamin.dumonde@example.com' - purchase.account.billing_info.external_hpp_type = 'adyen' + purchase.account.email = "benjamin.dumonde@example.com" + purchase.account.billing_info.external_hpp_type = "adyen" collection = purchase.pending() self.assertIsInstance(collection, InvoiceCollection) self.assertIsInstance(collection.charge_invoice, Invoice) - with self.mock_request('purchase/pending-ideal.xml'): + with self.mock_request("purchase/pending-ideal.xml"): purchase = create_purchase() - purchase.account.email = 'benjamin.dumonde@example.com' - purchase.account.billing_info.online_banking_payment_type = 'ideal' + purchase.account.email = "benjamin.dumonde@example.com" + purchase.account.billing_info.online_banking_payment_type = "ideal" collection = purchase.pending() self.assertIsInstance(collection, InvoiceCollection) self.assertIsInstance(collection.charge_invoice, Invoice) - with self.mock_request('purchase/invoiced-billing-info-uuid.xml'): + with self.mock_request("purchase/invoiced-billing-info-uuid.xml"): purchase = create_purchase() - purchase.account = Account(account_code = account_code) + purchase.account = Account(account_code=account_code) purchase.billing_info_uuid = "uniqueUuid" del purchase.adjustments collection = purchase.invoice() self.assertIsInstance(collection, InvoiceCollection) self.assertIsInstance(collection.charge_invoice, Invoice) - self.assertEqual(purchase.billing_info_uuid, 'uniqueUuid') + self.assertEqual(purchase.billing_info_uuid, "uniqueUuid") def test_account(self): - account_code = 'test%s' % self.test_id - with self.mock_request('account/does-not-exist.xml'): + account_code = "test%s" % self.test_id + with self.mock_request("account/does-not-exist.xml"): self.assertRaises(NotFoundError, Account.get, account_code) account = Account(account_code=account_code) - account.vat_number = '444444-UK' - account.preferred_locale = 'en-US' - with self.mock_request('account/created.xml'): + account.vat_number = "444444-UK" + account.preferred_locale = "en-US" + with self.mock_request("account/created.xml"): account.save() - self.assertEqual(account._url, urljoin(recurly.base_uri(), 'accounts/%s' % account_code)) - self.assertEqual(account.vat_number, '444444-UK') + self.assertEqual( + account._url, urljoin(recurly.base_uri(), "accounts/%s" % account_code) + ) + self.assertEqual(account.vat_number, "444444-UK") self.assertEqual(account.vat_location_enabled, True) - self.assertEqual(account.cc_emails, - 'test1@example.com,test2@example.com') - self.assertEqual(account.preferred_locale, 'en-US') + self.assertEqual(account.cc_emails, "test1@example.com,test2@example.com") + self.assertEqual(account.preferred_locale, "en-US") - with self.mock_request('account/list-active.xml'): + with self.mock_request("account/list-active.xml"): active = Account.all_active() self.assertTrue(len(active) >= 1) self.assertEqual(active[0].account_code, account_code) - with self.mock_request('account/exists.xml'): + with self.mock_request("account/exists.xml"): same_account = Account.get(account_code) self.assertTrue(isinstance(same_account, Account)) self.assertTrue(same_account is not account) self.assertEqual(same_account.account_code, account_code) self.assertTrue(same_account.first_name is None) - self.assertTrue(same_account.entity_use_code == 'I') - self.assertEqual(same_account._url, urljoin(recurly.base_uri(), 'accounts/%s' % account_code)) + self.assertTrue(same_account.entity_use_code == "I") + self.assertEqual( + same_account._url, urljoin(recurly.base_uri(), "accounts/%s" % account_code) + ) - with self.mock_request('account-balance/exists.xml'): + with self.mock_request("account-balance/exists.xml"): account_balance = same_account.account_balance() self.assertTrue(account_balance.past_due) balance = account_balance.balance_in_cents - self.assertTrue(balance['USD'] == 2910) - self.assertTrue(balance['EUR'] == -520) - processing_prepayment_balance = account_balance.processing_prepayment_balance_in_cents - self.assertTrue(processing_prepayment_balance['USD'] == -3000) - self.assertTrue(processing_prepayment_balance['EUR'] == 0) - - account.username = 'shmohawk58' - account.email = 'larry.david' - account.first_name = six.u('L\xe4rry') - account.last_name = 'David' - account.company_name = 'Home Box Office' - account.accept_language = 'en-US' - with self.mock_request('account/update-bad-email.xml'): + self.assertTrue(balance["USD"] == 2910) + self.assertTrue(balance["EUR"] == -520) + processing_prepayment_balance = ( + account_balance.processing_prepayment_balance_in_cents + ) + self.assertTrue(processing_prepayment_balance["USD"] == -3000) + self.assertTrue(processing_prepayment_balance["EUR"] == 0) + + account.username = "shmohawk58" + account.email = "larry.david" + account.first_name = six.u("L\xe4rry") + account.last_name = "David" + account.company_name = "Home Box Office" + account.accept_language = "en-US" + with self.mock_request("account/update-bad-email.xml"): try: account.save() except ValidationError as exc: self.assertTrue(isinstance(exc.errors, collections.Mapping)) - self.assertTrue('account.email' in exc.errors) - suberror = exc.errors['account.email'] - self.assertEqual(suberror.symbol, 'invalid_email') + self.assertTrue("account.email" in exc.errors) + suberror = exc.errors["account.email"] + self.assertEqual(suberror.symbol, "invalid_email") self.assertTrue(suberror.message) self.assertEqual(suberror.message, suberror.message) else: - self.fail("Updating account with invalid email address did not raise a ValidationError") + self.fail( + "Updating account with invalid email address did not raise a ValidationError" + ) - account.email = 'larry.david@example.com' - with self.mock_request('account/updated.xml'): + account.email = "larry.david@example.com" + with self.mock_request("account/updated.xml"): account.save() - with self.mock_request('account/deleted.xml'): + with self.mock_request("account/deleted.xml"): account.delete() - with self.mock_request('account/list-closed.xml'): + with self.mock_request("account/list-closed.xml"): closed = Account.all_closed() self.assertTrue(len(closed) >= 1) self.assertEqual(closed[0].account_code, account_code) - with self.mock_request('account/list-active-when-closed.xml'): + with self.mock_request("account/list-active-when-closed.xml"): active = Account.all_active() self.assertTrue(len(active) < 1 or active[0].account_code != account_code) # Make sure we can reopen a closed account. - with self.mock_request('account/reopened.xml'): + with self.mock_request("account/reopened.xml"): account.reopen() try: - with self.mock_request('account/list-active.xml'): + with self.mock_request("account/list-active.xml"): active = Account.all_active() self.assertTrue(len(active) >= 1) self.assertEqual(active[0].account_code, account_code) finally: - with self.mock_request('account/deleted.xml'): + with self.mock_request("account/deleted.xml"): account.delete() # Make sure numeric account codes work. - if self.test_id == 'mock': + if self.test_id == "mock": numeric_test_id = 58 else: numeric_test_id = int(self.test_id) account = Account(account_code=numeric_test_id) - with self.mock_request('account/numeric-created.xml'): + with self.mock_request("account/numeric-created.xml"): account.save() try: - self.assertEqual(account._url, urljoin(recurly.base_uri(), 'accounts/%d' % numeric_test_id)) + self.assertEqual( + account._url, + urljoin(recurly.base_uri(), "accounts/%d" % numeric_test_id), + ) finally: - with self.mock_request('account/numeric-deleted.xml'): + with self.mock_request("account/numeric-deleted.xml"): account.delete() """Get taxed account""" - with self.mock_request('account/show-taxed.xml'): + with self.mock_request("account/show-taxed.xml"): account = Account.get(account_code) self.assertTrue(account.tax_exempt) - self.assertEqual(account.exemption_certificate, 'Some Certificate') + self.assertEqual(account.exemption_certificate, "Some Certificate") def test_account_addresses(self): - account_code = 'test%s' % self.test_id + account_code = "test%s" % self.test_id """Create an account with an account level address""" account = Account(account_code=account_code) - account.address.address1 = '123 Main St' - account.address.city = 'San Francisco' - account.address.zip = '94105' - account.address.state = 'CA' - account.address.country = 'US' - account.address.phone = '8015559876' - - with self.mock_request('account/created-with-address.xml'): + account.address.address1 = "123 Main St" + account.address.city = "San Francisco" + account.address.zip = "94105" + account.address.state = "CA" + account.address.country = "US" + account.address.phone = "8015559876" + + with self.mock_request("account/created-with-address.xml"): account.save() - self.assertEqual(account.address.address1, '123 Main St') - self.assertEqual(account.address.city, 'San Francisco') - self.assertEqual(account.address.zip, '94105') - self.assertEqual(account.address.state, 'CA') - self.assertEqual(account.address.country, 'US') - self.assertEqual(account.address.phone, '8015559876') + self.assertEqual(account.address.address1, "123 Main St") + self.assertEqual(account.address.city, "San Francisco") + self.assertEqual(account.address.zip, "94105") + self.assertEqual(account.address.state, "CA") + self.assertEqual(account.address.country, "US") + self.assertEqual(account.address.phone, "8015559876") """Create an account with an account shipping address""" account = Account(account_code=account_code) shipping_address = ShippingAddress() - shipping_address.address1 = '123 Main St' - shipping_address.city = 'San Francisco' - shipping_address.zip = '94105' - shipping_address.state = 'CA' - shipping_address.country = 'US' - shipping_address.phone = '8015559876' - shipping_address.nickname = 'Work' + shipping_address.address1 = "123 Main St" + shipping_address.city = "San Francisco" + shipping_address.zip = "94105" + shipping_address.state = "CA" + shipping_address.country = "US" + shipping_address.phone = "8015559876" + shipping_address.nickname = "Work" account.shipping_addresses = [shipping_address] - with self.mock_request('account/created-with-shipping-address.xml'): + with self.mock_request("account/created-with-shipping-address.xml"): account.save() shipping_address = ShippingAddress() - shipping_address.address1 = '123 Dolores St' - shipping_address.city = 'San Francisco' - shipping_address.zip = '94105' - shipping_address.state = 'CA' - shipping_address.country = 'US' - shipping_address.phone = '8015559876' - shipping_address.nickname = 'Home' - - with self.mock_request('shipping_addresses/created-on-existing-account.xml'): + shipping_address.address1 = "123 Dolores St" + shipping_address.city = "San Francisco" + shipping_address.zip = "94105" + shipping_address.state = "CA" + shipping_address.country = "US" + shipping_address.phone = "8015559876" + shipping_address.nickname = "Home" + + with self.mock_request("shipping_addresses/created-on-existing-account.xml"): shipping_address = account.create_shipping_address(shipping_address) def test_account_billing_infos(self): - account = Account(account_code='binfo%s' % self.test_id) - with self.mock_request('billing-info/account-created.xml'): + account = Account(account_code="binfo%s" % self.test_id) + with self.mock_request("billing-info/account-created.xml"): account.save() - self.assertRaises(AttributeError, getattr, account, 'billing_info') + self.assertRaises(AttributeError, getattr, account, "billing_info") billing_info1 = BillingInfo( - first_name = 'Humberto', - last_name = 'DuMonde', - number = '4111111111111111', - verification_value = '123', - month = 10, - year = 2049, - address1 = '12345 Main St', - city = 'New Orleans', - state = 'LA', - zip = '70114', - country = 'US', - primary_payment_method = True, - backup_payment_method = False + first_name="Humberto", + last_name="DuMonde", + number="4111111111111111", + verification_value="123", + month=10, + year=2049, + address1="12345 Main St", + city="New Orleans", + state="LA", + zip="70114", + country="US", + primary_payment_method=True, + backup_payment_method=False, ) - with self.mock_request('billing-info/created-billing-infos.xml'): + with self.mock_request("billing-info/created-billing-infos.xml"): account.create_billing_info(billing_info1) self.assertTrue(billing_info1.primary_payment_method) self.assertFalse(billing_info1.backup_payment_method) - with self.mock_request('billing-info/account-exists.xml'): - same_account = Account.get('binfo%s' % self.test_id) - with self.mock_request('billing-info/exists-billing-infos.xml'): - binfo = same_account.get_billing_info('op9snjf3yjn8') - self.assertEquals(binfo.first_name, 'Humberto') + with self.mock_request("billing-info/account-exists.xml"): + same_account = Account.get("binfo%s" % self.test_id) + with self.mock_request("billing-info/exists-billing-infos.xml"): + binfo = same_account.get_billing_info("op9snjf3yjn8") + self.assertEquals(binfo.first_name, "Humberto") def test_account_custom_fields(self): - account_code = 'test%s' % self.test_id + account_code = "test%s" % self.test_id """Create an account with a custom field""" account = Account( account_code=account_code, - custom_fields=[ - CustomField(name="field_1", value="my field value") - ] + custom_fields=[CustomField(name="field_1", value="my field value")], ) - with self.mock_request('account/created-with-custom-fields.xml'): + with self.mock_request("account/created-with-custom-fields.xml"): account.save() - self.assertEquals(account.custom_fields[0].name, 'field_1') - self.assertEquals(account.custom_fields[0].value, 'my field value') + self.assertEquals(account.custom_fields[0].name, "field_1") + self.assertEquals(account.custom_fields[0].value, "my field value") """Update custom fields on an account""" - with self.mock_request('account/exists-custom-fields.xml'): + with self.mock_request("account/exists-custom-fields.xml"): existing_account = Account.get(account_code) fields = existing_account.custom_fields fields[1].value = "new value2" existing_account.custom_fields = fields - with self.mock_request('account/updated-custom-fields.xml'): + with self.mock_request("account/updated-custom-fields.xml"): existing_account.save() - self.assertEquals(existing_account.custom_fields[0].name, 'field1') - self.assertEquals(existing_account.custom_fields[0].value, 'original value1') - self.assertEquals(existing_account.custom_fields[1].name, 'field2') - self.assertEquals(existing_account.custom_fields[1].value, 'new value2') + self.assertEquals(existing_account.custom_fields[0].name, "field1") + self.assertEquals(existing_account.custom_fields[0].value, "original value1") + self.assertEquals(existing_account.custom_fields[1].name, "field2") + self.assertEquals(existing_account.custom_fields[1].value, "new value2") def test_account_hierarchy(self): - account_code = 'test%s' % self.test_id + account_code = "test%s" % self.test_id """Create an account with a parent""" account = Account( - account_code=account_code, - parent_account_code="parent-account" + account_code=account_code, parent_account_code="parent-account" ) - with self.mock_request('account/created-with-parent.xml'): + with self.mock_request("account/created-with-parent.xml"): account.save() """Get Parent account""" - with self.mock_request('account/exists-with-child-accounts.xml'): + with self.mock_request("account/exists-with-child-accounts.xml"): parent = account.parent_account() - self.assertEquals(parent.account_code, 'parent-account') + self.assertEquals(parent.account_code, "parent-account") """Get Child accounts""" - with self.mock_request('account/child-accounts.xml'): + with self.mock_request("account/child-accounts.xml"): for acct in parent.child_accounts(): - self.assertEquals(acct.account_code, 'test%s' % self.test_id) + self.assertEquals(acct.account_code, "test%s" % self.test_id) def test_account_acquisition(self): - account_code = 'test%s' % self.test_id + account_code = "test%s" % self.test_id """Create an account with an account acquisition""" account = Account(account_code=account_code) acquisition = AccountAcquisition() acquisition.cost_in_cents = 199 - acquisition.currency = 'USD' - acquisition.channel = 'blog' - acquisition.subchannel = 'Whitepaper Blog Post' - acquisition.campaign = 'mailchimp67a904de95.0914d8f4b4' + acquisition.currency = "USD" + acquisition.channel = "blog" + acquisition.subchannel = "Whitepaper Blog Post" + acquisition.campaign = "mailchimp67a904de95.0914d8f4b4" account.account_acquisition = acquisition - with self.mock_request('account/created-with-account-acquisition.xml'): + with self.mock_request("account/created-with-account-acquisition.xml"): account.save() """Get the acquisition from the account""" - with self.mock_request('account-acquisition/exists.xml'): + with self.mock_request("account-acquisition/exists.xml"): acquisition = account.account_acquisition() self.assertEquals(acquisition.cost_in_cents, 199) - self.assertEquals(acquisition.currency, 'USD') - self.assertEquals(acquisition.channel, 'blog') - self.assertEquals(acquisition.subchannel, 'Whitepaper Blog Post') - self.assertEquals(acquisition.campaign, 'mailchimp67a904de95.0914d8f4b4') + self.assertEquals(acquisition.currency, "USD") + self.assertEquals(acquisition.channel, "blog") + self.assertEquals(acquisition.subchannel, "Whitepaper Blog Post") + self.assertEquals(acquisition.campaign, "mailchimp67a904de95.0914d8f4b4") """Update the acquisition""" acquisition.cost_in_cents = 200 - acquisition.currency = 'EUR' - acquisition.channel = 'social_media' - acquisition.subchannel = 'Facebook Post' - acquisition.campaign = 'hubspot123456' + acquisition.currency = "EUR" + acquisition.channel = "social_media" + acquisition.subchannel = "Facebook Post" + acquisition.campaign = "hubspot123456" - with self.mock_request('account-acquisition/updated.xml'): + with self.mock_request("account-acquisition/updated.xml"): acquisition.save() self.assertEquals(acquisition.cost_in_cents, 200) - self.assertEquals(acquisition.currency, 'EUR') - self.assertEquals(acquisition.channel, 'social_media') - self.assertEquals(acquisition.subchannel, 'Facebook Post') - self.assertEquals(acquisition.campaign, 'hubspot123456') + self.assertEquals(acquisition.currency, "EUR") + self.assertEquals(acquisition.channel, "social_media") + self.assertEquals(acquisition.subchannel, "Facebook Post") + self.assertEquals(acquisition.campaign, "hubspot123456") - with self.mock_request('account-acquisition/deleted.xml'): + with self.mock_request("account-acquisition/deleted.xml"): acquisition.delete() def test_add_on(self): - plan_code = 'plan%s' % self.test_id - add_on_code = 'addon%s' % self.test_id + plan_code = "plan%s" % self.test_id + add_on_code = "addon%s" % self.test_id plan = Plan( plan_code=plan_code, - name='Mock Plan', + name="Mock Plan", setup_fee_in_cents=Money(0), unit_amount_in_cents=Money(1000), ) - with self.mock_request('add-on/plan-created.xml'): + with self.mock_request("add-on/plan-created.xml"): plan.save() try: # a usage based add on add_on = AddOn( add_on_code=add_on_code, - name='Mock Add-On', + name="Mock Add-On", add_on_type="usage", usage_type="price", measured_unit_id=123456, ) exc = None - with self.mock_request('add-on/need-amount.xml'): + with self.mock_request("add-on/need-amount.xml"): try: plan.create_add_on(add_on) except ValidationError as _exc: exc = _exc else: - self.fail("Creating a plan add-on without an amount did not raise a ValidationError") - error = exc.errors['add_on.unit_amount_in_cents'] - self.assertEqual(error.symbol, 'blank') + self.fail( + "Creating a plan add-on without an amount did not raise a ValidationError" + ) + error = exc.errors["add_on.unit_amount_in_cents"] + self.assertEqual(error.symbol, "blank") add_on.unit_amount_in_cents = Money(40) - with self.mock_request('add-on/created.xml'): + with self.mock_request("add-on/created.xml"): plan.create_add_on(add_on) self.assertEqual(add_on.add_on_code, add_on_code) - self.assertEqual(add_on.name, 'Mock Add-On') + self.assertEqual(add_on.name, "Mock Add-On") try: - with self.mock_request('add-on/exists.xml'): + with self.mock_request("add-on/exists.xml"): same_add_on = plan.get_add_on(add_on_code) self.assertEqual(same_add_on.add_on_code, add_on_code) - self.assertEqual(same_add_on.name, 'Mock Add-On') - self.assertEqual(same_add_on.unit_amount_in_cents['USD'], 40) + self.assertEqual(same_add_on.name, "Mock Add-On") + self.assertEqual(same_add_on.unit_amount_in_cents["USD"], 40) finally: - with self.mock_request('add-on/deleted.xml'): + with self.mock_request("add-on/deleted.xml"): add_on.delete() finally: - with self.mock_request('add-on/plan-deleted.xml'): + with self.mock_request("add-on/plan-deleted.xml"): plan.delete() def test_add_on_with_tiered_pricing(self): - plan_code = 'plan%s' % self.test_id - add_on_code = 'addon%s' % self.test_id + plan_code = "plan%s" % self.test_id + add_on_code = "addon%s" % self.test_id plan = Plan( plan_code=plan_code, - name='Mock Plan', + name="Mock Plan", setup_fee_in_cents=Money(0), unit_amount_in_cents=Money(1000), ) - with self.mock_request('add-on/plan-created.xml'): + with self.mock_request("add-on/plan-created.xml"): plan.save() try: add_on = AddOn( - add_on_code = add_on_code, - name = 'Mock Add-On', - tier_type = "tiered", - tiers = [ + add_on_code=add_on_code, + name="Mock Add-On", + tier_type="tiered", + tiers=[ recurly.Tier( - ending_quantity = 2000, - unit_amount_in_cents = recurly.Money(USD=1000) + ending_quantity=2000, + unit_amount_in_cents=recurly.Money(USD=1000), ), - recurly.Tier( - unit_amount_in_cents = recurly.Money(USD=800) - ) - ] + recurly.Tier(unit_amount_in_cents=recurly.Money(USD=800)), + ], ) - with self.mock_request('add-on/created-tiered.xml'): + with self.mock_request("add-on/created-tiered.xml"): plan.create_add_on(add_on) self.assertEqual(add_on.add_on_code, add_on_code) try: - with self.mock_request('add-on/exists-tiered.xml'): + with self.mock_request("add-on/exists-tiered.xml"): tiered_add_on = plan.get_add_on(add_on_code) self.assertEqual(tiered_add_on.add_on_code, add_on_code) self.assertEqual(tiered_add_on.tier_type, "tiered") finally: - with self.mock_request('add-on/deleted.xml'): + with self.mock_request("add-on/deleted.xml"): add_on.delete() finally: - with self.mock_request('add-on/plan-deleted.xml'): + with self.mock_request("add-on/plan-deleted.xml"): plan.delete() def test_add_on_with_percentage_tiered_pricing(self): - plan_code = 'plan%s' % self.test_id - add_on_code = 'addon%s' % self.test_id + plan_code = "plan%s" % self.test_id + add_on_code = "addon%s" % self.test_id plan = Plan( plan_code=plan_code, - name='Mock Plan', + name="Mock Plan", setup_fee_in_cents=Money(0), unit_amount_in_cents=Money(1000), ) - with self.mock_request('add-on/plan-created.xml'): + with self.mock_request("add-on/plan-created.xml"): plan.save() try: add_on = AddOn( - add_on_code = add_on_code, - name = 'Mock Add-On', - tier_type = "tiered", - add_on_type = "usage", - usage_type = "percentage", - measured_unit_id = "3473591245469944008", - display_quantity_on_hosted_page = True, - percentage_tiers = [ + add_on_code=add_on_code, + name="Mock Add-On", + tier_type="tiered", + add_on_type="usage", + usage_type="percentage", + measured_unit_id="3473591245469944008", + display_quantity_on_hosted_page=True, + percentage_tiers=[ recurly.CurrencyPercentageTier( - currency = 'USD', - tiers = [ + currency="USD", + tiers=[ recurly.PercentageTier( - ending_amount_in_cents = 20000, - usage_percentage = '20' + ending_amount_in_cents=20000, usage_percentage="20" ), recurly.PercentageTier( - ending_amount_in_cents = 40000, - usage_percentage = '25' + ending_amount_in_cents=40000, usage_percentage="25" ), - recurly.PercentageTier( - usage_percentage = '30' - ) - ] + recurly.PercentageTier(usage_percentage="30"), + ], ) ], ) - with self.mock_request('add-on/created-percentage-tiered.xml'): + with self.mock_request("add-on/created-percentage-tiered.xml"): plan.create_add_on(add_on) self.assertEqual(add_on.add_on_code, add_on_code) try: - with self.mock_request('add-on/exists-percentage-tiered.xml'): + with self.mock_request("add-on/exists-percentage-tiered.xml"): tiered_add_on = plan.get_add_on(add_on_code) self.assertEqual(tiered_add_on.add_on_code, add_on_code) self.assertEqual(tiered_add_on.tier_type, "tiered") @@ -596,104 +638,101 @@ def test_add_on_with_percentage_tiered_pricing(self): self.assertEqual(len(tiered_add_on.percentage_tiers[0].tiers), 3) finally: - with self.mock_request('add-on/deleted.xml'): + with self.mock_request("add-on/deleted.xml"): add_on.delete() finally: - with self.mock_request('add-on/plan-deleted.xml'): + with self.mock_request("add-on/plan-deleted.xml"): plan.delete() def test_item_backed_add_on(self): - plan_code = 'plan%s' % self.test_id - item_code = 'item%s' % self.test_id + plan_code = "plan%s" % self.test_id + item_code = "item%s" % self.test_id plan = Plan( plan_code=plan_code, - name='Mock Plan', + name="Mock Plan", setup_fee_in_cents=Money(0), unit_amount_in_cents=Money(1000), ) - with self.mock_request('add-on/plan-created.xml'): + with self.mock_request("add-on/plan-created.xml"): plan.save() try: - add_on = AddOn( - item_code= item_code, - unit_amount_in_cents = Money(500) - ) + add_on = AddOn(item_code=item_code, unit_amount_in_cents=Money(500)) - with self.mock_request('add-on/created-item-backed.xml'): + with self.mock_request("add-on/created-item-backed.xml"): plan.create_add_on(add_on) self.assertEqual(add_on.add_on_code, item_code) finally: - with self.mock_request('add-on/plan-deleted.xml'): + with self.mock_request("add-on/plan-deleted.xml"): plan.delete() def test_billing_info(self): logging.basicConfig(level=logging.DEBUG) # make sure it's init'ed - logger = logging.getLogger('recurly.http.request') + logger = logging.getLogger("recurly.http.request") logger.setLevel(logging.DEBUG) log_content = StringIO() log_handler = logging.StreamHandler(log_content) logger.addHandler(log_handler) - account = Account(account_code='binfo%s' % self.test_id) - with self.mock_request('billing-info/account-created.xml'): + account = Account(account_code="binfo%s" % self.test_id) + with self.mock_request("billing-info/account-created.xml"): account.save() logger.removeHandler(log_handler) - self.assertTrue(' 0) self.assertEqual(subs[0].uuid, sub.uuid) - with self.mock_request('subscription/all-subscriptions.xml'): + with self.mock_request("subscription/all-subscriptions.xml"): subs = Subscription.all() self.assertTrue(len(subs) > 0) self.assertEqual(subs[0].uuid, sub.uuid) - with self.mock_request('subscription/cancelled.xml'): + with self.mock_request("subscription/cancelled.xml"): sub.cancel() - with self.mock_request('subscription/reactivated.xml'): + with self.mock_request("subscription/reactivated.xml"): sub.reactivate() # Try modifying the subscription. - sub.timeframe = 'renewal' + sub.timeframe = "renewal" sub.unit_amount_in_cents = 800 - with self.mock_request('subscription/updated-at-renewal.xml'): + with self.mock_request("subscription/updated-at-renewal.xml"): sub.save() pending_sub = sub.pending_subscription self.assertTrue(isinstance(pending_sub, Subscription)) self.assertEqual(pending_sub.unit_amount_in_cents, 800) self.assertEqual(sub.unit_amount_in_cents, 1000) - with self.mock_request('subscription/terminated.xml'): - sub.terminate(refund='none') + with self.mock_request("subscription/terminated.xml"): + sub.terminate(refund="none") log_content = StringIO() log_handler = logging.StreamHandler(log_content) logger.addHandler(log_handler) sub = Subscription( - plan_code='basicplan', - currency='USD', + plan_code="basicplan", + currency="USD", account=Account( - account_code='subscribe%s' % self.test_id, + account_code="subscribe%s" % self.test_id, billing_info=BillingInfo( - first_name='Verena', - last_name='Example', - address1='123 Main St', - city=six.u('San Jos\xe9'), - state='CA', - zip='94105', - country='US', - type='credit_card', - number='4111 1111 1111 1111', - verification_value='7777', - year='2015', - month='12', + first_name="Verena", + last_name="Example", + address1="123 Main St", + city=six.u("San Jos\xe9"), + state="CA", + zip="94105", + country="US", + type="credit_card", + number="4111 1111 1111 1111", + verification_value="7777", + year="2015", + month="12", ), ), ) - with self.mock_request('subscription/subscribed-billing-info.xml'): + with self.mock_request("subscription/subscribed-billing-info.xml"): account.subscribe(sub) logger.removeHandler(log_handler) log_content = log_content.getvalue() - self.assertTrue(' 0) - account_code = 'transbalance%s' % self.test_id + account_code = "transbalance%s" % self.test_id account = Account(account_code=account_code) - with self.mock_request('transaction-balance/account-created.xml'): + with self.mock_request("transaction-balance/account-created.xml"): account.save() try: # Try to charge without billing info, should break. transaction = Transaction( amount_in_cents=1000, - currency='USD', + currency="USD", account=account, ) error = None - with self.mock_request('transaction-balance/transaction-no-billing-fails.xml'): + with self.mock_request( + "transaction-balance/transaction-no-billing-fails.xml" + ): try: transaction.save() except ValidationError as _error: error = _error else: - self.fail("Posting a transaction without billing info did not raise a ValidationError") + self.fail( + "Posting a transaction without billing info did not raise a ValidationError" + ) # Make sure there really were errors. self.assertTrue(len(error.errors) > 0) binfo = BillingInfo( - first_name='Verena', - last_name='Example', - address1='123 Main St', - city=six.u('San Jos\xe9'), - state='CA', - zip='94105', - country='US', - type='credit_card', - number='4111 1111 1111 1111', - verification_value='7777', - year='2015', - month='12', + first_name="Verena", + last_name="Example", + address1="123 Main St", + city=six.u("San Jos\xe9"), + state="CA", + zip="94105", + country="US", + type="credit_card", + number="4111 1111 1111 1111", + verification_value="7777", + year="2015", + month="12", ) - with self.mock_request('transaction-balance/set-billing-info.xml'): + with self.mock_request("transaction-balance/set-billing-info.xml"): account.update_billing_info(binfo) # Try to charge now, should be okay. transaction = Transaction( amount_in_cents=1000, - currency='USD', + currency="USD", account=account, ) - with self.mock_request('transaction-balance/transacted.xml'): + with self.mock_request("transaction-balance/transacted.xml"): transaction.save() # Give the account a credit. - credit = Adjustment(unit_amount_in_cents=-2000, currency='USD', description='transaction test credit') - with self.mock_request('transaction-balance/credited.xml'): + credit = Adjustment( + unit_amount_in_cents=-2000, + currency="USD", + description="transaction test credit", + ) + with self.mock_request("transaction-balance/credited.xml"): # TODO: maybe this should be adjust()? account.charge(credit) # Try to charge less than the account balance, which should fail (not a CC transaction). transaction = Transaction( amount_in_cents=500, - currency='USD', + currency="USD", account=account, ) - with self.mock_request('transaction-balance/transacted-2.xml'): + with self.mock_request("transaction-balance/transacted-2.xml"): transaction.save() # The transaction doesn't actually save. self.assertTrue(transaction._url is None) @@ -2299,50 +2405,50 @@ def test_transaction_with_balance(self): # Try to charge more than the account balance, which should work. transaction = Transaction( amount_in_cents=3000, - currency='USD', + currency="USD", account=account, ) - with self.mock_request('transaction-balance/transacted-3.xml'): + with self.mock_request("transaction-balance/transacted-3.xml"): transaction.save() # This transaction should be recorded. self.assertTrue(transaction._url is not None) finally: - with self.mock_request('transaction-balance/account-deleted.xml'): + with self.mock_request("transaction-balance/account-deleted.xml"): account.delete() def _build_gift_card(self): - account_code = 'e0004e3c-216c-4254-8767-9be605cd0b03' + account_code = "e0004e3c-216c-4254-8767-9be605cd0b03" account = recurly.Account(account_code=account_code) - account.email = 'verena@example.com' - account.first_name = 'Verena' - account.last_name = 'Example' + account.email = "verena@example.com" + account.first_name = "Verena" + account.last_name = "Example" billing_info = BillingInfo() - billing_info.first_name = 'Verena' - billing_info.last_name = 'Example' - billing_info.number = '4111-1111-1111-1111' - billing_info.verification_value = '123' + billing_info.first_name = "Verena" + billing_info.last_name = "Example" + billing_info.number = "4111-1111-1111-1111" + billing_info.verification_value = "123" billing_info.month = 11 billing_info.year = 2019 - billing_info.country = 'US' + billing_info.country = "US" address = Address() - address.address1 = '400 Alabama St' - address.zip = '94110' - address.city = 'San Francisco' - address.state = 'CA' - address.country = 'US' + address.address1 = "400 Alabama St" + address.zip = "94110" + address.city = "San Francisco" + address.state = "CA" + address.country = "US" delivery = Delivery() - delivery.method = 'email' - delivery.email_address = 'john@email.com' - delivery.first_name = 'John' - delivery.last_name = 'Smith' + delivery.method = "email" + delivery.email_address = "john@email.com" + delivery.first_name = "John" + delivery.last_name = "Smith" gift_card = GiftCard() - gift_card.product_code = 'test_gift_card' - gift_card.currency = 'USD' + gift_card.product_code = "test_gift_card" + gift_card.currency = "USD" gift_card.unit_amount_in_cents = 2000 delivery.address = address @@ -2356,11 +2462,11 @@ def test_gift_cards_purchase(self): gift_card = self._build_gift_card() # now allowed to send a top-level billing info along - gift_card.billing_info = BillingInfo(token_id='1234') + gift_card.billing_info = BillingInfo(token_id="1234") - self.assertFalse('_url' in gift_card.attributes) + self.assertFalse("_url" in gift_card.attributes) - with self.mock_request('gift_cards/created.xml'): + with self.mock_request("gift_cards/created.xml"): gift_card.save() self.assertTrue(gift_card._url is not None) @@ -2370,33 +2476,33 @@ def test_gift_cards_purchase(self): def test_gift_cards_preview(self): gift_card = self._build_gift_card() - self.assertFalse('_url' in gift_card.attributes) + self.assertFalse("_url" in gift_card.attributes) - with self.mock_request('gift_cards/preview.xml'): + with self.mock_request("gift_cards/preview.xml"): gift_card.preview() self.assertTrue(gift_card.id is None) - self.assertFalse('_url' in gift_card.attributes) + self.assertFalse("_url" in gift_card.attributes) def test_gift_cards_redeem(self): - gift_card = GiftCard(redemption_code='9FC359369CD3892E') + gift_card = GiftCard(redemption_code="9FC359369CD3892E") - with self.mock_request('gift_cards/redeem.xml'): - gift_card.redeem('e0004e3c-216c-4254-8767-9be605cd0b03') + with self.mock_request("gift_cards/redeem.xml"): + gift_card.redeem("e0004e3c-216c-4254-8767-9be605cd0b03") self.assertTrue(gift_card.redeemed_at is not None) def test_gift_cards_redeem_with_url(self): - gift_card = GiftCard(redemption_code='9FC359369CD3892E') - gift_card._url = 'https://api.recurly.com/v2/gift_cards/9FC359369CD3892E' + gift_card = GiftCard(redemption_code="9FC359369CD3892E") + gift_card._url = "https://api.recurly.com/v2/gift_cards/9FC359369CD3892E" - with self.mock_request('gift_cards/redeem.xml'): - gift_card.redeem('e0004e3c-216c-4254-8767-9be605cd0b03') + with self.mock_request("gift_cards/redeem.xml"): + gift_card.redeem("e0004e3c-216c-4254-8767-9be605cd0b03") self.assertTrue(gift_card.redeemed_at is not None) def test_export_date(self): - with self.mock_request('export-date/export-date.xml'): + with self.mock_request("export-date/export-date.xml"): export_dates = ExportDate.all() self.assertEqual(len(export_dates), 1) @@ -2405,27 +2511,40 @@ def test_export_date(self): def test_export_date_files(self): export_date = ExportDate() - with self.mock_request('export-date-files/export-date-files-list.xml'): + with self.mock_request("export-date-files/export-date-files-list.xml"): export_date_files = export_date.files("2019-05-09") self.assertEqual(len(export_date_files), 1) - self.assertEqual(export_date_files[0].name, "churned_subscriptions_v2_expires.csv.gz") + self.assertEqual( + export_date_files[0].name, "churned_subscriptions_v2_expires.csv.gz" + ) def test_export_date_files_download_information(self): export_date = ExportDate() - with self.mock_request('export-date-files/export-date-files-list.xml'): + with self.mock_request("export-date-files/export-date-files-list.xml"): export_date_files = export_date.files("2019-05-09") - with self.mock_request('export-date-files/export-date-file-download-information.xml'): - export_date_file_download_information = export_date_files[0].download_information() + with self.mock_request( + "export-date-files/export-date-file-download-information.xml" + ): + export_date_file_download_information = export_date_files[ + 0 + ].download_information() self.assertEqual( - export_date_file_download_information.expires_at.strftime("%Y-%m-%d %H:%M:%S"), "2019-05-09 14:00:00" + export_date_file_download_information.expires_at.strftime( + "%Y-%m-%d %H:%M:%S" + ), + "2019-05-09 14:00:00", + ) + self.assertEqual( + export_date_file_download_information.download_url, + "https://api.recurly.com/download", ) - self.assertEqual(export_date_file_download_information.download_url, "https://api.recurly.com/download") -if __name__ == '__main__': +if __name__ == "__main__": import unittest + unittest.main() diff --git a/tests/tests_errors.py b/tests/tests_errors.py index b83d1cd2..5f30105c 100644 --- a/tests/tests_errors.py +++ b/tests/tests_errors.py @@ -1,35 +1,38 @@ import recurly from recurly import Account, Transaction, ValidationError -from recurly.errors import UnexpectedStatusError, UnexpectedClientError, UnexpectedServerError +from recurly.errors import ( + UnexpectedStatusError, + UnexpectedClientError, + UnexpectedServerError, +) from recurlytests import RecurlyTest + class RecurlyExceptionTests(RecurlyTest): def test_error_printable(self): - """ Make sure __str__/__unicode__ works correctly in Python 2/3""" - str(recurly.UnauthorizedError('recurly.API_KEY not set')) + """Make sure __str__/__unicode__ works correctly in Python 2/3""" + str(recurly.UnauthorizedError("recurly.API_KEY not set")) def test_validationerror_printable(self): - """ Make sure __str__/__unicode__ works correctly in Python 2/3""" - error = recurly.ValidationError.Suberror('field', 'symbol', 'message') + """Make sure __str__/__unicode__ works correctly in Python 2/3""" + error = recurly.ValidationError.Suberror("field", "symbol", "message") suberrors = dict() - suberrors['field'] = error - validation_error = recurly.ValidationError('') - validation_error.__dict__['errors'] = suberrors + suberrors["field"] = error + validation_error = recurly.ValidationError("") + validation_error.__dict__["errors"] = suberrors str(validation_error) def test_transaction_error_property(self): - """ Test ValidationError class 'transaction_error' property""" + """Test ValidationError class 'transaction_error' property""" transaction = Transaction( amount_in_cents=1000, - currency='USD', - account=Account( - account_code='transactionmock' - ) + currency="USD", + account=Account(account_code="transactionmock"), ) # Mock 'save transaction' request to throw declined # transaction validation error - with self.mock_request('transaction/declined-transaction.xml'): + with self.mock_request("transaction/declined-transaction.xml"): try: transaction.save() except ValidationError as e: @@ -37,44 +40,46 @@ def test_transaction_error_property(self): transaction_error = error.transaction_error - self.assertEqual(transaction_error.error_code, 'insufficient_funds') - self.assertEqual(transaction_error.error_category, 'soft') - self.assertEqual(transaction_error.customer_message, "The transaction was declined due to insufficient funds in your account. Please use a different card or contact your bank.") - self.assertEqual(transaction_error.merchant_message, "The card has insufficient funds to cover the cost of the transaction.") + self.assertEqual(transaction_error.error_code, "insufficient_funds") + self.assertEqual(transaction_error.error_category, "soft") + self.assertEqual( + transaction_error.customer_message, + "The transaction was declined due to insufficient funds in your account. Please use a different card or contact your bank.", + ) + self.assertEqual( + transaction_error.merchant_message, + "The card has insufficient funds to cover the cost of the transaction.", + ) self.assertEqual(transaction_error.gateway_error_code, "123") def test_transaction_error_code_property(self): - """ Test ValidationError class 'transaction_error_code' property""" + """Test ValidationError class 'transaction_error_code' property""" transaction = Transaction( amount_in_cents=1000, - currency='USD', - account=Account( - account_code='transactionmock' - ) + currency="USD", + account=Account(account_code="transactionmock"), ) # Mock 'save transaction' request to throw declined # transaction validation error - with self.mock_request('transaction/declined-transaction.xml'): + with self.mock_request("transaction/declined-transaction.xml"): try: transaction.save() except ValidationError as e: error = e - self.assertEqual(error.transaction_error_code, 'insufficient_funds') + self.assertEqual(error.transaction_error_code, "insufficient_funds") def test_unexpected_errors_thrown(self): - """ Test UnexpectedClientError class """ + """Test UnexpectedClientError class""" transaction = Transaction( amount_in_cents=1000, - currency='USD', - account=Account( - account_code='transactionmock' - ) + currency="USD", + account=Account(account_code="transactionmock"), ) # Mock 'save transaction' request to throw unexpected client error - with self.mock_request('transaction/error-teapot.xml'): + with self.mock_request("transaction/error-teapot.xml"): try: transaction.save() except UnexpectedStatusError as e: @@ -83,7 +88,7 @@ def test_unexpected_errors_thrown(self): self.assertIsInstance(error, UnexpectedClientError) # Mock 'save transaction' request to throw another unexpected client error - with self.mock_request('transaction/error-client.xml'): + with self.mock_request("transaction/error-client.xml"): try: transaction.save() except UnexpectedStatusError as e: @@ -92,7 +97,7 @@ def test_unexpected_errors_thrown(self): self.assertIsInstance(error, UnexpectedClientError) # Mock 'save transaction' request to throw unexpected server error - with self.mock_request('transaction/error-server.xml'): + with self.mock_request("transaction/error-server.xml"): try: transaction.save() except UnexpectedStatusError as e: