diff --git a/setup.py b/setup.py index 996816191..01a650ed7 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,9 @@ author='Stripe', author_email='support@stripe.com', url='https://github.com/stripe/stripe-python', - packages=['stripe', 'stripe.test', 'stripe.test.resources'], + packages=['stripe', 'stripe.api_resources', + 'stripe.api_resources.abstract', + 'stripe.test', 'stripe.test.resources'], package_data={'stripe': ['data/ca-certificates.crt']}, install_requires=install_requires, test_suite='stripe.test.all', diff --git a/stripe/api_resources/__init__.py b/stripe/api_resources/__init__.py new file mode 100644 index 000000000..4e2891131 --- /dev/null +++ b/stripe/api_resources/__init__.py @@ -0,0 +1,42 @@ +# flake8: noqa + +from stripe.api_resources.list_object import ListObject + +from stripe.api_resources.account import Account +from stripe.api_resources.alipay_account import AlipayAccount +from stripe.api_resources.apple_pay_domain import ApplePayDomain +from stripe.api_resources.application_fee import ApplicationFee +from stripe.api_resources.application_fee_refund import ApplicationFeeRefund +from stripe.api_resources.balance import Balance +from stripe.api_resources.balance_transaction import BalanceTransaction +from stripe.api_resources.bank_account import BankAccount +from stripe.api_resources.bitcoin_receiver import BitcoinReceiver +from stripe.api_resources.bitcoin_transaction import BitcoinTransaction +from stripe.api_resources.card import Card +from stripe.api_resources.charge import Charge +from stripe.api_resources.country_spec import CountrySpec +from stripe.api_resources.coupon import Coupon +from stripe.api_resources.customer import Customer +from stripe.api_resources.dispute import Dispute +from stripe.api_resources.ephemeral_key import EphemeralKey +from stripe.api_resources.event import Event +from stripe.api_resources.file_upload import FileUpload +from stripe.api_resources.invoice import Invoice +from stripe.api_resources.invoice_item import InvoiceItem +from stripe.api_resources.login_link import LoginLink +from stripe.api_resources.order import Order +from stripe.api_resources.order_return import OrderReturn +from stripe.api_resources.payout import Payout +from stripe.api_resources.plan import Plan +from stripe.api_resources.product import Product +from stripe.api_resources.recipient import Recipient +from stripe.api_resources.recipient_transfer import RecipientTransfer +from stripe.api_resources.refund import Refund +from stripe.api_resources.reversal import Reversal +from stripe.api_resources.sku import SKU +from stripe.api_resources.source import Source +from stripe.api_resources.subscription import Subscription +from stripe.api_resources.subscription_item import SubscriptionItem +from stripe.api_resources.three_d_secure import ThreeDSecure +from stripe.api_resources.token import Token +from stripe.api_resources.transfer import Transfer diff --git a/stripe/api_resources/abstract/__init__.py b/stripe/api_resources/abstract/__init__.py new file mode 100644 index 000000000..1e88a2840 --- /dev/null +++ b/stripe/api_resources/abstract/__init__.py @@ -0,0 +1,20 @@ +# flake8: noqa + +from stripe.api_resources.abstract.api_resource import APIResource +from stripe.api_resources.abstract.singleton_api_resource import ( + SingletonAPIResource +) + +from stripe.api_resources.abstract.createable_api_resource import ( + CreateableAPIResource +) +from stripe.api_resources.abstract.updateable_api_resource import ( + UpdateableAPIResource +) +from stripe.api_resources.abstract.deletable_api_resource import ( + DeletableAPIResource +) +from stripe.api_resources.abstract.listable_api_resource import ( + ListableAPIResource +) +from stripe.api_resources.abstract.verify_mixin import VerifyMixin diff --git a/stripe/api_resources/abstract/api_resource.py b/stripe/api_resources/abstract/api_resource.py new file mode 100644 index 000000000..0bf71ba66 --- /dev/null +++ b/stripe/api_resources/abstract/api_resource.py @@ -0,0 +1,44 @@ +import urllib + +from stripe import error, util +from stripe.stripe_object import StripeObject + + +class APIResource(StripeObject): + + @classmethod + def retrieve(cls, id, api_key=None, **params): + instance = cls(id, api_key, **params) + instance.refresh() + return instance + + def refresh(self): + self.refresh_from(self.request('get', self.instance_url())) + return self + + @classmethod + def class_name(cls): + if cls == APIResource: + raise NotImplementedError( + 'APIResource is an abstract class. You should perform ' + 'actions on its subclasses (e.g. Charge, Customer)') + return str(urllib.quote_plus(cls.__name__.lower())) + + @classmethod + def class_url(cls): + cls_name = cls.class_name() + return "/v1/%ss" % (cls_name,) + + def instance_url(self): + id = self.get('id') + + if not isinstance(id, basestring): + raise error.InvalidRequestError( + 'Could not determine which URL to request: %s instance ' + 'has invalid ID: %r, %s. ID should be of type `str` (or' + ' `unicode`)' % (type(self).__name__, id, type(id)), 'id') + + id = util.utf8(id) + base = self.class_url() + extn = urllib.quote_plus(id) + return "%s/%s" % (base, extn) diff --git a/stripe/api_resources/abstract/createable_api_resource.py b/stripe/api_resources/abstract/createable_api_resource.py new file mode 100644 index 000000000..666e4b3da --- /dev/null +++ b/stripe/api_resources/abstract/createable_api_resource.py @@ -0,0 +1,17 @@ +from stripe.api_resources.abstract.api_resource import APIResource +from stripe import api_requestor, util + + +class CreateableAPIResource(APIResource): + + @classmethod + def create(cls, api_key=None, idempotency_key=None, + stripe_version=None, stripe_account=None, **params): + requestor = api_requestor.APIRequestor(api_key, + api_version=stripe_version, + account=stripe_account) + url = cls.class_url() + headers = util.populate_headers(idempotency_key) + response, api_key = requestor.request('post', url, params, headers) + return util.convert_to_stripe_object(response, api_key, stripe_version, + stripe_account) diff --git a/stripe/api_resources/abstract/deletable_api_resource.py b/stripe/api_resources/abstract/deletable_api_resource.py new file mode 100644 index 000000000..851038795 --- /dev/null +++ b/stripe/api_resources/abstract/deletable_api_resource.py @@ -0,0 +1,8 @@ +from stripe.api_resources.abstract.api_resource import APIResource + + +class DeletableAPIResource(APIResource): + + def delete(self, **params): + self.refresh_from(self.request('delete', self.instance_url(), params)) + return self diff --git a/stripe/api_resources/abstract/listable_api_resource.py b/stripe/api_resources/abstract/listable_api_resource.py new file mode 100644 index 000000000..895975d91 --- /dev/null +++ b/stripe/api_resources/abstract/listable_api_resource.py @@ -0,0 +1,34 @@ +import warnings + +from stripe import api_requestor, util +from stripe.api_resources.abstract.api_resource import APIResource + + +class ListableAPIResource(APIResource): + + @classmethod + def all(cls, *args, **params): + warnings.warn("The `all` class method is deprecated and will" + "be removed in future versions. Please use the " + "`list` class method instead", + DeprecationWarning) + return cls.list(*args, **params) + + @classmethod + def auto_paging_iter(cls, *args, **params): + return cls.list(*args, **params).auto_paging_iter() + + @classmethod + def list(cls, api_key=None, stripe_version=None, stripe_account=None, + **params): + requestor = api_requestor.APIRequestor(api_key, + api_base=cls.api_base(), + api_version=stripe_version, + account=stripe_account) + url = cls.class_url() + response, api_key = requestor.request('get', url, params) + stripe_object = util.convert_to_stripe_object(response, api_key, + stripe_version, + stripe_account) + stripe_object._retrieve_params = params + return stripe_object diff --git a/stripe/api_resources/abstract/singleton_api_resource.py b/stripe/api_resources/abstract/singleton_api_resource.py new file mode 100644 index 000000000..1658cc3cb --- /dev/null +++ b/stripe/api_resources/abstract/singleton_api_resource.py @@ -0,0 +1,16 @@ +from stripe.api_resources.abstract.api_resource import APIResource + + +class SingletonAPIResource(APIResource): + + @classmethod + def retrieve(cls, **params): + return super(SingletonAPIResource, cls).retrieve(None, **params) + + @classmethod + def class_url(cls): + cls_name = cls.class_name() + return "/v1/%s" % (cls_name,) + + def instance_url(self): + return self.class_url() diff --git a/stripe/api_resources/abstract/updateable_api_resource.py b/stripe/api_resources/abstract/updateable_api_resource.py new file mode 100644 index 000000000..159b0a672 --- /dev/null +++ b/stripe/api_resources/abstract/updateable_api_resource.py @@ -0,0 +1,34 @@ +import urllib + +from stripe import api_requestor, util +from stripe.api_resources.abstract.api_resource import APIResource + + +class UpdateableAPIResource(APIResource): + + @classmethod + def _modify(cls, url, api_key=None, idempotency_key=None, + stripe_version=None, stripe_account=None, **params): + requestor = api_requestor.APIRequestor(api_key, + api_version=stripe_version, + account=stripe_account) + headers = util.populate_headers(idempotency_key) + response, api_key = requestor.request('post', url, params, headers) + return util.convert_to_stripe_object(response, api_key, stripe_version, + stripe_account) + + @classmethod + def modify(cls, sid, **params): + url = "%s/%s" % (cls.class_url(), urllib.quote_plus(util.utf8(sid))) + return cls._modify(url, **params) + + def save(self, idempotency_key=None): + updated_params = self.serialize(None) + headers = util.populate_headers(idempotency_key) + + if updated_params: + self.refresh_from(self.request('post', self.instance_url(), + updated_params, headers)) + else: + util.logger.debug("Trying to save already saved object %r", self) + return self diff --git a/stripe/api_resources/abstract/verify_mixin.py b/stripe/api_resources/abstract/verify_mixin.py new file mode 100644 index 000000000..35687d4d8 --- /dev/null +++ b/stripe/api_resources/abstract/verify_mixin.py @@ -0,0 +1,10 @@ +from stripe import util + + +class VerifyMixin(object): + + def verify(self, idempotency_key=None, **params): + url = self.instance_url() + '/verify' + headers = util.populate_headers(idempotency_key) + self.refresh_from(self.request('post', url, params, headers)) + return self diff --git a/stripe/api_resources/account.py b/stripe/api_resources/account.py new file mode 100644 index 000000000..4e198946b --- /dev/null +++ b/stripe/api_resources/account.py @@ -0,0 +1,57 @@ +import urllib + +from stripe import oauth, util +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Account(CreateableAPIResource, ListableAPIResource, + UpdateableAPIResource, DeletableAPIResource): + OBJECT_NAME = 'account' + + @classmethod + def retrieve(cls, id=None, api_key=None, **params): + instance = cls(id, api_key, **params) + instance.refresh() + return instance + + @classmethod + def modify(cls, id=None, **params): + return cls._modify(cls._build_instance_url(id), **params) + + @classmethod + def _build_instance_url(cls, sid): + if not sid: + return "/v1/account" + sid = util.utf8(sid) + base = cls.class_url() + extn = urllib.quote_plus(sid) + return "%s/%s" % (base, extn) + + def instance_url(self): + return self._build_instance_url(self.get('id')) + + def reject(self, reason=None, idempotency_key=None): + url = self.instance_url() + '/reject' + headers = util.populate_headers(idempotency_key) + if reason: + params = {"reason": reason} + else: + params = {} + self.refresh_from( + self.request('post', url, params, headers) + ) + return self + + def deauthorize(self, **params): + params['stripe_user_id'] = self.id + return oauth.OAuth.deauthorize(**params) + + @classmethod + def modify_external_account(cls, sid, external_account_id, **params): + url = "%s/%s/external_accounts/%s" % ( + cls.class_url(), urllib.quote_plus(util.utf8(sid)), + urllib.quote_plus(util.utf8(external_account_id))) + return cls._modify(url, **params) diff --git a/stripe/api_resources/alipay_account.py b/stripe/api_resources/alipay_account.py new file mode 100644 index 000000000..6b2624258 --- /dev/null +++ b/stripe/api_resources/alipay_account.py @@ -0,0 +1,36 @@ +import urllib + +from stripe import util +from stripe.api_resources.customer import Customer +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource + + +class AlipayAccount(UpdateableAPIResource, DeletableAPIResource): + OBJECT_NAME = 'alipay_account' + + @classmethod + def _build_instance_url(cls, customer, sid): + token = util.utf8(sid) + extn = urllib.quote_plus(token) + customer = util.utf8(customer) + + base = Customer.class_url() + owner_extn = urllib.quote_plus(customer) + + return "%s/%s/sources/%s" % (base, owner_extn, extn) + + def instance_url(self): + return self._build_instance_url(self.customer, self.id) + + @classmethod + def modify(cls, customer, id, **params): + url = cls._build_instance_url(customer, id) + return cls._modify(url, **params) + + @classmethod + def retrieve(cls, id, api_key=None, stripe_version=None, + stripe_account=None, **params): + raise NotImplementedError( + "Can't retrieve an Alipay account without a customer ID. " + "Use customer.sources.retrieve('alipay_account_id') instead.") diff --git a/stripe/api_resources/apple_pay_domain.py b/stripe/api_resources/apple_pay_domain.py new file mode 100644 index 000000000..71df719bc --- /dev/null +++ b/stripe/api_resources/apple_pay_domain.py @@ -0,0 +1,12 @@ +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class ApplePayDomain(CreateableAPIResource, ListableAPIResource, + DeletableAPIResource): + OBJECT_NAME = 'apple_pay_domain' + + @classmethod + def class_url(cls): + return '/v1/apple_pay/domains' diff --git a/stripe/api_resources/application_fee.py b/stripe/api_resources/application_fee.py new file mode 100644 index 000000000..bc47a29c7 --- /dev/null +++ b/stripe/api_resources/application_fee.py @@ -0,0 +1,16 @@ +from stripe import util +from stripe.api_resources.abstract import ListableAPIResource + + +class ApplicationFee(ListableAPIResource): + OBJECT_NAME = 'application_fee' + + @classmethod + def class_name(cls): + return 'application_fee' + + def refund(self, idempotency_key=None, **params): + headers = util.populate_headers(idempotency_key) + url = self.instance_url() + '/refund' + self.refresh_from(self.request('post', url, params, headers)) + return self diff --git a/stripe/api_resources/application_fee_refund.py b/stripe/api_resources/application_fee_refund.py new file mode 100644 index 000000000..ef8748be4 --- /dev/null +++ b/stripe/api_resources/application_fee_refund.py @@ -0,0 +1,32 @@ +import urllib + +from stripe import util +from stripe.api_resources import ApplicationFee +from stripe.api_resources.abstract import UpdateableAPIResource + + +class ApplicationFeeRefund(UpdateableAPIResource): + OBJECT_NAME = 'fee_refund' + + @classmethod + def _build_instance_url(cls, fee, sid): + fee = util.utf8(fee) + sid = util.utf8(sid) + base = ApplicationFee.class_url() + cust_extn = urllib.quote_plus(fee) + extn = urllib.quote_plus(sid) + return "%s/%s/refunds/%s" % (base, cust_extn, extn) + + @classmethod + def modify(cls, fee, sid, **params): + url = cls._build_instance_url(fee, sid) + return cls._modify(url, **params) + + def instance_url(self): + return self._build_instance_url(self.fee, self.id) + + @classmethod + def retrieve(cls, id, api_key=None, **params): + raise NotImplementedError( + "Can't retrieve a refund without an application fee ID. " + "Use application_fee.refunds.retrieve('refund_id') instead.") diff --git a/stripe/api_resources/balance.py b/stripe/api_resources/balance.py new file mode 100644 index 000000000..cb887e7be --- /dev/null +++ b/stripe/api_resources/balance.py @@ -0,0 +1,5 @@ +from stripe.api_resources.abstract import SingletonAPIResource + + +class Balance(SingletonAPIResource): + OBJECT_NAME = 'balance' diff --git a/stripe/api_resources/balance_transaction.py b/stripe/api_resources/balance_transaction.py new file mode 100644 index 000000000..54de3dfd1 --- /dev/null +++ b/stripe/api_resources/balance_transaction.py @@ -0,0 +1,9 @@ +from stripe.api_resources.abstract import ListableAPIResource + + +class BalanceTransaction(ListableAPIResource): + OBJECT_NAME = 'balance_transaction' + + @classmethod + def class_url(cls): + return '/v1/balance/history' diff --git a/stripe/api_resources/bank_account.py b/stripe/api_resources/bank_account.py new file mode 100644 index 000000000..0cc435b14 --- /dev/null +++ b/stripe/api_resources/bank_account.py @@ -0,0 +1,51 @@ +import urllib + +from stripe import error, util +from stripe.api_resources.account import Account +from stripe.api_resources.customer import Customer +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import VerifyMixin + + +class BankAccount(UpdateableAPIResource, DeletableAPIResource, VerifyMixin): + OBJECT_NAME = 'bank_account' + + def instance_url(self): + token = util.utf8(self.id) + extn = urllib.quote_plus(token) + if hasattr(self, 'customer'): + customer = util.utf8(self.customer) + + base = Customer.class_url() + owner_extn = urllib.quote_plus(customer) + class_base = "sources" + + elif hasattr(self, 'account'): + account = util.utf8(self.account) + + base = Account.class_url() + owner_extn = urllib.quote_plus(account) + class_base = "external_accounts" + + else: + raise error.InvalidRequestError( + "Could not determine whether bank_account_id %s is " + "attached to a customer or an account." % token, 'id') + + return "%s/%s/%s/%s" % (base, owner_extn, class_base, extn) + + @classmethod + def modify(cls, sid, **params): + raise NotImplementedError( + "Can't modify a bank account without a customer or account ID. " + "Call save on customer.sources.retrieve('bank_account_id') or " + "account.external_accounts.retrieve('bank_account_id') instead.") + + @classmethod + def retrieve(cls, id, api_key=None, stripe_version=None, + stripe_account=None, **params): + raise NotImplementedError( + "Can't retrieve a bank account without a customer or account ID. " + "Use customer.sources.retrieve('bank_account_id') or " + "account.external_accounts.retrieve('bank_account_id') instead.") diff --git a/stripe/api_resources/bitcoin_receiver.py b/stripe/api_resources/bitcoin_receiver.py new file mode 100644 index 000000000..78de6424e --- /dev/null +++ b/stripe/api_resources/bitcoin_receiver.py @@ -0,0 +1,30 @@ +import urllib + +from stripe import util +from stripe.api_resources.customer import Customer +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class BitcoinReceiver(CreateableAPIResource, UpdateableAPIResource, + DeletableAPIResource, ListableAPIResource): + OBJECT_NAME = 'bitcoin_receiver' + + def instance_url(self): + token = util.utf8(self.id) + extn = urllib.quote_plus(token) + + if hasattr(self, 'customer'): + customer = util.utf8(self.customer) + base = Customer.class_url() + cust_extn = urllib.quote_plus(customer) + return "%s/%s/sources/%s" % (base, cust_extn, extn) + else: + base = BitcoinReceiver.class_url() + return "%s/%s" % (base, extn) + + @classmethod + def class_url(cls): + return '/v1/bitcoin/receivers' diff --git a/stripe/api_resources/bitcoin_transaction.py b/stripe/api_resources/bitcoin_transaction.py new file mode 100644 index 000000000..37367e3a2 --- /dev/null +++ b/stripe/api_resources/bitcoin_transaction.py @@ -0,0 +1,5 @@ +from stripe.stripe_object import StripeObject + + +class BitcoinTransaction(StripeObject): + OBJECT_NAME = 'bitcoin_transaction' diff --git a/stripe/api_resources/card.py b/stripe/api_resources/card.py new file mode 100644 index 000000000..76e86297e --- /dev/null +++ b/stripe/api_resources/card.py @@ -0,0 +1,61 @@ +import urllib + +from stripe import error, util +from stripe.api_resources.account import Account +from stripe.api_resources.customer import Customer +from stripe.api_resources.recipient import Recipient +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource + + +class Card(UpdateableAPIResource, DeletableAPIResource): + OBJECT_NAME = 'card' + + def instance_url(self): + token = util.utf8(self.id) + extn = urllib.quote_plus(token) + if hasattr(self, 'customer'): + customer = util.utf8(self.customer) + + base = Customer.class_url() + owner_extn = urllib.quote_plus(customer) + class_base = "sources" + + elif hasattr(self, 'recipient'): + recipient = util.utf8(self.recipient) + + base = Recipient.class_url() + owner_extn = urllib.quote_plus(recipient) + class_base = "cards" + + elif hasattr(self, 'account'): + account = util.utf8(self.account) + + base = Account.class_url() + owner_extn = urllib.quote_plus(account) + class_base = "external_accounts" + + else: + raise error.InvalidRequestError( + "Could not determine whether card_id %s is " + "attached to a customer, recipient, or " + "account." % token, 'id') + + return "%s/%s/%s/%s" % (base, owner_extn, class_base, extn) + + @classmethod + def modify(cls, sid, **params): + raise NotImplementedError( + "Can't modify a card without a customer, recipient or account " + "ID. Call save on customer.sources.retrieve('card_id'), " + "recipient.cards.retrieve('card_id'), or " + "account.external_accounts.retrieve('card_id') instead.") + + @classmethod + def retrieve(cls, id, api_key=None, stripe_version=None, + stripe_account=None, **params): + raise NotImplementedError( + "Can't retrieve a card without a customer, recipient or account " + "ID. Use customer.sources.retrieve('card_id'), " + "recipient.cards.retrieve('card_id'), or " + "account.external_accounts.retrieve('card_id') instead.") diff --git a/stripe/api_resources/charge.py b/stripe/api_resources/charge.py new file mode 100644 index 000000000..19eb1c1be --- /dev/null +++ b/stripe/api_resources/charge.py @@ -0,0 +1,59 @@ +from stripe import api_requestor, util +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Charge(CreateableAPIResource, ListableAPIResource, + UpdateableAPIResource): + OBJECT_NAME = 'charge' + + def refund(self, idempotency_key=None, **params): + url = self.instance_url() + '/refund' + headers = util.populate_headers(idempotency_key) + self.refresh_from(self.request('post', url, params, headers)) + return self + + def capture(self, idempotency_key=None, **params): + url = self.instance_url() + '/capture' + headers = util.populate_headers(idempotency_key) + self.refresh_from(self.request('post', url, params, headers)) + return self + + def update_dispute(self, idempotency_key=None, **params): + requestor = api_requestor.APIRequestor(self.api_key, + api_version=self.stripe_version, + account=self.stripe_account) + url = self.instance_url() + '/dispute' + headers = util.populate_headers(idempotency_key) + response, api_key = requestor.request('post', url, params, headers) + self.refresh_from({'dispute': response}, api_key, True) + return self.dispute + + def close_dispute(self, idempotency_key=None): + requestor = api_requestor.APIRequestor(self.api_key, + api_version=self.stripe_version, + account=self.stripe_account) + url = self.instance_url() + '/dispute/close' + headers = util.populate_headers(idempotency_key) + response, api_key = requestor.request('post', url, {}, headers) + self.refresh_from({'dispute': response}, api_key, True) + return self.dispute + + def mark_as_fraudulent(self, idempotency_key=None): + params = { + 'fraud_details': {'user_report': 'fraudulent'} + } + url = self.instance_url() + headers = util.populate_headers(idempotency_key) + self.refresh_from(self.request('post', url, params, headers)) + return self + + def mark_as_safe(self, idempotency_key=None): + params = { + 'fraud_details': {'user_report': 'safe'} + } + url = self.instance_url() + headers = util.populate_headers(idempotency_key) + self.refresh_from(self.request('post', url, params, headers)) + return self diff --git a/stripe/api_resources/country_spec.py b/stripe/api_resources/country_spec.py new file mode 100644 index 000000000..9ab721ef4 --- /dev/null +++ b/stripe/api_resources/country_spec.py @@ -0,0 +1,9 @@ +from stripe.api_resources import abstract + + +class CountrySpec(abstract.ListableAPIResource): + OBJECT_NAME = 'country_spec' + + @classmethod + def class_name(cls): + return 'country_spec' diff --git a/stripe/api_resources/coupon.py b/stripe/api_resources/coupon.py new file mode 100644 index 000000000..c4bc9baf5 --- /dev/null +++ b/stripe/api_resources/coupon.py @@ -0,0 +1,9 @@ +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Coupon(CreateableAPIResource, UpdateableAPIResource, + DeletableAPIResource, ListableAPIResource): + OBJECT_NAME = 'coupon' diff --git a/stripe/api_resources/customer.py b/stripe/api_resources/customer.py new file mode 100644 index 000000000..dbdcc013f --- /dev/null +++ b/stripe/api_resources/customer.py @@ -0,0 +1,83 @@ +import urllib +import warnings + +from stripe import api_requestor, util +from stripe.api_resources.charge import Charge +from stripe.api_resources.invoice import Invoice +from stripe.api_resources.invoice_item import InvoiceItem +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Customer(CreateableAPIResource, UpdateableAPIResource, + ListableAPIResource, DeletableAPIResource): + OBJECT_NAME = 'customer' + + def add_invoice_item(self, idempotency_key=None, **params): + params['customer'] = self.id + ii = InvoiceItem.create(self.api_key, + idempotency_key=idempotency_key, **params) + return ii + + def invoices(self, **params): + params['customer'] = self.id + invoices = Invoice.list(self.api_key, **params) + return invoices + + def invoice_items(self, **params): + params['customer'] = self.id + iis = InvoiceItem.list(self.api_key, **params) + return iis + + def charges(self, **params): + params['customer'] = self.id + charges = Charge.list(self.api_key, **params) + return charges + + def update_subscription(self, idempotency_key=None, **params): + warnings.warn( + 'The `update_subscription` method is deprecated. Instead, use the ' + '`subscriptions` resource on the customer object to update a ' + 'subscription', + DeprecationWarning) + requestor = api_requestor.APIRequestor(self.api_key, + api_version=self.stripe_version, + account=self.stripe_account) + url = self.instance_url() + '/subscription' + headers = util.populate_headers(idempotency_key) + response, api_key = requestor.request('post', url, params, headers) + self.refresh_from({'subscription': response}, api_key, True) + return self.subscription + + def cancel_subscription(self, idempotency_key=None, **params): + warnings.warn( + 'The `cancel_subscription` method is deprecated. Instead, use the ' + '`subscriptions` resource on the customer object to cancel a ' + 'subscription', + DeprecationWarning) + requestor = api_requestor.APIRequestor(self.api_key, + api_version=self.stripe_version, + account=self.stripe_account) + url = self.instance_url() + '/subscription' + headers = util.populate_headers(idempotency_key) + response, api_key = requestor.request('delete', url, params, headers) + self.refresh_from({'subscription': response}, api_key, True) + return self.subscription + + # TODO: Remove arg in next major release. + def delete_discount(self, **params): + requestor = api_requestor.APIRequestor(self.api_key, + api_version=self.stripe_version, + account=self.stripe_account) + url = self.instance_url() + '/discount' + _, api_key = requestor.request('delete', url) + self.refresh_from({'discount': None}, api_key, True) + + @classmethod + def modify_source(cls, sid, source_id, **params): + url = "%s/%s/sources/%s" % ( + cls.class_url(), urllib.quote_plus(util.utf8(sid)), + urllib.quote_plus(util.utf8(source_id))) + return cls._modify(url, **params) diff --git a/stripe/api_resources/dispute.py b/stripe/api_resources/dispute.py new file mode 100644 index 000000000..7f28942f0 --- /dev/null +++ b/stripe/api_resources/dispute.py @@ -0,0 +1,15 @@ +from stripe import util +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Dispute(CreateableAPIResource, ListableAPIResource, + UpdateableAPIResource): + OBJECT_NAME = 'dispute' + + def close(self, idempotency_key=None): + url = self.instance_url() + '/close' + headers = util.populate_headers(idempotency_key) + self.refresh_from(self.request('post', url, {}, headers)) + return self diff --git a/stripe/api_resources/ephemeral_key.py b/stripe/api_resources/ephemeral_key.py new file mode 100644 index 000000000..31845635d --- /dev/null +++ b/stripe/api_resources/ephemeral_key.py @@ -0,0 +1,40 @@ +import warnings + +from stripe import api_requestor, util +from stripe.api_resources.abstract import DeletableAPIResource + + +class EphemeralKey(DeletableAPIResource): + OBJECT_NAME = 'ephemeral_key' + + @classmethod + def class_name(cls): + return 'ephemeral_key' + + @classmethod + def create(cls, api_key=None, idempotency_key=None, + stripe_version=None, stripe_account=None, + api_version=None, **params): + if stripe_version is None: + if api_version is not None: + stripe_version = api_version + warnings.warn( + "The `api_version` parameter when creating an ephemeral " + "key is deprecated. Please use `stripe_version` instead.", + DeprecationWarning) + else: + raise ValueError( + "stripe_version must be specified to create an ephemeral " + "key") + + requestor = api_requestor.APIRequestor( + api_key, + api_version=stripe_version, + account=stripe_account + ) + + url = cls.class_url() + headers = util.populate_headers(idempotency_key) + response, api_key = requestor.request('post', url, params, headers) + return util.convert_to_stripe_object(response, api_key, stripe_version, + stripe_account) diff --git a/stripe/api_resources/event.py b/stripe/api_resources/event.py new file mode 100644 index 000000000..5ba3f66a2 --- /dev/null +++ b/stripe/api_resources/event.py @@ -0,0 +1,5 @@ +from stripe.api_resources import abstract + + +class Event(abstract.ListableAPIResource): + OBJECT_NAME = 'event' diff --git a/stripe/api_resources/file_upload.py b/stripe/api_resources/file_upload.py new file mode 100644 index 000000000..2db9eedf2 --- /dev/null +++ b/stripe/api_resources/file_upload.py @@ -0,0 +1,29 @@ +from stripe import api_requestor, upload_api_base, util +from stripe.api_resources.abstract import ListableAPIResource + + +class FileUpload(ListableAPIResource): + OBJECT_NAME = 'file_upload' + + @classmethod + def api_base(cls): + return upload_api_base + + @classmethod + def class_name(cls): + return 'file' + + @classmethod + def create(cls, api_key=None, api_version=None, stripe_account=None, + **params): + requestor = api_requestor.APIRequestor( + api_key, api_base=cls.api_base(), api_version=api_version, + account=stripe_account) + url = cls.class_url() + supplied_headers = { + "Content-Type": "multipart/form-data" + } + response, api_key = requestor.request( + 'post', url, params=params, headers=supplied_headers) + return util.convert_to_stripe_object(response, api_key, api_version, + stripe_account) diff --git a/stripe/api_resources/invoice.py b/stripe/api_resources/invoice.py new file mode 100644 index 000000000..9131cab86 --- /dev/null +++ b/stripe/api_resources/invoice.py @@ -0,0 +1,28 @@ +from stripe import api_requestor, util +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Invoice(CreateableAPIResource, ListableAPIResource, + UpdateableAPIResource): + OBJECT_NAME = 'invoice' + + def pay(self, idempotency_key=None, **params): + headers = util.populate_headers(idempotency_key) + return self.request( + 'post', self.instance_url() + '/pay', params, headers) + + @classmethod + def upcoming(cls, api_key=None, stripe_version=None, stripe_account=None, + **params): + if "subscription_items" in params: + items = util.convert_array_to_dict(params["subscription_items"]) + params["subscription_items"] = items + requestor = api_requestor.APIRequestor(api_key, + api_version=stripe_version, + account=stripe_account) + url = cls.class_url() + '/upcoming' + response, api_key = requestor.request('get', url, params) + return util.convert_to_stripe_object(response, api_key, stripe_version, + stripe_account) diff --git a/stripe/api_resources/invoice_item.py b/stripe/api_resources/invoice_item.py new file mode 100644 index 000000000..cf08ca80b --- /dev/null +++ b/stripe/api_resources/invoice_item.py @@ -0,0 +1,9 @@ +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class InvoiceItem(CreateableAPIResource, UpdateableAPIResource, + ListableAPIResource, DeletableAPIResource): + OBJECT_NAME = 'invoice_item' diff --git a/stripe/api_resources/list_object.py b/stripe/api_resources/list_object.py new file mode 100644 index 000000000..1672ab3cd --- /dev/null +++ b/stripe/api_resources/list_object.py @@ -0,0 +1,53 @@ +import urllib +import warnings + +from stripe import util +from stripe.stripe_object import StripeObject + + +class ListObject(StripeObject): + OBJECT_NAME = 'list' + + def list(self, **params): + return self.request('get', self['url'], params) + + def all(self, **params): + warnings.warn("The `all` method is deprecated and will" + "be removed in future versions. Please use the " + "`list` method instead", + DeprecationWarning) + return self.list(**params) + + def auto_paging_iter(self): + page = self + params = dict(self._retrieve_params) + + while True: + item_id = None + for item in page: + item_id = item.get('id', None) + yield item + + if not getattr(page, 'has_more', False) or item_id is None: + return + + params['starting_after'] = item_id + page = self.list(**params) + + def create(self, idempotency_key=None, **params): + headers = util.populate_headers(idempotency_key) + return self.request('post', self['url'], params, headers) + + def retrieve(self, id, **params): + base = self.get('url') + id = util.utf8(id) + extn = urllib.quote_plus(id) + url = "%s/%s" % (base, extn) + + return self.request('get', url, params) + + def __iter__(self): + return getattr(self, 'data', []).__iter__() + + def __len__(self): + return getattr(self, 'data', []).__len__() diff --git a/stripe/api_resources/login_link.py b/stripe/api_resources/login_link.py new file mode 100644 index 000000000..ad709e6d1 --- /dev/null +++ b/stripe/api_resources/login_link.py @@ -0,0 +1,5 @@ +from stripe.stripe_object import StripeObject + + +class LoginLink(StripeObject): + OBJECT_NAME = 'login_link' diff --git a/stripe/api_resources/order.py b/stripe/api_resources/order.py new file mode 100644 index 000000000..408071b50 --- /dev/null +++ b/stripe/api_resources/order.py @@ -0,0 +1,27 @@ +from stripe import util +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Order(CreateableAPIResource, UpdateableAPIResource, + ListableAPIResource): + OBJECT_NAME = 'order' + + @classmethod + def create(cls, **params): + if "items" in params: + params["items"] = util.convert_array_to_dict(params["items"]) + return super(Order, cls).create(**params) + + def pay(self, idempotency_key=None, **params): + headers = util.populate_headers(idempotency_key) + return self.request( + 'post', self.instance_url() + '/pay', params, headers) + + def return_order(self, idempotency_key=None, **params): + if "items" in params: + params["items"] = util.convert_array_to_dict(params["items"]) + headers = util.populate_headers(idempotency_key) + return self.request( + 'post', self.instance_url() + '/returns', params, headers) diff --git a/stripe/api_resources/order_return.py b/stripe/api_resources/order_return.py new file mode 100644 index 000000000..7792dbaad --- /dev/null +++ b/stripe/api_resources/order_return.py @@ -0,0 +1,9 @@ +from stripe.api_resources.abstract import ListableAPIResource + + +class OrderReturn(ListableAPIResource): + OBJECT_NAME = 'order_return' + + @classmethod + def class_url(cls): + return '/v1/order_returns' diff --git a/stripe/api_resources/payout.py b/stripe/api_resources/payout.py new file mode 100644 index 000000000..3aaabbf6f --- /dev/null +++ b/stripe/api_resources/payout.py @@ -0,0 +1,12 @@ +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Payout(CreateableAPIResource, UpdateableAPIResource, + ListableAPIResource): + OBJECT_NAME = 'payout' + + def cancel(self): + self.refresh_from(self.request('post', + self.instance_url() + '/cancel')) diff --git a/stripe/api_resources/plan.py b/stripe/api_resources/plan.py new file mode 100644 index 000000000..b3caa1fee --- /dev/null +++ b/stripe/api_resources/plan.py @@ -0,0 +1,9 @@ +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Plan(CreateableAPIResource, DeletableAPIResource, + UpdateableAPIResource, ListableAPIResource): + OBJECT_NAME = 'plan' diff --git a/stripe/api_resources/product.py b/stripe/api_resources/product.py new file mode 100644 index 000000000..f3ada6050 --- /dev/null +++ b/stripe/api_resources/product.py @@ -0,0 +1,9 @@ +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Product(CreateableAPIResource, UpdateableAPIResource, + ListableAPIResource, DeletableAPIResource): + OBJECT_NAME = 'product' diff --git a/stripe/api_resources/recipient.py b/stripe/api_resources/recipient.py new file mode 100644 index 000000000..c17480f14 --- /dev/null +++ b/stripe/api_resources/recipient.py @@ -0,0 +1,15 @@ +from stripe.api_resources.transfer import Transfer +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Recipient(CreateableAPIResource, UpdateableAPIResource, + ListableAPIResource, DeletableAPIResource): + OBJECT_NAME = 'recipient' + + def transfers(self, **params): + params['recipient'] = self.id + transfers = Transfer.list(self.api_key, **params) + return transfers diff --git a/stripe/api_resources/recipient_transfer.py b/stripe/api_resources/recipient_transfer.py new file mode 100644 index 000000000..434891a09 --- /dev/null +++ b/stripe/api_resources/recipient_transfer.py @@ -0,0 +1,6 @@ +from stripe.stripe_object import StripeObject + + +# This resource can only be instantiated when expanded on a BalanceTransaction +class RecipientTransfer(StripeObject): + OBJECT_NAME = 'recipient_transfer' diff --git a/stripe/api_resources/refund.py b/stripe/api_resources/refund.py new file mode 100644 index 000000000..d1f3d425a --- /dev/null +++ b/stripe/api_resources/refund.py @@ -0,0 +1,8 @@ +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Refund(CreateableAPIResource, ListableAPIResource, + UpdateableAPIResource): + OBJECT_NAME = 'refund' diff --git a/stripe/api_resources/reversal.py b/stripe/api_resources/reversal.py new file mode 100644 index 000000000..61cbf4d49 --- /dev/null +++ b/stripe/api_resources/reversal.py @@ -0,0 +1,29 @@ +import urllib + +from stripe import util +from stripe.api_resources.transfer import Transfer +from stripe.api_resources.abstract import UpdateableAPIResource + + +class Reversal(UpdateableAPIResource): + OBJECT_NAME = 'transfer_reversal' + + def instance_url(self): + token = util.utf8(self.id) + transfer = util.utf8(self.transfer) + base = Transfer.class_url() + cust_extn = urllib.quote_plus(transfer) + extn = urllib.quote_plus(token) + return "%s/%s/reversals/%s" % (base, cust_extn, extn) + + @classmethod + def modify(cls, sid, **params): + raise NotImplementedError( + "Can't modify a reversal without a transfer" + "ID. Call save on transfer.reversals.retrieve('reversal_id')") + + @classmethod + def retrieve(cls, id, api_key=None, **params): + raise NotImplementedError( + "Can't retrieve a reversal without a transfer" + "ID. Use transfer.reversals.retrieve('reversal_id')") diff --git a/stripe/api_resources/sku.py b/stripe/api_resources/sku.py new file mode 100644 index 000000000..ddc8f9166 --- /dev/null +++ b/stripe/api_resources/sku.py @@ -0,0 +1,9 @@ +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class SKU(CreateableAPIResource, UpdateableAPIResource, + ListableAPIResource, DeletableAPIResource): + OBJECT_NAME = 'sku' diff --git a/stripe/api_resources/source.py b/stripe/api_resources/source.py new file mode 100644 index 000000000..6de13c979 --- /dev/null +++ b/stripe/api_resources/source.py @@ -0,0 +1,35 @@ +import urllib +import warnings + +from stripe import util +from stripe.api_resources import Customer +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import VerifyMixin + + +class Source(CreateableAPIResource, UpdateableAPIResource, VerifyMixin): + OBJECT_NAME = 'source' + + def detach(self, **params): + if hasattr(self, 'customer') and self.customer: + extn = urllib.quote_plus(util.utf8(self.id)) + customer = util.utf8(self.customer) + base = Customer.class_url() + owner_extn = urllib.quote_plus(customer) + url = "%s/%s/sources/%s" % (base, owner_extn, extn) + + self.refresh_from(self.request('delete', url, params)) + return self + + else: + raise NotImplementedError( + "This source object does not appear to be currently attached " + "to a customer object.") + + def delete(self, **params): + warnings.warn("The `Source.delete` method is deprecated and will " + "be removed in future versions. Please use the " + "`Source.detach` method instead", + DeprecationWarning) + self.detach(**params) diff --git a/stripe/api_resources/subscription.py b/stripe/api_resources/subscription.py new file mode 100644 index 000000000..05ee52a4a --- /dev/null +++ b/stripe/api_resources/subscription.py @@ -0,0 +1,38 @@ +from stripe import api_requestor, util +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Subscription(CreateableAPIResource, DeletableAPIResource, + UpdateableAPIResource, ListableAPIResource): + OBJECT_NAME = 'subscription' + + # TODO: Remove arg in next major release. + def delete_discount(self, **params): + requestor = api_requestor.APIRequestor(self.api_key, + api_version=self.stripe_version, + account=self.stripe_account) + url = self.instance_url() + '/discount' + _, api_key = requestor.request('delete', url) + self.refresh_from({'discount': None}, api_key, True) + + @classmethod + def modify(cls, sid, **params): + if "items" in params: + params["items"] = util.convert_array_to_dict(params["items"]) + return super(Subscription, cls).modify(sid, **params) + + @classmethod + def create(cls, **params): + if "items" in params: + params["items"] = util.convert_array_to_dict(params["items"]) + return super(Subscription, cls).create(**params) + + def serialize(self, previous): + updated_params = super(UpdateableAPIResource, self).serialize(previous) + if "items" in updated_params: + updated_params["items"] = util.convert_array_to_dict( + updated_params["items"]) + return updated_params diff --git a/stripe/api_resources/subscription_item.py b/stripe/api_resources/subscription_item.py new file mode 100644 index 000000000..99711b99b --- /dev/null +++ b/stripe/api_resources/subscription_item.py @@ -0,0 +1,13 @@ +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import DeletableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class SubscriptionItem(CreateableAPIResource, DeletableAPIResource, + UpdateableAPIResource, ListableAPIResource): + OBJECT_NAME = 'subscription_item' + + @classmethod + def class_name(cls): + return 'subscription_item' diff --git a/stripe/api_resources/three_d_secure.py b/stripe/api_resources/three_d_secure.py new file mode 100644 index 000000000..4735c8392 --- /dev/null +++ b/stripe/api_resources/three_d_secure.py @@ -0,0 +1,9 @@ +from stripe.api_resources.abstract import CreateableAPIResource + + +class ThreeDSecure(CreateableAPIResource): + OBJECT_NAME = 'three_d_secure' + + @classmethod + def class_url(cls): + return '/v1/3d_secure' diff --git a/stripe/api_resources/token.py b/stripe/api_resources/token.py new file mode 100644 index 000000000..88ff0af87 --- /dev/null +++ b/stripe/api_resources/token.py @@ -0,0 +1,5 @@ +from stripe.api_resources.abstract import CreateableAPIResource + + +class Token(CreateableAPIResource): + OBJECT_NAME = 'token' diff --git a/stripe/api_resources/transfer.py b/stripe/api_resources/transfer.py new file mode 100644 index 000000000..ff030ebfb --- /dev/null +++ b/stripe/api_resources/transfer.py @@ -0,0 +1,12 @@ +from stripe.api_resources.abstract import CreateableAPIResource +from stripe.api_resources.abstract import UpdateableAPIResource +from stripe.api_resources.abstract import ListableAPIResource + + +class Transfer(CreateableAPIResource, UpdateableAPIResource, + ListableAPIResource): + OBJECT_NAME = 'transfer' + + def cancel(self): + self.refresh_from(self.request('post', + self.instance_url() + '/cancel')) diff --git a/stripe/resource.py b/stripe/resource.py index 43ee36aa2..66747585e 100644 --- a/stripe/resource.py +++ b/stripe/resource.py @@ -1,335 +1,27 @@ -import urllib -import warnings -import sys -from copy import deepcopy - -from stripe import api_requestor, error, oauth, util, upload_api_base - - -def convert_to_stripe_object(resp, api_key=None, stripe_version=None, - stripe_account=None): - types = { - 'account': Account, - 'alipay_account': AlipayAccount, - 'apple_pay_domain': ApplePayDomain, - 'application_fee': ApplicationFee, - 'bank_account': BankAccount, - 'bitcoin_receiver': BitcoinReceiver, - 'bitcoin_transaction': BitcoinTransaction, - 'card': Card, - 'charge': Charge, - 'country_spec': CountrySpec, - 'coupon': Coupon, - 'customer': Customer, - 'dispute': Dispute, - 'ephemeral_key': EphemeralKey, - 'event': Event, - 'fee_refund': ApplicationFeeRefund, - 'file_upload': FileUpload, - 'invoice': Invoice, - 'invoiceitem': InvoiceItem, - 'list': ListObject, - 'login_link': LoginLink, - 'payout': Payout, - 'plan': Plan, - 'recipient': Recipient, - 'recipient_transfer': RecipientTransfer, - 'refund': Refund, - 'source': Source, - 'subscription': Subscription, - 'subscription_item': SubscriptionItem, - 'three_d_secure': ThreeDSecure, - 'token': Token, - 'transfer': Transfer, - 'transfer_reversal': Reversal, - 'product': Product, - 'sku': SKU, - 'order': Order, - 'order_return': OrderReturn - } - - if isinstance(resp, list): - return [convert_to_stripe_object(i, api_key, stripe_version, - stripe_account) for i in resp] - elif isinstance(resp, dict) and not isinstance(resp, StripeObject): - resp = resp.copy() - klass_name = resp.get('object') - if isinstance(klass_name, basestring): - klass = types.get(klass_name, StripeObject) - else: - klass = StripeObject - return klass.construct_from(resp, api_key, - stripe_version=stripe_version, - stripe_account=stripe_account) - else: - return resp - - -def convert_array_to_dict(arr): - if isinstance(arr, list): - d = {} - for i, value in enumerate(arr): - d[str(i)] = value - return d - else: - return arr - - -def populate_headers(idempotency_key): - if idempotency_key is not None: - return {"Idempotency-Key": idempotency_key} - return None - - -def _compute_diff(current, previous): - if isinstance(current, dict): - previous = previous or {} - diff = current.copy() - for key in set(previous.keys()) - set(diff.keys()): - diff[key] = "" - return diff - return current if current is not None else "" - - -def _serialize_list(array, previous): - array = array or [] - previous = previous or [] - params = {} - - for i, v in enumerate(array): - previous_item = previous[i] if len(previous) > i else None - if hasattr(v, 'serialize'): - params[str(i)] = v.serialize(previous_item) - else: - params[str(i)] = _compute_diff(v, previous_item) - - return params - - -class StripeObject(dict): - def __init__(self, id=None, api_key=None, stripe_version=None, - stripe_account=None, **params): - super(StripeObject, self).__init__() - - self._unsaved_values = set() - self._transient_values = set() - - self._retrieve_params = params - self._previous = None - - object.__setattr__(self, 'api_key', api_key) - object.__setattr__(self, 'stripe_version', stripe_version) - object.__setattr__(self, 'stripe_account', stripe_account) - - if id: - self['id'] = id - - def update(self, update_dict): - for k in update_dict: - self._unsaved_values.add(k) - - return super(StripeObject, self).update(update_dict) - - def __setattr__(self, k, v): - if k[0] == '_' or k in self.__dict__: - return super(StripeObject, self).__setattr__(k, v) - - self[k] = v - return None - - def __getattr__(self, k): - if k[0] == '_': - raise AttributeError(k) - - try: - return self[k] - except KeyError as err: - raise AttributeError(*err.args) - - def __delattr__(self, k): - if k[0] == '_' or k in self.__dict__: - return super(StripeObject, self).__delattr__(k) - else: - del self[k] - - def __setitem__(self, k, v): - if v == "": - raise ValueError( - "You cannot set %s to an empty string. " - "We interpret empty strings as None in requests." - "You may set %s.%s = None to delete the property" % ( - k, str(self), k)) - - super(StripeObject, self).__setitem__(k, v) - - # Allows for unpickling in Python 3.x - if not hasattr(self, '_unsaved_values'): - self._unsaved_values = set() - - self._unsaved_values.add(k) - - def __getitem__(self, k): - try: - return super(StripeObject, self).__getitem__(k) - except KeyError as err: - if k in self._transient_values: - raise KeyError( - "%r. HINT: The %r attribute was set in the past." - "It was then wiped when refreshing the object with " - "the result returned by Stripe's API, probably as a " - "result of a save(). The attributes currently " - "available on this object are: %s" % - (k, k, ', '.join(self.keys()))) - else: - raise err - - def __delitem__(self, k): - super(StripeObject, self).__delitem__(k) +# +# This module doesn't serve much purpose anymore. It's only here to maintain +# backwards compatibility. +# +# TODO: get rid of this module in the next major version. +# - # Allows for unpickling in Python 3.x - if hasattr(self, '_unsaved_values'): - self._unsaved_values.remove(k) - - @classmethod - def construct_from(cls, values, key, stripe_version=None, - stripe_account=None): - instance = cls(values.get('id'), api_key=key, - stripe_version=stripe_version, - stripe_account=stripe_account) - instance.refresh_from(values, api_key=key, - stripe_version=stripe_version, - stripe_account=stripe_account) - return instance - - def refresh_from(self, values, api_key=None, partial=False, - stripe_version=None, stripe_account=None): - self.api_key = api_key or getattr(values, 'api_key', None) - self.stripe_version = \ - stripe_version or getattr(values, 'stripe_version', None) - self.stripe_account = \ - stripe_account or getattr(values, 'stripe_account', None) - - # Wipe old state before setting new. This is useful for e.g. - # updating a customer, where there is no persistent card - # parameter. Mark those values which don't persist as transient - if partial: - self._unsaved_values = (self._unsaved_values - set(values)) - else: - removed = set(self.keys()) - set(values) - self._transient_values = self._transient_values | removed - self._unsaved_values = set() - self.clear() - - self._transient_values = self._transient_values - set(values) - - for k, v in values.iteritems(): - super(StripeObject, self).__setitem__( - k, convert_to_stripe_object(v, api_key, stripe_version, - stripe_account)) - - self._previous = values - - @classmethod - def api_base(cls): - return None - - def request(self, method, url, params=None, headers=None): - if params is None: - params = self._retrieve_params - requestor = api_requestor.APIRequestor( - key=self.api_key, api_base=self.api_base(), - api_version=self.stripe_version, account=self.stripe_account) - response, api_key = requestor.request(method, url, params, headers) - - return convert_to_stripe_object(response, api_key, self.stripe_version, - self.stripe_account) - - def __repr__(self): - ident_parts = [type(self).__name__] - - if isinstance(self.get('object'), basestring): - ident_parts.append(self.get('object')) - - if isinstance(self.get('id'), basestring): - ident_parts.append('id=%s' % (self.get('id'),)) - - unicode_repr = '<%s at %s> JSON: %s' % ( - ' '.join(ident_parts), hex(id(self)), str(self)) - - if sys.version_info[0] < 3: - return unicode_repr.encode('utf-8') - else: - return unicode_repr - - def __str__(self): - return util.json.dumps(self, sort_keys=True, indent=2) - - def to_dict(self): - warnings.warn( - 'The `to_dict` method is deprecated and will be removed in ' - 'version 2.0 of the Stripe bindings. The StripeObject is ' - 'itself now a subclass of `dict`.', - DeprecationWarning) - - return dict(self) - - @property - def stripe_id(self): - return self.id - - def serialize(self, previous): - params = {} - unsaved_keys = self._unsaved_values or set() - previous = previous or self._previous or {} - - for k, v in self.items(): - if k == 'id' or (isinstance(k, str) and k.startswith('_')): - continue - elif isinstance(v, APIResource): - continue - elif hasattr(v, 'serialize'): - params[k] = v.serialize(previous.get(k, None)) - elif k in unsaved_keys: - params[k] = _compute_diff(v, previous.get(k, None)) - elif k == 'additional_owners' and v is not None: - params[k] = _serialize_list(v, previous.get(k, None)) - - return params - - # This class overrides __setitem__ to throw exceptions on inputs that it - # doesn't like. This can cause problems when we try to copy an object - # wholesale because some data that's returned from the API may not be valid - # if it was set to be set manually. Here we override the class' copy - # arguments so that we can bypass these possible exceptions on __setitem__. - def __copy__(self): - copied = StripeObject(self.get('id'), self.api_key, - stripe_version=self.stripe_version, - stripe_account=self.stripe_account) - - copied._retrieve_params = self._retrieve_params - - for k, v in self.items(): - # Call parent's __setitem__ to avoid checks that we've added in the - # overridden version that can throw exceptions. - super(StripeObject, copied).__setitem__(k, v) - - return copied - - # This class overrides __setitem__ to throw exceptions on inputs that it - # doesn't like. This can cause problems when we try to copy an object - # wholesale because some data that's returned from the API may not be valid - # if it was set to be set manually. Here we override the class' copy - # arguments so that we can bypass these possible exceptions on __setitem__. - def __deepcopy__(self, memo): - copied = self.__copy__() - memo[id(self)] = copied - - for k, v in self.items(): - # Call parent's __setitem__ to avoid checks that we've added in the - # overridden version that can throw exceptions. - super(StripeObject, copied).__setitem__(k, deepcopy(v, memo)) +import warnings - return copied +from stripe import util +from stripe.util import ( # noqa + convert_array_to_dict, + convert_to_stripe_object, +) +from stripe.stripe_object import StripeObject # noqa +from stripe.api_resources.abstract import ( # noqa + APIResource, + CreateableAPIResource, + DeletableAPIResource, + ListableAPIResource, + SingletonAPIResource, + UpdateableAPIResource, +) +from stripe.api_resources import * # noqa class StripeObjectEncoder(util.json.JSONEncoder): @@ -342,867 +34,3 @@ def __init__(self, *args, **kwargs): 'json library.', DeprecationWarning) super(StripeObjectEncoder, self).__init__(*args, **kwargs) - - -class APIResource(StripeObject): - - @classmethod - def retrieve(cls, id, api_key=None, **params): - instance = cls(id, api_key, **params) - instance.refresh() - return instance - - def refresh(self): - self.refresh_from(self.request('get', self.instance_url())) - return self - - @classmethod - def class_name(cls): - if cls == APIResource: - raise NotImplementedError( - 'APIResource is an abstract class. You should perform ' - 'actions on its subclasses (e.g. Charge, Customer)') - return str(urllib.quote_plus(cls.__name__.lower())) - - @classmethod - def class_url(cls): - cls_name = cls.class_name() - return "/v1/%ss" % (cls_name,) - - def instance_url(self): - id = self.get('id') - - if not isinstance(id, basestring): - raise error.InvalidRequestError( - 'Could not determine which URL to request: %s instance ' - 'has invalid ID: %r, %s. ID should be of type `str` (or' - ' `unicode`)' % (type(self).__name__, id, type(id)), 'id') - - id = util.utf8(id) - base = self.class_url() - extn = urllib.quote_plus(id) - return "%s/%s" % (base, extn) - - -class ListObject(StripeObject): - - def list(self, **params): - return self.request('get', self['url'], params) - - def all(self, **params): - warnings.warn("The `all` method is deprecated and will" - "be removed in future versions. Please use the " - "`list` method instead", - DeprecationWarning) - return self.list(**params) - - def auto_paging_iter(self): - page = self - params = dict(self._retrieve_params) - - while True: - item_id = None - for item in page: - item_id = item.get('id', None) - yield item - - if not getattr(page, 'has_more', False) or item_id is None: - return - - params['starting_after'] = item_id - page = self.list(**params) - - def create(self, idempotency_key=None, **params): - headers = populate_headers(idempotency_key) - return self.request('post', self['url'], params, headers) - - def retrieve(self, id, **params): - base = self.get('url') - id = util.utf8(id) - extn = urllib.quote_plus(id) - url = "%s/%s" % (base, extn) - - return self.request('get', url, params) - - def __iter__(self): - return getattr(self, 'data', []).__iter__() - - def __len__(self): - return getattr(self, 'data', []).__len__() - - -class SingletonAPIResource(APIResource): - - @classmethod - def retrieve(cls, **params): - return super(SingletonAPIResource, cls).retrieve(None, **params) - - @classmethod - def class_url(cls): - cls_name = cls.class_name() - return "/v1/%s" % (cls_name,) - - def instance_url(self): - return self.class_url() - - -# Classes of API operations - - -class ListableAPIResource(APIResource): - - @classmethod - def all(cls, *args, **params): - warnings.warn("The `all` class method is deprecated and will" - "be removed in future versions. Please use the " - "`list` class method instead", - DeprecationWarning) - return cls.list(*args, **params) - - @classmethod - def auto_paging_iter(cls, *args, **params): - return cls.list(*args, **params).auto_paging_iter() - - @classmethod - def list(cls, api_key=None, stripe_version=None, stripe_account=None, - **params): - requestor = api_requestor.APIRequestor(api_key, - api_base=cls.api_base(), - api_version=stripe_version, - account=stripe_account) - url = cls.class_url() - response, api_key = requestor.request('get', url, params) - stripe_object = convert_to_stripe_object(response, api_key, - stripe_version, - stripe_account) - stripe_object._retrieve_params = params - return stripe_object - - -class CreateableAPIResource(APIResource): - - @classmethod - def create(cls, api_key=None, idempotency_key=None, - stripe_version=None, stripe_account=None, **params): - requestor = api_requestor.APIRequestor(api_key, - api_version=stripe_version, - account=stripe_account) - url = cls.class_url() - headers = populate_headers(idempotency_key) - response, api_key = requestor.request('post', url, params, headers) - return convert_to_stripe_object(response, api_key, stripe_version, - stripe_account) - - -class UpdateableAPIResource(APIResource): - - @classmethod - def _modify(cls, url, api_key=None, idempotency_key=None, - stripe_version=None, stripe_account=None, **params): - requestor = api_requestor.APIRequestor(api_key, - api_version=stripe_version, - account=stripe_account) - headers = populate_headers(idempotency_key) - response, api_key = requestor.request('post', url, params, headers) - return convert_to_stripe_object(response, api_key, stripe_version, - stripe_account) - - @classmethod - def modify(cls, sid, **params): - url = "%s/%s" % (cls.class_url(), urllib.quote_plus(util.utf8(sid))) - return cls._modify(url, **params) - - def save(self, idempotency_key=None): - updated_params = self.serialize(None) - headers = populate_headers(idempotency_key) - - if updated_params: - self.refresh_from(self.request('post', self.instance_url(), - updated_params, headers)) - else: - util.logger.debug("Trying to save already saved object %r", self) - return self - - -class DeletableAPIResource(APIResource): - - def delete(self, **params): - self.refresh_from(self.request('delete', self.instance_url(), params)) - return self - - -# API objects -class Account(CreateableAPIResource, ListableAPIResource, - UpdateableAPIResource, DeletableAPIResource): - @classmethod - def retrieve(cls, id=None, api_key=None, **params): - instance = cls(id, api_key, **params) - instance.refresh() - return instance - - @classmethod - def modify(cls, id=None, **params): - return cls._modify(cls._build_instance_url(id), **params) - - @classmethod - def _build_instance_url(cls, sid): - if not sid: - return "/v1/account" - sid = util.utf8(sid) - base = cls.class_url() - extn = urllib.quote_plus(sid) - return "%s/%s" % (base, extn) - - def instance_url(self): - return self._build_instance_url(self.get('id')) - - def reject(self, reason=None, idempotency_key=None): - url = self.instance_url() + '/reject' - headers = populate_headers(idempotency_key) - if reason: - params = {"reason": reason} - else: - params = {} - self.refresh_from( - self.request('post', url, params, headers) - ) - return self - - def deauthorize(self, **params): - params['stripe_user_id'] = self.id - return oauth.OAuth.deauthorize(**params) - - @classmethod - def modify_external_account(cls, sid, external_account_id, **params): - url = "%s/%s/external_accounts/%s" % ( - cls.class_url(), urllib.quote_plus(util.utf8(sid)), - urllib.quote_plus(util.utf8(external_account_id))) - return cls._modify(url, **params) - - -class AlipayAccount(UpdateableAPIResource, DeletableAPIResource): - - @classmethod - def _build_instance_url(cls, customer, sid): - token = util.utf8(sid) - extn = urllib.quote_plus(token) - customer = util.utf8(customer) - - base = Customer.class_url() - owner_extn = urllib.quote_plus(customer) - - return "%s/%s/sources/%s" % (base, owner_extn, extn) - - def instance_url(self): - return self._build_instance_url(self.customer, self.id) - - @classmethod - def modify(cls, customer, id, **params): - url = cls._build_instance_url(customer, id) - return cls._modify(url, **params) - - @classmethod - def retrieve(cls, id, api_key=None, stripe_version=None, - stripe_account=None, **params): - raise NotImplementedError( - "Can't retrieve an Alipay account without a customer ID. " - "Use customer.sources.retrieve('alipay_account_id') instead.") - - -class Balance(SingletonAPIResource): - pass - - -class BalanceTransaction(ListableAPIResource): - - @classmethod - def class_url(cls): - return '/v1/balance/history' - - -class Card(UpdateableAPIResource, DeletableAPIResource): - - def instance_url(self): - token = util.utf8(self.id) - extn = urllib.quote_plus(token) - if hasattr(self, 'customer'): - customer = util.utf8(self.customer) - - base = Customer.class_url() - owner_extn = urllib.quote_plus(customer) - class_base = "sources" - - elif hasattr(self, 'recipient'): - recipient = util.utf8(self.recipient) - - base = Recipient.class_url() - owner_extn = urllib.quote_plus(recipient) - class_base = "cards" - - elif hasattr(self, 'account'): - account = util.utf8(self.account) - - base = Account.class_url() - owner_extn = urllib.quote_plus(account) - class_base = "external_accounts" - - else: - raise error.InvalidRequestError( - "Could not determine whether card_id %s is " - "attached to a customer, recipient, or " - "account." % token, 'id') - - return "%s/%s/%s/%s" % (base, owner_extn, class_base, extn) - - @classmethod - def modify(cls, sid, **params): - raise NotImplementedError( - "Can't modify a card without a customer, recipient or account " - "ID. Call save on customer.sources.retrieve('card_id'), " - "recipient.cards.retrieve('card_id'), or " - "account.external_accounts.retrieve('card_id') instead.") - - @classmethod - def retrieve(cls, id, api_key=None, stripe_version=None, - stripe_account=None, **params): - raise NotImplementedError( - "Can't retrieve a card without a customer, recipient or account " - "ID. Use customer.sources.retrieve('card_id'), " - "recipient.cards.retrieve('card_id'), or " - "account.external_accounts.retrieve('card_id') instead.") - - -class VerifyMixin(object): - - def verify(self, idempotency_key=None, **params): - url = self.instance_url() + '/verify' - headers = populate_headers(idempotency_key) - self.refresh_from(self.request('post', url, params, headers)) - return self - - -class BankAccount(UpdateableAPIResource, DeletableAPIResource, VerifyMixin): - - def instance_url(self): - token = util.utf8(self.id) - extn = urllib.quote_plus(token) - if hasattr(self, 'customer'): - customer = util.utf8(self.customer) - - base = Customer.class_url() - owner_extn = urllib.quote_plus(customer) - class_base = "sources" - - elif hasattr(self, 'account'): - account = util.utf8(self.account) - - base = Account.class_url() - owner_extn = urllib.quote_plus(account) - class_base = "external_accounts" - - else: - raise error.InvalidRequestError( - "Could not determine whether bank_account_id %s is " - "attached to a customer or an account." % token, 'id') - - return "%s/%s/%s/%s" % (base, owner_extn, class_base, extn) - - @classmethod - def modify(cls, sid, **params): - raise NotImplementedError( - "Can't modify a bank account without a customer or account ID. " - "Call save on customer.sources.retrieve('bank_account_id') or " - "account.external_accounts.retrieve('bank_account_id') instead.") - - @classmethod - def retrieve(cls, id, api_key=None, stripe_version=None, - stripe_account=None, **params): - raise NotImplementedError( - "Can't retrieve a bank account without a customer or account ID. " - "Use customer.sources.retrieve('bank_account_id') or " - "account.external_accounts.retrieve('bank_account_id') instead.") - - -class Charge(CreateableAPIResource, ListableAPIResource, - UpdateableAPIResource): - - def refund(self, idempotency_key=None, **params): - url = self.instance_url() + '/refund' - headers = populate_headers(idempotency_key) - self.refresh_from(self.request('post', url, params, headers)) - return self - - def capture(self, idempotency_key=None, **params): - url = self.instance_url() + '/capture' - headers = populate_headers(idempotency_key) - self.refresh_from(self.request('post', url, params, headers)) - return self - - def update_dispute(self, idempotency_key=None, **params): - requestor = api_requestor.APIRequestor(self.api_key, - api_version=self.stripe_version, - account=self.stripe_account) - url = self.instance_url() + '/dispute' - headers = populate_headers(idempotency_key) - response, api_key = requestor.request('post', url, params, headers) - self.refresh_from({'dispute': response}, api_key, True) - return self.dispute - - def close_dispute(self, idempotency_key=None): - requestor = api_requestor.APIRequestor(self.api_key, - api_version=self.stripe_version, - account=self.stripe_account) - url = self.instance_url() + '/dispute/close' - headers = populate_headers(idempotency_key) - response, api_key = requestor.request('post', url, {}, headers) - self.refresh_from({'dispute': response}, api_key, True) - return self.dispute - - def mark_as_fraudulent(self, idempotency_key=None): - params = { - 'fraud_details': {'user_report': 'fraudulent'} - } - url = self.instance_url() - headers = populate_headers(idempotency_key) - self.refresh_from(self.request('post', url, params, headers)) - return self - - def mark_as_safe(self, idempotency_key=None): - params = { - 'fraud_details': {'user_report': 'safe'} - } - url = self.instance_url() - headers = populate_headers(idempotency_key) - self.refresh_from(self.request('post', url, params, headers)) - return self - - -class Dispute(CreateableAPIResource, ListableAPIResource, - UpdateableAPIResource): - - def close(self, idempotency_key=None): - url = self.instance_url() + '/close' - headers = populate_headers(idempotency_key) - self.refresh_from(self.request('post', url, {}, headers)) - return self - - -class Customer(CreateableAPIResource, UpdateableAPIResource, - ListableAPIResource, DeletableAPIResource): - - def add_invoice_item(self, idempotency_key=None, **params): - params['customer'] = self.id - ii = InvoiceItem.create(self.api_key, - idempotency_key=idempotency_key, **params) - return ii - - def invoices(self, **params): - params['customer'] = self.id - invoices = Invoice.list(self.api_key, **params) - return invoices - - def invoice_items(self, **params): - params['customer'] = self.id - iis = InvoiceItem.list(self.api_key, **params) - return iis - - def charges(self, **params): - params['customer'] = self.id - charges = Charge.list(self.api_key, **params) - return charges - - def update_subscription(self, idempotency_key=None, **params): - warnings.warn( - 'The `update_subscription` method is deprecated. Instead, use the ' - '`subscriptions` resource on the customer object to update a ' - 'subscription', - DeprecationWarning) - requestor = api_requestor.APIRequestor(self.api_key, - api_version=self.stripe_version, - account=self.stripe_account) - url = self.instance_url() + '/subscription' - headers = populate_headers(idempotency_key) - response, api_key = requestor.request('post', url, params, headers) - self.refresh_from({'subscription': response}, api_key, True) - return self.subscription - - def cancel_subscription(self, idempotency_key=None, **params): - warnings.warn( - 'The `cancel_subscription` method is deprecated. Instead, use the ' - '`subscriptions` resource on the customer object to cancel a ' - 'subscription', - DeprecationWarning) - requestor = api_requestor.APIRequestor(self.api_key, - api_version=self.stripe_version, - account=self.stripe_account) - url = self.instance_url() + '/subscription' - headers = populate_headers(idempotency_key) - response, api_key = requestor.request('delete', url, params, headers) - self.refresh_from({'subscription': response}, api_key, True) - return self.subscription - - # TODO: Remove arg in next major release. - def delete_discount(self, **params): - requestor = api_requestor.APIRequestor(self.api_key, - api_version=self.stripe_version, - account=self.stripe_account) - url = self.instance_url() + '/discount' - _, api_key = requestor.request('delete', url) - self.refresh_from({'discount': None}, api_key, True) - - @classmethod - def modify_source(cls, sid, source_id, **params): - url = "%s/%s/sources/%s" % ( - cls.class_url(), urllib.quote_plus(util.utf8(sid)), - urllib.quote_plus(util.utf8(source_id))) - return cls._modify(url, **params) - - -class Invoice(CreateableAPIResource, ListableAPIResource, - UpdateableAPIResource): - - def pay(self, idempotency_key=None, **params): - headers = populate_headers(idempotency_key) - return self.request( - 'post', self.instance_url() + '/pay', params, headers) - - @classmethod - def upcoming(cls, api_key=None, stripe_version=None, stripe_account=None, - **params): - if "subscription_items" in params: - items = convert_array_to_dict(params["subscription_items"]) - params["subscription_items"] = items - requestor = api_requestor.APIRequestor(api_key, - api_version=stripe_version, - account=stripe_account) - url = cls.class_url() + '/upcoming' - response, api_key = requestor.request('get', url, params) - return convert_to_stripe_object(response, api_key, stripe_version, - stripe_account) - - -class InvoiceItem(CreateableAPIResource, UpdateableAPIResource, - ListableAPIResource, DeletableAPIResource): - pass - - -class Plan(CreateableAPIResource, DeletableAPIResource, - UpdateableAPIResource, ListableAPIResource): - pass - - -class Subscription(CreateableAPIResource, DeletableAPIResource, - UpdateableAPIResource, ListableAPIResource): - - # TODO: Remove arg in next major release. - def delete_discount(self, **params): - requestor = api_requestor.APIRequestor(self.api_key, - api_version=self.stripe_version, - account=self.stripe_account) - url = self.instance_url() + '/discount' - _, api_key = requestor.request('delete', url) - self.refresh_from({'discount': None}, api_key, True) - - @classmethod - def modify(cls, sid, **params): - if "items" in params: - params["items"] = convert_array_to_dict(params["items"]) - return super(Subscription, cls).modify(sid, **params) - - @classmethod - def create(cls, **params): - if "items" in params: - params["items"] = convert_array_to_dict(params["items"]) - return super(Subscription, cls).create(**params) - - def serialize(self, previous): - updated_params = super(UpdateableAPIResource, self).serialize(previous) - if "items" in updated_params: - updated_params["items"] = convert_array_to_dict( - updated_params["items"]) - return updated_params - - -class SubscriptionItem(CreateableAPIResource, DeletableAPIResource, - UpdateableAPIResource, ListableAPIResource): - @classmethod - def class_name(cls): - return 'subscription_item' - - -class Refund(CreateableAPIResource, ListableAPIResource, - UpdateableAPIResource): - pass - - -class Token(CreateableAPIResource): - pass - - -class Coupon(CreateableAPIResource, UpdateableAPIResource, - DeletableAPIResource, ListableAPIResource): - pass - - -class EphemeralKey(DeletableAPIResource): - @classmethod - def class_name(cls): - return 'ephemeral_key' - - @classmethod - def create(cls, api_key=None, idempotency_key=None, - stripe_version=None, stripe_account=None, - api_version=None, **params): - if stripe_version is None: - if api_version is not None: - stripe_version = api_version - warnings.warn( - "The `api_version` parameter when creating an ephemeral " - "key is deprecated. Please use `stripe_version` instead.", - DeprecationWarning) - else: - raise ValueError( - "stripe_version must be specified to create an ephemeral " - "key") - - requestor = api_requestor.APIRequestor( - api_key, - api_version=stripe_version, - account=stripe_account - ) - - url = cls.class_url() - headers = populate_headers(idempotency_key) - response, api_key = requestor.request('post', url, params, headers) - return convert_to_stripe_object(response, api_key, stripe_version, - stripe_account) - - -class Event(ListableAPIResource): - pass - - -class LoginLink(StripeObject): - pass - - -class Payout(CreateableAPIResource, UpdateableAPIResource, - ListableAPIResource): - - def cancel(self): - self.refresh_from(self.request('post', - self.instance_url() + '/cancel')) - - -class Transfer(CreateableAPIResource, UpdateableAPIResource, - ListableAPIResource): - - def cancel(self): - self.refresh_from(self.request('post', - self.instance_url() + '/cancel')) - - -class Reversal(UpdateableAPIResource): - - def instance_url(self): - token = util.utf8(self.id) - transfer = util.utf8(self.transfer) - base = Transfer.class_url() - cust_extn = urllib.quote_plus(transfer) - extn = urllib.quote_plus(token) - return "%s/%s/reversals/%s" % (base, cust_extn, extn) - - @classmethod - def modify(cls, sid, **params): - raise NotImplementedError( - "Can't modify a reversal without a transfer" - "ID. Call save on transfer.reversals.retrieve('reversal_id')") - - @classmethod - def retrieve(cls, id, api_key=None, **params): - raise NotImplementedError( - "Can't retrieve a reversal without a transfer" - "ID. Use transfer.reversals.retrieve('reversal_id')") - - -class Recipient(CreateableAPIResource, UpdateableAPIResource, - ListableAPIResource, DeletableAPIResource): - - def transfers(self, **params): - params['recipient'] = self.id - transfers = Transfer.list(self.api_key, **params) - return transfers - - -# This resource can only be instantiated when expanded on a BalanceTransaction -class RecipientTransfer(StripeObject): - pass - - -class FileUpload(ListableAPIResource): - @classmethod - def api_base(cls): - return upload_api_base - - @classmethod - def class_name(cls): - return 'file' - - @classmethod - def create(cls, api_key=None, api_version=None, stripe_account=None, - **params): - requestor = api_requestor.APIRequestor( - api_key, api_base=cls.api_base(), api_version=api_version, - account=stripe_account) - url = cls.class_url() - supplied_headers = { - "Content-Type": "multipart/form-data" - } - response, api_key = requestor.request( - 'post', url, params=params, headers=supplied_headers) - return convert_to_stripe_object(response, api_key, api_version, - stripe_account) - - -class ApplicationFee(ListableAPIResource): - @classmethod - def class_name(cls): - return 'application_fee' - - def refund(self, idempotency_key=None, **params): - headers = populate_headers(idempotency_key) - url = self.instance_url() + '/refund' - self.refresh_from(self.request('post', url, params, headers)) - return self - - -class ApplicationFeeRefund(UpdateableAPIResource): - - @classmethod - def _build_instance_url(cls, fee, sid): - fee = util.utf8(fee) - sid = util.utf8(sid) - base = ApplicationFee.class_url() - cust_extn = urllib.quote_plus(fee) - extn = urllib.quote_plus(sid) - return "%s/%s/refunds/%s" % (base, cust_extn, extn) - - @classmethod - def modify(cls, fee, sid, **params): - url = cls._build_instance_url(fee, sid) - return cls._modify(url, **params) - - def instance_url(self): - return self._build_instance_url(self.fee, self.id) - - @classmethod - def retrieve(cls, id, api_key=None, **params): - raise NotImplementedError( - "Can't retrieve a refund without an application fee ID. " - "Use application_fee.refunds.retrieve('refund_id') instead.") - - -class BitcoinReceiver(CreateableAPIResource, UpdateableAPIResource, - DeletableAPIResource, ListableAPIResource): - - def instance_url(self): - token = util.utf8(self.id) - extn = urllib.quote_plus(token) - - if hasattr(self, 'customer'): - customer = util.utf8(self.customer) - base = Customer.class_url() - cust_extn = urllib.quote_plus(customer) - return "%s/%s/sources/%s" % (base, cust_extn, extn) - else: - base = BitcoinReceiver.class_url() - return "%s/%s" % (base, extn) - - @classmethod - def class_url(cls): - return '/v1/bitcoin/receivers' - - -class BitcoinTransaction(StripeObject): - pass - - -class Product(CreateableAPIResource, UpdateableAPIResource, - ListableAPIResource, DeletableAPIResource): - pass - - -class SKU(CreateableAPIResource, UpdateableAPIResource, - ListableAPIResource, DeletableAPIResource): - pass - - -class Order(CreateableAPIResource, UpdateableAPIResource, - ListableAPIResource): - @classmethod - def create(cls, **params): - if "items" in params: - params["items"] = convert_array_to_dict(params["items"]) - return super(Order, cls).create(**params) - - def pay(self, idempotency_key=None, **params): - headers = populate_headers(idempotency_key) - return self.request( - 'post', self.instance_url() + '/pay', params, headers) - - def return_order(self, idempotency_key=None, **params): - if "items" in params: - params["items"] = convert_array_to_dict(params["items"]) - headers = populate_headers(idempotency_key) - return self.request( - 'post', self.instance_url() + '/returns', params, headers) - - -class OrderReturn(ListableAPIResource): - @classmethod - def class_url(cls): - return '/v1/order_returns' - - -class CountrySpec(ListableAPIResource): - @classmethod - def class_name(cls): - return 'country_spec' - - -class ThreeDSecure(CreateableAPIResource): - @classmethod - def class_url(cls): - return '/v1/3d_secure' - - -class ApplePayDomain(CreateableAPIResource, ListableAPIResource, - DeletableAPIResource): - @classmethod - def class_url(cls): - return '/v1/apple_pay/domains' - - -class Source(CreateableAPIResource, UpdateableAPIResource, VerifyMixin): - def detach(self, **params): - if hasattr(self, 'customer') and self.customer: - extn = urllib.quote_plus(util.utf8(self.id)) - customer = util.utf8(self.customer) - base = Customer.class_url() - owner_extn = urllib.quote_plus(customer) - url = "%s/%s/sources/%s" % (base, owner_extn, extn) - - self.refresh_from(self.request('delete', url, params)) - return self - - else: - raise NotImplementedError( - "This source object does not appear to be currently attached " - "to a customer object.") - - def delete(self, **params): - warnings.warn("The `Source.delete` method is deprecated and will " - "be removed in future versions. Please use the " - "`Source.detach` method instead", - DeprecationWarning) - self.detach(**params) diff --git a/stripe/stripe_object.py b/stripe/stripe_object.py new file mode 100644 index 000000000..8b1351aca --- /dev/null +++ b/stripe/stripe_object.py @@ -0,0 +1,258 @@ +import sys +import warnings +from copy import deepcopy + +import stripe +from stripe import api_requestor, util + + +def _compute_diff(current, previous): + if isinstance(current, dict): + previous = previous or {} + diff = current.copy() + for key in set(previous.keys()) - set(diff.keys()): + diff[key] = "" + return diff + return current if current is not None else "" + + +def _serialize_list(array, previous): + array = array or [] + previous = previous or [] + params = {} + + for i, v in enumerate(array): + previous_item = previous[i] if len(previous) > i else None + if hasattr(v, 'serialize'): + params[str(i)] = v.serialize(previous_item) + else: + params[str(i)] = _compute_diff(v, previous_item) + + return params + + +class StripeObject(dict): + def __init__(self, id=None, api_key=None, stripe_version=None, + stripe_account=None, **params): + super(StripeObject, self).__init__() + + self._unsaved_values = set() + self._transient_values = set() + + self._retrieve_params = params + self._previous = None + + object.__setattr__(self, 'api_key', api_key) + object.__setattr__(self, 'stripe_version', stripe_version) + object.__setattr__(self, 'stripe_account', stripe_account) + + if id: + self['id'] = id + + def update(self, update_dict): + for k in update_dict: + self._unsaved_values.add(k) + + return super(StripeObject, self).update(update_dict) + + def __setattr__(self, k, v): + if k[0] == '_' or k in self.__dict__: + return super(StripeObject, self).__setattr__(k, v) + + self[k] = v + return None + + def __getattr__(self, k): + if k[0] == '_': + raise AttributeError(k) + + try: + return self[k] + except KeyError as err: + raise AttributeError(*err.args) + + def __delattr__(self, k): + if k[0] == '_' or k in self.__dict__: + return super(StripeObject, self).__delattr__(k) + else: + del self[k] + + def __setitem__(self, k, v): + if v == "": + raise ValueError( + "You cannot set %s to an empty string. " + "We interpret empty strings as None in requests." + "You may set %s.%s = None to delete the property" % ( + k, str(self), k)) + + super(StripeObject, self).__setitem__(k, v) + + # Allows for unpickling in Python 3.x + if not hasattr(self, '_unsaved_values'): + self._unsaved_values = set() + + self._unsaved_values.add(k) + + def __getitem__(self, k): + try: + return super(StripeObject, self).__getitem__(k) + except KeyError as err: + if k in self._transient_values: + raise KeyError( + "%r. HINT: The %r attribute was set in the past." + "It was then wiped when refreshing the object with " + "the result returned by Stripe's API, probably as a " + "result of a save(). The attributes currently " + "available on this object are: %s" % + (k, k, ', '.join(self.keys()))) + else: + raise err + + def __delitem__(self, k): + super(StripeObject, self).__delitem__(k) + + # Allows for unpickling in Python 3.x + if hasattr(self, '_unsaved_values'): + self._unsaved_values.remove(k) + + @classmethod + def construct_from(cls, values, key, stripe_version=None, + stripe_account=None): + instance = cls(values.get('id'), api_key=key, + stripe_version=stripe_version, + stripe_account=stripe_account) + instance.refresh_from(values, api_key=key, + stripe_version=stripe_version, + stripe_account=stripe_account) + return instance + + def refresh_from(self, values, api_key=None, partial=False, + stripe_version=None, stripe_account=None): + self.api_key = api_key or getattr(values, 'api_key', None) + self.stripe_version = \ + stripe_version or getattr(values, 'stripe_version', None) + self.stripe_account = \ + stripe_account or getattr(values, 'stripe_account', None) + + # Wipe old state before setting new. This is useful for e.g. + # updating a customer, where there is no persistent card + # parameter. Mark those values which don't persist as transient + if partial: + self._unsaved_values = (self._unsaved_values - set(values)) + else: + removed = set(self.keys()) - set(values) + self._transient_values = self._transient_values | removed + self._unsaved_values = set() + self.clear() + + self._transient_values = self._transient_values - set(values) + + for k, v in values.iteritems(): + super(StripeObject, self).__setitem__( + k, util.convert_to_stripe_object(v, api_key, stripe_version, + stripe_account)) + + self._previous = values + + @classmethod + def api_base(cls): + return None + + def request(self, method, url, params=None, headers=None): + if params is None: + params = self._retrieve_params + requestor = api_requestor.APIRequestor( + key=self.api_key, api_base=self.api_base(), + api_version=self.stripe_version, account=self.stripe_account) + response, api_key = requestor.request(method, url, params, headers) + + return util.convert_to_stripe_object(response, api_key, + self.stripe_version, + self.stripe_account) + + def __repr__(self): + ident_parts = [type(self).__name__] + + if isinstance(self.get('object'), basestring): + ident_parts.append(self.get('object')) + + if isinstance(self.get('id'), basestring): + ident_parts.append('id=%s' % (self.get('id'),)) + + unicode_repr = '<%s at %s> JSON: %s' % ( + ' '.join(ident_parts), hex(id(self)), str(self)) + + if sys.version_info[0] < 3: + return unicode_repr.encode('utf-8') + else: + return unicode_repr + + def __str__(self): + return util.json.dumps(self, sort_keys=True, indent=2) + + def to_dict(self): + warnings.warn( + 'The `to_dict` method is deprecated and will be removed in ' + 'version 2.0 of the Stripe bindings. The StripeObject is ' + 'itself now a subclass of `dict`.', + DeprecationWarning) + + return dict(self) + + @property + def stripe_id(self): + return self.id + + def serialize(self, previous): + params = {} + unsaved_keys = self._unsaved_values or set() + previous = previous or self._previous or {} + + for k, v in self.items(): + if k == 'id' or (isinstance(k, str) and k.startswith('_')): + continue + elif isinstance(v, stripe.api_resources.abstract.APIResource): + continue + elif hasattr(v, 'serialize'): + params[k] = v.serialize(previous.get(k, None)) + elif k in unsaved_keys: + params[k] = _compute_diff(v, previous.get(k, None)) + elif k == 'additional_owners' and v is not None: + params[k] = _serialize_list(v, previous.get(k, None)) + + return params + + # This class overrides __setitem__ to throw exceptions on inputs that it + # doesn't like. This can cause problems when we try to copy an object + # wholesale because some data that's returned from the API may not be valid + # if it was set to be set manually. Here we override the class' copy + # arguments so that we can bypass these possible exceptions on __setitem__. + def __copy__(self): + copied = StripeObject(self.get('id'), self.api_key, + stripe_version=self.stripe_version, + stripe_account=self.stripe_account) + + copied._retrieve_params = self._retrieve_params + + for k, v in self.items(): + # Call parent's __setitem__ to avoid checks that we've added in the + # overridden version that can throw exceptions. + super(StripeObject, copied).__setitem__(k, v) + + return copied + + # This class overrides __setitem__ to throw exceptions on inputs that it + # doesn't like. This can cause problems when we try to copy an object + # wholesale because some data that's returned from the API may not be valid + # if it was set to be set manually. Here we override the class' copy + # arguments so that we can bypass these possible exceptions on __setitem__. + def __deepcopy__(self, memo): + copied = self.__copy__() + memo[id(self)] = copied + + for k, v in self.items(): + # Call parent's __setitem__ to avoid checks that we've added in the + # overridden version that can throw exceptions. + super(StripeObject, copied).__setitem__(k, deepcopy(v, memo)) + + return copied diff --git a/stripe/util.py b/stripe/util.py index 33eff057c..4bf411995 100644 --- a/stripe/util.py +++ b/stripe/util.py @@ -155,3 +155,104 @@ def secure_compare(val1, val2): for x, y in zip(val1, val2): result |= ord(x) ^ ord(y) return result == 0 + + +OBJECT_CLASSES = {} + + +def load_object_classes(): + # This is here to avoid a circular dependency + from stripe import api_resources + + global OBJECT_CLASSES + + OBJECT_CLASSES = { + # data structures + api_resources.ListObject.OBJECT_NAME: api_resources.ListObject, + + # business objects + api_resources.Account.OBJECT_NAME: api_resources.Account, + api_resources.AlipayAccount.OBJECT_NAME: api_resources.AlipayAccount, + api_resources.ApplePayDomain.OBJECT_NAME: api_resources.ApplePayDomain, + api_resources.ApplicationFee.OBJECT_NAME: api_resources.ApplicationFee, + api_resources.ApplicationFeeRefund.OBJECT_NAME: + api_resources.ApplicationFeeRefund, + api_resources.Balance.OBJECT_NAME: api_resources.Balance, + api_resources.BankAccount.OBJECT_NAME: api_resources.BankAccount, + api_resources.BitcoinReceiver.OBJECT_NAME: + api_resources.BitcoinReceiver, + api_resources.BitcoinTransaction.OBJECT_NAME: + api_resources.BitcoinTransaction, + api_resources.Card.OBJECT_NAME: api_resources.Card, + api_resources.Charge.OBJECT_NAME: api_resources.Charge, + api_resources.CountrySpec.OBJECT_NAME: api_resources.CountrySpec, + api_resources.Coupon.OBJECT_NAME: api_resources.Coupon, + api_resources.Customer.OBJECT_NAME: api_resources.Customer, + api_resources.Dispute.OBJECT_NAME: api_resources.Dispute, + api_resources.EphemeralKey.OBJECT_NAME: api_resources.EphemeralKey, + api_resources.Event.OBJECT_NAME: api_resources.Event, + api_resources.FileUpload.OBJECT_NAME: api_resources.FileUpload, + api_resources.Invoice.OBJECT_NAME: api_resources.Invoice, + api_resources.InvoiceItem.OBJECT_NAME: api_resources.InvoiceItem, + api_resources.LoginLink.OBJECT_NAME: api_resources.LoginLink, + api_resources.Order.OBJECT_NAME: api_resources.Order, + api_resources.OrderReturn.OBJECT_NAME: api_resources.OrderReturn, + api_resources.Payout.OBJECT_NAME: api_resources.Payout, + api_resources.Plan.OBJECT_NAME: api_resources.Plan, + api_resources.Product.OBJECT_NAME: api_resources.Product, + api_resources.Recipient.OBJECT_NAME: api_resources.Recipient, + api_resources.RecipientTransfer.OBJECT_NAME: + api_resources.RecipientTransfer, + api_resources.Refund.OBJECT_NAME: api_resources.Refund, + api_resources.Reversal.OBJECT_NAME: api_resources.Reversal, + api_resources.SKU.OBJECT_NAME: api_resources.SKU, + api_resources.Source.OBJECT_NAME: api_resources.Source, + api_resources.Subscription.OBJECT_NAME: api_resources.Subscription, + api_resources.SubscriptionItem.OBJECT_NAME: + api_resources.SubscriptionItem, + api_resources.ThreeDSecure.OBJECT_NAME: api_resources.ThreeDSecure, + api_resources.Token.OBJECT_NAME: api_resources.Token, + api_resources.Transfer.OBJECT_NAME: api_resources.Transfer, + } + + +def convert_to_stripe_object(resp, api_key=None, stripe_version=None, + stripe_account=None): + global OBJECT_CLASSES + + if len(OBJECT_CLASSES) == 0: + load_object_classes() + types = OBJECT_CLASSES.copy() + + if isinstance(resp, list): + return [convert_to_stripe_object(i, api_key, stripe_version, + stripe_account) for i in resp] + elif isinstance(resp, dict) and \ + not isinstance(resp, stripe.stripe_object.StripeObject): + resp = resp.copy() + klass_name = resp.get('object') + if isinstance(klass_name, basestring): + klass = types.get(klass_name, stripe.stripe_object.StripeObject) + else: + klass = stripe.stripe_object.StripeObject + return klass.construct_from(resp, api_key, + stripe_version=stripe_version, + stripe_account=stripe_account) + else: + return resp + + +def convert_array_to_dict(arr): + if isinstance(arr, list): + d = {} + for i, value in enumerate(arr): + d[str(i)] = value + return d + else: + return arr + + +def populate_headers(idempotency_key): + if idempotency_key is not None: + return {"Idempotency-Key": idempotency_key} + return None