Compare commits
10 commits
e7342c582e
...
f65e353cd9
Author | SHA1 | Date | |
---|---|---|---|
|
f65e353cd9 | ||
|
c4561a0026 | ||
|
85f1aa66a6 | ||
|
f3c5c92b49 | ||
|
09754b9788 | ||
|
b0d5fe5aa8 | ||
|
1b0740ad5d | ||
|
86b8eddc2e | ||
|
c625a57aad | ||
|
efaeb53e91 |
10 changed files with 164 additions and 91 deletions
2
CODE.rst
2
CODE.rst
|
@ -14,4 +14,4 @@ The ``paypal-query`` tool is implemented in ``cliquery.py``. Other submodules li
|
|||
Running tests
|
||||
-------------
|
||||
|
||||
Run ``./setup.py test`` to run unit tests. Run ``./setup.py typecheck`` to run the type checker. Run ``tox`` to run both on all supported Pythons.
|
||||
Run ``pytest`` to run unit tests. Run ``mypy paypal_rest`` to run the type checker. Run ``tox`` to run both on all supported Pythons.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
include LICENSE.txt
|
||||
include *.rst
|
||||
include tests/*.py
|
||||
include paypal_rest/py.typed
|
||||
|
|
10
README.rst
10
README.rst
|
@ -9,7 +9,7 @@ Introduction
|
|||
paypal-query tool
|
||||
-----------------
|
||||
|
||||
This library includes a command line tool, ``paypal-query``, to quickly get information from the API; provide an illustration of using the library; and help with debugging. To use it, first write a configuration file ``~/.config/paypal_rest/config.ini`` with your client credentials from PayPal::
|
||||
This library includes a command line tool, ``paypal-query``, to quickly get information from the API; provide an illustration of using the library; and help with debugging. To use it, first write a configuration file ``~/.config/paypal_rest/config.ini`` with your REST API app credentials from PayPal::
|
||||
|
||||
[query]
|
||||
client_id = ...
|
||||
|
@ -21,7 +21,7 @@ To see an overview of transactions over a time period::
|
|||
|
||||
paypal-query [--begin DATETIME] [--end DATETIME]
|
||||
|
||||
Specify all datetimes in ISO8601 format: ``YYYY-MM-DDTHH:MM:SS``. You can omit any part of the time, or the whole thing. You can also add a timezone offset, like ``-04:00`` or ``+01:00``, or ``Z`` for UTC.
|
||||
Specify all datetimes in ISO8601 format: ``YYYY-MM-DDTHH:MM:SS``. You can stop at any divider and omit the rest. You can also add a timezone offset, like ``-04:00`` or ``+01:00``, or ``Z`` for UTC.
|
||||
|
||||
To see details of a specific transaction or subscription::
|
||||
|
||||
|
@ -32,10 +32,12 @@ The PayPal API does not let you look up an individual transaction by ID; you hav
|
|||
Library quickstart
|
||||
------------------
|
||||
|
||||
Create a ``paypal_rest.client.PayPalAPIClient`` using one of the classmethod constructors, then call its methods and handle the results::
|
||||
Create a ``paypal_rest.PayPalAPIClient`` using one of the classmethod constructors, then call its methods and handle the results::
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(os.path.expanduser('~/.config/paypal_rest/config.ini'))
|
||||
paypal = paypal_rest.client.PayPalAPIClient.from_config(config['query'])
|
||||
paypal = paypal_rest.PayPalAPIClient.from_config(config['query'])
|
||||
for txn in paypal.iter_transactions(start_date, end_date):
|
||||
... # txn is a paypal_rest.transaction.Transaction object you can query.
|
||||
|
||||
For more details, refer to the pydoc for ``paypal_rest.PayPalAPIClient`` and ``paypal_rest.transaction.Transaction``.
|
||||
|
|
|
@ -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,
|
||||
|
@ -301,7 +343,7 @@ class PayPalAPIClient:
|
|||
end_date: Optional[datetime.datetime]=None,
|
||||
start_date: Optional[datetime.datetime]=None,
|
||||
fields: TransactionFields=TransactionFields.TRANSACTION,
|
||||
) -> APIResponse:
|
||||
) -> Transaction:
|
||||
"""Find and return a transaction by its id
|
||||
|
||||
The PayPal API does not provide a way to look up transactions solely by
|
||||
|
@ -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)
|
||||
|
|
|
@ -189,7 +189,7 @@ def main(
|
|||
paypal_obj = paypal.get_transaction(
|
||||
paypal_id, args.end_date, args.start_date, args.transaction_fields,
|
||||
)
|
||||
yaml.dump(paypal_obj, stdout, Dumper=YAMLDumper)
|
||||
yaml.dump([paypal_obj], stdout, Dumper=YAMLDumper)
|
||||
return 0
|
||||
|
||||
entry_point = cliutil.make_entry_point(__name__, PROGNAME)
|
||||
|
|
0
paypal_rest/py.typed
Normal file
0
paypal_rest/py.typed
Normal file
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
[build-system]
|
||||
requires = ["setuptools >= 40.6.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
55
setup.cfg
55
setup.cfg
|
@ -1,6 +1,20 @@
|
|||
[aliases]
|
||||
test=pytest
|
||||
typecheck=pytest --addopts="--mypy paypal_rest"
|
||||
[metadata]
|
||||
name = paypal_rest
|
||||
version = 1.0.4
|
||||
author = Software Freedom Conservancy
|
||||
author_email = info@sfconservancy.org
|
||||
description = Library to access PayPal’s REST API
|
||||
license = GNU AGPLv3+
|
||||
license_file = LICENSE.txt
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst; charset=UTF-8
|
||||
project_urls =
|
||||
Documentation = %(url)s
|
||||
Source = %(url)s
|
||||
url = https://k.sfconservancy.org/NPO-Accounting/paypal_rest
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[mypy]
|
||||
disallow_any_unimported = False
|
||||
|
@ -13,14 +27,39 @@ warn_return_any = False
|
|||
warn_unreachable = True
|
||||
warn_unused_configs = True
|
||||
|
||||
[options]
|
||||
include_package_data = True
|
||||
install_requires =
|
||||
iso8601>=0.1
|
||||
oauthlib>=2.0
|
||||
pyxdg>=0.2
|
||||
PyYAML>=3.0
|
||||
requests>=2.0
|
||||
requests-oauthlib>=1.0
|
||||
packages = find:
|
||||
python_requires = >=3.6
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
paypal-query = paypal_rest.cliquery:entry_point
|
||||
|
||||
[options.packages.find]
|
||||
exclude =
|
||||
tests
|
||||
|
||||
[testenv]
|
||||
deps =
|
||||
mypy>=0.770
|
||||
pytest>=3.0
|
||||
pytest-mypy
|
||||
|
||||
commands =
|
||||
pytest
|
||||
pytest --mypy paypal_rest
|
||||
|
||||
[tool:pytest]
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning:^socks$
|
||||
|
||||
[tox:tox]
|
||||
envlist = py36,py37
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
./setup.py test
|
||||
./setup.py typecheck
|
||||
|
|
62
setup.py
62
setup.py
|
@ -1,61 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
"""setup.py - paypal_rest installation script"""
|
||||
# Copyright © 2020 Brett Smith
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""setup.py - setuptools compatibility shim"""
|
||||
|
||||
from pathlib import Path
|
||||
from setuptools import setup
|
||||
import setuptools
|
||||
|
||||
README_PATH = Path(__file__).with_name('README.rst')
|
||||
|
||||
with README_PATH.open() as readme_file:
|
||||
long_description = readme_file.read()
|
||||
|
||||
setup(
|
||||
name='paypal_rest',
|
||||
version='1.0.0',
|
||||
author='Software Freedom Conservancy',
|
||||
author_email='info@sfconservancy.org',
|
||||
license='GNU AGPLv3+',
|
||||
url='https://k.sfconservancy.org/NPO-Accounting/paypal_rest',
|
||||
description="Library to access PayPal's REST API",
|
||||
long_description=long_description,
|
||||
|
||||
python_requires='>=3.6',
|
||||
install_requires=[
|
||||
'iso8601>=0.1', # Debian:python3-iso8601
|
||||
'oauthlib>=2.0', # Debian:python3-oauthlib
|
||||
'pyxdg>=0.2', # Debian:python3-xdg
|
||||
'requests>=2.0', # Debian:python3-requests
|
||||
'requests-oauthlib>=1.0', # Debian:python3-requests-oauthlib
|
||||
],
|
||||
setup_requires=[
|
||||
'pytest-mypy',
|
||||
'pytest-runner', # Debian:python3-pytest-runner
|
||||
],
|
||||
tests_require=[
|
||||
'mypy>=0.770', # Debian:python3-mypy
|
||||
'pytest', # Debian:python3-pytest
|
||||
],
|
||||
|
||||
packages=[
|
||||
'paypal_rest',
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'paypal-query = paypal_rest.cliquery:entry_point',
|
||||
],
|
||||
},
|
||||
)
|
||||
if __name__ == "__main__":
|
||||
setuptools.setup()
|
||||
|
|
|
@ -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