├── .gitattributes ├── .gitignore ├── LICENSE.txt ├── README.md ├── python_paystack ├── __init__.py ├── managers.py ├── mixins.py ├── objects │ ├── __init__.py │ ├── base.py │ ├── customers.py │ ├── errors.py │ ├── filters.py │ ├── plans.py │ ├── subaccounts.py │ ├── transactions.py │ └── transfers.py └── paystack_config.py ├── setup.cfg └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | 3 | 4 | # Windows image file caches 5 | Thumbs.db 6 | ehthumbs.db 7 | 8 | # Folder config file 9 | Desktop.ini 10 | 11 | # Recycle Bin used on file shares 12 | $RECYCLE.BIN/ 13 | 14 | # Windows Installer files 15 | *.cab 16 | *.msi 17 | *.msm 18 | *.msp 19 | 20 | # Windows shortcuts 21 | *.lnk 22 | 23 | # ========================= 24 | # Operating System Files 25 | # ========================= 26 | 27 | # OSX 28 | # ========================= 29 | 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | 34 | # Thumbnails 35 | ._* 36 | 37 | # Files that might appear in the root of a volume 38 | .DocumentRevisions-V100 39 | .fseventsd 40 | .Spotlight-V100 41 | .TemporaryItems 42 | .Trashes 43 | .VolumeIcon.icns 44 | 45 | # Directories potentially created on remote AFP share 46 | .AppleDB 47 | .AppleDesktop 48 | Network Trash Folder 49 | Temporary Items 50 | .apdisk 51 | 52 | # Compiled python modules. 53 | *.pyc 54 | 55 | # Setuptools distribution folder. 56 | /dist/ 57 | 58 | # Python egg metadata, regenerated from source files by setuptools. 59 | /*.egg-info 60 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License - MIT 2 | Copyright (c) 2017 Nwalor Chibuzor 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-paystack 2 | 3 | Python API wrapper for paystack ( https://paystack.com/ ) 4 | 5 | # Installation 6 | 7 | pip install python-paystack 8 | 9 | # Configuration 10 | 11 | To get started, import PaystackConfig from python_paystack.paystack_config and instantiate your public and secret keys. 12 | Other settings which are instatiated by default include the paystack api url (PAYSTACK_URL), PASS_ON_TRANSACTION_COST which determines if the cost per transaction is passed to the end user, LOCAL_COST and INTL_COST are the paystack charges for local and international cards respectively. 13 | 14 | ```python 15 | from python_paystack.paystack_config import PaystackConfig 16 | 17 | PaystackConfig.SECRET_KEY = PAYSTACK_SECRET_KEY 18 | PaystackConfig.PUBLIC_KEY = PAYSTACK_PUBLIC_KEY 19 | 20 | ``` 21 | 22 | # Usage 23 | 24 | Most of the library's functionality lies in the managers.py file which contains the TransactionsManager, CustomersManager, PlanManager and the TransfersManager. 25 | 26 | The Manager classes handle every direct interaction with the Paystack API. 27 | 28 | # Transactions 29 | 30 | You can initialize transactions using all 3 methods supported by paystack i.e Standard, Inline and Inline Embed. 31 | Both the inline and inline embed methods return a dictionary of values while the standard method returns a Transaction object which contains an authorization url. 32 | 33 | **Starting and verifying a standard transaction** 34 | 35 | ```python 36 | from python_paystack.objects.transactions import Transaction 37 | from python_paystack.managers import TransactionsManager 38 | 39 | transaction = Transaction(2000, 'email@test.com') 40 | transaction_manager = TransactionsManager() 41 | transaction = transaction_manager.initialize_transaction('STANDARD', transaction) 42 | #Starts a standard transaction and returns a transaction object 43 | 44 | transaction.authorization_url 45 | #Gives the authorization_url for the transaction 46 | 47 | #Transactions can easily be verified like so 48 | transaction = transaction_manager.verify_transaction(transaction) 49 | 50 | ``` 51 | 52 | **Starting an inline transaction** 53 | ```python 54 | transaction_manager.initialize_transaction('INLINE', transaction) 55 | 56 | ``` 57 | 58 | **Starting an inline embed transaction** 59 | ```python 60 | transaction_manager.initialize_transaction('INLINE EMBED', transaction) 61 | ``` 62 | 63 | 64 | 65 | 66 | # Customers 67 | 68 | **Registering a customer with paystack** 69 | 70 | A customer can be registered using the CustomersManager.create_customer method which accepts a Customer object as an argument. 71 | All the customer information to be sent to paystack is taken from the Customer object. 72 | Misc. data can also be sent using the meta argument. 73 | ```python 74 | from python_paystack.managers import CustomersManager 75 | from python_paystack.objects.customers import Customer 76 | 77 | customer = Customer('test@email.com') 78 | customer_manager = CustomersManager() 79 | customer_manager.create(customer) 80 | ``` 81 | 82 | **Getting existing customers** 83 | ```python 84 | customer_manager = CustomersManager() 85 | customer_manager.get_all() 86 | #Returns a list containing every customer 87 | 88 | customer_manager.get(id) 89 | #Returns customer with the specified id 90 | ``` 91 | 92 | 93 | 94 | # Transfers 95 | 96 | **Making a transfer with paystack** 97 | ```python 98 | from python_paystack.objects.transfers import Transfer 99 | from python_paystack.managers import TransfersManager 100 | 101 | transfer = Transfer(2000, "RCP_123456") 102 | transfer_manager = TransfersManager() 103 | transfer = transfer_manager.create(transfer) 104 | 105 | 106 | ``` 107 | 108 | 109 | # TODO : 110 | 111 | Tests 112 | 113 | -------------------------------------------------------------------------------- /python_paystack/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chibuzor-IN/python-paystack/7002d02cbcd4a894bb92eaad9a6910835428f932/python_paystack/__init__.py -------------------------------------------------------------------------------- /python_paystack/managers.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Managers.py 3 | ''' 4 | 5 | import json 6 | import requests 7 | import validators 8 | 9 | from .objects.base import Manager 10 | from .objects.customers import Customer 11 | from .objects.errors import APIConnectionFailedError, URLValidationError 12 | from .objects.filters import Filter 13 | from .objects.plans import Plan 14 | from .objects.transfers import Transfer 15 | from .objects.transactions import Transaction 16 | from .objects.subaccounts import SubAccount 17 | 18 | from .paystack_config import PaystackConfig 19 | from .mixins import CreatableMixin, RetrieveableMixin, UpdateableMixin 20 | 21 | class Utils(Manager): 22 | ''' 23 | 24 | ''' 25 | 26 | def __init__(self): 27 | super().__init__() 28 | 29 | 30 | def resolve_card_bin(self, card_bin, endpoint='/decision/bin/'): 31 | ''' 32 | 33 | ''' 34 | card_bin = card_bin[:6] 35 | url = self.PAYSTACK_URL + endpoint + card_bin 36 | headers, _ = self.build_request_args() 37 | 38 | response = requests.get(url, headers=headers) 39 | content = self.parse_response_content(response.content) 40 | 41 | status, message = self.get_content_status(content) 42 | if status: 43 | return content['data'] 44 | 45 | def get_banks(self, endpoint='/bank'): 46 | ''' 47 | 48 | ''' 49 | url = self.PAYSTACK_URL + endpoint 50 | headers, _ = self.build_request_args() 51 | 52 | response = requests.get(url, headers=headers) 53 | content = self.parse_response_content(response.content) 54 | 55 | status, message = self.get_content_status(content) 56 | if status: 57 | return content['data'] 58 | 59 | def resolve_bvn(self, bvn, endpoint='/bank/resolve_bvn/'): 60 | url = self.PAYSTACK_URL + endpoint + bvn 61 | headers, _ = self.build_request_args() 62 | 63 | response = requests.get(url, headers=headers) 64 | content = self.parse_response_content(response.content) 65 | 66 | status, message = self.get_content_status(content) 67 | if status: 68 | return content['data'] 69 | 70 | def resolve_account_number(self, account_number, bank_code, endpoint='/bank/resolve'): 71 | ''' 72 | ''' 73 | params = "?account_number=%s&bank_code=%s" % (account_number, bank_code) 74 | url = self.PAYSTACK_URL + endpoint + params 75 | 76 | headers, _ = self.build_request_args() 77 | 78 | response = requests.get(url, headers=headers) 79 | content = self.parse_response_content(response.content) 80 | 81 | status, message = self.get_content_status(content) 82 | if status: 83 | return content['data'] 84 | 85 | 86 | 87 | class TransactionsManager(RetrieveableMixin, Manager): 88 | ''' 89 | TransactionsManager class that handles every part of a transaction 90 | 91 | Attributes: 92 | amount : Transaction cost 93 | email : Buyer's email 94 | reference 95 | authorization_url 96 | card_locale : Card location for application of paystack charges 97 | ''' 98 | 99 | LOCAL_COST = PaystackConfig.LOCAL_COST 100 | INTL_COST = PaystackConfig.INTL_COST 101 | PASS_ON_TRANSACTION_COST = PaystackConfig.PASS_ON_TRANSACTION_COST 102 | 103 | _endpoint = '/transaction' 104 | _object_class = Transaction 105 | 106 | 107 | def __init__(self, endpoint='/transaction'): 108 | super().__init__() 109 | self._endpoint = endpoint 110 | 111 | 112 | def initialize_transaction(self, method, transaction: Transaction, 113 | callback_url='', endpoint='/initialize'): 114 | ''' 115 | Initializes a paystack transaction. 116 | Returns an authorization url which points to a paystack form if the method is standard. 117 | Returns a dict containing transaction information if the method is inline or inline embed 118 | 119 | Arguments: 120 | method : Specifies whether to use paystack inline, standard or inline embed 121 | callback_url : URL paystack redirects to after a user enters their card details 122 | plan_code : Payment plan code 123 | endpoint : Paystack API endpoint for intializing transactions 124 | ''' 125 | 126 | method = method.upper() 127 | if method not in ('STANDARD', 'INLINE', 'INLINE EMBED'): 128 | raise ValueError("method argument should be STANDARD, INLINE or INLINE EMBED") 129 | 130 | if self.PASS_ON_TRANSACTION_COST: 131 | transaction.amount = transaction.full_transaction_cost(transaction.card_locale, 132 | self.LOCAL_COST, self.INTL_COST) 133 | 134 | data = json.JSONDecoder().decode(transaction.to_json()) 135 | 136 | if callback_url: 137 | if validators.url(callback_url): 138 | data['callback_url'] = callback_url 139 | 140 | else: 141 | raise URLValidationError 142 | 143 | if method in ('INLINE', 'INLINE EMBED'): 144 | data['key'] = PaystackConfig.PUBLIC_KEY 145 | return data 146 | 147 | headers, data = self.build_request_args(data) 148 | 149 | url = self.PAYSTACK_URL + self._endpoint + endpoint 150 | response = requests.post(url, headers=headers, data=data) 151 | content = response.content 152 | content = self.parse_response_content(content) 153 | 154 | 155 | status, message = self.get_content_status(content) 156 | 157 | #status = True for a successful connection 158 | if status: 159 | data = json.dumps(content['data']) 160 | transaction = Transaction.from_json(data) 161 | return transaction 162 | else: 163 | #Connection failed 164 | raise APIConnectionFailedError(message) 165 | 166 | def verify_transaction(self, transaction_reference : str, endpoint='/verify/'): 167 | ''' 168 | Verifies a payment using the transaction reference. 169 | 170 | Arguments: 171 | endpoint : Paystack API endpoint for verifying transactions 172 | ''' 173 | 174 | endpoint += transaction_reference 175 | url = self.PAYSTACK_URL + self._endpoint + endpoint 176 | 177 | headers, _ = self.build_request_args() 178 | response = requests.get(url, headers=headers) 179 | content = response.content 180 | content = self.parse_response_content(content) 181 | 182 | status, message = self.get_content_status(content) 183 | 184 | if status: 185 | data_dict = content['data'] 186 | data = json.dumps(content['data']) 187 | transaction = Transaction.from_json(data) 188 | transaction.email = data_dict['customer']['email'] 189 | transaction.authorization_code = data_dict['authorization']['authorization_code'] 190 | return transaction 191 | else: 192 | raise APIConnectionFailedError(message) 193 | 194 | def charge_authorization(self, transaction: Transaction, endpoint='/charge_authorization'): 195 | data = transaction.to_json() 196 | headers, _ = self.build_request_args() 197 | 198 | response = requests.post(self.PAYSTACK_URL + self._endpoint + endpoint, 199 | headers=headers, data=data) 200 | content = response.content 201 | content = self.parse_response_content(content) 202 | 203 | status, message = self.get_content_status(content) 204 | 205 | #status = True for a successful connection 206 | if status: 207 | return content['data'] 208 | else: 209 | #Connection failed 210 | raise APIConnectionFailedError(message) 211 | 212 | def get_transactions(self,filter=None): 213 | ''' 214 | Returns all transactions with the option of filtering by the transation status 215 | Transaction statuses include : 'failed', 'success', 'abandoned' 216 | ''' 217 | url = self.PAYSTACK_URL + self._endpoint 218 | if filter: 219 | url += '/?status={}'.format(filter) 220 | 221 | config = PaystackConfig() 222 | headers = { 223 | 'Authorization':'Bearer '+config.SECRET_KEY 224 | } 225 | r = requests.get(url,headers=headers) 226 | 227 | return r.json() 228 | 229 | def get_total_transactions(self): 230 | ''' 231 | Get total amount recieved from transactions 232 | ''' 233 | headers, _ = self.build_request_args() 234 | url = self.PAYSTACK_URL + self._endpoint 235 | url += '/totals' 236 | response = requests.get(url, headers=headers) 237 | 238 | content = response.content 239 | content = self.parse_response_content(content) 240 | 241 | status, message = self.get_content_status(content) 242 | 243 | if status: 244 | return content['data'] 245 | else: 246 | raise APIConnectionFailedError(message) 247 | 248 | 249 | def filter_transactions(self, amount_range: range, transactions): 250 | ''' 251 | Returns all transactions with amounts in the given amount_range 252 | ''' 253 | results = [] 254 | for transaction in transactions: 255 | if Filter.filter_amount(amount_range, transaction): 256 | results.append(transaction) 257 | 258 | return results 259 | 260 | 261 | class CustomersManager(CreatableMixin, RetrieveableMixin, UpdateableMixin, Manager): 262 | ''' 263 | CustomersManager class which handels actions for Paystack Customers 264 | 265 | Attributes : 266 | _endpoint : Paystack API endpoint for 'customers' actions 267 | 268 | ''' 269 | _endpoint = '/customer' 270 | _object_class = Customer 271 | 272 | def __init__(self): 273 | super().__init__() 274 | 275 | 276 | def set_risk_action(self, risk_action, customer: Customer): 277 | ''' 278 | Method for either blacklisting or whitelisting a customer 279 | 280 | Arguments : 281 | risk_action : (allow or deny) 282 | customer_id : Customer id 283 | 284 | ''' 285 | 286 | if not isinstance(customer, Customer): 287 | raise TypeError("customer argument should be of type 'Customer' ") 288 | 289 | endpoint = '/set_risk_action' 290 | 291 | if risk_action not in ('allow', 'deny'): 292 | raise ValueError("Invalid risk action") 293 | 294 | else: 295 | data = {'customer' : customer.id, 'risk_action' : risk_action} 296 | headers, data = self.build_request_args(data) 297 | url = "%s%s" % (self.PAYSTACK_URL + self._endpoint, endpoint) 298 | 299 | response = requests.post(url, headers=headers, data=data) 300 | 301 | content = response.content 302 | content = self.parse_response_content(content) 303 | 304 | status, message = self.get_content_status(content) 305 | 306 | if status: 307 | data = json.dumps(content['data']) 308 | return Customer.from_json(data) 309 | else: 310 | raise APIConnectionFailedError(message) 311 | 312 | def deactive_authorization(self, authorization_code): 313 | ''' 314 | Method to deactivate an existing authorization 315 | 316 | Arguments : 317 | authorization_code : Code for the transaction to be deactivated 318 | 319 | ''' 320 | data = {'authorization_code' : authorization_code} 321 | headers, data = self.build_request_args(data) 322 | 323 | url = "%s/deactivate_authorization" % (self.PAYSTACK_URL + self._endpoint) 324 | response = requests.post(url, headers=headers, data=data) 325 | 326 | content = response.content 327 | content = self.parse_response_content(content) 328 | 329 | status, message = self.get_content_status(content) 330 | 331 | if status: 332 | return content['data'] 333 | else: 334 | raise APIConnectionFailedError(message) 335 | 336 | 337 | 338 | 339 | class PlanManager(CreatableMixin, RetrieveableMixin, UpdateableMixin, Manager): 340 | ''' 341 | Plan Manager class 342 | ''' 343 | 344 | _endpoint = '/plan' 345 | _object_class = Plan 346 | 347 | def __init__(self, endpoint='/plan'): 348 | super().__init__() 349 | self._endpoint = endpoint 350 | 351 | 352 | 353 | 354 | class TransfersManager(CreatableMixin, RetrieveableMixin, UpdateableMixin, Manager): 355 | ''' 356 | TransfersManager class 357 | ''' 358 | 359 | _endpoint = '/transfer' 360 | _object_class = Transfer 361 | 362 | def __init__(self, endpoint='/transfer'): 363 | super().__init__() 364 | self._endpoint = endpoint 365 | 366 | 367 | def finalize_transfer(self, transfer_id, otp): 368 | ''' 369 | Method for finalizing transfers 370 | ''' 371 | transfer_id = str(transfer_id) 372 | otp = str(otp) 373 | 374 | data = {'transfer_code' : transfer_id, 'otp' : otp} 375 | headers, data = self.build_request_args(data) 376 | 377 | url = self.PAYSTACK_URL + self._endpoint 378 | url += '/finalize_transfer' 379 | response = requests.post(url, headers=headers, data=data) 380 | content = response.content 381 | content = self.parse_response_content(content) 382 | 383 | 384 | status, message = self.get_content_status(content) 385 | 386 | if status: 387 | data = content['data'] 388 | return data 389 | 390 | else: 391 | #Connection failed 392 | raise APIConnectionFailedError(message) 393 | 394 | 395 | 396 | 397 | class SubAccountManager(CreatableMixin, RetrieveableMixin, UpdateableMixin, Manager): 398 | ''' 399 | 400 | ''' 401 | _endpoint = None 402 | _object_class = SubAccount 403 | 404 | def __init__(self, endpoint='/subaccount'): 405 | super().__init__() 406 | self._endpoint = endpoint 407 | 408 | -------------------------------------------------------------------------------- /python_paystack/mixins.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | ''' 4 | import json 5 | import requests 6 | 7 | from .objects.errors import APIConnectionFailedError 8 | 9 | class CreatableMixin(object): 10 | def create(self, target_object): 11 | ''' 12 | 13 | ''' 14 | url = self.PAYSTACK_URL + self._endpoint 15 | 16 | data = target_object.to_json() 17 | headers, _ = self.build_request_args() 18 | response = requests.post(url, headers=headers, data=data) 19 | 20 | content = response.content 21 | content = self.parse_response_content(content) 22 | status, message = self.get_content_status(content) 23 | 24 | if status: 25 | data = json.dumps(content['data']) 26 | return self._object_class.from_json(data) 27 | else: 28 | raise APIConnectionFailedError(message) 29 | 30 | 31 | class RetrieveableMixin(object): 32 | ''' 33 | 34 | ''' 35 | 36 | def get_all(self): 37 | ''' 38 | 39 | ''' 40 | headers, _ = self.build_request_args() 41 | response = requests.get(self.PAYSTACK_URL + self._endpoint, headers=headers) 42 | 43 | content = response.content 44 | content = self.parse_response_content(content) 45 | 46 | status, message = self.get_content_status(content) 47 | 48 | if status: 49 | data = content['data'] 50 | meta = content['meta'] 51 | objects = [] 52 | for item in data: 53 | item = json.dumps(item) 54 | objects.append(self._object_class.from_json(item)) 55 | return (objects, meta) 56 | else: 57 | raise APIConnectionFailedError(message) 58 | 59 | 60 | def get(self, object_id): 61 | ''' 62 | Method for getting an object with the specified id 63 | ''' 64 | headers, _ = self.build_request_args() 65 | 66 | url = "%s%s/%s" % (self.PAYSTACK_URL, self._endpoint, object_id) 67 | response = requests.get(url, headers=headers) 68 | 69 | content = response.content 70 | content = self.parse_response_content(content) 71 | 72 | status, message = self.get_content_status(content) 73 | 74 | if status: 75 | data = json.dumps(content['data']) 76 | return self._object_class.from_json(data) 77 | else: 78 | raise APIConnectionFailedError(message) 79 | 80 | 81 | class UpdateableMixin(object): 82 | def update(self, object_id, updated_object): 83 | ''' 84 | Method for updating existing plan 85 | ''' 86 | if not isinstance(updated_object, self._object_class): 87 | raise TypeError 88 | 89 | data = updated_object.to_json() 90 | headers, _ = self.build_request_args() 91 | url = "%s%s/%s" % (self.PAYSTACK_URL, self._endpoint, object_id) 92 | 93 | response = requests.put(url, headers=headers, data=data) 94 | content = response.content 95 | content = self.parse_response_content(content) 96 | 97 | status, message = self.get_content_status(content) 98 | if status or message: 99 | return (status, message) 100 | else: 101 | raise APIConnectionFailedError(message) -------------------------------------------------------------------------------- /python_paystack/objects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chibuzor-IN/python-paystack/7002d02cbcd4a894bb92eaad9a6910835428f932/python_paystack/objects/__init__.py -------------------------------------------------------------------------------- /python_paystack/objects/base.py: -------------------------------------------------------------------------------- 1 | ''' 2 | base.py 3 | ''' 4 | import json 5 | import jsonpickle 6 | from .errors import InvalidInstance 7 | from ..paystack_config import PaystackConfig 8 | 9 | class Base(): 10 | ''' 11 | Abstract Base Class 12 | ''' 13 | def __init__(self): 14 | if type(self) is Base: 15 | raise TypeError("Can not make instance of abstract base class") 16 | 17 | 18 | def to_json(self, pickled=False): 19 | ''' 20 | Method to serialize class instance 21 | ''' 22 | if pickled: 23 | return jsonpickle.encode(self) 24 | else: 25 | data = json.JSONDecoder().decode(jsonpickle.encode(self)) 26 | data.pop("py/object") 27 | return json.dumps(data) 28 | 29 | @classmethod 30 | def from_json(cls, data, pickled=False): 31 | ''' 32 | Method to return a class instance from given json dict 33 | ''' 34 | class_name = cls.__name__ 35 | class_object = None 36 | if pickled: 37 | class_object = jsonpickle.decode(data) 38 | else: 39 | py_object = str(cls).replace('', '') 41 | py_object = py_object.replace("'", "") 42 | data = json.JSONDecoder().decode(data) 43 | data['py/object'] = py_object 44 | data = json.JSONEncoder().encode(data) 45 | 46 | class_object = jsonpickle.decode(data) 47 | 48 | if isinstance(class_object, cls): 49 | return class_object 50 | else: 51 | raise InvalidInstance(class_name) 52 | 53 | class Manager(Base): 54 | ''' 55 | Abstract base class for 'Manager' Classes 56 | ''' 57 | 58 | PAYSTACK_URL = None 59 | SECRET_KEY = None 60 | LOCAL_COST = None 61 | INTL_COST = None 62 | PASS_ON_TRANSACTION_COST = None 63 | 64 | decoder = json.JSONDecoder() 65 | 66 | def __init__(self): 67 | super().__init__() 68 | if type(self) is Manager: 69 | raise TypeError("Can not make instance of abstract base class") 70 | 71 | if not PaystackConfig.SECRET_KEY or not PaystackConfig.PUBLIC_KEY: 72 | raise ValueError("No secret key or public key found," 73 | "assign values using PaystackConfig.SECRET_KEY = SECRET_KEY and" 74 | "PaystackConfig.PUBLIC_KEY = PUBLIC_KEY") 75 | 76 | self.PAYSTACK_URL = PaystackConfig.PAYSTACK_URL 77 | self.SECRET_KEY = PaystackConfig.SECRET_KEY 78 | 79 | 80 | def get_content_status(self, content): 81 | ''' 82 | Method to return the status and message from an API response 83 | 84 | Arguments : 85 | content : Response as a dict 86 | ''' 87 | 88 | if not isinstance(content, dict): 89 | raise TypeError("Content argument should be a dict") 90 | 91 | return (content['status'], content['message']) 92 | 93 | def parse_response_content(self, content): 94 | ''' 95 | Method to convert a response's content in bytes to a string. 96 | 97 | Arguments: 98 | content : Response in bytes 99 | ''' 100 | content = bytes.decode(content) 101 | content = self.decoder.decode(content) 102 | return content 103 | 104 | def build_request_args(self, data=None): 105 | ''' 106 | Method for generating required headers. 107 | Returns a tuple containing the generated headers and the data in json. 108 | 109 | Arguments : 110 | data(Dict) : An optional data argument which holds the body of the request. 111 | ''' 112 | headers = {'Authorization' : 'Bearer %s' % self.SECRET_KEY, 113 | 'Content-Type' : 'application/json', 114 | 'cache-control' : 'no-cache' 115 | } 116 | 117 | data = json.dumps(data) 118 | 119 | return (headers, data) 120 | -------------------------------------------------------------------------------- /python_paystack/objects/customers.py: -------------------------------------------------------------------------------- 1 | ''' 2 | customers.py 3 | ''' 4 | 5 | import validators 6 | from .errors import InvalidEmailError 7 | from .base import Base 8 | 9 | class Customer(Base): 10 | ''' 11 | Customer class that holds customer properties 12 | ''' 13 | 14 | phone = None 15 | email = None 16 | customer_code = None 17 | risk_action = None 18 | first_name = None 19 | last_name = None 20 | id = None 21 | metadata = None 22 | 23 | def __init__(self, email, first_name=None, last_name=None, 24 | phone=None, risk_action=None, id=None, metadata=None): 25 | super().__init__() 26 | if validators.email(email): 27 | self.email = email 28 | self.first_name = first_name 29 | self.last_name = last_name 30 | self.phone = phone 31 | self.risk_action = risk_action 32 | # self.id = id 33 | else: 34 | raise InvalidEmailError 35 | 36 | if metadata and not isinstance(metadata, dict): 37 | raise TypeError("meta argument should be a dict") 38 | else: 39 | self.metadata = metadata 40 | 41 | def __str__(self): 42 | value = self.email 43 | if self.first_name: 44 | value += ' %s' % (self.first_name) 45 | return value 46 | -------------------------------------------------------------------------------- /python_paystack/objects/errors.py: -------------------------------------------------------------------------------- 1 | ''' 2 | errors.py 3 | ''' 4 | 5 | class Error(BaseException): 6 | pass 7 | 8 | class InvalidInstance(Error): 9 | ''' 10 | InvalidInstance class 11 | ''' 12 | message = 'Not a valid instance of type : ' 13 | manager = '' 14 | 15 | def __init__(self, manager): 16 | self.manager = manager 17 | 18 | def __str__(self): 19 | return '%s %s' % (self.message, self.manager) 20 | 21 | class APIConnectionFailedError(Error): 22 | ''' 23 | APIConnectionFailedError class 24 | ''' 25 | message = '' 26 | 27 | def __init__(self, message): 28 | self.message = message 29 | 30 | def __str__(self): 31 | return self.message 32 | 33 | class InvalidEmailError(Error): 34 | ''' 35 | InvalidEmailError class 36 | ''' 37 | message = "Invalid email address" 38 | 39 | def __str__(self): 40 | return self.message 41 | 42 | 43 | class URLValidationError(Error): 44 | ''' 45 | URLValidationError Excpetion class for invalid urls 46 | 47 | Attributes: 48 | message : Error description 49 | ''' 50 | 51 | message = 'Invalid URL' 52 | 53 | def __str__(self): 54 | return self.message 55 | -------------------------------------------------------------------------------- /python_paystack/objects/filters.py: -------------------------------------------------------------------------------- 1 | class Filter(): 2 | ''' 3 | Filter class for checking through dicts 4 | ''' 5 | 6 | @staticmethod 7 | def find_key_value(key, dataset): 8 | ''' 9 | Function for getting the value of a the key passed in (provided it exists) 10 | Returns True and the value if the key is found or False and 0 when it isn't 11 | 12 | Arguments: 13 | key : dictionary key to be searched for 14 | ''' 15 | 16 | dicts = [] 17 | 18 | if not isinstance(dataset, dict): 19 | raise TypeError("dataset argument should be a dictionary") 20 | 21 | for item in dataset: 22 | 23 | if isinstance(dataset[item], dict): 24 | dicts.append(dataset[item]) 25 | continue 26 | if item == key: 27 | return (True, dataset[item]) 28 | 29 | for dataset in dicts: 30 | return Filter.find_key_value(key, dataset) 31 | 32 | return (False, 0) 33 | 34 | 35 | @staticmethod 36 | def filter_amount(amount_range: range, dataset, amount_key='amount'): 37 | ''' 38 | Checks if there is an amount in the amount_range given in the dataset 39 | ''' 40 | if not isinstance(dataset, dict): 41 | raise TypeError("dataset argument should be a dictionary") 42 | 43 | if not isinstance(amount_range, range): 44 | raise TypeError("amount_range should be of type 'range' ") 45 | 46 | status, value = Filter.find_key_value(amount_key, dataset) 47 | 48 | if status: 49 | if value in amount_range: 50 | return True 51 | else: 52 | raise AttributeError("'amount_key' key not found in dataset") 53 | 54 | return False 55 | -------------------------------------------------------------------------------- /python_paystack/objects/plans.py: -------------------------------------------------------------------------------- 1 | ''' 2 | plans.py 3 | ''' 4 | from forex_python.converter import CurrencyCodes 5 | from .base import Base 6 | 7 | class Plan(Base): 8 | ''' 9 | Plan class for making payment plans 10 | ''' 11 | 12 | interval = None 13 | name = None 14 | amount = None 15 | plan_code = None 16 | currency = None 17 | id = None 18 | send_sms = True 19 | send_invoices = True 20 | description = None 21 | __interval_values = ('hourly', 'daily', 'weekly', 'monthly', 'annually') 22 | 23 | def __init__(self, name, interval, amount, currency='NGN', plan_code=None, 24 | id=None, send_sms=None, send_invoices=None, description=None): 25 | super().__init__() 26 | #Check if currency supplied is valid 27 | if not CurrencyCodes().get_symbol(currency.upper()): 28 | raise ValueError("Invalid currency supplied") 29 | 30 | if interval.lower() not in self.__interval_values: 31 | raise ValueError("Interval should be one of 'hourly'," 32 | "'daily', 'weekly', 'monthly','annually'" 33 | ) 34 | 35 | try: 36 | amount = int(amount) 37 | except ValueError: 38 | raise ValueError("Invalid amount") 39 | else: 40 | self.interval = interval.lower() 41 | self.name = name 42 | self.interval = interval 43 | self.amount = amount 44 | self.currency = currency 45 | self.plan_code = plan_code 46 | self.id = id 47 | self.send_sms = send_sms 48 | self.send_invoices = send_invoices 49 | self.description = description 50 | 51 | def __str__(self): 52 | return "%s plan" % self.name 53 | -------------------------------------------------------------------------------- /python_paystack/objects/subaccounts.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | ''' 4 | from .base import Base 5 | 6 | class SubAccount(Base): 7 | ''' 8 | 9 | ''' 10 | business_name = None 11 | settlement_bank = None 12 | account_number = None 13 | percentage_charge = None 14 | 15 | primary_contact_email = None 16 | primary_contact_name = None 17 | primary_contact_phone = None 18 | settlement_schedule = None 19 | 20 | def __init__(self, business_name, settlement_bank, account_number, percentage_charge): 21 | super().__init__() 22 | self.business_name = business_name 23 | self.settlement_bank = settlement_bank 24 | self.account_number = account_number 25 | self.percentage_charge = percentage_charge 26 | 27 | def __str__(self): 28 | return "Sub Account for %s - %s" % (self.business_name, self.account_number) -------------------------------------------------------------------------------- /python_paystack/objects/transactions.py: -------------------------------------------------------------------------------- 1 | ''' 2 | transactions.py 3 | ''' 4 | import math, uuid 5 | from datetime import datetime 6 | import validators 7 | from .base import Base 8 | from .errors import InvalidEmailError 9 | 10 | class Transaction(Base): 11 | ''' 12 | Transactions class 13 | ''' 14 | reference = None 15 | amount = None 16 | email = None 17 | plan = None 18 | transaction_charge = None 19 | metadata = None 20 | card_locale = 'LOCAL' 21 | authorization_url = None 22 | authorization_code = None 23 | 24 | def __init__(self, amount: int, email): 25 | super().__init__() 26 | try: 27 | amount = int(amount) 28 | except ValueError: 29 | raise ValueError("Invalid amount. Amount(in kobo) should be an integer") 30 | 31 | else: 32 | if validators.email(email): 33 | self.amount = amount 34 | self.email = email 35 | else: 36 | raise InvalidEmailError 37 | 38 | def generate_reference_code(self): 39 | ''' 40 | Generates a unique transaction reference code 41 | ''' 42 | return uuid.uuid4() 43 | 44 | def full_transaction_cost(self, locale, local_cost, intl_cost): 45 | ''' 46 | Adds on paystack transaction charges and returns updated cost 47 | 48 | Arguments: 49 | locale : Card location (LOCAL or INTERNATIONAL) 50 | ''' 51 | if self.amount: 52 | 53 | if locale not in ('LOCAL', 'INTERNATIONAL'): 54 | raise ValueError("Invalid locale, locale should be 'LOCAL' or 'INTERNATIONAL'") 55 | 56 | else: 57 | locale_cost = {'LOCAL' : local_cost, 'INTERNATIONAL' : intl_cost} 58 | 59 | cost = self.amount / (1 - locale_cost[locale]) 60 | 61 | if cost > 250000: 62 | cost = (self.amount + 100)/ (1 - locale_cost[locale]) 63 | 64 | paystack_charge = locale_cost[locale] * cost 65 | #Paystack_charge is capped at N2000 66 | if paystack_charge > 200000: 67 | cost = self.amount + 200000 68 | 69 | return math.ceil(cost) 70 | 71 | else: 72 | raise AttributeError("Amount not set") 73 | -------------------------------------------------------------------------------- /python_paystack/objects/transfers.py: -------------------------------------------------------------------------------- 1 | ''' 2 | transfers.py 3 | ''' 4 | from forex_python.converter import CurrencyCodes 5 | from .base import Base 6 | 7 | class Transfer(Base): 8 | ''' 9 | Transfer class 10 | ''' 11 | source = None 12 | amount = None 13 | currency = None 14 | reason = None 15 | recipient = None 16 | status = None 17 | id = None 18 | transfer_code = None 19 | otp = None 20 | 21 | def __init__(self, amount, recipient, source = 'balance', reason='', currency='NGN'): 22 | super().__init__() 23 | try: 24 | amount = int(amount) 25 | except ValueError: 26 | raise ValueError("Invalid amount. Amount(in kobo) should be an integer") 27 | 28 | if not CurrencyCodes().get_symbol(currency.upper()): 29 | raise ValueError("Invalid currency supplied") 30 | 31 | self.source = source 32 | self.amount = amount 33 | self.recipient = recipient 34 | self.reason = reason 35 | self.currency = currency 36 | 37 | def __str__(self): 38 | value = "Transfer of %s %s from %s to %s %s" % (self.amount, self.currency, 39 | self.source, self.recipient, self.reason) 40 | 41 | return value 42 | -------------------------------------------------------------------------------- /python_paystack/paystack_config.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Paystack settings file. 3 | Contains PaystackConfig class 4 | ''' 5 | 6 | import os 7 | 8 | class PaystackConfig(): 9 | ''' 10 | PaystackConfig class. 11 | ''' 12 | PAYSTACK_URL = "https://api.paystack.co" 13 | 14 | SECRET_KEY = os.environ['PAYSTACK_SECRET_KEY'] 15 | 16 | PUBLIC_KEY = os.environ['PAYSTACK_PUBLIC_KEY'] 17 | 18 | PASS_ON_TRANSACTION_COST = True 19 | 20 | LOCAL_COST = 0.015 21 | INTL_COST = 0.039 22 | 23 | def __new__(cls): 24 | raise TypeError("Can not make instance of class") 25 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='python_paystack', 4 | version='1.2.0', 5 | description='A Paystack API wrapper', 6 | url='', 7 | author='Nwalor Chibuzor', 8 | author_email='nwalorc@gmail.com', 9 | license='MIT', 10 | packages=['python_paystack', 'python_paystack.objects'], 11 | install_requires=[ 12 | 'requests', 13 | 'validators', 14 | 'jsonpickle', 15 | 'forex_python' 16 | ] 17 | ) 18 | --------------------------------------------------------------------------------