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 logging
|
||||
import math
|
||||
import operator
|
||||
import urllib.parse as urlparse
|
||||
|
||||
import requests
|
||||
|
@ -285,6 +286,47 @@ class PayPalAPIClient:
|
|||
]
|
||||
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(
|
||||
self,
|
||||
subscription_id: str,
|
||||
|
@ -323,21 +365,15 @@ class PayPalAPIClient:
|
|||
if start_date is None:
|
||||
# The API only goes back three years
|
||||
start_date = now - datetime.timedelta(days=365 * 3)
|
||||
date_diff = datetime.timedelta(days=30)
|
||||
response: APIResponse = {'transaction_details': None}
|
||||
while end_date > start_date and not response['transaction_details']:
|
||||
search_start = max(end_date - date_diff, start_date)
|
||||
response = self._get_json('/v1/reporting/transactions', {
|
||||
for params in self._iter_date_params(end_date, start_date, {
|
||||
'transaction_id': transaction_id,
|
||||
'fields': fields.param_value(),
|
||||
'start_date': search_start.isoformat(timespec='seconds'),
|
||||
'end_date': end_date.isoformat(timespec='seconds'),
|
||||
})
|
||||
end_date = search_start
|
||||
if response['transaction_details']:
|
||||
return Transaction(response['transaction_details'][0])
|
||||
else:
|
||||
raise ValueError(f"transaction {transaction_id!r} not found")
|
||||
}):
|
||||
response = self._get_json('/v1/reporting/transactions', params)
|
||||
if response['transaction_details']:
|
||||
return Transaction(response['transaction_details'][0])
|
||||
raise ValueError(f"transaction {transaction_id!r} not found")
|
||||
|
||||
def iter_transactions(
|
||||
self,
|
||||
|
@ -351,10 +387,9 @@ class PayPalAPIClient:
|
|||
``fields`` is a TransactionsFields object that flags the information to
|
||||
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(),
|
||||
'start_date': start_date.isoformat(timespec='seconds'),
|
||||
'end_date': end_date.isoformat(timespec='seconds'),
|
||||
}):
|
||||
for txn_source in page['transaction_details']:
|
||||
yield Transaction(txn_source)
|
||||
for page in self._iter_pages('/v1/reporting/transactions', params):
|
||||
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(
|
||||
name='paypal_rest',
|
||||
version='1.0.0',
|
||||
version='1.0.1',
|
||||
author='Software Freedom Conservancy',
|
||||
author_email='info@sfconservancy.org',
|
||||
license='GNU AGPLv3+',
|
||||
|
|
|
@ -26,7 +26,7 @@ from paypal_rest import client as client_mod
|
|||
from paypal_rest import transaction as txn_mod
|
||||
|
||||
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():
|
||||
txn_id = 'TYPETEST123456789'
|
||||
|
@ -87,6 +87,53 @@ def test_transaction_fields_formatting(fields):
|
|||
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', [
|
||||
'BAD_REQUEST',
|
||||
'UNAUTHORIZED',
|
||||
|
|
Loading…
Reference in a new issue