Skip to content

Commit

Permalink
Merge pull request #350 from stripe/ob-nested-resource-class-methods
Browse files Browse the repository at this point in the history
Nested resource class methods
  • Loading branch information
brandur-stripe authored Oct 19, 2017
2 parents 0422c70 + 872b354 commit e6ee7be
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 44 deletions.
112 changes: 100 additions & 12 deletions stripe/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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):
Expand Down
92 changes: 76 additions & 16 deletions stripe/test/resources/test_accounts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import stripe
from stripe.test.helper import (StripeResourceTest, NOW)
from stripe.test.helper import StripeResourceTest


class AccountTest(StripeResourceTest):
Expand Down Expand Up @@ -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({
Expand All @@ -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
)
65 changes: 65 additions & 0 deletions stripe/test/resources/test_api_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
49 changes: 49 additions & 0 deletions stripe/test/resources/test_application_fees.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Loading

0 comments on commit e6ee7be

Please sign in to comment.