From e0e78de614de5869b78749050ed68478d460b712 Mon Sep 17 00:00:00 2001 From: Mark Mohades Date: Wed, 23 Sep 2020 16:25:39 -0400 Subject: [PATCH] added getting requested payments info and cancel/remind a payment --- .gitignore | 3 +- setup.py | 2 +- venmo_api/__init__.py | 10 +-- venmo_api/apis/payment_api.py | 108 +++++++++++++++++++++++++++++++- venmo_api/apis/user_api.py | 8 ++- venmo_api/models/exception.py | 19 +++++- venmo_api/models/json_schema.py | 64 +++++++++++++++++++ venmo_api/models/payment.py | 74 ++++++++++++++++++++++ venmo_api/utils/api_util.py | 2 +- venmo_api/venmo.py | 10 +-- 10 files changed, 280 insertions(+), 20 deletions(-) create mode 100644 venmo_api/models/payment.py diff --git a/.gitignore b/.gitignore index c53767b..d8c7c86 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,7 @@ media ### Other ### -tests.py -venmo_api/test.py +test*.py ### Linux ### *~ diff --git a/setup.py b/setup.py index 4797b56..07ed976 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def requirements(): setup( name='venmo-api', - version='0.1.7', + version='0.2.0', author="Mark Mohades", license="GNU General Public License v3", url='https://github.com/mmohades/venmo', diff --git a/venmo_api/__init__.py b/venmo_api/__init__.py index 27aa723..84c4271 100644 --- a/venmo_api/__init__.py +++ b/venmo_api/__init__.py @@ -3,6 +3,7 @@ from .models.json_schema import JSONSchema from .models.user import User from .models.transaction import Transaction +from .models.payment import Payment, PaymentStatus from .models.payment_method import (PaymentMethod, PaymentRole, PaymentPrivacy) from .utils.api_util import (deserialize, wrap_callback, warn, get_user_id, confirm, validate_access_token) from .utils.api_client import ApiClient @@ -13,9 +14,10 @@ __all__ = ["AuthenticationFailedError", "InvalidArgumentError", "InvalidHttpMethodError", "ArgumentMissingError", "JSONDecodeError", "ResourceNotFoundError", "HttpCodeError", "NoPaymentMethodFoundError", - "string_to_timestamp", "get_phone_model_from_json", "random_device_id", "deserialize", "wrap_callback", - "warn", "confirm", "get_user_id", "validate_access_token", - "JSONSchema", "User", "Transaction", "PaymentMethod", "PaymentRole", "PaymentPrivacy", - "ApiClient", "AuthenticationApi", "UserApi", "PaymentApi", + "NoPendingPaymentToUpdateError", "AlreadyRemindedPaymentError", "string_to_timestamp", + "get_phone_model_from_json", "random_device_id", + "deserialize", "wrap_callback", "warn", "confirm", "get_user_id", "validate_access_token", + "JSONSchema", "User", "Transaction", "Payment", "PaymentStatus", "PaymentMethod", "PaymentRole", + "PaymentPrivacy", "ApiClient", "AuthenticationApi", "UserApi", "PaymentApi", "Client" ] diff --git a/venmo_api/apis/payment_api.py b/venmo_api/apis/payment_api.py index 58c36c4..a93f47b 100644 --- a/venmo_api/apis/payment_api.py +++ b/venmo_api/apis/payment_api.py @@ -1,4 +1,5 @@ -from venmo_api import ApiClient +from venmo_api import ApiClient, Payment, ArgumentMissingError, AlreadyRemindedPaymentError, \ + NoPendingPaymentToUpdateError from venmo_api import User, PaymentMethod, PaymentRole, PaymentPrivacy from venmo_api import NoPaymentMethodFoundError from venmo_api import deserialize, wrap_callback, get_user_id @@ -7,9 +8,75 @@ class PaymentApi(object): - def __init__(self, api_client: ApiClient): + def __init__(self, profile, api_client: ApiClient): super().__init__() + self.__profile = profile self.__api_client = api_client + self.__payment_update_error_codes = { + "already_reminded_error": 2907, + "no_pending_payment_error": 2901, + "no_pending_payment_error2": 2905 + } + + def get_charge_payments(self, callback=None): + """ + Get a list of charge ongoing payments (pending request money) + :param callback: + :return: + """ + return self.__get_payments(action="charge", + callback=callback) + + def get_pay_payments(self, callback=None): + """ + Get a list of pay ongoing payments (pending requested money from your profile) + :param callback: + :return: + """ + return self.__get_payments(action="pay", + callback=callback) + + def remind_payment(self, payment: Payment = None, payment_id: int = None) -> bool: + """ + Send a reminder for payment/payment_id + :param payment: either payment object or payment_id must be be provided + :param payment_id: + :return: True or raises AlreadyRemindedPaymentError + """ + + # if the reminder has already sent + payment_id = payment_id or payment.id + action = 'remind' + + response = self.__update_payment(action=action, + payment_id=payment_id) + + # if the reminder has already sent + if 'error' in response.get('body'): + if response['body']['error']['code'] == self.__payment_update_error_codes['no_pending_payment_error2']: + raise NoPendingPaymentToUpdateError(payment_id=payment_id, + action=action) + raise AlreadyRemindedPaymentError(payment_id=payment_id) + return True + + def cancel_payment(self, payment: Payment = None, payment_id: int = None) -> bool: + """ + Cancel the payment/payment_id provided. Only applicable to payments you have access to (requested payments) + :param payment: + :param payment_id: + :return: True or raises NoPendingPaymentToCancelError + """ + # if the reminder has already sent + payment_id = payment_id or payment.id + action = 'cancel' + + response = self.__update_payment(action=action, + payment_id=payment_id) + + if 'error' in response.get('body'): + raise NoPendingPaymentToUpdateError(payment_id=payment_id, + action=action) + return True def get_payment_methods(self, callback=None) -> Union[List[PaymentMethod], None]: """ @@ -85,6 +152,43 @@ def request_money(self, amount: float, target_user=target_user, callback=callback) + def __update_payment(self, action, payment_id): + + if not payment_id: + raise ArgumentMissingError(arguments=('payment', 'payment_id')) + + resource_path = f'/payments/{payment_id}' + body = { + "action": action, + } + return self.__api_client.call_api(resource_path=resource_path, + body=body, + method='PUT', + ok_error_codes=list(self.__payment_update_error_codes.values())) + + def __get_payments(self, action, callback=None): + """ + Get a list of ongoing payments with the given action + :return: + """ + wrapped_callback = wrap_callback(callback=callback, + data_type=Payment) + + resource_path = '/payments' + parameters = { + "action": action, + "actor": self.__profile.id, + "limit": 100000 + } + response = self.__api_client.call_api(resource_path=resource_path, + params=parameters, + method='GET', + callback=wrapped_callback) + if callback: + return + + return deserialize(response=response, data_type=Payment) + def __send_or_request_money(self, amount: float, note: str, is_send_money, diff --git a/venmo_api/apis/user_api.py b/venmo_api/apis/user_api.py index 92fa6b5..bd7da3c 100644 --- a/venmo_api/apis/user_api.py +++ b/venmo_api/apis/user_api.py @@ -10,12 +10,15 @@ class UserApi(object): def __init__(self, api_client): super().__init__() self.__api_client = api_client + self.__profile = None - def get_my_profile(self, callback=None): + def get_my_profile(self, callback=None, force_update=False) -> Union[User, None]: """ Get my profile info and return as a :return my_profile: """ + if self.__profile and not force_update: + return self.__profile # Prepare the request resource_path = '/account' @@ -31,7 +34,8 @@ def get_my_profile(self, callback=None): if callback: return - return deserialize(response=response, data_type=User, nested_response=nested_response) + self.__profile = deserialize(response=response, data_type=User, nested_response=nested_response) + return self.__profile def search_for_users(self, query: str, callback=None, page: int = 1, count: int = 50) -> Union[List[User], None]: diff --git a/venmo_api/models/exception.py b/venmo_api/models/exception.py index 43c07ae..6d6d7cc 100644 --- a/venmo_api/models/exception.py +++ b/venmo_api/models/exception.py @@ -41,8 +41,8 @@ def __init__(self, response=None, msg: str = None): except JSONDecodeError: json = "Invalid Json" - self.msg = msg or f"HTTP Status code invalid. Could not make the request -> "\ - f"{status_code} {reason}.\nJSON: {json}" + self.msg = msg or f"HTTP Status code is invalid. Could not make the request because -> "\ + f"{status_code} {reason}.\nError: {json}" super(HttpCodeError, self).__init__(self.msg) @@ -73,6 +73,19 @@ def __init__(self, msg: str = None, reason=None): super(NoPaymentMethodFoundError, self).__init__(self.msg) +class AlreadyRemindedPaymentError(Exception): + def __init__(self, payment_id: int): + self.msg = f"A reminder has already been sent to the recipient of this transaction: {payment_id}." + super(AlreadyRemindedPaymentError, self).__init__(self.msg) + + +class NoPendingPaymentToUpdateError(Exception): + def __init__(self, payment_id: int, action: str): + self.msg = f"There is no *pending* transaction with the specified id: {payment_id}, to be {action}ed." + super(NoPendingPaymentToUpdateError, self).__init__(self.msg) + + __all__ = ["AuthenticationFailedError", "InvalidArgumentError", "InvalidHttpMethodError", "ArgumentMissingError", - "JSONDecodeError", "ResourceNotFoundError", "HttpCodeError", "NoPaymentMethodFoundError" + "JSONDecodeError", "ResourceNotFoundError", "HttpCodeError", "NoPaymentMethodFoundError", + "AlreadyRemindedPaymentError", "NoPendingPaymentToUpdateError" ] diff --git a/venmo_api/models/json_schema.py b/venmo_api/models/json_schema.py index f363b9c..ea8ad59 100644 --- a/venmo_api/models/json_schema.py +++ b/venmo_api/models/json_schema.py @@ -13,6 +13,10 @@ def user(json, is_profile=None): def payment_method(json): return PaymentMethodParser(json) + @staticmethod + def payment(json): + return PaymentParser(json) + class TransactionParser: @@ -199,3 +203,63 @@ def get_payment_method_type(self): 'name': 'name', 'type': 'type' } + + +class PaymentParser: + + def __init__(self, json): + self.json = json + + def get_id(self): + return self.json.get(payment_request_json_format['id']) + + def get_actor(self): + return self.json.get(payment_request_json_format['actor']) + + def get_target(self): + return self.json.get(payment_request_json_format['target'])\ + .get(payment_request_json_format['target_user']) + + def get_action(self): + return self.json.get(payment_request_json_format['action']) + + def get_amount(self): + return self.json.get(payment_request_json_format['amount']) + + def get_audience(self): + return self.json.get(payment_request_json_format['audience']) + + def get_date_authorized(self): + return self.json.get(payment_request_json_format['date_authorized']) + + def get_date_completed(self): + return self.json.get(payment_request_json_format['date_completed']) + + def get_date_created(self): + return self.json.get(payment_request_json_format['date_created']) + + def get_date_reminded(self): + return self.json.get(payment_request_json_format['date_reminded']) + + def get_note(self): + return self.json.get(payment_request_json_format['note']) + + def get_status(self): + return self.json.get(payment_request_json_format['status']) + + +payment_request_json_format = { + 'id': 'id', + 'actor': 'actor', + 'target': 'target', + 'target_user': 'user', + 'action': 'action', + 'amount': 'amount', + 'audience': 'audience', + 'date_authorized': 'date_authorized', + 'date_completed': 'date_completed', + 'date_created': 'date_created', + 'date_reminded': 'date_reminded', + 'note': 'note', + 'status': 'status' +} diff --git a/venmo_api/models/payment.py b/venmo_api/models/payment.py new file mode 100644 index 0000000..1d27b2c --- /dev/null +++ b/venmo_api/models/payment.py @@ -0,0 +1,74 @@ +from enum import Enum + +from venmo_api import string_to_timestamp, User +from venmo_api import JSONSchema + + +class Payment(object): + + def __init__(self, id_, actor, target, action, amount, audience, date_created, date_reminded, date_completed, + note, status): + """ + Create a Payment object + :param id_: + :param actor: + :param target: + :param action: + :param amount: + :param audience: + :param date_created: + :param date_reminded: + :param date_completed: + :param note: + :param status: + """ + super().__init__() + self.id = id_ + self.actor = actor + self.target = target + self.action = action + self.amount = amount + self.audience = audience + self.date_created = date_created + self.date_reminded = date_reminded + self.date_completed = date_completed + self.note = note + self.status = status + + @classmethod + def from_json(cls, json): + """ + init a new Payment form JSON + :param json: + :return: + """ + if not json: + return + + parser = JSONSchema.payment(json) + + return cls( + id_=parser.get_id(), + actor=User.from_json(parser.get_actor()), + target=User.from_json(parser.get_target()), + action=parser.get_action(), + amount=parser.get_amount(), + audience=parser.get_amount(), + date_created=string_to_timestamp(parser.get_date_created()), + date_reminded=string_to_timestamp(parser.get_date_reminded()), + date_completed=string_to_timestamp(parser.get_date_completed()), + note=parser.get_note(), + status=PaymentStatus(parser.get_status()) + ) + + def __str__(self) -> str: + return '%s(%s)' % ( + type(self).__name__, + ', '.join('%s=%s' % item for item in vars(self).items()) + ) + + +class PaymentStatus(Enum): + SETTLED = 'settled' + CANCELLED = 'cancelled' + PENDING = 'pending' diff --git a/venmo_api/utils/api_util.py b/venmo_api/utils/api_util.py index 02531db..78f2705 100644 --- a/venmo_api/utils/api_util.py +++ b/venmo_api/utils/api_util.py @@ -13,7 +13,7 @@ def validate_access_token(access_token): return if access_token[:6] != 'Bearer': - return f"Barear {access_token}" + return f"Bearer {access_token}" return access_token diff --git a/venmo_api/venmo.py b/venmo_api/venmo.py index bcedd6b..a48ea4d 100644 --- a/venmo_api/venmo.py +++ b/venmo_api/venmo.py @@ -12,18 +12,18 @@ def __init__(self, access_token: str): self.__access_token = validate_access_token(access_token=access_token) self.__api_client = ApiClient(access_token=access_token) self.user = UserApi(self.__api_client) - self.payment = PaymentApi(self.__api_client) - self.__profile = None + self.__profile = self.user.get_my_profile() + self.payment = PaymentApi(profile=self.__profile, + api_client=self.__api_client) def my_profile(self, force_update=False): """ Get your profile info. It can be cached from the prev time. :return: """ - if self.__profile and not force_update: - return self.__profile + if force_update: + self.__profile = self.user.get_my_profile(force_update=force_update) - self.__profile = self.user.get_my_profile() return self.__profile @staticmethod