Skip to content

Commit

Permalink
custom_method decorator for defining static methods for custom API …
Browse files Browse the repository at this point in the history
…requests (#547)

* `custom_method` decorator for defining static methods for custom API requests

* Add custom methods to all resources
  • Loading branch information
ob-stripe authored Apr 3, 2019
1 parent 88d2a2b commit 97205a1
Show file tree
Hide file tree
Showing 34 changed files with 301 additions and 18 deletions.
2 changes: 2 additions & 0 deletions stripe/api_resources/abstract/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
)
from stripe.api_resources.abstract.verify_mixin import VerifyMixin

from stripe.api_resources.abstract.custom_method import custom_method

from stripe.api_resources.abstract.nested_resource_class_methods import (
nested_resource_class_methods,
)
43 changes: 43 additions & 0 deletions stripe/api_resources/abstract/custom_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import absolute_import, division, print_function

from stripe import util
from stripe.six.moves.urllib.parse import quote_plus


def custom_method(name, http_verb, http_path=None):
if http_verb not in ["get", "post", "delete"]:
raise ValueError(
"Invalid http_verb: %s. Must be one of 'get', 'post' or 'delete'"
% http_verb
)
if http_path is None:
http_path = name

def wrapper(cls):
def custom_method_request(cls, sid, **params):
url = "%s/%s/%s" % (
cls.class_url(),
quote_plus(util.utf8(sid)),
http_path,
)
return cls._static_request(http_verb, url, **params)

existing_method = getattr(cls, name, None)
if existing_method is None:
setattr(cls, name, classmethod(custom_method_request))
else:
# If a method with the same name we want to use already exists on
# the class, we assume it's an instance method. In this case, the
# new class method is prefixed with `_cls_`, and the original
# instance method is decorated with `util.class_method_variant` so
# that the new class method is called when the original method is
# called as a class method.
setattr(cls, "_cls_" + name, classmethod(custom_method_request))
instance_method = util.class_method_variant("_cls_" + name)(
existing_method
)
setattr(cls, name, instance_method)

return cls

return wrapper
3 changes: 2 additions & 1 deletion stripe/api_resources/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import nested_resource_class_methods

from stripe.api_resources.abstract import custom_method
from stripe.six.moves.urllib.parse import quote_plus


