client: iter_transactions() supports date ranges longer than a month.
This works by extracting the date-crawling code from get_transaction() to a separate method. Now iter_transactions() will similarly make API requests with different month-long date ranges until covers the entire date range the user requested.
This commit is contained in:
parent
e7342c582e
commit
efaeb53e91
3 changed files with 101 additions and 19 deletions
|
@ -19,6 +19,7 @@ import datetime
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import operator
|
||||||
import urllib.parse as urlparse
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
@ -285,6 +286,47 @@ class PayPalAPIClient:
|
||||||
]
|
]
|
||||||
self.logger.error(" — ".join(parts))
|
self.logger.error(" — ".join(parts))
|
||||||
|
|
||||||
|
def _iter_date_params(
|
||||||
|
self,
|
||||||
|
start_date: datetime.datetime,
|
||||||
|
end_date: datetime.datetime,
|
||||||
|
params: Optional[Params]=None,
|
||||||
|
) -> Iterator[Params]:
|
||||||
|
"""Generate parameters with date windows to cover a wider span
|
||||||
|
|
||||||
|
The PayPal transaction search API only allows the ``start_date`` and
|
||||||
|
``end_date`` to be a month apart. Given a user's desired date range and
|
||||||
|
other API parameters, this method generates parameters with different
|
||||||
|
date pairs to cover the entire date range.
|
||||||
|
|
||||||
|
Normally the method keeps incrementing ``start_date`` until the user's
|
||||||
|
desired ``end_date`` is reached. If the ``start_date`` if later than the
|
||||||
|
``end_date``, instead it works backwards: it uses the ``start_date``
|
||||||
|
argument as the first ``end_date`` parameter, and keeps decrementing it
|
||||||
|
until the ``start_date`` parameter reaches the other end of the range.
|
||||||
|
"""
|
||||||
|
if start_date > end_date:
|
||||||
|
key1 = 'end_date'
|
||||||
|
key2 = 'start_date'
|
||||||
|
days_sign = operator.neg
|
||||||
|
pred = operator.gt
|
||||||
|
limit_func = max
|
||||||
|
else:
|
||||||
|
key1 = 'start_date'
|
||||||
|
key2 = 'end_date'
|
||||||
|
days_sign = operator.pos
|
||||||
|
pred = operator.lt
|
||||||
|
limit_func = min
|
||||||
|
retval = collections.ChainMap(params or {}).new_child()
|
||||||
|
retval[key1] = start_date.isoformat(timespec='seconds')
|
||||||
|
next_date = start_date
|
||||||
|
date_diff = datetime.timedelta(days=days_sign(30))
|
||||||
|
while pred(next_date, end_date):
|
||||||
|
next_date = limit_func(next_date + date_diff, end_date)
|
||||||
|
retval[key2] = next_date.isoformat(timespec='seconds')
|
||||||
|
yield retval
|
||||||
|
retval[key1] = retval[key2]
|
||||||
|
|
||||||
def get_subscription(
|
def get_subscription(
|
||||||
self,
|
self,
|
||||||
subscription_id: str,
|
subscription_id: str,
|
||||||
|
@ -323,21 +365,15 @@ class PayPalAPIClient:
|
||||||
if start_date is None:
|
if start_date is None:
|
||||||
# The API only goes back three years
|
# The API only goes back three years
|
||||||
start_date = now - datetime.timedelta(days=365 * 3)
|
start_date = now - datetime.timedelta(days=365 * 3)
|
||||||
date_diff = datetime.timedelta(days=30)
|
|
||||||
response: APIResponse = {'transaction_details': None}
|
response: APIResponse = {'transaction_details': None}
|
||||||
while end_date > start_date and not response['transaction_details']:
|
for params in self._iter_date_params(end_date, start_date, {
|
||||||
search_start = max(end_date - date_diff, start_date)
|
|
||||||
response = self._get_json('/v1/reporting/transactions', {
|
|
||||||
'transaction_id': transaction_id,
|
'transaction_id': transaction_id,
|
||||||
'fields': fields.param_value(),
|
'fields': fields.param_value(),
|
||||||
'start_date': search_start.isoformat(timespec='seconds'),
|
}):
|
||||||
'end_date': end_date.isoformat(timespec='seconds'),
|
response = self._get_json('/v1/reporting/transactions', params)
|
||||||
})
|
if response['transaction_details']:
|
||||||
end_date = search_start
|
return Transaction(response['transaction_details'][0])
|
||||||
if response['transaction_details']:
|
raise ValueError(f"transaction {transaction_id!r} not found")
|
||||||
return Transaction(response['transaction_details'][0])
|
|
||||||
else:
|
|
||||||
raise ValueError(f"transaction {transaction_id!r} not found")
|
|
||||||
|
|
||||||
def iter_transactions(
|
def iter_transactions(
|
||||||
self,
|
self,
|
||||||
|
@ -351,10 +387,9 @@ class PayPalAPIClient:
|
||||||
``fields`` is a TransactionsFields object that flags the information to
|
``fields`` is a TransactionsFields object that flags the information to
|
||||||
include in returned Transactions.
|
include in returned Transactions.
|
||||||
"""
|
"""
|
||||||
for page in self._iter_pages('/v1/reporting/transactions', {
|
for params in self._iter_date_params(start_date, end_date, {
|
||||||
'fields': fields.param_value(),
|
'fields': fields.param_value(),
|
||||||
'start_date': start_date.isoformat(timespec='seconds'),
|
|
||||||
'end_date': end_date.isoformat(timespec='seconds'),
|
|
||||||
}):
|
}):
|
||||||
for txn_source in page['transaction_details']:
|
for page in self._iter_pages('/v1/reporting/transactions', params):
|
||||||
yield Transaction(txn_source)
|
for txn_source in page['transaction_details']:
|
||||||
|
yield Transaction(txn_source)
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -25,7 +25,7 @@ with README_PATH.open() as readme_file:
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='paypal_rest',
|
name='paypal_rest',
|
||||||
version='1.0.0',
|
version='1.0.1',
|
||||||
author='Software Freedom Conservancy',
|
author='Software Freedom Conservancy',
|
||||||
author_email='info@sfconservancy.org',
|
author_email='info@sfconservancy.org',
|
||||||
license='GNU AGPLv3+',
|
license='GNU AGPLv3+',
|
||||||
|
|
|
@ -26,7 +26,7 @@ from paypal_rest import client as client_mod
|
||||||
from paypal_rest import transaction as txn_mod
|
from paypal_rest import transaction as txn_mod
|
||||||
|
|
||||||
START_DATE = datetime.datetime(2020, 10, 1, 12, tzinfo=datetime.timezone.utc)
|
START_DATE = datetime.datetime(2020, 10, 1, 12, tzinfo=datetime.timezone.utc)
|
||||||
END_DATE = START_DATE.replace(month=START_DATE.month + 1)
|
END_DATE = START_DATE.replace(day=25)
|
||||||
|
|
||||||
def test_transaction_type():
|
def test_transaction_type():
|
||||||
txn_id = 'TYPETEST123456789'
|
txn_id = 'TYPETEST123456789'
|
||||||
|
@ -87,6 +87,53 @@ def test_transaction_fields_formatting(fields):
|
||||||
for name in session._requests[0].params.get('fields', '').split(',')
|
for name in session._requests[0].params.get('fields', '').split(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('days_diff', [10, 45, 89])
|
||||||
|
def test_iter_transactions_date_window(days_diff):
|
||||||
|
start_date = START_DATE
|
||||||
|
end_date = start_date + datetime.timedelta(days=days_diff)
|
||||||
|
session = MockSession(
|
||||||
|
MockResponse({'page': 1, 'total_pages': 1, 'transaction_details': []}),
|
||||||
|
infinite=True,
|
||||||
|
)
|
||||||
|
paypal = client_mod.PayPalAPIClient(session)
|
||||||
|
assert not any(paypal.iter_transactions(start_date, end_date))
|
||||||
|
req_count = len(session._requests)
|
||||||
|
assert req_count == ((days_diff // 30) + 1)
|
||||||
|
start_str = start_date.isoformat(timespec='seconds')
|
||||||
|
end_str = end_date.isoformat(timespec='seconds')
|
||||||
|
prev_end = start_str
|
||||||
|
for number, request in enumerate(session._requests, 1):
|
||||||
|
assert request.params['start_date'] == prev_end
|
||||||
|
if number == req_count:
|
||||||
|
assert request.params['end_date'] == end_str
|
||||||
|
else:
|
||||||
|
assert prev_end < request.params['end_date'] < end_str
|
||||||
|
prev_end = request.params['end_date']
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('days_diff', [10, 45, 89])
|
||||||
|
def test_get_transactions_date_window(days_diff):
|
||||||
|
end_date = END_DATE
|
||||||
|
start_date = end_date - datetime.timedelta(days=days_diff)
|
||||||
|
session = MockSession(
|
||||||
|
MockResponse({'page': 1, 'total_pages': 1, 'transaction_details': []}),
|
||||||
|
infinite=True,
|
||||||
|
)
|
||||||
|
paypal = client_mod.PayPalAPIClient(session)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
paypal.get_transaction('DATEWINDOW1234567', end_date, start_date)
|
||||||
|
req_count = len(session._requests)
|
||||||
|
assert req_count == ((days_diff // 30) + 1)
|
||||||
|
start_str = start_date.isoformat(timespec='seconds')
|
||||||
|
end_str = end_date.isoformat(timespec='seconds')
|
||||||
|
prev_start = end_str
|
||||||
|
for number, request in enumerate(session._requests, 1):
|
||||||
|
assert request.params['end_date'] == prev_start
|
||||||
|
if number == req_count:
|
||||||
|
assert request.params['start_date'] == start_str
|
||||||
|
else:
|
||||||
|
assert prev_start > request.params['start_date'] > start_str
|
||||||
|
prev_start = request.params['start_date']
|
||||||
|
|
||||||
@pytest.mark.parametrize('name', [
|
@pytest.mark.parametrize('name', [
|
||||||
'BAD_REQUEST',
|
'BAD_REQUEST',
|
||||||
'UNAUTHORIZED',
|
'UNAUTHORIZED',
|
||||||
|
|
Loading…
Reference in a new issue