Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
@@ -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 .
43 changes: 27 additions & 16 deletions payjp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
)
130 changes: 75 additions & 55 deletions payjp/api_requestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,142 +38,158 @@ 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)
return response, my_api_key

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 = <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())
Expand All @@ -182,29 +198,33 @@ 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):
yield (key, _encode_datetime(value))
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))
Loading