@custom_method("reject", http_verb="post")
@nested_resource_class_methods(
"external_account",
operations=["create", "retrieve", "update", "delete", "list"],
Expand Down
2 changes: 2 additions & 0 deletions stripe/api_resources/charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
from stripe.api_resources.abstract import CreateableAPIResource
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("capture", http_verb="post")
class Charge(
CreateableAPIResource, ListableAPIResource, UpdateableAPIResource
):
Expand Down
2 changes: 2 additions & 0 deletions stripe/api_resources/customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
from stripe.api_resources.abstract import DeletableAPIResource
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import custom_method
from stripe.api_resources.abstract import nested_resource_class_methods


@custom_method("delete_discount", http_verb="delete", http_path="discount")
@nested_resource_class_methods(
"source", operations=["create", "retrieve", "update", "delete", "list"]
)
Expand Down
2 changes: 2 additions & 0 deletions stripe/api_resources/dispute.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from stripe import util
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("close", http_verb="post")
class Dispute(ListableAPIResource, UpdateableAPIResource):
OBJECT_NAME = "dispute"

Expand Down
6 changes: 6 additions & 0 deletions stripe/api_resources/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
from stripe.api_resources.abstract import DeletableAPIResource
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("finalize_invoice", http_verb="post", http_path="finalize")
@custom_method("mark_uncollectible", http_verb="post")
@custom_method("pay", http_verb="post")
@custom_method("send_invoice", http_verb="post", http_path="send")
@custom_method("void_invoice", http_verb="post", http_path="void")
class Invoice(
CreateableAPIResource,
UpdateableAPIResource,
Expand Down
3 changes: 3 additions & 0 deletions stripe/api_resources/issuing/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from stripe import util
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("approve", http_verb="post")
@custom_method("decline", http_verb="post")
class Authorization(ListableAPIResource, UpdateableAPIResource):
OBJECT_NAME = "issuing.authorization"

Expand Down
2 changes: 2 additions & 0 deletions stripe/api_resources/issuing/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from stripe.api_resources.abstract import CreateableAPIResource
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("details", http_verb="get")
class Card(CreateableAPIResource, ListableAPIResource, UpdateableAPIResource):
OBJECT_NAME = "issuing.card"

Expand Down
3 changes: 3 additions & 0 deletions stripe/api_resources/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
from stripe.api_resources.abstract import CreateableAPIResource
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("pay", http_verb="post")
@custom_method("return_order", http_verb="post", http_path="returns")
class Order(CreateableAPIResource, UpdateableAPIResource, ListableAPIResource):
OBJECT_NAME = "order"

Expand Down
4 changes: 4 additions & 0 deletions stripe/api_resources/payment_intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
from stripe.api_resources.abstract import CreateableAPIResource
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("cancel", http_verb="post")
@custom_method("capture", http_verb="post")
@custom_method("confirm", http_verb="post")
class PaymentIntent(
CreateableAPIResource, UpdateableAPIResource, ListableAPIResource
):
Expand Down
3 changes: 3 additions & 0 deletions stripe/api_resources/payment_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
from stripe.api_resources.abstract import CreateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("attach", http_verb="post")
@custom_method("detach", http_verb="post")
class PaymentMethod(
CreateableAPIResource, ListableAPIResource, UpdateableAPIResource
):
Expand Down
2 changes: 2 additions & 0 deletions stripe/api_resources/payout.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
from stripe.api_resources.abstract import CreateableAPIResource
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("cancel", http_verb="post")
class Payout(
CreateableAPIResource, UpdateableAPIResource, ListableAPIResource
):
Expand Down
2 changes: 2 additions & 0 deletions stripe/api_resources/review.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from stripe import util
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("approve", http_verb="post")
class Review(ListableAPIResource):
OBJECT_NAME = "review"

Expand Down
2 changes: 2 additions & 0 deletions stripe/api_resources/subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from stripe.api_resources.abstract import DeletableAPIResource
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("delete_discount", http_verb="delete", http_path="discount")
class Subscription(
CreateableAPIResource,
DeletableAPIResource,
Expand Down
3 changes: 3 additions & 0 deletions stripe/api_resources/subscription_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import nested_resource_class_methods
from stripe.api_resources.abstract import custom_method


@custom_method("cancel", http_verb="post")
@custom_method("release", http_verb="post")
@nested_resource_class_methods("revision", operations=["retrieve", "list"])
class SubscriptionSchedule(
CreateableAPIResource, UpdateableAPIResource, ListableAPIResource
Expand Down
2 changes: 2 additions & 0 deletions stripe/api_resources/topup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
from stripe.api_resources.abstract import CreateableAPIResource
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import custom_method


@custom_method("cancel", http_verb="post")
class Topup(CreateableAPIResource, ListableAPIResource, UpdateableAPIResource):
OBJECT_NAME = "topup"

Expand Down
2 changes: 2 additions & 0 deletions stripe/api_resources/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from stripe.api_resources.abstract import UpdateableAPIResource
from stripe.api_resources.abstract import ListableAPIResource
from stripe.api_resources.abstract import nested_resource_class_methods
from stripe.api_resources.abstract import custom_method


@custom_method("cancel", http_verb="post")
@nested_resource_class_methods(
"reversal", operations=["create", "retrieve", "update", "list"]
)
Expand Down
18 changes: 16 additions & 2 deletions tests/api_resources/issuing/test_authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_is_saveable(self, request_mock):
assert isinstance(resource, stripe.issuing.Authorization)
assert resource is authorization

def test_is_approveable(self, request_mock):
def test_can_approve(self, request_mock):
resource = stripe.issuing.Authorization.retrieve(TEST_RESOURCE_ID)
authorization = resource.approve()
request_mock.assert_requested(
Expand All @@ -48,11 +48,25 @@ def test_is_approveable(self, request_mock):
assert isinstance(resource, stripe.issuing.Authorization)
assert resource is authorization

def test_is_declineable(self, request_mock):
def test_can_approve_classmethod(self, request_mock):
resource = stripe.issuing.Authorization.approve(TEST_RESOURCE_ID)
request_mock.assert_requested(
"post", "/v1/issuing/authorizations/%s/approve" % TEST_RESOURCE_ID
)
assert isinstance(resource, stripe.issuing.Authorization)

def test_can_decline(self, request_mock):
resource = stripe.issuing.Authorization.retrieve(TEST_RESOURCE_ID)
authorization = resource.decline()
request_mock.assert_requested(
"post", "/v1/issuing/authorizations/%s/decline" % TEST_RESOURCE_ID
)
assert isinstance(resource, stripe.issuing.Authorization)
assert resource is authorization

def test_can_decline_classmethod(self, request_mock):
resource = stripe.issuing.Authorization.decline(TEST_RESOURCE_ID)
request_mock.assert_requested(
"post", "/v1/issuing/authorizations/%s/decline" % TEST_RESOURCE_ID
)
assert isinstance(resource, stripe.issuing.Authorization)
7 changes: 7 additions & 0 deletions tests/api_resources/issuing/test_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,10 @@ def test_can_retrieve_details(self, request_mock):
"get", "/v1/issuing/cards/%s/details" % TEST_RESOURCE_ID
)
assert isinstance(card_details, stripe.issuing.CardDetails)

def test_can_retrieve_details_classmethod(self, request_mock):
card_details = stripe.issuing.Card.details(TEST_RESOURCE_ID)
request_mock.assert_requested(
"get", "/v1/issuing/cards/%s/details" % TEST_RESOURCE_ID
)
assert isinstance(card_details, stripe.issuing.CardDetails)
13 changes: 12 additions & 1 deletion tests/api_resources/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,22 @@ def test_can_reject(self, request_mock):
account = stripe.Account.retrieve(TEST_RESOURCE_ID)
resource = account.reject(reason="fraud")
request_mock.assert_requested(
"post", "/v1/accounts/%s/reject" % TEST_RESOURCE_ID
"post",
"/v1/accounts/%s/reject" % TEST_RESOURCE_ID,
{"reason": "fraud"},
)
assert isinstance(resource, stripe.Account)
assert resource is account

def test_can_reject_classmethod(self, request_mock):
resource = stripe.Account.reject(TEST_RESOURCE_ID, reason="fraud")
request_mock.assert_requested(
"post",
"/v1/accounts/%s/reject" % TEST_RESOURCE_ID,
{"reason": "fraud"},
)
assert isinstance(resource, stripe.Account)

def test_is_deauthorizable(self, request_mock):
account = stripe.Account.retrieve(TEST_RESOURCE_ID)
request_mock.stub_request(
Expand Down
9 changes: 8 additions & 1 deletion tests/api_resources/test_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,21 @@ def test_is_refundable(self, request_mock):
)
assert isinstance(resource, stripe.Charge)

def test_is_capturable(self, request_mock):
def test_can_capture(self, request_mock):
charge = stripe.Charge.retrieve(TEST_RESOURCE_ID)
resource = charge.capture()
request_mock.assert_requested(
"post", "/v1/charges/%s/capture" % TEST_RESOURCE_ID
)
assert isinstance(resource, stripe.Charge)

def test_can_capture_classmethod(self, request_mock):
resource = stripe.Charge.capture(TEST_RESOURCE_ID)
request_mock.assert_requested(
"post", "/v1/charges/%s/capture" % TEST_RESOURCE_ID
)
assert isinstance(resource, stripe.Charge)

def test_can_update_dispute(self, request_mock):
charge = stripe.Charge.retrieve(TEST_RESOURCE_ID)
resource = charge.update_dispute()
Expand Down
6 changes: 6 additions & 0 deletions tests/api_resources/test_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ def test_can_delete_discount(self, request_mock):
"delete", "/v1/customers/%s/discount" % TEST_RESOURCE_ID
)

def test_can_delete_discount_class_method(self, request_mock):
stripe.Customer.delete_discount(TEST_RESOURCE_ID)
request_mock.assert_requested(
"delete", "/v1/customers/%s/discount" % TEST_RESOURCE_ID
)


class TestCustomerSources(object):
def test_is_creatable(self, request_mock):
Expand Down
10 changes: 9 additions & 1 deletion tests/api_resources/test_dispute.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,17 @@ def test_is_modifiable(self, request_mock):
)
assert isinstance(resource, stripe.Dispute)

def test_is_closeable(self, request_mock):
def test_can_close(self, request_mock):
resource = stripe.Dispute.retrieve(TEST_RESOURCE_ID)
resource.close()
request_mock.assert_requested(
"post", "/v1/disputes/%s/close" % TEST_RESOURCE_ID
)
assert isinstance(resource, stripe.Dispute)

def test_can_close_classmethod(self, request_mock):
resource = stripe.Dispute.close(TEST_RESOURCE_ID)
request_mock.assert_requested(
"post", "/v1/disputes/%s/close" % TEST_RESOURCE_ID
)
assert isinstance(resource, stripe.Dispute)
Loading

0 comments on commit 97205a1

Please sign in to comment.