diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..5826787 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,24 @@ +name: Ruff + +on: [push, pull_request] + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.13' + cache: 'pip' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + - name: Lint with ruff + run: | + ruff check . + - name: Format with ruff + run: | + ruff format --check . \ No newline at end of file diff --git a/payjp/__init__.py b/payjp/__init__.py index 867ab20..a7c935f 100644 --- a/payjp/__init__.py +++ b/payjp/__init__.py @@ -3,7 +3,7 @@ # Configuration variables api_key = None -api_base = 'https://api.pay.jp' +api_base = "https://api.pay.jp" api_version = None max_retry = 0 @@ -12,22 +12,33 @@ # TODO include Card? __all__ = [ - 'Account', - 'Card', - 'Charge', - 'Customer', - 'Event', - 'Plan', - 'Subscription', - 'Token', - 'Transfer', - 'Statement', - 'Term', - 'Balance', - 'ThreeDSecureRequest' + "Account", + "Card", + "Charge", + "Customer", + "Event", + "Plan", + "Subscription", + "Token", + "Transfer", + "Statement", + "Term", + "Balance", + "ThreeDSecureRequest", ] # Resource from payjp.resource import ( # noqa - Account, Charge, Customer, Event, Plan, Subscription, Token, Transfer, Statement, Term, Balance, ThreeDSecureRequest) - + Account, + Charge, + Customer, + Event, + Plan, + Subscription, + Token, + Transfer, + Statement, + Term, + Balance, + ThreeDSecureRequest, +) diff --git a/payjp/api_requestor.py b/payjp/api_requestor.py index 168bff4..7caf360 100644 --- a/payjp/api_requestor.py +++ b/payjp/api_requestor.py @@ -6,22 +6,22 @@ import json import logging import platform -import time import random +import time from urllib.parse import urlencode, urlsplit, urlunsplit import payjp + from . import ( error, http_client, version, ) -logger = logging.getLogger('payjp') +logger = logging.getLogger("payjp") class APIRequestor(object): - def __init__(self, key=None, client=None, api_base=None, account=None): if api_base: self.api_base = api_base @@ -38,19 +38,20 @@ def _get_retry_delay(self, retry_count): Based on "Exponential backoff with equal jitter" algorithm. https://aws.amazon.com/jp/blogs/architecture/exponential-backoff-and-jitter/ """ - wait = min(payjp.retry_max_delay, payjp.retry_initial_delay * 2 ** retry_count) - return (wait / 2 + random.uniform(0, wait / 2)) + wait = min(payjp.retry_max_delay, payjp.retry_initial_delay * 2**retry_count) + return wait / 2 + random.uniform(0, wait / 2) def request(self, method, url, params=None, headers=None): max_retry = payjp.max_retry or 0 for i in range(max_retry + 1): body, code, my_api_key = self.request_raw( - method.lower(), url, params, headers) + method.lower(), url, params, headers + ) if code != 429: break elif i != max_retry: wait = self._get_retry_delay(i) - logger.debug('Retry after %s seconds.' % wait) + logger.debug("Retry after %s seconds." % wait) time.sleep(wait) response = self.interpret_response(body, code) @@ -58,122 +59,137 @@ def request(self, method, url, params=None, headers=None): def handle_api_error(self, body, code, response): try: - err = response['error'] + err = response["error"] except (KeyError, TypeError): raise error.APIError( "Invalid response object from API: %r (HTTP response code " "was %d)" % (body, code), - body, code, response) + body, + code, + response, + ) if code in [400, 404]: raise error.InvalidRequestError( - err.get('message'), err.get('param'), body, code, response) + err.get("message"), err.get("param"), body, code, response + ) elif code == 401: - raise error.AuthenticationError( - err.get('message'), body, code, response) + raise error.AuthenticationError(err.get("message"), body, code, response) elif code == 402: - raise error.CardError(err.get('message'), err.get('param'), - err.get('code'), body, code, response) + raise error.CardError( + err.get("message"), + err.get("param"), + err.get("code"), + body, + code, + response, + ) else: - raise error.APIError(err.get('message'), body, code, response) + raise error.APIError(err.get("message"), body, code, response) def request_raw(self, method, url, params=None, supplied_headers=None): - from payjp import api_version if self.api_key: my_api_key = self.api_key else: from payjp import api_key + my_api_key = api_key if my_api_key is None: raise error.AuthenticationError( - 'No API key provided. (HINT: set your API key using ' + "No API key provided. (HINT: set your API key using " '"payjp.api_key = "). You can generate API keys ' - 'from the Payjp web interface. See https://docs.pay.jp' - 'for details, or email support@pay.jp if you have any ' - 'questions.') + "from the Payjp web interface. See https://docs.pay.jp" + "for details, or email support@pay.jp if you have any " + "questions." + ) - abs_url = '%s%s' % (self.api_base, url) + abs_url = "%s%s" % (self.api_base, url) encoded_params = urlencode(list(_api_encode(params or {}))) - if method in ('get', 'delete'): + if method in ("get", "delete"): if params: abs_url = _build_api_url(abs_url, encoded_params) post_data = None - elif method == 'post': + elif method == "post": post_data = encoded_params else: - raise error.APIConnectionError( - 'Unrecognized HTTP method %r.' % (method,)) + raise error.APIConnectionError("Unrecognized HTTP method %r." % (method,)) ua = { - 'bindings_version': version.VERSION, - 'lang': 'python', - 'publisher': 'payjp', - 'httplib': self._client.name, + "bindings_version": version.VERSION, + "lang": "python", + "publisher": "payjp", + "httplib": self._client.name, } - for attr, func in [['lang_version', platform.python_version], - ['platform', platform.platform], - ['uname', lambda: ' '.join(platform.uname())]]: + for attr, func in [ + ["lang_version", platform.python_version], + ["platform", platform.platform], + ["uname", lambda: " ".join(platform.uname())], + ]: try: val = func() except Exception as e: - val = '!! %s' % (e,) + val = "!! %s" % (e,) ua[attr] = val encoded_api_key = str( - base64.b64encode( - bytes(''.join([my_api_key, ':']), 'utf-8')), 'utf-8') + base64.b64encode(bytes("".join([my_api_key, ":"]), "utf-8")), "utf-8" + ) headers = { - 'X-Payjp-Client-User-Agent': json.dumps(ua), - 'User-Agent': 'Payjp/v1 PythonBindings/%s' % (version.VERSION,), - 'Authorization': 'Basic %s' % encoded_api_key + "X-Payjp-Client-User-Agent": json.dumps(ua), + "User-Agent": "Payjp/v1 PythonBindings/%s" % (version.VERSION,), + "Authorization": "Basic %s" % encoded_api_key, } if self.payjp_account: - headers['Payjp-Account'] = self.payjp_account + headers["Payjp-Account"] = self.payjp_account - if method == 'post': - headers['Content-Type'] = 'application/x-www-form-urlencoded' + if method == "post": + headers["Content-Type"] = "application/x-www-form-urlencoded" if api_version is not None: - headers['Payjp-Version'] = api_version + headers["Payjp-Version"] = api_version if supplied_headers is not None: for key, value in supplied_headers.items(): headers[key] = value - body, code = self._client.request( - method, abs_url, headers, post_data) + body, code = self._client.request(method, abs_url, headers, post_data) - logger.info('%s %s %d', method.upper(), abs_url, code) + logger.info("%s %s %d", method.upper(), abs_url, code) logger.debug( - 'API request to %s returned (response code, response body) of ' - '(%d, %r)', - abs_url, code, body) + "API request to %s returned (response code, response body) of (%d, %r)", + abs_url, + code, + body, + ) return body, code, my_api_key def interpret_response(self, body, code): try: - if hasattr(body, 'decode'): - body = body.decode('utf-8') + if hasattr(body, "decode"): + body = body.decode("utf-8") response = json.loads(body) except Exception: raise error.APIError( "Invalid response body from API: %s " "(HTTP response code was %d)" % (body, code), - body, code) + body, + code, + ) if not (200 <= code < 300): self.handle_api_error(body, code, response) return response + def _encode_datetime(dttime): if dttime.tzinfo and dttime.tzinfo.utcoffset(dttime) is not None: utc_timestamp = calendar.timegm(dttime.utctimetuple()) @@ -182,18 +198,21 @@ def _encode_datetime(dttime): return int(utc_timestamp) + def _api_encode(data): for key, value in data.items(): if value is None: continue - elif hasattr(value, 'payjp_id'): + elif hasattr(value, "payjp_id"): yield (key, value.payjp_id) elif isinstance(value, list) or isinstance(value, tuple): for subvalue in value: yield ("%s[]" % (key,), subvalue) elif isinstance(value, dict): - subdict = dict(('%s[%s]' % (key, subkey), subvalue) for - subkey, subvalue in value.items()) + subdict = dict( + ("%s[%s]" % (key, subkey), subvalue) + for subkey, subvalue in value.items() + ) for subkey, subvalue in _api_encode(subdict): yield (subkey, subvalue) elif isinstance(value, datetime.datetime): @@ -201,10 +220,11 @@ def _api_encode(data): else: yield (key, value) + def _build_api_url(url, query): scheme, netloc, path, base_query, fragment = urlsplit(url) if base_query: - query = '%s&%s' % (base_query, query) + query = "%s&%s" % (base_query, query) return urlunsplit((scheme, netloc, path, query, fragment)) diff --git a/payjp/error.py b/payjp/error.py index 027f130..6566c9a 100644 --- a/payjp/error.py +++ b/payjp/error.py @@ -2,16 +2,16 @@ class PayjpException(Exception): - def __init__(self, message=None, http_body=None, http_status=None, - json_body=None): + def __init__(self, message=None, http_body=None, http_status=None, json_body=None): super(PayjpException, self).__init__(message) - if http_body and hasattr(http_body, 'decode'): + if http_body and hasattr(http_body, "decode"): try: - http_body = http_body.decode('utf-8') - except: - http_body = ('') + http_body = http_body.decode("utf-8") + except Exception: + http_body = ( + "" + ) self.http_body = http_body @@ -28,10 +28,10 @@ class APIConnectionError(PayjpException): class CardError(PayjpException): - def __init__(self, message, param, code, http_body=None, - http_status=None, json_body=None): - super(CardError, self).__init__(message, - http_body, http_status, json_body) + def __init__( + self, message, param, code, http_body=None, http_status=None, json_body=None + ): + super(CardError, self).__init__(message, http_body, http_status, json_body) self.param = param self.code = code @@ -41,9 +41,10 @@ class AuthenticationError(PayjpException): class InvalidRequestError(PayjpException): - - def __init__(self, message, param, http_body=None, - http_status=None, json_body=None): + def __init__( + self, message, param, http_body=None, http_status=None, json_body=None + ): super(InvalidRequestError, self).__init__( - message, http_body, http_status, json_body) + message, http_body, http_status, json_body + ) self.param = param diff --git a/payjp/example.py b/payjp/example.py index b824209..0355413 100644 --- a/payjp/example.py +++ b/payjp/example.py @@ -2,21 +2,16 @@ import payjp -payjp.api_key = 'sk_test_c62fade9d045b54cd76d7036' +payjp.api_key = "sk_test_c62fade9d045b54cd76d7036" -print('Attempting charge...') +print("Attempting charge...") resp = payjp.Charge.create( amount=10, - currency='jpy', - card={ - 'number': '4242424242424242', - 'exp_month': 12, - 'exp_year': 2018 - }, - description='a TIROL Choco' + currency="jpy", + card={"number": "4242424242424242", "exp_month": 12, "exp_year": 2018}, + description="a TIROL Choco", ) print(resp) -print(('Success: %r') % (resp, )) - +print(("Success: %r") % (resp,)) diff --git a/payjp/http_client.py b/payjp/http_client.py index 0a3e79f..6044e99 100644 --- a/payjp/http_client.py +++ b/payjp/http_client.py @@ -1,12 +1,11 @@ # coding: utf-8 import textwrap -import warnings - -from payjp import error import requests +from payjp import error + def new_default_http_client(*args, **kwargs): impl = RequestsClient @@ -15,34 +14,30 @@ def new_default_http_client(*args, **kwargs): class HTTPClient(object): - def request(self, method, url, headers, post_data=None): - raise NotImplementedError( - 'HTTPClient subclasses must implement `request`') + raise NotImplementedError("HTTPClient subclasses must implement `request`") class RequestsClient(HTTPClient): - name = 'requests' + name = "requests" def request(self, method, url, headers, post_data=None): kwargs = {} try: try: - result = requests.request(method, - url, - headers=headers, - data=post_data, - timeout=80, - **kwargs) + result = requests.request( + method, url, headers=headers, data=post_data, timeout=80, **kwargs + ) except TypeError as e: raise TypeError( - 'Warning: It looks like your installed version of the ' + "Warning: It looks like your installed version of the " '"requests" library is not compatible with Payjp\'s ' - 'usage thereof. (HINT: The most likely cause is that ' + "usage thereof. (HINT: The most likely cause is that " 'your "requests" library is out of date. You can fix ' 'that by running "pip install -U requests".) The ' - 'underlying error was: %s' % (e,)) + "underlying error was: %s" % (e,) + ) # This causes the content to actually be read, which could cause # e.g. a socket timeout. TODO: The other fetch methods probably @@ -57,15 +52,19 @@ def request(self, method, url, headers, post_data=None): def _handle_request_error(self, e): if isinstance(e, requests.exceptions.RequestException): - msg = ("Unexpected error communicating with Payjp. " - "If this problem persists, let us know at " - "support@payjp.com.") + msg = ( + "Unexpected error communicating with Payjp. " + "If this problem persists, let us know at " + "support@payjp.com." + ) err = "%s: %s" % (type(e).__name__, str(e)) else: - msg = ("Unexpected error communicating with Payjp. " - "It looks like there's probably a configuration " - "issue locally. If this problem persists, let us " - "know at support@payjp.com.") + msg = ( + "Unexpected error communicating with Payjp. " + "It looks like there's probably a configuration " + "issue locally. If this problem persists, let us " + "know at support@payjp.com." + ) err = "A %s was raised" % (type(e).__name__,) if str(e): err += " with error message %s" % (str(e),) diff --git a/payjp/resource.py b/payjp/resource.py index b4e9f8b..f455b49 100644 --- a/payjp/resource.py +++ b/payjp/resource.py @@ -7,39 +7,43 @@ from payjp import api_requestor, error -logger = logging.getLogger('payjp') +logger = logging.getLogger("payjp") + def convert_to_payjp_object(resp, api_key, account, api_base=None): types = { - 'account': Account, - 'card': Card, - 'charge': Charge, - 'customer': Customer, - 'event': Event, - 'plan': Plan, - 'subscription': Subscription, - 'token': Token, - 'transfer': Transfer, - 'statement': Statement, - 'list': ListObject, - 'term': Term, - 'balance': Balance, - 'three_d_secure_request': ThreeDSecureRequest, + "account": Account, + "card": Card, + "charge": Charge, + "customer": Customer, + "event": Event, + "plan": Plan, + "subscription": Subscription, + "token": Token, + "transfer": Transfer, + "statement": Statement, + "list": ListObject, + "term": Term, + "balance": Balance, + "three_d_secure_request": ThreeDSecureRequest, } if isinstance(resp, list): return [convert_to_payjp_object(i, api_key, account, api_base) for i in resp] elif isinstance(resp, dict) and not isinstance(resp, PayjpObject): resp = resp.copy() - klass_name = resp.get('object') + klass_name = resp.get("object") if isinstance(klass_name, str): klass = types.get(klass_name, PayjpObject) else: klass = PayjpObject - return klass.construct_from(resp, api_key, payjp_account=account, api_base=api_base) + return klass.construct_from( + resp, api_key, payjp_account=account, api_base=api_base + ) else: return resp + def _compute_diff(current, previous): if isinstance(current, dict): previous = previous or {} @@ -49,6 +53,7 @@ def _compute_diff(current, previous): return diff return current if current is not None else "" + def _serialize_list(array, previous): array = array or [] previous = previous or [] @@ -56,7 +61,7 @@ def _serialize_list(array, previous): for i, v in enumerate(array): previous_item = previous[i] if len(previous) > i else None - if hasattr(v, 'serialize'): + if hasattr(v, "serialize"): params[str(i)] = v.serialize(previous_item) else: params[str(i)] = _compute_diff(v, previous_item) @@ -65,8 +70,9 @@ def _serialize_list(array, previous): class PayjpObject(dict): - - def __init__(self, id=None, api_key=None, payjp_account=None, api_base=None, **kwargs): + def __init__( + self, id=None, api_key=None, payjp_account=None, api_base=None, **kwargs + ): super(PayjpObject, self).__init__() self._unsaved_values = set() @@ -76,20 +82,20 @@ def __init__(self, id=None, api_key=None, payjp_account=None, api_base=None, **k self._previous = None self._api_base = api_base - object.__setattr__(self, 'api_key', api_key) - object.__setattr__(self, 'payjp_account', payjp_account) + object.__setattr__(self, "api_key", api_key) + object.__setattr__(self, "payjp_account", payjp_account) if id: - self['id'] = id + self["id"] = id def __setattr__(self, k, v): - if k[0] == '_' or k in self.__dict__: + if k[0] == "_" or k in self.__dict__: return super(PayjpObject, self).__setattr__(k, v) else: self[k] = v def __getattr__(self, k): - if k[0] == '_': + if k[0] == "_": raise AttributeError(k) try: @@ -102,13 +108,13 @@ def __setitem__(self, k, 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)) + "You may set %s.%s = None to delete the property" % (k, str(self), k) + ) super(PayjpObject, self).__setitem__(k, v) # Allows for unpickling in Python 3.x - if not hasattr(self, '_unsaved_values'): + if not hasattr(self, "_unsaved_values"): self._unsaved_values = set() self._unsaved_values.add(k) @@ -123,30 +129,30 @@ def __getitem__(self, k): "It was then wiped when refreshing the object with " "the result returned by PAY.JP's API, probably as a " "result of a save(). The attributes currently " - "available on this object are: %s" % - (k, k, ', '.join(self.keys()))) + "available on this object are: %s" % (k, k, ", ".join(self.keys())) + ) else: raise err def __delitem__(self, k): raise TypeError( "You cannot delete attributes on a PayjpObject. " - "To unset a property, set it to None.") + "To unset a property, set it to None." + ) @classmethod def construct_from(cls, values, key, payjp_account=None, api_base=None): - instance = cls(values.get('id'), api_key=key, - payjp_account=payjp_account) - instance.refresh_from(values, api_key=key, - payjp_account=payjp_account, - api_base=api_base) + instance = cls(values.get("id"), api_key=key, payjp_account=payjp_account) + instance.refresh_from( + values, api_key=key, payjp_account=payjp_account, api_base=api_base + ) return instance - def refresh_from(self, values, api_key=None, partial=False, - payjp_account=None, api_base=None): - self.api_key = api_key or getattr(values, 'api_key', None) - self.payjp_account = \ - payjp_account or getattr(values, 'payjp_account', None) + def refresh_from( + self, values, api_key=None, partial=False, payjp_account=None, api_base=None + ): + self.api_key = api_key or getattr(values, "api_key", None) + self.payjp_account = payjp_account or getattr(values, "payjp_account", None) if self.api_base is not None: self._api_base = api_base @@ -154,7 +160,7 @@ def refresh_from(self, values, api_key=None, partial=False, # 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)) + self._unsaved_values = self._unsaved_values - set(values) else: removed = set(self.keys()) - set(values) self._transient_values = self._transient_values | removed @@ -165,7 +171,8 @@ def refresh_from(self, values, api_key=None, partial=False, for k, v in values.items(): super(PayjpObject, self).__setitem__( - k, convert_to_payjp_object(v, api_key, payjp_account, api_base)) + k, convert_to_payjp_object(v, api_key, payjp_account, api_base) + ) self._previous = values @@ -175,15 +182,15 @@ def serialize(self, previous): previous = previous or self._previous or {} for k, v in self.items(): - if k == 'id' or (isinstance(k, str) and k.startswith('_')): + if k == "id" or (isinstance(k, str) and k.startswith("_")): continue elif isinstance(v, APIResource): continue - elif hasattr(v, 'serialize'): + 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: + elif k == "additional_owners" and v is not None: params[k] = _serialize_list(v, previous.get(k, None)) return params @@ -195,26 +202,31 @@ 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(), - account=self.payjp_account) + key=self.api_key, api_base=self.api_base(), account=self.payjp_account + ) response, api_key = requestor.request(method, url, params, headers) - return convert_to_payjp_object(response, api_key, self.payjp_account, self.api_base()) + return convert_to_payjp_object( + response, api_key, self.payjp_account, self.api_base() + ) def __repr__(self): ident_parts = [type(self).__name__] - if isinstance(self.get('object'), str): - ident_parts.append(self.get('object')) + if isinstance(self.get("object"), str): + ident_parts.append(self.get("object")) - if isinstance(self.get('id'), str): - ident_parts.append('id=%s' % (self.get('id'),)) + if isinstance(self.get("id"), str): + ident_parts.append("id=%s" % (self.get("id"),)) - unicode_repr = '<%s at %s> JSON: %s' % ( - ' '.join(ident_parts), hex(id(self)), str(self)) + 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') + return unicode_repr.encode("utf-8") else: return unicode_repr @@ -223,30 +235,33 @@ def __str__(self): class ListObject(PayjpObject): - def all(self, **params): - return self.request('get', self['url'], params) + return self.request("get", self["url"], params) def create(self, **params): # TODO divide into another parent class - if hasattr(self, 'object') and self.object == 'list' and \ - hasattr(self, 'count') and self.count > 0 and \ - isinstance(self.data[0], Subscription): - raise NotImplementedError( - "Can't create a subscription via customer object. " - "Use payjp.Subscription.create({'customer_id'}) instead.") - return self.request('post', self['url'], params) + if ( + hasattr(self, "object") + and self.object == "list" + and hasattr(self, "count") + and self.count > 0 + and isinstance(self.data[0], Subscription) + ): + raise NotImplementedError( + "Can't create a subscription via customer object. " + "Use payjp.Subscription.create({'customer_id'}) instead." + ) + return self.request("post", self["url"], params) def retrieve(self, id, **params): - base = self.get('url') + base = self.get("url") extn = quote_plus(id) url = "%s/%s" % (base, extn) - return self.request('get', url, params) + return self.request("get", url, params) class APIResource(PayjpObject): - @classmethod def class_name(cls): return str(quote_plus(cls.__name__.lower())) @@ -254,17 +269,18 @@ def class_name(cls): @classmethod def class_url(cls): cls_name = cls.class_name() - return '/v1/{0}s'.format(cls_name) + return "/v1/{0}s".format(cls_name) def instance_url(self): - id = self.get('id') + id = self.get("id") if not id: - raise error.InvalidRequestError("Could not create instance url without it's id", - None) + raise error.InvalidRequestError( + "Could not create instance url without it's id", None + ) base = self.class_url() ext = quote_plus(id) - return '{0}/{1}'.format(base, ext) + return "{0}/{1}".format(base, ext) @classmethod def retrieve(cls, id, api_key=None, payjp_account=None, api_base=None, **kwargs): @@ -273,110 +289,115 @@ def retrieve(cls, id, api_key=None, payjp_account=None, api_base=None, **kwargs) return instance def refresh(self): - self.refresh_from(self.request('get', self.instance_url())) + self.refresh_from(self.request("get", self.instance_url())) return self class ListableAPIResource(APIResource): - @classmethod def all(cls, api_key=None, payjp_account=None, api_base=None, **params): - requestor = api_requestor.APIRequestor(api_key, account=payjp_account, api_base=api_base) + requestor = api_requestor.APIRequestor( + api_key, account=payjp_account, api_base=api_base + ) url = cls.class_url() - response, api_key = requestor.request('get', url, params) + response, api_key = requestor.request("get", url, params) return convert_to_payjp_object(response, api_key, payjp_account, api_base) class CreateableAPIResource(APIResource): - @classmethod def create(cls, api_key=None, payjp_account=None, headers=None, **params): requestor = api_requestor.APIRequestor(api_key, account=payjp_account) url = cls.class_url() - response, api_key = requestor.request('post', url, params, headers) + response, api_key = requestor.request("post", url, params, headers) return convert_to_payjp_object(response, api_key, payjp_account) class UpdateableAPIResource(APIResource): - def save(self): updated_params = self.serialize(None) if updated_params: - self.refresh_from(self.request('post', self.instance_url(), - updated_params)) + self.refresh_from(self.request("post", self.instance_url(), updated_params)) else: 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)) + self.refresh_from(self.request("delete", self.instance_url(), params)) return self + # resources -class Token(CreateableAPIResource): +class Token(CreateableAPIResource): def tds_finish(self, **kwargs): - url = self.instance_url() + '/tds_finish' - self.refresh_from(self.request('post', url, kwargs)) + url = self.instance_url() + "/tds_finish" + self.refresh_from(self.request("post", url, kwargs)) return self -class Charge(CreateableAPIResource, ListableAPIResource, - UpdateableAPIResource): - +class Charge(CreateableAPIResource, ListableAPIResource, UpdateableAPIResource): def capture(self, **kwargs): - url = self.instance_url() + '/capture' - self.refresh_from(self.request('post', url, kwargs)) + url = self.instance_url() + "/capture" + self.refresh_from(self.request("post", url, kwargs)) return self def refund(self, **kwargs): - url = self.instance_url() + '/refund' - self.refresh_from(self.request('post', url, kwargs)) + url = self.instance_url() + "/refund" + self.refresh_from(self.request("post", url, kwargs)) return self def reauth(self, **kwargs): - url = self.instance_url() + '/reauth' - self.refresh_from(self.request('post', url, kwargs)) + url = self.instance_url() + "/reauth" + self.refresh_from(self.request("post", url, kwargs)) return self def tds_finish(self, **kwargs): - url = self.instance_url() + '/tds_finish' - self.refresh_from(self.request('post', url, kwargs)) + url = self.instance_url() + "/tds_finish" + self.refresh_from(self.request("post", url, kwargs)) return self + class Event(ListableAPIResource): pass -class Customer(CreateableAPIResource, UpdateableAPIResource, - ListableAPIResource, DeletableAPIResource): - +class Customer( + CreateableAPIResource, + UpdateableAPIResource, + ListableAPIResource, + DeletableAPIResource, +): def charges(self, **params): - params['customer'] = self.id + params["customer"] = self.id charges = Charge.all(self.api_key, params) return charges -class Plan(CreateableAPIResource, DeletableAPIResource, - UpdateableAPIResource, ListableAPIResource): +class Plan( + CreateableAPIResource, + DeletableAPIResource, + UpdateableAPIResource, + ListableAPIResource, +): pass class Account(APIResource): - @classmethod - def retrieve(cls, id=None, api_key=None, payjp_account=None, api_base=None, **params): + def retrieve( + cls, id=None, api_key=None, payjp_account=None, api_base=None, **params + ): instance = cls(id, api_key, payjp_account, api_base, **params) instance.refresh() return instance def instance_url(self): - id = self.get('id') + id = self.get("id") if not id: return "/v1/accounts" base = self.class_url() @@ -385,10 +406,9 @@ def instance_url(self): class Card(UpdateableAPIResource, DeletableAPIResource): - def instance_url(self): extn = quote_plus(self.id) - if (hasattr(self, 'customer')): + if hasattr(self, "customer"): base = Customer.class_url() owner_extn = quote_plus(self.customer) @@ -396,7 +416,9 @@ def instance_url(self): raise error.InvalidRequestError( "Could not determine whether card_id %s is " "attached to a customer " - "or a recipient." % self.id, 'id') + "or a recipient." % self.id, + "id", + ) return "%s/%s/cards/%s" % (base, owner_extn, extn) @@ -405,25 +427,29 @@ def retrieve(cls, id, api_key=None, payjp_account=None, api_base=None, **params) raise NotImplementedError( "Can't retrieve a card without a customer ID." "Use customer.cards.retrieve('card_id') or " - "recipient.cards.retrieve('card_id') instead.") - + "recipient.cards.retrieve('card_id') instead." + ) -class Subscription(CreateableAPIResource, DeletableAPIResource, - UpdateableAPIResource, ListableAPIResource): +class Subscription( + CreateableAPIResource, + DeletableAPIResource, + UpdateableAPIResource, + ListableAPIResource, +): def pause(self, **kwargs): - url = self.instance_url() + '/pause' - self.refresh_from(self.request('post', url, kwargs)) + url = self.instance_url() + "/pause" + self.refresh_from(self.request("post", url, kwargs)) return self def resume(self, **kwargs): - url = self.instance_url() + '/resume' - self.refresh_from(self.request('post', url, kwargs)) + url = self.instance_url() + "/resume" + self.refresh_from(self.request("post", url, kwargs)) return self def cancel(self, **kwargs): - url = self.instance_url() + '/cancel' - self.refresh_from(self.request('post', url, kwargs)) + url = self.instance_url() + "/cancel" + self.refresh_from(self.request("post", url, kwargs)) return self @@ -432,10 +458,9 @@ class Transfer(ListableAPIResource): class Statement(ListableAPIResource): - def statement_urls(self, **kwargs): - url = self.instance_url() + '/statement_urls' - self.refresh_from(self.request('post', url, kwargs)) + url = self.instance_url() + "/statement_urls" + self.refresh_from(self.request("post", url, kwargs)) return self @@ -446,20 +471,24 @@ class Term(ListableAPIResource): class Balance(ListableAPIResource): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - object.__setattr__(self, 'statement_urls', self.__statement_urls) + object.__setattr__(self, "statement_urls", self.__statement_urls) def __statement_urls(self, *args, **kwargs): - return self.__class__.statement_urls(self.get('id'), *args, **kwargs) + return self.__class__.statement_urls(self.get("id"), *args, **kwargs) @classmethod - def statement_urls(cls, id, api_key=None, payjp_account=None, api_base=None, **params): - requestor = api_requestor.APIRequestor(api_key, account=payjp_account, api_base=api_base) - url = cls.class_url() + f'/{id}/statement_urls' - response, api_key = requestor.request('post', url, params) + def statement_urls( + cls, id, api_key=None, payjp_account=None, api_base=None, **params + ): + requestor = api_requestor.APIRequestor( + api_key, account=payjp_account, api_base=api_base + ) + url = cls.class_url() + f"/{id}/statement_urls" + response, api_key = requestor.request("post", url, params) return convert_to_payjp_object(response, api_key, payjp_account, api_base) class ThreeDSecureRequest(CreateableAPIResource, ListableAPIResource): @classmethod def class_name(cls): - return 'three_d_secure_request' + return "three_d_secure_request" diff --git a/payjp/test/__init__.py b/payjp/test/__init__.py index 81b7fb2..0a7435d 100644 --- a/payjp/test/__init__.py +++ b/payjp/test/__init__.py @@ -4,8 +4,8 @@ def all_names(): for _, modname, _ in pkgutil.iter_modules(__path__): - if modname.startswith('test_'): - yield 'payjp.test.' + modname + if modname.startswith("test_"): + yield "payjp.test." + modname def all(): @@ -13,5 +13,5 @@ def all(): def unit(): - unit_names = [name for name in all_names() if 'integration' not in name] + unit_names = [name for name in all_names() if "integration" not in name] return unittest.defaultTestLoader.loadTestsFromNames(unit_names) diff --git a/payjp/test/helper.py b/payjp/test/helper.py index ae6d366..eda865a 100644 --- a/payjp/test/helper.py +++ b/payjp/test/helper.py @@ -1,46 +1,46 @@ import datetime -import json import os import random import re import string import unittest -from mock import patch, Mock +from mock import Mock, patch import payjp NOW = datetime.datetime.now() DUMMY_CARD = { - 'number': '4242424242424242', - 'exp_month': NOW.month, - 'exp_year': NOW.year + 4 + "number": "4242424242424242", + "exp_month": NOW.month, + "exp_year": NOW.year + 4, } -DUMMY_CHARGE = { - 'amount': 100, - 'currency': 'jpy', - 'card': DUMMY_CARD -} +DUMMY_CHARGE = {"amount": 100, "currency": "jpy", "card": DUMMY_CARD} DUMMY_PLAN = { - 'amount': 2000, - 'interval': 'month', - 'name': 'Amazing Gold Plan', - 'currency': 'jpy', - 'id': ('payjp-test-gold-' + - ''.join(random.choice(string.ascii_lowercase) for x in range(10))) + "amount": 2000, + "interval": "month", + "name": "Amazing Gold Plan", + "currency": "jpy", + "id": ( + "payjp-test-gold-" + + "".join(random.choice(string.ascii_lowercase) for x in range(10)) + ), } -DUMMY_TRANSFER = { - 'amount': 400, - 'currency': 'jpy', - 'recipient': 'self' -} +DUMMY_TRANSFER = {"amount": 400, "currency": "jpy", "recipient": "self"} + class PayjpTestCase(unittest.TestCase): - RESTORE_ATTRIBUTES = ('api_version', 'api_key', 'max_retry', 'retry_initial_delay', 'retry_max_delay') + RESTORE_ATTRIBUTES = ( + "api_version", + "api_key", + "max_retry", + "retry_initial_delay", + "retry_max_delay", + ) def setUp(self): super(PayjpTestCase, self).setUp() @@ -50,11 +50,12 @@ def setUp(self): for attr in self.RESTORE_ATTRIBUTES: self._payjp_original_attributes[attr] = getattr(payjp, attr) - api_base = os.environ.get('PAYJP_API_BASE') + api_base = os.environ.get("PAYJP_API_BASE") if api_base: payjp.api_base = api_base payjp.api_key = os.environ.get( - 'PAYJP_API_KEY', 'sk_test_c62fade9d045b54cd76d7036') + "PAYJP_API_KEY", "sk_test_c62fade9d045b54cd76d7036" + ) def tearDown(self): super(PayjpTestCase, self).tearDown() @@ -73,15 +74,15 @@ def assertRaisesRegexp(self, exception, regexp, callable, *args, **kwargs): if isinstance(regexp, str): regexp = re.compile(regexp) if not regexp.search(str(err)): - raise self.failureException('"%s" does not match "%s"' % - (regexp.pattern, str(err))) + raise self.failureException( + '"%s" does not match "%s"' % (regexp.pattern, str(err)) + ) else: - raise self.failureException( - '%s was not raised' % (exception.__name__,)) + raise self.failureException("%s was not raised" % (exception.__name__,)) class PayjpUnitTestCase(PayjpTestCase): - REQUEST_LIBRARIES = ['requests'] + REQUEST_LIBRARIES = ["requests"] def setUp(self): super(PayjpUnitTestCase, self).setUp() @@ -102,11 +103,10 @@ def tearDown(self): class PayjpApiTestCase(PayjpTestCase): - def setUp(self): super(PayjpApiTestCase, self).setUp() - self.requestor_patcher = patch('payjp.api_requestor.APIRequestor') + self.requestor_patcher = patch("payjp.api_requestor.APIRequestor") self.requestor_class_mock = self.requestor_patcher.start() self.requestor_mock = self.requestor_class_mock.return_value @@ -116,7 +116,7 @@ def tearDown(self): self.requestor_patcher.stop() def mock_response(self, res): - self.requestor_mock.request = Mock(return_value=(res, 'reskey')) + self.requestor_mock.request = Mock(return_value=(res, "reskey")) class MyResource(payjp.resource.APIResource): @@ -139,8 +139,10 @@ class MyDeletable(payjp.resource.DeletableAPIResource): pass -class MyComposite(payjp.resource.ListableAPIResource, - payjp.resource.CreateableAPIResource, - payjp.resource.UpdateableAPIResource, - payjp.resource.DeletableAPIResource): +class MyComposite( + payjp.resource.ListableAPIResource, + payjp.resource.CreateableAPIResource, + payjp.resource.UpdateableAPIResource, + payjp.resource.DeletableAPIResource, +): pass diff --git a/payjp/test/test_http_client.py b/payjp/test/test_http_client.py index 82429ff..2fe036b 100644 --- a/payjp/test/test_http_client.py +++ b/payjp/test/test_http_client.py @@ -1,26 +1,25 @@ # coding: utf-8 import unittest +import warnings from mock import Mock import payjp - from payjp.test.helper import PayjpUnitTestCase -VALID_API_METHODS = ('get', 'post', 'delete') +VALID_API_METHODS = ("get", "post", "delete") class HttpClientTests(PayjpUnitTestCase): - def setUp(self): super(HttpClientTests, self).setUp() - self.original_filters = payjp.http_client.warnings.filters[:] - payjp.http_client.warnings.simplefilter('ignore') + self.original_filters = warnings.filters[:] + warnings.simplefilter("ignore") def tearDown(self): - payjp.http_client.warnings.filters = self.original_filters + warnings.filters = self.original_filters super(HttpClientTests, self).tearDown() @@ -33,63 +32,61 @@ def check_default(self, none_libs, expected): self.assertTrue(isinstance(inst, expected)) def test_new_default_http_client_requests(self): - self.check_default((), - payjp.http_client.RequestsClient) - + self.check_default((), payjp.http_client.RequestsClient) -class ClientTestBase(): +class ClientTestBase: @property def request_mock(self): return self.request_mocks[self.request_client.name] @property - def valid_url(self, path='/foo'): - return 'https://api.pay.jp%s' % (path,) + def valid_url(self, path="/foo"): + return "https://api.pay.jp%s" % (path,) def make_request(self, method, url, headers, post_data): client = self.request_client() return client.request(method, url, headers, post_data) def mock_response(self, body, code): - raise NotImplementedError( - 'You must implement this in your test subclass') + raise NotImplementedError("You must implement this in your test subclass") def mock_error(self, error): - raise NotImplementedError( - 'You must implement this in your test subclass') + raise NotImplementedError("You must implement this in your test subclass") def check_call(self, meth, abs_url, headers, params): - raise NotImplementedError( - 'You must implement this in your test subclass') + raise NotImplementedError("You must implement this in your test subclass") def test_request(self): self.mock_response(self.request_mock, '{"foo": "baz"}', 200) for meth in VALID_API_METHODS: abs_url = self.valid_url - data = '' + data = "" - if meth != 'post': - abs_url = '%s?%s' % (abs_url, data) + if meth != "post": + abs_url = "%s?%s" % (abs_url, data) data = None - headers = {'my-header': 'header val'} + headers = {"my-header": "header val"} - body, code = self.make_request( - meth, abs_url, headers, data) + body, code = self.make_request(meth, abs_url, headers, data) self.assertEqual(200, code) self.assertEqual('{"foo": "baz"}', body) - self.check_call(self.request_mock, meth, abs_url, - data, headers) + self.check_call(self.request_mock, meth, abs_url, data, headers) def test_exception(self): self.mock_error(self.request_mock) - self.assertRaises(payjp.error.APIConnectionError, - self.make_request, - 'get', self.valid_url, {}, None) + self.assertRaises( + payjp.error.APIConnectionError, + self.make_request, + "get", + self.valid_url, + {}, + None, + ) class RequestsClientTests(PayjpUnitTestCase, ClientTestBase): @@ -107,11 +104,10 @@ def mock_error(self, mock): mock.request.side_effect = mock.exceptions.RequestException() def check_call(self, mock, meth, url, post_data, headers): - mock.request.assert_called_with(meth, url, - headers=headers, - data=post_data, - timeout=80) + mock.request.assert_called_with( + meth, url, headers=headers, data=post_data, timeout=80 + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/payjp/test/test_integration.py b/payjp/test/test_integration.py index 749201b..ebaeace 100644 --- a/payjp/test/test_integration.py +++ b/payjp/test/test_integration.py @@ -1,17 +1,16 @@ # coding: utf-8 import unittest -import payjp -from payjp.test.helper import (PayjpTestCase, NOW, DUMMY_CARD) +import payjp +from payjp.test.helper import DUMMY_CARD, NOW, PayjpTestCase class AuthenticationErrorTest(PayjpTestCase): - def test_invalid_credentials(self): key = payjp.api_key try: - payjp.api_key = 'invalid' + payjp.api_key = "invalid" payjp.Customer.create() except payjp.error.AuthenticationError as e: self.assertEqual(401, e.http_status) @@ -22,13 +21,12 @@ def test_invalid_credentials(self): class CardErrorTest(PayjpTestCase): - def test_invalid_card_props(self): EXPIRED_CARD = DUMMY_CARD.copy() - EXPIRED_CARD['exp_month'] = NOW.month - EXPIRED_CARD['exp_year'] = NOW.year + EXPIRED_CARD["exp_month"] = NOW.month + EXPIRED_CARD["exp_year"] = NOW.year try: - payjp.Charge.create(amount=100, currency='jpy', card=EXPIRED_CARD) + payjp.Charge.create(amount=100, currency="jpy", card=EXPIRED_CARD) except payjp.error.InvalidRequestError as e: self.assertEqual(400, e.http_status) self.assertTrue(isinstance(e.http_body, str)) @@ -36,10 +34,9 @@ def test_invalid_card_props(self): class InvalidRequestErrorTest(PayjpTestCase): - def test_nonexistent_object(self): try: - payjp.Charge.retrieve('invalid') + payjp.Charge.retrieve("invalid") except payjp.error.InvalidRequestError as e: self.assertEqual(404, e.http_status) self.assertTrue(isinstance(e.http_body, str)) @@ -54,5 +51,5 @@ def test_invalid_data(self): self.assertTrue(isinstance(e.json_body, dict)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/payjp/test/test_requestor.py b/payjp/test/test_requestor.py index cdc79d3..1a3b7f5 100644 --- a/payjp/test/test_requestor.py +++ b/payjp/test/test_requestor.py @@ -5,17 +5,15 @@ import unittest from urllib.parse import parse_qsl, urlsplit -from mock import Mock, MagicMock, patch +from mock import Mock, patch import payjp - from payjp.test.helper import PayjpUnitTestCase -VALID_API_METHODS = ('get', 'post', 'delete') +VALID_API_METHODS = ("get", "post", "delete") class GMT1(datetime.tzinfo): - def utcoffset(self, dt): return datetime.timedelta(hours=1) @@ -27,7 +25,7 @@ def tzname(self, dt): class APIHeaderMatcher(object): - EXP_KEYS = ['X-Payjp-Client-User-Agent', 'User-Agent', 'Authorization'] + EXP_KEYS = ["X-Payjp-Client-User-Agent", "User-Agent", "Authorization"] METHOD_EXTRA_KEYS = {"post": ["Content-Type"]} def __init__(self, api_key=None, extra={}, request_method=None): @@ -39,25 +37,27 @@ def __init__(self, api_key=None, extra={}, request_method=None): self.extra = extra def __eq__(self, other): - return (self._keys_match(other) and - self._auth_match(other) and - self._extra_match(other)) + return ( + self._keys_match(other) + and self._auth_match(other) + and self._extra_match(other) + ) def _encode(self, api_key): - return str( - base64.b64encode( - bytes(''.join([api_key, ':']), 'utf-8')), 'utf-8') + return str(base64.b64encode(bytes("".join([api_key, ":"]), "utf-8")), "utf-8") def _keys_match(self, other): expected_keys = self.EXP_KEYS + list(self.extra.keys()) - if self.request_method is not None and self.request_method in \ - self.METHOD_EXTRA_KEYS: + if ( + self.request_method is not None + and self.request_method in self.METHOD_EXTRA_KEYS + ): expected_keys.extend(self.METHOD_EXTRA_KEYS[self.request_method]) - return (sorted(other.keys()) == sorted(expected_keys)) + return sorted(other.keys()) == sorted(expected_keys) def _auth_match(self, other): - return other['Authorization'] == "Basic %s" % (self.api_key,) + return other["Authorization"] == "Basic %s" % (self.api_key,) def _extra_match(self, other): for k, v in self.extra.items(): @@ -85,12 +85,11 @@ def __init__(self, expected): def __eq__(self, other): other_parts = urlsplit(other) - for part in ('scheme', 'netloc', 'path', 'fragment'): + for part in ("scheme", "netloc", "path", "fragment"): expected = getattr(self.exp_parts, part) actual = getattr(other_parts, part) if expected != actual: - print((('Expected %s "%s" but got "%s"') % ( - part, expected, actual))) + print((('Expected %s "%s" but got "%s"') % (part, expected, actual))) return False q_matcher = QueryMatcher(parse_qsl(self.exp_parts.query)) @@ -99,83 +98,80 @@ def __eq__(self, other): class APIRequestorRequestTests(PayjpUnitTestCase): ENCODE_INPUTS = { - 'dict': { - 'astring': 'bar', - 'anint': 5, - 'anull': None, - 'adatetime': datetime.datetime(2013, 1, 1, tzinfo=GMT1()), - 'atuple': (1, 2), - 'adict': {'foo': 'bar', 'boz': 5}, - 'alist': ['foo', 'bar'], + "dict": { + "astring": "bar", + "anint": 5, + "anull": None, + "adatetime": datetime.datetime(2013, 1, 1, tzinfo=GMT1()), + "atuple": (1, 2), + "adict": {"foo": "bar", "boz": 5}, + "alist": ["foo", "bar"], }, - 'list': [1, 'foo', 'baz'], - 'string': 'boo', - 'unicode': u'\u1234', - 'datetime': datetime.datetime(2013, 1, 1, second=1, tzinfo=GMT1()), - 'none': None, + "list": [1, "foo", "baz"], + "string": "boo", + "unicode": "\u1234", + "datetime": datetime.datetime(2013, 1, 1, second=1, tzinfo=GMT1()), + "none": None, } ENCODE_EXPECTATIONS = { - 'dict': [ - ('%s[astring]', 'bar'), - ('%s[anint]', 5), - ('%s[adatetime]', 1356994800), - ('%s[adict][foo]', 'bar'), - ('%s[adict][boz]', 5), - ('%s[alist][]', 'foo'), - ('%s[alist][]', 'bar'), - ('%s[atuple][]', 1), - ('%s[atuple][]', 2), + "dict": [ + ("%s[astring]", "bar"), + ("%s[anint]", 5), + ("%s[adatetime]", 1356994800), + ("%s[adict][foo]", "bar"), + ("%s[adict][boz]", 5), + ("%s[alist][]", "foo"), + ("%s[alist][]", "bar"), + ("%s[atuple][]", 1), + ("%s[atuple][]", 2), ], - 'list': [ - ('%s[]', 1), - ('%s[]', 'foo'), - ('%s[]', 'baz'), + "list": [ + ("%s[]", 1), + ("%s[]", "foo"), + ("%s[]", "baz"), ], - 'string': [('%s', 'boo')], - 'unicode': [('%s', u'\u1234')], - 'datetime': [('%s', 1356994801)], - 'none': [], + "string": [("%s", "boo")], + "unicode": [("%s", "\u1234")], + "datetime": [("%s", 1356994801)], + "none": [], } def setUp(self): super(APIRequestorRequestTests, self).setUp() self.http_client = Mock(payjp.http_client.HTTPClient) - self.http_client.name = 'mockclient' + self.http_client.name = "mockclient" - self.requestor = payjp.api_requestor.APIRequestor( - client=self.http_client) + self.requestor = payjp.api_requestor.APIRequestor(client=self.http_client) def mock_response(self, return_body, return_code, requestor=None): if not requestor: requestor = self.requestor - self.http_client.request = Mock( - return_value=(return_body, return_code)) + self.http_client.request = Mock(return_value=(return_body, return_code)) - def check_call(self, meth, abs_url=None, headers=None, - post_data=None, requestor=None): + def check_call( + self, meth, abs_url=None, headers=None, post_data=None, requestor=None + ): if not abs_url: - abs_url = 'https://api.pay.jp%s' % (self.valid_path,) + abs_url = "https://api.pay.jp%s" % (self.valid_path,) if not requestor: requestor = self.requestor if not headers: headers = APIHeaderMatcher(request_method=meth) - self.http_client.request.assert_called_with( - meth, abs_url, headers, post_data) + self.http_client.request.assert_called_with(meth, abs_url, headers, post_data) @property def valid_path(self): - return '/foo' + return "/foo" def encoder_check(self, key): stk_key = "my%s" % (key,) value = self.ENCODE_INPUTS[key] - expectation = [(k % (stk_key,), v) for k, v in - self.ENCODE_EXPECTATIONS[key]] + expectation = [(k % (stk_key,), v) for k, v in self.ENCODE_EXPECTATIONS[key]] stk = [] fn = getattr(payjp.api_requestor.APIRequestor, "encode_%s" % (key,)) @@ -191,7 +187,8 @@ def _test_encode_naive_datetime(self): stk = [] payjp.api_requestor.APIRequestor.encode_datetime( - stk, 'test', datetime.datetime(2013, 1, 1)) + stk, "test", datetime.datetime(2013, 1, 1) + ) # Naive datetimes will encode differently depending on your system # local time. Since we don't know the local time of your system, @@ -199,62 +196,58 @@ def _test_encode_naive_datetime(self): self.assertTrue(60 * 60 * 24 > abs(stk[0][1] - 1356994800)) def test_param_encoding(self): - self.mock_response('{}', 200) + self.mock_response("{}", 200) - self.requestor.request('get', '', self.ENCODE_INPUTS) + self.requestor.request("get", "", self.ENCODE_INPUTS) expectation = [] for type_, values in self.ENCODE_EXPECTATIONS.items(): expectation.extend([(k % (type_,), str(v)) for k, v in values]) - self.check_call('get', QueryMatcher(expectation)) + self.check_call("get", QueryMatcher(expectation)) def test_dictionary_list_encoding(self): params = { - 'foo': { - '0': { - 'bar': 'bat', + "foo": { + "0": { + "bar": "bat", } } } encoded = list(payjp.api_requestor._api_encode(params)) key, value = encoded[0] - self.assertEqual('foo[0][bar]', key) - self.assertEqual('bat', value) + self.assertEqual("foo[0][bar]", key) + self.assertEqual("bat", value) def test_url_construction(self): CASES = ( - ('https://api.pay.jp?foo=bar', '', {'foo': 'bar'}), - ('https://api.pay.jp?foo=bar', '?', {'foo': 'bar'}), - ('https://api.pay.jp', '', {}), + ("https://api.pay.jp?foo=bar", "", {"foo": "bar"}), + ("https://api.pay.jp?foo=bar", "?", {"foo": "bar"}), + ("https://api.pay.jp", "", {}), ( - 'https://api.pay.jp/%20spaced?foo=bar%24&baz=5', - '/%20spaced?foo=bar%24', - {'baz': '5'} - ), - ( - 'https://api.pay.jp?foo=bar&foo=bar', - '?foo=bar', - {'foo': 'bar'} + "https://api.pay.jp/%20spaced?foo=bar%24&baz=5", + "/%20spaced?foo=bar%24", + {"baz": "5"}, ), + ("https://api.pay.jp?foo=bar&foo=bar", "?foo=bar", {"foo": "bar"}), ) for expected, url, params in CASES: - self.mock_response('{}', 200) + self.mock_response("{}", 200) - self.requestor.request('get', url, params) + self.requestor.request("get", url, params) - self.check_call('get', expected) + self.check_call("get", expected) def test_empty_methods(self): for meth in VALID_API_METHODS: - self.mock_response('{}', 200) + self.mock_response("{}", 200) body, key = self.requestor.request(meth, self.valid_path, {}) - if meth == 'post': - post_data = '' + if meth == "post": + post_data = "" else: post_data = None @@ -266,139 +259,166 @@ def test_methods_with_params_and_response(self): self.mock_response('{"foo": "bar", "baz": 6}', 200) params = { - 'alist': [1, 2, 3], - 'adict': {'frobble': 'bits'}, - 'adatetime': datetime.datetime(2013, 1, 1, tzinfo=GMT1()) + "alist": [1, 2, 3], + "adict": {"frobble": "bits"}, + "adatetime": datetime.datetime(2013, 1, 1, tzinfo=GMT1()), } - encoded = ('adict%5Bfrobble%5D=bits&adatetime=1356994800&' - 'alist%5B%5D=1&alist%5B%5D=2&alist%5B%5D=3') + encoded = ( + "adict%5Bfrobble%5D=bits&adatetime=1356994800&" + "alist%5B%5D=1&alist%5B%5D=2&alist%5B%5D=3" + ) - body, key = self.requestor.request(meth, self.valid_path, - params) - self.assertEqual({'foo': 'bar', 'baz': 6}, body) + body, key = self.requestor.request(meth, self.valid_path, params) + self.assertEqual({"foo": "bar", "baz": 6}, body) - if meth == 'post': - self.check_call( - meth, - post_data=QueryMatcher(parse_qsl(encoded))) + if meth == "post": + self.check_call(meth, post_data=QueryMatcher(parse_qsl(encoded))) else: - abs_url = "https://api.pay.jp%s?%s" % ( - self.valid_path, encoded) + abs_url = "https://api.pay.jp%s?%s" % (self.valid_path, encoded) self.check_call(meth, abs_url=UrlMatcher(abs_url)) def test_uses_headers(self): - self.mock_response('{}', 200) - self.requestor.request('get', self.valid_path, {}, {'foo': 'bar'}) - self.check_call('get', headers=APIHeaderMatcher(extra={'foo': 'bar'})) + self.mock_response("{}", 200) + self.requestor.request("get", self.valid_path, {}, {"foo": "bar"}) + self.check_call("get", headers=APIHeaderMatcher(extra={"foo": "bar"})) def test_uses_instance_key(self): - key = 'fookey' - requestor = payjp.api_requestor.APIRequestor(key, - client=self.http_client) + key = "fookey" + requestor = payjp.api_requestor.APIRequestor(key, client=self.http_client) - self.mock_response('{}', 200, requestor=requestor) + self.mock_response("{}", 200, requestor=requestor) - body, used_key = requestor.request('get', self.valid_path, {}) + body, used_key = requestor.request("get", self.valid_path, {}) - self.check_call('get', headers=APIHeaderMatcher(key, - request_method='get'), requestor=requestor) + self.check_call( + "get", + headers=APIHeaderMatcher(key, request_method="get"), + requestor=requestor, + ) self.assertEqual(key, used_key) def test_passes_api_version(self): - payjp.api_version = 'fooversion' + payjp.api_version = "fooversion" - self.mock_response('{}', 200) + self.mock_response("{}", 200) - body, key = self.requestor.request('get', self.valid_path, {}) + body, key = self.requestor.request("get", self.valid_path, {}) - self.check_call('get', headers=APIHeaderMatcher( - extra={'Payjp-Version': 'fooversion'}, request_method='get')) + self.check_call( + "get", + headers=APIHeaderMatcher( + extra={"Payjp-Version": "fooversion"}, request_method="get" + ), + ) def test_uses_instance_account(self): - account = 'acct_foo' - requestor = payjp.api_requestor.APIRequestor(account=account, - client=self.http_client) + account = "acct_foo" + requestor = payjp.api_requestor.APIRequestor( + account=account, client=self.http_client + ) - self.mock_response('{}', 200, requestor=requestor) + self.mock_response("{}", 200, requestor=requestor) - requestor.request('get', self.valid_path, {}) + requestor.request("get", self.valid_path, {}) self.check_call( - 'get', + "get", requestor=requestor, headers=APIHeaderMatcher( - extra={'Payjp-Account': account}, - request_method='get' + extra={"Payjp-Account": account}, request_method="get" ), ) def test_fails_without_api_key(self): payjp.api_key = None - self.assertRaises(payjp.error.AuthenticationError, - self.requestor.request, - 'get', self.valid_path, {}) + self.assertRaises( + payjp.error.AuthenticationError, + self.requestor.request, + "get", + self.valid_path, + {}, + ) def test_not_found(self): self.mock_response('{"error": {}}', 404) - self.assertRaises(payjp.error.InvalidRequestError, - self.requestor.request, - 'get', self.valid_path, {}) + self.assertRaises( + payjp.error.InvalidRequestError, + self.requestor.request, + "get", + self.valid_path, + {}, + ) def test_authentication_error(self): self.mock_response('{"error": {}}', 401) - self.assertRaises(payjp.error.AuthenticationError, - self.requestor.request, - 'get', self.valid_path, {}) + self.assertRaises( + payjp.error.AuthenticationError, + self.requestor.request, + "get", + self.valid_path, + {}, + ) def test_card_error(self): self.mock_response('{"error": {}}', 402) - self.assertRaises(payjp.error.CardError, - self.requestor.request, - 'get', self.valid_path, {}) + self.assertRaises( + payjp.error.CardError, self.requestor.request, "get", self.valid_path, {} + ) def test_too_many_request_error(self): self.mock_response('{"error": {}}', 429) - self.assertRaises(payjp.error.APIError, - self.requestor.request, - 'get', self.valid_path, {}) + self.assertRaises( + payjp.error.APIError, self.requestor.request, "get", self.valid_path, {} + ) def test_server_error(self): self.mock_response('{"error": {}}', 500) - self.assertRaises(payjp.error.APIError, - self.requestor.request, - 'get', self.valid_path, {}) + self.assertRaises( + payjp.error.APIError, self.requestor.request, "get", self.valid_path, {} + ) def test_invalid_json(self): - self.mock_response('{', 200) + self.mock_response("{", 200) - self.assertRaises(payjp.error.APIError, - self.requestor.request, - 'get', self.valid_path, {}) + self.assertRaises( + payjp.error.APIError, self.requestor.request, "get", self.valid_path, {} + ) def test_invalid_method(self): - self.assertRaises(payjp.error.APIConnectionError, - self.requestor.request, - 'foo', 'bar') + self.assertRaises( + payjp.error.APIConnectionError, self.requestor.request, "foo", "bar" + ) -class APIRequestorRetryTest(PayjpUnitTestCase): +class APIRequestorRetryTest(PayjpUnitTestCase): def setUp(self): super(APIRequestorRetryTest, self).setUp() self.return_values = [] + def return_value_generator(): for status in self.return_values: - yield ('{{"error": {{"status": {status}, "message": "test"}}}}'.format(status=status), status, 'sk_live_aaa') + yield ( + '{{"error": {{"status": {status}, "message": "test"}}}}'.format( + status=status + ), + status, + "sk_live_aaa", + ) + gen = return_value_generator() + def request_raw(*args, **kw): return next(gen) - self.request_raw_patch = patch('payjp.api_requestor.APIRequestor.request_raw', request_raw) + self.request_raw_patch = patch( + "payjp.api_requestor.APIRequestor.request_raw", request_raw + ) self.requestor = payjp.api_requestor.APIRequestor() @@ -408,7 +428,7 @@ def test_retry_disabled(self): self.return_values = [499, 599] # returns 599 at 2nd try with self.request_raw_patch: with self.assertRaises(payjp.error.APIError) as error: - self.requestor.request('get', '/test', {}) + self.requestor.request("get", "/test", {}) self.assertEqual(error.exception.http_status, 499) @@ -418,7 +438,7 @@ def test_no_retry(self): self.return_values = [599, 429, 429, 429] # returns 599 at first try with self.request_raw_patch: with self.assertRaises(payjp.error.APIError) as error: - self.requestor.request('get', '/test', {}) + self.requestor.request("get", "/test", {}) self.assertEqual(error.exception.http_status, 599) @@ -426,26 +446,36 @@ def test_full_retry(self): """Returns 429 after exceeds max retry""" payjp.max_retry = 2 payjp.retry_initial_delay = 0.1 - self.return_values = [429, 429, 429, 200] # first try + 2 retries + unexpected 200 + self.return_values = [ + 429, + 429, + 429, + 200, + ] # first try + 2 retries + unexpected 200 with self.request_raw_patch: with self.assertRaises(payjp.error.APIError) as error: - self.requestor.request('get', '/test', {}) + self.requestor.request("get", "/test", {}) self.assertEqual(error.exception.http_status, 429) def test_success_at_halfway_of_retries(self): payjp.max_retry = 5 payjp.retry_initial_delay = 0.1 - self. return_values = [429, 599, 429, 429, 429] # returns not 429 status at 2nd try + self.return_values = [ + 429, + 599, + 429, + 429, + 429, + ] # returns not 429 status at 2nd try with self.request_raw_patch: with self.assertRaises(payjp.error.APIError) as error: - self.requestor.request('get', '/test', {}) + self.requestor.request("get", "/test", {}) self.assertEqual(error.exception.http_status, 599) class APIRequestorRetryIntervalTest(PayjpUnitTestCase): - def setUp(self): super(APIRequestorRetryIntervalTest, self).setUp() self.requestor = payjp.api_requestor.APIRequestor() @@ -460,7 +490,5 @@ def test_retry_initial_delay(self): self.assertTrue(16 <= self.requestor._get_retry_delay(10) <= 32) - - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/payjp/test/test_resources.py b/payjp/test/test_resources.py index eb625ab..0a47bb8 100644 --- a/payjp/test/test_resources.py +++ b/payjp/test/test_resources.py @@ -1,251 +1,256 @@ # coding: utf-8 import pickle -import sys import unittest import payjp import payjp.resource - from payjp.test.helper import ( - PayjpUnitTestCase, PayjpApiTestCase, - MyListable, MyCreatable, MyUpdateable, MyDeletable, - MyResource, NOW, DUMMY_CARD, DUMMY_CHARGE, - DUMMY_PLAN) + DUMMY_CARD, + DUMMY_CHARGE, + DUMMY_PLAN, + NOW, + MyCreatable, + MyDeletable, + MyListable, + MyResource, + MyUpdateable, + PayjpApiTestCase, + PayjpUnitTestCase, +) class PayjpObjectTests(PayjpUnitTestCase): - def test_initializes_with_parameters(self): - obj = payjp.resource.PayjpObject( - 'foo', 'bar', myparam=5, yourparam='boo') + obj = payjp.resource.PayjpObject("foo", "bar", myparam=5, yourparam="boo") - self.assertEqual('foo', obj.id) - self.assertEqual('bar', obj.api_key) + self.assertEqual("foo", obj.id) + self.assertEqual("bar", obj.api_key) def test_access(self): - obj = payjp.resource.PayjpObject('myid', 'mykey', myparam=5) + obj = payjp.resource.PayjpObject("myid", "mykey", myparam=5) # Empty - self.assertRaises(AttributeError, getattr, obj, 'myattr') - self.assertRaises(KeyError, obj.__getitem__, 'myattr') - self.assertEqual('def', obj.get('myattr', 'def')) - self.assertEqual(None, obj.get('myattr')) + self.assertRaises(AttributeError, getattr, obj, "myattr") + self.assertRaises(KeyError, obj.__getitem__, "myattr") + self.assertEqual("def", obj.get("myattr", "def")) + self.assertEqual(None, obj.get("myattr")) # Setters - obj.myattr = 'myval' - obj['myitem'] = 'itval' - self.assertEqual('sdef', obj.setdefault('mydef', 'sdef')) + obj.myattr = "myval" + obj["myitem"] = "itval" + self.assertEqual("sdef", obj.setdefault("mydef", "sdef")) # Getters - self.assertEqual('myval', obj.setdefault('myattr', 'sdef')) - self.assertEqual('myval', obj.myattr) - self.assertEqual('myval', obj['myattr']) - self.assertEqual('myval', obj.get('myattr')) + self.assertEqual("myval", obj.setdefault("myattr", "sdef")) + self.assertEqual("myval", obj.myattr) + self.assertEqual("myval", obj["myattr"]) + self.assertEqual("myval", obj.get("myattr")) - self.assertEqual(['id', 'myattr', 'mydef', 'myitem'], - sorted(obj.keys())) - self.assertEqual(['itval', 'myid', 'myval', 'sdef'], - sorted(obj.values())) + self.assertEqual(["id", "myattr", "mydef", "myitem"], sorted(obj.keys())) + self.assertEqual(["itval", "myid", "myval", "sdef"], sorted(obj.values())) # Illegal operations - self.assertRaises(ValueError, setattr, obj, 'foo', '') - self.assertRaises(TypeError, obj.__delitem__, 'myattr') + self.assertRaises(ValueError, setattr, obj, "foo", "") + self.assertRaises(TypeError, obj.__delitem__, "myattr") def test_refresh_from(self): - obj = payjp.resource.PayjpObject.construct_from({ - 'foo': 'bar', - 'trans': 'me', - }, 'mykey') - - self.assertEqual('mykey', obj.api_key) - self.assertEqual('bar', obj.foo) - self.assertEqual('me', obj['trans']) + obj = payjp.resource.PayjpObject.construct_from( + { + "foo": "bar", + "trans": "me", + }, + "mykey", + ) + + self.assertEqual("mykey", obj.api_key) + self.assertEqual("bar", obj.foo) + self.assertEqual("me", obj["trans"]) self.assertEqual(None, obj.payjp_account) - obj.refresh_from({ - 'foo': 'baz', - 'johnny': 5, - }, 'key2', payjp_account='acct_foo') + obj.refresh_from( + { + "foo": "baz", + "johnny": 5, + }, + "key2", + payjp_account="acct_foo", + ) self.assertEqual(5, obj.johnny) - self.assertEqual('baz', obj.foo) - self.assertRaises(AttributeError, getattr, obj, 'trans') - self.assertEqual('key2', obj.api_key) - self.assertEqual('acct_foo', obj.payjp_account) + self.assertEqual("baz", obj.foo) + self.assertRaises(AttributeError, getattr, obj, "trans") + self.assertEqual("key2", obj.api_key) + self.assertEqual("acct_foo", obj.payjp_account) - obj.refresh_from({ - 'trans': 4, - 'metadata': {'amount': 42} - }, 'key2', True) + obj.refresh_from({"trans": 4, "metadata": {"amount": 42}}, "key2", True) - self.assertEqual('baz', obj.foo) + self.assertEqual("baz", obj.foo) self.assertEqual(4, obj.trans) def test_passing_nested_refresh(self): - obj = payjp.resource.PayjpObject.construct_from({ - 'foos': { - 'type': 'list', - 'data': [ - {'id': 'nested'} - ], - } - }, 'key', payjp_account='acct_foo') + obj = payjp.resource.PayjpObject.construct_from( + { + "foos": { + "type": "list", + "data": [{"id": "nested"}], + } + }, + "key", + payjp_account="acct_foo", + ) nested = obj.foos.data[0] - self.assertEqual('key', obj.api_key) - self.assertEqual('nested', nested.id) - self.assertEqual('key', nested.api_key) - self.assertEqual('acct_foo', nested.payjp_account) + self.assertEqual("key", obj.api_key) + self.assertEqual("nested", nested.id) + self.assertEqual("key", nested.api_key) + self.assertEqual("acct_foo", nested.payjp_account) def check_invoice_data(self, data): # Check rough structure self.assertEqual(20, len(data.keys())) - self.assertEqual(3, len(data['lines'].keys())) - self.assertEqual(0, len(data['lines']['invoiceitems'])) - self.assertEqual(1, len(data['lines']['subscriptions'])) + self.assertEqual(3, len(data["lines"].keys())) + self.assertEqual(0, len(data["lines"]["invoiceitems"])) + self.assertEqual(1, len(data["lines"]["subscriptions"])) # Check various data types - self.assertEqual(1338238728, data['date']) - self.assertEqual(None, data['next_payment_attempt']) - self.assertEqual(False, data['livemode']) - self.assertEqual('month', - data['lines']['subscriptions'][0]['plan']['interval']) + self.assertEqual(1338238728, data["date"]) + self.assertEqual(None, data["next_payment_attempt"]) + self.assertEqual(False, data["livemode"]) + self.assertEqual("month", data["lines"]["subscriptions"][0]["plan"]["interval"]) def test_repr(self): - obj = payjp.resource.PayjpObject( - 'foo', 'bar', myparam=5) + obj = payjp.resource.PayjpObject("foo", "bar", myparam=5) - obj['object'] = u'\u4e00boo\u1f00' + obj["object"] = "\u4e00boo\u1f00" res = repr(obj) - if sys.version_info[0] < 3: - res = unicode(repr(obj), 'utf-8') - - self.assertTrue(u'=42", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.ruff] +target-version = "py38" +line-length = 88 + +[tool.ruff.lint] +select = [ + "E", + "F", + "I", + "W", +] +ignore = ["E501"] + +[tool.ruff.lint.isort] +known-first-party = ["payjp"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +line-ending = "auto" \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..2505d6c --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +-r requirements.txt +pytest>=7.0.0 +mock>=1.0.1 +ruff>=0.1.0 +tox>=4.0.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 04f256f..537775c 100644 --- a/setup.py +++ b/setup.py @@ -5,18 +5,20 @@ install_requires = [] if sys.version_info < (3, 8): - raise DeprecationWarning('Python versions below 3.8 are no longer supported by PAY.JP. Please use Python 3.8 or higher.') + raise DeprecationWarning( + "Python versions below 3.8 are no longer supported by PAY.JP. Please use Python 3.8 or higher." + ) -install_requires.append('requests >= 2.7.0') +install_requires.append("requests >= 2.7.0") setup( name="payjp", version="1.6.0", - description='PAY.JP python bindings', + description="PAY.JP python bindings", author="PAY.JP", - author_email='support@pay.jp', - packages=['payjp', 'payjp.test'], - url='https://github.com/payjp/payjp-python', + author_email="support@pay.jp", + packages=["payjp", "payjp.test"], + url="https://github.com/payjp/payjp-python", install_requires=install_requires, - python_requires='>=3.0', + python_requires=">=3.0", ) diff --git a/tox.ini b/tox.ini index 369962e..6fc0571 100644 --- a/tox.ini +++ b/tox.ini @@ -12,3 +12,10 @@ deps = mock>=1.0.1 pytest commands = pytest {posargs} # pytestを使用してテストを実行 + +[testenv:ruff] +deps = ruff +skip_install = true +commands = + ruff check . + ruff format --check .