diff --git a/stripe/resource.py b/stripe/resource.py index 43ee36aa2..b6a128c88 100644 --- a/stripe/resource.py +++ b/stripe/resource.py @@ -106,6 +106,89 @@ def _serialize_list(array, previous): return params +def nested_resource_class_methods(resource, path=None, operations=None): + if path is None: + path = "%ss" % resource + if operations is None: + raise ValueError("operations list required") + + def wrapper(cls): + def nested_resource_url(cls, id, nested_id=None): + url = "%s/%s/%s" % (cls.class_url(), urllib.quote_plus(id), + urllib.quote_plus(path)) + if nested_id is not None: + url += "/%s" % urllib.quote_plus(nested_id) + return url + resource_url_method = "%ss_url" % resource + setattr(cls, resource_url_method, classmethod(nested_resource_url)) + + def nested_resource_request(cls, method, 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(method, url, params, headers) + return convert_to_stripe_object(response, api_key, stripe_version, + stripe_account) + resource_request_method = "%ss_request" % resource + setattr(cls, resource_request_method, + classmethod(nested_resource_request)) + + for operation in operations: + if operation == 'create': + def create_nested_resource(cls, id, **params): + url = getattr(cls, resource_url_method)(id) + return getattr(cls, resource_request_method)('post', url, + **params) + create_method = "create_%s" % resource + setattr(cls, create_method, + classmethod(create_nested_resource)) + + elif operation == 'retrieve': + def retrieve_nested_resource(cls, id, nested_id, **params): + url = getattr(cls, resource_url_method)(id, nested_id) + return getattr(cls, resource_request_method)('get', url, + **params) + retrieve_method = "retrieve_%s" % resource + setattr(cls, retrieve_method, + classmethod(retrieve_nested_resource)) + + elif operation == 'update': + def modify_nested_resource(cls, id, nested_id, **params): + url = getattr(cls, resource_url_method)(id, nested_id) + return getattr(cls, resource_request_method)('post', url, + **params) + modify_method = "modify_%s" % resource + setattr(cls, modify_method, + classmethod(modify_nested_resource)) + + elif operation == 'delete': + def delete_nested_resource(cls, id, nested_id, **params): + url = getattr(cls, resource_url_method)(id, nested_id) + return getattr(cls, resource_request_method)('delete', url, + **params) + delete_method = "delete_%s" % resource + setattr(cls, delete_method, + classmethod(delete_nested_resource)) + + elif operation == 'list': + def list_nested_resources(cls, id, **params): + url = getattr(cls, resource_url_method)(id) + return getattr(cls, resource_request_method)('get', url, + **params) + list_method = "list_%ss" % resource + setattr(cls, list_method, classmethod(list_nested_resources)) + + else: + raise ValueError("Unknown operation: %s" % operation) + + return cls + + return wrapper + + class StripeObject(dict): def __init__(self, id=None, api_key=None, stripe_version=None, stripe_account=None, **params): @@ -532,6 +615,11 @@ def delete(self, **params): # API objects +@nested_resource_class_methods( + 'external_account', + operations=['create', 'retrieve', 'update', 'delete', 'list'] +) +@nested_resource_class_methods('login_link', operations=['create']) class Account(CreateableAPIResource, ListableAPIResource, UpdateableAPIResource, DeletableAPIResource): @classmethod @@ -572,13 +660,6 @@ 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): @@ -787,6 +868,10 @@ def close(self, idempotency_key=None): return self +@nested_resource_class_methods( + 'source', + operations=['create', 'retrieve', 'update', 'delete', 'list'] +) class Customer(CreateableAPIResource, UpdateableAPIResource, ListableAPIResource, DeletableAPIResource): @@ -850,12 +935,11 @@ def delete_discount(self, **params): _, api_key = requestor.request('delete', url) self.refresh_from({'discount': None}, api_key, True) + # The API request for deleting a card or bank account and for detaching a + # source object are the same. @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) + def detach_source(cls, id, source_id, **params): + return cls.delete_source(id, source_id, **params) class Invoice(CreateableAPIResource, ListableAPIResource, @@ -994,6 +1078,8 @@ def cancel(self): self.instance_url() + '/cancel')) +@nested_resource_class_methods('reversal', operations=['create', 'retrieve', + 'update', 'list']) class Transfer(CreateableAPIResource, UpdateableAPIResource, ListableAPIResource): @@ -1064,6 +1150,8 @@ def create(cls, api_key=None, api_version=None, stripe_account=None, stripe_account) +@nested_resource_class_methods('refund', operations=['create', 'retrieve', + 'update', 'list']) class ApplicationFee(ListableAPIResource): @classmethod def class_name(cls): diff --git a/stripe/test/resources/test_accounts.py b/stripe/test/resources/test_accounts.py index b966ac3cf..0bc286d34 100644 --- a/stripe/test/resources/test_accounts.py +++ b/stripe/test/resources/test_accounts.py @@ -1,5 +1,5 @@ import stripe -from stripe.test.helper import (StripeResourceTest, NOW) +from stripe.test.helper import StripeResourceTest class AccountTest(StripeResourceTest): @@ -153,21 +153,6 @@ def test_verify_additional_owner(self): None, ) - def test_modify_external_account(self): - stripe.Account.modify_external_account( - 'acct_test', 'card_test', - exp_month=NOW.month, exp_year=NOW.year + 1) - - self.requestor_mock.request.assert_called_with( - 'post', - '/v1/accounts/acct_test/external_accounts/card_test', - { - 'exp_month': NOW.month, - 'exp_year': NOW.year + 1, - }, - None - ) - def test_login_link_create(self): acct_id = 'acct_EXPRESS' acct = stripe.Account.construct_from({ @@ -187,3 +172,78 @@ def test_login_link_create(self): {}, None ) + + +class AccountExternalAccountsTests(StripeResourceTest): + def test_create_external_account(self): + stripe.Account.create_external_account( + 'acct_123', + source='btok_123' + ) + self.requestor_mock.request.assert_called_with( + 'post', + '/v1/accounts/acct_123/external_accounts', + {'source': 'btok_123'}, + None + ) + + def test_retrieve_external_account(self): + stripe.Account.retrieve_external_account( + 'acct_123', + 'ba_123' + ) + self.requestor_mock.request.assert_called_with( + 'get', + '/v1/accounts/acct_123/external_accounts/ba_123', + {}, + None + ) + + def test_modify_external_account(self): + stripe.Account.modify_external_account( + 'acct_123', + 'ba_123', + metadata={'foo': 'bar'} + ) + self.requestor_mock.request.assert_called_with( + 'post', + '/v1/accounts/acct_123/external_accounts/ba_123', + {'metadata': {'foo': 'bar'}}, + None + ) + + def test_delete_external_account(self): + stripe.Account.delete_external_account( + 'acct_123', + 'ba_123' + ) + self.requestor_mock.request.assert_called_with( + 'delete', + '/v1/accounts/acct_123/external_accounts/ba_123', + {}, + None + ) + + def test_list_external_accounts(self): + stripe.Account.list_external_accounts( + 'acct_123' + ) + self.requestor_mock.request.assert_called_with( + 'get', + '/v1/accounts/acct_123/external_accounts', + {}, + None + ) + + +class AccountLoginLinksTests(StripeResourceTest): + def test_create_login_link(self): + stripe.Account.create_login_link( + 'acct_123' + ) + self.requestor_mock.request.assert_called_with( + 'post', + '/v1/accounts/acct_123/login_links', + {}, + None + ) diff --git a/stripe/test/resources/test_api_resource.py b/stripe/test/resources/test_api_resource.py index 9c4f33684..0db30eda6 100644 --- a/stripe/test/resources/test_api_resource.py +++ b/stripe/test/resources/test_api_resource.py @@ -116,3 +116,68 @@ def test_retrieve(self): 'get', '/v1/mysingleton', {}, None) self.assertEqual('ton', res.single) + + +class NestedResourceClassMethodsTests(StripeApiTestCase): + @stripe.resource.nested_resource_class_methods( + 'nested', + operations=['create', 'retrieve', 'update', 'delete', 'list'] + ) + class MainResource(stripe.resource.APIResource): + pass + + def test_create_nested(self): + self.mock_response({ + 'id': 'nested_id', + 'object': 'nested', + 'foo': 'bar', + }) + nested_resource = self.MainResource.create_nested('id', foo='bar') + self.requestor_mock.request.assert_called_with( + 'post', '/v1/mainresources/id/nesteds', {'foo': 'bar'}, None) + self.assertEqual('bar', nested_resource.foo) + + def test_retrieve_nested(self): + self.mock_response({ + 'id': 'nested_id', + 'object': 'nested', + 'foo': 'bar', + }) + nested_resource = self.MainResource.retrieve_nested('id', 'nested_id') + self.requestor_mock.request.assert_called_with( + 'get', '/v1/mainresources/id/nesteds/nested_id', {}, None) + self.assertEqual('bar', nested_resource.foo) + + def test_modify_nested(self): + self.mock_response({ + 'id': 'nested_id', + 'object': 'nested', + 'foo': 'baz', + }) + nested_resource = self.MainResource.modify_nested('id', 'nested_id', + foo='baz') + self.requestor_mock.request.assert_called_with( + 'post', '/v1/mainresources/id/nesteds/nested_id', {'foo': 'baz'}, + None) + self.assertEqual('baz', nested_resource.foo) + + def test_delete_nested(self): + self.mock_response({ + 'id': 'nested_id', + 'object': 'nested', + 'deleted': True, + }) + nested_resource = self.MainResource.delete_nested('id', 'nested_id') + self.requestor_mock.request.assert_called_with( + 'delete', '/v1/mainresources/id/nesteds/nested_id', {}, None) + self.assertEqual(True, nested_resource.deleted) + + def test_list_nesteds(self): + self.mock_response({ + 'object': 'list', + 'data': [], + }) + nested_resource = self.MainResource.list_nesteds('id') + self.requestor_mock.request.assert_called_with( + 'get', '/v1/mainresources/id/nesteds', {}, None) + self.assertTrue(isinstance(nested_resource.data, list)) diff --git a/stripe/test/resources/test_application_fees.py b/stripe/test/resources/test_application_fees.py index 6738dd714..c1d72da15 100644 --- a/stripe/test/resources/test_application_fees.py +++ b/stripe/test/resources/test_application_fees.py @@ -111,3 +111,52 @@ def test_modify_refund(self): }, None ) + + +class ApplicationFeeRefundsTests(StripeResourceTest): + def test_create_refund(self): + stripe.ApplicationFee.create_refund( + 'fee_123' + ) + self.requestor_mock.request.assert_called_with( + 'post', + '/v1/application_fees/fee_123/refunds', + {}, + None + ) + + def test_retrieve_refund(self): + stripe.ApplicationFee.retrieve_refund( + 'fee_123', + 'fr_123' + ) + self.requestor_mock.request.assert_called_with( + 'get', + '/v1/application_fees/fee_123/refunds/fr_123', + {}, + None + ) + + def test_modify_refund(self): + stripe.ApplicationFee.modify_refund( + 'fee_123', + 'fr_123', + metadata={'foo': 'bar'} + ) + self.requestor_mock.request.assert_called_with( + 'post', + '/v1/application_fees/fee_123/refunds/fr_123', + {'metadata': {'foo': 'bar'}}, + None + ) + + def test_list_refunds(self): + stripe.ApplicationFee.list_refunds( + 'fee_123' + ) + self.requestor_mock.request.assert_called_with( + 'get', + '/v1/application_fees/fee_123/refunds', + {}, + None + ) diff --git a/stripe/test/resources/test_customers.py b/stripe/test/resources/test_customers.py index 9577afc0c..910c8547e 100644 --- a/stripe/test/resources/test_customers.py +++ b/stripe/test/resources/test_customers.py @@ -3,7 +3,7 @@ import warnings import stripe -from stripe.test.helper import (StripeResourceTest, DUMMY_PLAN, NOW) +from stripe.test.helper import (StripeResourceTest, DUMMY_PLAN) class CustomerTest(StripeResourceTest): @@ -223,21 +223,6 @@ def test_customer_verify_bank_account(self): None ) - def test_customer_modify_source(self): - stripe.Customer.modify_source( - 'cus_test', 'card_test', - exp_month=NOW.month, exp_year=NOW.year + 1) - - self.requestor_mock.request.assert_called_with( - 'post', - '/v1/customers/cus_test/sources/card_test', - { - 'exp_month': NOW.month, - 'exp_year': NOW.year + 1, - }, - None - ) - class CustomerPlanTest(StripeResourceTest): @@ -381,3 +366,65 @@ def test_delete_customer_subscription(self): {}, None ) + + +class CustomerSourcesTests(StripeResourceTest): + def test_create_source(self): + stripe.Customer.create_source( + 'cus_123', + source='tok_123' + ) + self.requestor_mock.request.assert_called_with( + 'post', + '/v1/customers/cus_123/sources', + {'source': 'tok_123'}, + None + ) + + def test_retrieve_source(self): + stripe.Customer.retrieve_source( + 'cus_123', + 'ba_123' + ) + self.requestor_mock.request.assert_called_with( + 'get', + '/v1/customers/cus_123/sources/ba_123', + {}, + None + ) + + def test_modify_source(self): + stripe.Customer.modify_source( + 'cus_123', + 'ba_123', + metadata={'foo': 'bar'} + ) + self.requestor_mock.request.assert_called_with( + 'post', + '/v1/customers/cus_123/sources/ba_123', + {'metadata': {'foo': 'bar'}}, + None + ) + + def test_delete_source(self): + stripe.Customer.delete_source( + 'cus_123', + 'ba_123' + ) + self.requestor_mock.request.assert_called_with( + 'delete', + '/v1/customers/cus_123/sources/ba_123', + {}, + None + ) + + def test_list_sources(self): + stripe.Customer.list_sources( + 'cus_123' + ) + self.requestor_mock.request.assert_called_with( + 'get', + '/v1/customers/cus_123/sources', + {}, + None + ) diff --git a/stripe/test/resources/test_transfers.py b/stripe/test/resources/test_transfers.py index d44ee25a2..cc12485b4 100644 --- a/stripe/test/resources/test_transfers.py +++ b/stripe/test/resources/test_transfers.py @@ -22,3 +22,53 @@ def test_cancel_transfer(self): {}, None ) + + +class TransferReversalsTests(StripeResourceTest): + def test_create_reversal(self): + stripe.Transfer.create_reversal( + 'tr_123', + amount=100 + ) + self.requestor_mock.request.assert_called_with( + 'post', + '/v1/transfers/tr_123/reversals', + {'amount': 100}, + None + ) + + def test_retrieve_reversal(self): + stripe.Transfer.retrieve_reversal( + 'tr_123', + 'trr_123' + ) + self.requestor_mock.request.assert_called_with( + 'get', + '/v1/transfers/tr_123/reversals/trr_123', + {}, + None + ) + + def test_modify_reversal(self): + stripe.Transfer.modify_reversal( + 'tr_123', + 'trr_123', + metadata={'foo': 'bar'} + ) + self.requestor_mock.request.assert_called_with( + 'post', + '/v1/transfers/tr_123/reversals/trr_123', + {'metadata': {'foo': 'bar'}}, + None + ) + + def test_list_reversals(self): + stripe.Transfer.list_reversals( + 'tr_123' + ) + self.requestor_mock.request.assert_called_with( + 'get', + '/v1/transfers/tr_123/reversals', + {}, + None + )