paypal_rest/paypal_rest/cliquery.py
Brett Smith 1b0740ad5d cliquery: Dump individual objects as an array.
This provides nicer output when querying multiple objects in one run.
2020-11-24 09:22:59 -05:00

198 lines
7.2 KiB
Python
Executable file

#!/usr/bin/env python3
"""cliquery.py - Command line tool to query PayPal transactions and subscriptions"""
# 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/>.
import argparse
import collections
import datetime
import logging
import sys
from pathlib import Path
import yaml
from . import client as clientmod
from . import cliutil
from . import config as configmod
from . import paypal_types
from .transaction import CartItem, Transaction
from typing import (
Optional,
Sequence,
TextIO,
Type,
)
PROGNAME = 'paypal-query'
logger = logging.getLogger('paypal_rest.cliquery')
class YAMLDumper(yaml.SafeDumper):
TRANSACTION_FIELD_ORDER = [
clientmod.TransactionFields.SHIPPING,
clientmod.TransactionFields.PAYER,
clientmod.TransactionFields.TRANSACTION,
clientmod.TransactionFields.CART,
clientmod.TransactionFields.STORE,
clientmod.TransactionFields.AUCTION,
clientmod.TransactionFields.INCENTIVE,
]
@classmethod
def add_transaction_representer(cls, fields: clientmod.TransactionFields) -> None:
txn_key_order = [
f'{flag.name.lower()}_info'
for flag in cls.TRANSACTION_FIELD_ORDER
if flag & fields
]
def transaction_representer(self: 'YAMLDumper', data: Transaction) -> yaml.nodes.MappingNode:
return self.represent_dict((key, data[key]) for key in txn_key_order)
cls.add_representer(Transaction, transaction_representer)
def add_fields_argument(
parser: argparse.ArgumentParser,
field_type: Type[clientmod.PayPalFields],
*short_flags: str,
) -> argparse.Action:
type_name = field_type.__name__[:-6].lower()
return parser.add_argument(
f'--{type_name}-fields', *short_flags,
action='append',
metavar='FIELD',
type=field_type.from_arg,
help=f"Only show these field(s) in {type_name} results."
" You can specify this option multiple times."
f" Choices are {', '.join(field_type.choices())}."
)
def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace:
parser = argparse.ArgumentParser(prog=PROGNAME)
cliutil.add_version_argument(parser)
parser.add_argument(
'--config-file', '-C',
type=Path,
metavar='PATH',
help="""Read client configuration from this INI file
""")
parser.add_argument(
'--config-section', '-c',
default='query',
help="""Read client configuration from this section of the config file
""")
parser.add_argument(
'--begin', '--start', '-b',
dest='start_date',
metavar='DATETIME',
type=paypal_types.parse_datetime,
help="""Datetime to begin the search, in ISO 8601 format
""")
parser.add_argument(
'--end', '--stop', '-e',
dest='end_date',
metavar='DATETIME',
type=paypal_types.parse_datetime,
help="""Datetime to end the search, in ISO 8601 format
""")
add_fields_argument(parser, clientmod.TransactionFields, '--txn-fields', '-T')
add_fields_argument(parser, clientmod.SubscriptionFields, '--sub-fields', '-S')
parser.add_argument(
'paypal_ids',
metavar='ID',
nargs='*',
help="""ID of PayPal object(s) to look up and return. If no IDs are
specified, lists all transactions in your specified date range (default last
24 hours).
""")
cliutil.add_loglevel_argument(parser)
args = parser.parse_args(arglist)
args.transaction_fields = clientmod.TransactionFields.combine(args.transaction_fields)
args.subscription_fields = clientmod.SubscriptionFields.combine(args.subscription_fields)
if args.end_date is None:
args.end_date = datetime.datetime.now(datetime.timezone.utc)
return args
def summarize_transaction(txn: Transaction, stream: TextIO) -> None:
date_s = txn.updated_date().astimezone(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M')
status = txn.status().value
try:
from_s = f"\t{txn.payer_fullname()} ({txn.payer_email()})"
except KeyError:
from_s = ""
print(f"{date_s}\t{txn.transaction_id()}\t{status}{from_s}", file=stream)
cart = list(txn.cart_items())
if not cart:
txn_name = txn['transaction_info'].get('transaction_subject', "Gross Amount")
txn_amt = txn.amount()
cart.append(CartItem(None, txn_name, None, 1, txn_amt, txn_amt))
fee_amt = txn.fee_amount()
if fee_amt is not None:
cart.append(CartItem(None, "PayPal Fee", None, 1, fee_amt, fee_amt))
names = [
item.name or item.description or item.code or "Unknown Item"
for item in cart
]
amounts = [str(item.total_price) for item in cart]
name_len = max(len(name) for name in names)
amt_len = max(len(amt_s) for amt_s in amounts)
line_fmt = f' {{:>{name_len}}}{{:>{amt_len}}}{{}}'
for item, name, amt_s in zip(cart, names, amounts):
if item.quantity != 1:
unit_s = f" ({item.quantity:,g} @ {item.unit_price})"
else:
unit_s = ""
print(line_fmt.format(name, amt_s, unit_s), file=stream)
def main(
arglist: Optional[Sequence[str]]=None,
stdout: TextIO=sys.stdout,
stderr: TextIO=sys.stderr,
) -> int:
args = parse_arguments(arglist)
cliutil.set_loglevel(logger, args.loglevel)
config = configmod.load_config(args.config_file)
if args.config_section not in config:
config.add_section(args.config_section)
paypal = clientmod.PayPalAPIClient.from_config(config[args.config_section])
if not args.paypal_ids:
if args.start_date is None:
args.start_date = args.end_date - datetime.timedelta(hours=24)
args.transaction_fields |= clientmod.TransactionFields.TRANSACTION
for txn in paypal.iter_transactions(
args.start_date, args.end_date, args.transaction_fields,
):
summarize_transaction(txn, stdout)
else:
YAMLDumper.add_transaction_representer(args.transaction_fields)
for paypal_id in args.paypal_ids:
paypal_id = paypal_id.upper()
paypal_obj: paypal_types.APIResponse
if paypal_id.startswith('I-'):
paypal_obj = paypal.get_subscription(paypal_id, fields=args.subscription_fields)
else:
paypal_obj = paypal.get_transaction(
paypal_id, args.end_date, args.start_date, args.transaction_fields,
)
yaml.dump([paypal_obj], stdout, Dumper=YAMLDumper)
return 0
entry_point = cliutil.make_entry_point(__name__, PROGNAME)
if __name__ == '__main__':
exit(entry_point())