├── setup.cfg ├── tests ├── __init__.py └── test_thepeer.py ├── thepeer ├── utils │ ├── __init__.py │ ├── exceptions │ │ ├── __init__.py │ │ ├── types │ │ │ ├── __init__.py │ │ │ ├── forbidden.py │ │ │ ├── notfound.py │ │ │ ├── unauthorized.py │ │ │ ├── notacceptable.py │ │ │ ├── servererror.py │ │ │ ├── base.py │ │ │ ├── serviceunavailable.py │ │ │ └── unprocessableentity.py │ │ └── handleErrors.py │ └── constants.py ├── __init__.py └── main.py ├── requirements.txt ├── run_tests.sh ├── MANIFEST.in ├── pyproject.toml ├── .pre-commit-config.yaml ├── .github └── workflows │ └── pytests.yml ├── setup.py ├── .gitignore └── README.md /setup.cfg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /thepeer/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /thepeer/utils/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /thepeer/utils/exceptions/types/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httpx 2 | setuptools>=42 3 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | python -m unittest discover -s './tests' -p 'test_*.py' -------------------------------------------------------------------------------- /thepeer/utils/constants.py: -------------------------------------------------------------------------------- 1 | BASE_URL = "https://api.thepeer.co" 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include reader/*.txt 3 | exclude tests/* -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "httpx"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /thepeer/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import Thepeer 2 | from .utils import exceptions 3 | 4 | __all__ = ( 5 | "Thepeer", 6 | "exceptions" 7 | ) -------------------------------------------------------------------------------- /thepeer/utils/exceptions/types/forbidden.py: -------------------------------------------------------------------------------- 1 | from .base import BaseException as error 2 | 3 | 4 | class ForbiddenException(error): 5 | """_summary_: ForbiddenException 6 | _description_: The forbidden exception for thepeer sdk 7 | _usage_: 8 | raise ForbiddenException(errors) 9 | _example_: 10 | raise ForbiddenException({message: "you shall not pass"}) 11 | """ 12 | 13 | def __init__(self, errors): 14 | super().__init__(errors) 15 | -------------------------------------------------------------------------------- /thepeer/utils/exceptions/types/notfound.py: -------------------------------------------------------------------------------- 1 | from .base import BaseException as error 2 | 3 | 4 | class NotFoundException(error): 5 | """_summary_: NotFoundException 6 | _description_: The not found exception for thepeer sdk 7 | _usage_: 8 | raise NotFoundException(errors) 9 | _example_: 10 | raise NotFoundException({message: "you are definitely lost"}) 11 | """ 12 | 13 | def __init__(self, errors): 14 | super().__init__(errors) 15 | -------------------------------------------------------------------------------- /thepeer/utils/exceptions/types/unauthorized.py: -------------------------------------------------------------------------------- 1 | from .base import BaseException as error 2 | 3 | 4 | class UnauthorizedException(error): 5 | """_summary_: UnauthorizedException 6 | _description_: The unauthorized exception for thepeer sdk 7 | _usage_: 8 | raise UnauthorizedException(errors) 9 | _example_: 10 | raise UnauthorizedException({message: "who are you?"}) 11 | """ 12 | 13 | def __init__(self, errors): 14 | super().__init__(errors) 15 | -------------------------------------------------------------------------------- /thepeer/utils/exceptions/types/notacceptable.py: -------------------------------------------------------------------------------- 1 | from .base import BaseException as error 2 | 3 | 4 | class NotAcceptableException(error): 5 | """_summary_: NotAcceptableException 6 | _description_: The not acceptable exception for thepeer sdk 7 | _usage_: 8 | raise NotAcceptableException(errors) 9 | _example_: 10 | raise NotAcceptableException({message: "you need to work to get some money"}) 11 | """ 12 | 13 | def __init__(self, errors): 14 | super().__init__(errors) 15 | -------------------------------------------------------------------------------- /thepeer/utils/exceptions/types/servererror.py: -------------------------------------------------------------------------------- 1 | from .base import BaseException as error 2 | 3 | 4 | class ServerErrorException(error): 5 | """_summary_: ServerErrorException 6 | _description_: The server error exception for thepeer sdk 7 | _usage_: 8 | raise ServerErrorException(errors) 9 | _example_: 10 | raise ServerErrorException({message: "We played a little bit too much and broke something"}) 11 | """ 12 | 13 | def __init__(self, errors): 14 | super().__init__(errors) 15 | -------------------------------------------------------------------------------- /thepeer/utils/exceptions/types/base.py: -------------------------------------------------------------------------------- 1 | # the base error for thepeer sdk inheriting from the Exception class 2 | class BaseException(Exception): 3 | """_summary_: BaseException 4 | _description_: The base exception for thepeer sdk 5 | _usage_: 6 | raise BaseException(errors) 7 | _example_: 8 | raise BaseException({message: "an error occurred"}) 9 | """ 10 | 11 | def __init__(self, errors={}): 12 | self.errors = errors 13 | 14 | def __str__(self): 15 | return self.errors 16 | -------------------------------------------------------------------------------- /thepeer/utils/exceptions/types/serviceunavailable.py: -------------------------------------------------------------------------------- 1 | from .base import BaseException as error 2 | 3 | 4 | class ServiceUnavailableException(error): 5 | """_summary_: ServiceUnavailableException 6 | _description_: The service unavailable exception for thepeer sdk 7 | _usage_: 8 | raise ServiceUnavailableException(errors) 9 | _example_: 10 | raise ServiceUnavailableException({message: "apparently our servers are taking a little vacation of reality"}) 11 | """ 12 | 13 | def __init__(self, errors): 14 | super().__init__(errors) 15 | -------------------------------------------------------------------------------- /thepeer/utils/exceptions/types/unprocessableentity.py: -------------------------------------------------------------------------------- 1 | from .base import BaseException as error 2 | 3 | 4 | class UnprocessableEntityException(error): 5 | """_summary_: UnprocessableEntityException 6 | _description_: The unprocessable entity exception for thepeer sdk 7 | _usage_: 8 | raise UnprocessableEntityException(errors) 9 | _example_: 10 | raise UnprocessableEntityException({message: "The given data was invalid.", 11 | "errors": { 12 | "name": [ 13 | "The name field is required." 14 | ], 15 | "email": [ 16 | "The email field is required." 17 | ], 18 | "identifier": [ 19 | "The identifier field is required." 20 | ] 21 | }}) 22 | """ 23 | 24 | def __init__(self, errors): 25 | super().__init__(errors) 26 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: [commit, push] 2 | default_language_version: 3 | python: python3.9 4 | repos: 5 | - repo: https://github.com/psf/black 6 | rev: 22.3.0 7 | hooks: 8 | - id: black 9 | args: [ 10 | --line-length=100, 11 | --target-version=py39, 12 | ] 13 | exclude: ^(venv/|docs/) 14 | types: ['python'] 15 | 16 | - repo: https://github.com/pre-commit/pre-commit-hooks 17 | rev: v4.2.0 18 | hooks: 19 | - id: trailing-whitespace 20 | - id: check-docstring-first 21 | - id: check-yaml 22 | - id: debug-statements 23 | # - id: name-tests-test 24 | - id: requirements-txt-fixer 25 | - repo: https://github.com/PyCQA/flake8 26 | rev: 4.0.1 27 | hooks: 28 | - id: flake8 29 | args: [ 30 | --max-line-length=100 31 | ] 32 | exclude: ^(venv/|docs/) 33 | types: ['python'] 34 | - repo: https://github.com/pre-commit/mirrors-mypy 35 | rev: v0.950 36 | hooks: 37 | - id: mypy 38 | additional_dependencies: [types-all] 39 | -------------------------------------------------------------------------------- /.github/workflows/pytests.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | branches: 9 | - "main" 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | python-version: ["3.9", "3.10", "3.11"] 18 | 19 | steps: 20 | # Checkout the latest code from the repo 21 | - name: Checkout repo 22 | uses: actions/checkout@v2 23 | # Setup which version of Python to use 24 | - name: Set Up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | # Display the Python version being used 29 | - name: Display Python version 30 | run: python -c "import sys; print(sys.version)" 31 | # Install the package using the setup.py 32 | - name: Install package 33 | run: pip install -e . 34 | # Install coverage (you can use some other testing utility) 35 | - name: Install coverage 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install coverage 39 | # Run the tests. I'm using unittest and coverage and the file is in the tests directory. 40 | - name: Run tests 41 | run: coverage run -m unittest 42 | -------------------------------------------------------------------------------- /thepeer/utils/exceptions/handleErrors.py: -------------------------------------------------------------------------------- 1 | from .types.forbidden import ForbiddenException # type: ignore 2 | from .types.notacceptable import NotAcceptableException # type: ignore 3 | from .types.notfound import NotFoundException # type: ignore 4 | from .types.servererror import ServerErrorException # type: ignore 5 | from .types.serviceunavailable import ServiceUnavailableException # type: ignore 6 | from .types.unauthorized import UnauthorizedException # type: ignore 7 | from .types.unprocessableentity import UnprocessableEntityException # type: ignore 8 | 9 | 10 | class SwitchErrorStates: 11 | """_summary_ 12 | A class for switching between various error status codes returned from the peer servers 13 | """ 14 | 15 | def __init__(self, error): 16 | self.errordata = error.response.data.error 17 | 18 | def switch(self): 19 | status = self.error.response.status 20 | error_message = self.error.response.data.message 21 | if status == 401: 22 | raise UnauthorizedException({"message": error_message}) 23 | elif status == 403: 24 | raise ForbiddenException({"message": error_message}) 25 | elif status == 404: 26 | raise NotFoundException({"message": error_message}) 27 | elif status == 406: 28 | raise NotAcceptableException({"message": error_message}) 29 | elif status == 422: 30 | raise UnprocessableEntityException({"message": error_message}) 31 | elif status == 500: 32 | raise ServerErrorException({"message": error_message}) 33 | elif status == 503: 34 | raise ServiceUnavailableException({"message": error_message}) 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import find_packages # noqa: F401 3 | from distutils.core import setup 4 | 5 | 6 | # The directory containing this file 7 | HERE = pathlib.Path(__file__).parent 8 | 9 | # The text of the README file 10 | README = (HERE / "README.md").read_text() 11 | 12 | 13 | setup( 14 | name="pythepeer", 15 | version="0.0.6", 16 | description="official python sdk for interacting with thepeer payment processing \ 17 | infrastructure", 18 | author="Osagie Iyayi", 19 | packages=find_packages(), 20 | author_email="iyayiemmanuel1@gmail.com", 21 | url="https://github.com/thepeerstack/python-sdk", 22 | license="MIT", 23 | install_requires=["httpx"], 24 | include_package_data=True, 25 | classifiers=[ 26 | "Development Status :: 5 - Production/Stable", 27 | # Chose either "3 - Alpha", "4 - Beta" or "5 - Production/Stable" 28 | # as the current state of your package 29 | "Intended Audience :: Developers", # Define that your audience are developers 30 | "Topic :: Software Development :: Build Tools", 31 | "License :: OSI Approved :: MIT License", # Again, pick a license 32 | "Programming Language :: Python :: 3", 33 | # Specify which python versions that you want to support 34 | "Programming Language :: Python :: 3.6", 35 | "Programming Language :: Python :: 3.7", 36 | "Programming Language :: Python :: 3.8", 37 | "Programming Language :: Python :: 3.9", 38 | "Programming Language :: Python :: 3.10", 39 | "Programming Language :: Python :: 3.11", 40 | "Topic :: Software Development :: Libraries", 41 | ], 42 | long_description=README, 43 | long_description_content_type="text/markdown", 44 | keywords=["python", "fintech", "peer-to-peer"], 45 | zip_safe=False, 46 | ) 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | *.sh 132 | !run_tests.sh 133 | notes.md 134 | guide.md 135 | .DS_Store 136 | junk* 137 | .pypirc -------------------------------------------------------------------------------- /tests/test_thepeer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from thepeer import Thepeer # type: ignore 3 | 4 | thepeer_test_suites = Thepeer("pssk_test_vwww1yvvpymamtut26x5tvpx1znrcmeis2k0kvcmwzjax") 5 | 6 | 7 | class ThePeerInitMethods(unittest.TestCase): 8 | def test_validate_signature(self): 9 | self.assertEqual.__self__.maxDiff = None 10 | self.assertEqual( 11 | thepeer_test_suites.validate_signature( 12 | "I love the peer", "27154e4751dc49d1b40281b18deecd4fd2392e43" 13 | ), 14 | True, 15 | ) 16 | 17 | def test_index_user(self): 18 | self.assertEqual.__self__.maxDiff = None 19 | self.assertEqual( 20 | thepeer_test_suites.index_user( 21 | "Osagie Iyayi", "iyayiemmanuel1@gmail.com", "iyayiemmanuel1@gmail.com" 22 | ), 23 | {"message": "identifier exists"}, 24 | ) 25 | 26 | def test_view_user(self): 27 | self.assertEqual.__self__.maxDiff = None 28 | self.assertEqual( 29 | thepeer_test_suites.view_user("d2cb0c2c-7bd4-40a0-9744-824fbce176b7"), 30 | { 31 | "indexed_user": { 32 | "reference": "d2cb0c2c-7bd4-40a0-9744-824fbce176b7", 33 | "name": "Osagie Iyayi", 34 | "identifier": "iyayiemmanuel1@gmail.com", 35 | "identifier_type": "email", 36 | "email": "iyayiemmanuel1@gmail.com", 37 | "created_at": "2022-05-04T07:37:44.000000Z", 38 | "updated_at": "2022-05-04T07:37:44.000000Z", 39 | } 40 | }, 41 | ) 42 | 43 | def test_all_users(self): 44 | self.assertEqual.__self__.maxDiff = None 45 | self.assertEqual( 46 | thepeer_test_suites.all_users(), 47 | { 48 | "indexed_users": { 49 | "data": [ 50 | { 51 | "reference": "5c4961bf-56ca-4a28-80af-214543cdf382", 52 | "name": "Adesubomi Jaiyeola", 53 | "identifier": "08095743249", 54 | "identifier_type": "email", 55 | "email": "subomi.ja@gmail.com", 56 | "created_at": "2023-04-04T14:49:30.000000Z", 57 | "updated_at": "2023-04-04T14:49:30.000000Z", 58 | }, 59 | { 60 | "reference": "d2cb0c2c-7bd4-40a0-9744-824fbce176b7", 61 | "name": "Osagie Iyayi", 62 | "identifier": "iyayiemmanuel1@gmail.com", 63 | "identifier_type": "email", 64 | "email": "iyayiemmanuel1@gmail.com", 65 | "created_at": "2022-05-04T07:37:44.000000Z", 66 | "updated_at": "2022-05-04T07:37:44.000000Z", 67 | }, 68 | { 69 | "reference": "e9d09152-fb47-4bb7-a162-7a326cd35c32", 70 | "name": "Jenni Madu", 71 | "identifier": "jennifermadu903@gmail.com", 72 | "identifier_type": "email", 73 | "email": "jennifermadu903@gmail.com", 74 | "created_at": "2023-04-13T14:04:32.000000Z", 75 | "updated_at": "2023-04-13T14:04:32.000000Z", 76 | }, 77 | ] 78 | }, 79 | "meta": {"page": 1, "total": 3, "pageCount": 1, "perPage": 15}, 80 | }, 81 | ) 82 | 83 | def test_user_links(self): 84 | self.assertEqual.__self__.maxDiff = None 85 | self.assertEqual( 86 | thepeer_test_suites.get_user_links("d2cb0c2c-7bd4-40a0-9744-824fbce176b7"), 87 | {"links": None}, 88 | ) 89 | 90 | def test_single_link(self): 91 | self.assertEqual.__self__.maxDiff = None 92 | self.assertEqual( 93 | thepeer_test_suites.get_single_link("3bbb0fbf-82fa-48a0-80eb-d2c0338fe7dd"), 94 | {"message": "link not found"}, 95 | ) 96 | 97 | def test_charge_link(self): 98 | self.assertEqual.__self__.maxDiff = None 99 | self.assertEqual( 100 | thepeer_test_suites.charge_link( 101 | "3bbb0fbf-82fa-48a0-80eb-d2c0338fe7dd", 2000, "try thepeer" 102 | ), 103 | { 104 | "errors": { 105 | "amount": ["The amount field must be numeric value between 10000 and 100000000"] 106 | }, 107 | "message": "The given data was invalid", 108 | }, 109 | ) 110 | 111 | def test_charge_link_2(self): 112 | self.assertEqual.__self__.maxDiff = None 113 | self.assertEqual( 114 | thepeer_test_suites.charge_link( 115 | "3bbb0fbf-82fa-48a0-80eb-d2c0338fe7dd", 10000, "try thepeer" 116 | ), 117 | {"message": "link not found"}, 118 | ) 119 | 120 | def test_authorize_charge(self): 121 | self.assertEqual.__self__.maxDiff = None 122 | self.assertEqual( 123 | thepeer_test_suites.authorize_charge("3bbb0fbf-82fa-48a0-80eb-d2c0338fe7dd", "success"), 124 | {"message": "resource not found"}, 125 | ) 126 | 127 | def test_transaction_detail(self): 128 | self.assertEqual.__self__.maxDiff = None 129 | self.assertEqual( 130 | thepeer_test_suites.get_transaction_detail("d761e113-1e6d-456d-8341-79a963234511"), 131 | { 132 | "transaction": { 133 | "id": "d761e113-1e6d-456d-8341-79a963234511", 134 | "amount": 1000000, 135 | "channel": "checkout", 136 | "refund": False, 137 | "checkout": { 138 | "id": "402bc104-b17e-4046-bef5-4e3df34aac5c", 139 | "amount": 1000000, 140 | "email": "iyayiemmanuel1@gmail.com", 141 | "currency": "NGN", 142 | "status": "paid", 143 | "linked_account": { 144 | "user": { 145 | "name": "Trojan Okoh", 146 | "identifier": "trojan", 147 | "identifier_type": "username", 148 | }, 149 | "business": { 150 | "name": "Cash App", 151 | "logo": "https://palaciodepeer.s3.us-east-2.amazonaws.com/business_logos/UJimBqYOu7KQIM3DwCWOuKjkDbBbVLYRuYRTgxKh.png", # noqa: E501 152 | "logo_colour": "#77cc33", 153 | }, 154 | }, 155 | "meta": {}, 156 | "updated_at": "2023-07-22T23:55:21.000000Z", 157 | "created_at": "2023-07-22T23:48:15.000000Z", 158 | }, 159 | "user": { 160 | "reference": "2992e5b4-acb7-49b4-848d-f5a7ca225413", 161 | "name": "Checkout", 162 | "identifier": "checkout", 163 | "identifier_type": "email", 164 | "email": "iyayiemmanuel1@gmail.com", 165 | "created_at": "2023-07-22T23:55:19.000000Z", 166 | "updated_at": "2023-07-22T23:55:19.000000Z", 167 | }, 168 | "charge": 10000, 169 | "currency": "NGN", 170 | "mode": "credit", 171 | "reference": "c3f0a7b1ec0dbb3242eeaea7f380e96e", 172 | "remark": "checkout", 173 | "status": "success", 174 | "type": "peer", 175 | "meta": None, 176 | "peer": { 177 | "business": { 178 | "name": "Cash App", 179 | "logo": "https://palaciodepeer.s3.us-east-2.amazonaws.com/business_logos/UJimBqYOu7KQIM3DwCWOuKjkDbBbVLYRuYRTgxKh.png", # noqa: E501 180 | "logo_colour": "#77cc33", 181 | }, 182 | "user": { 183 | "name": "Trojan Okoh", 184 | "identifier": "trojan", 185 | "identifier_type": "username", 186 | }, 187 | }, 188 | "updated_at": "2023-07-22T23:55:20.000000Z", 189 | "created_at": "2023-07-22T23:55:20.000000Z", 190 | } 191 | }, 192 | ) 193 | 194 | def test_refund_transaction(self): 195 | self.assertEqual.__self__.maxDiff = None 196 | self.assertEqual( 197 | thepeer_test_suites.refund_transaction( 198 | "d761e113-1e6d-456d-8341-79a963234511", "possibly fraudulent" 199 | ), 200 | {"message": "transactions can only be refunded on live mode"}, 201 | ) 202 | -------------------------------------------------------------------------------- /thepeer/main.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import hashlib 3 | import json # type: ignore 4 | 5 | import httpx # noqa: E402 6 | from .utils.constants import BASE_URL # noqa: E402 7 | from .utils.exceptions.handleErrors import SwitchErrorStates # noqa: E402 8 | from typing import Union 9 | 10 | 11 | class Thepeer: 12 | def __init__(self, secret: str): 13 | # pass a default value for the url 14 | self.url = BASE_URL 15 | self.secret = secret 16 | # set default headers to be used in all requests 17 | self.headers = {"x-api-key": self.secret, "content-Type": "application/json"} 18 | 19 | def get_businesses(self, channel: str): 20 | """This endpoint returns businesses based on the API they integrated. 21 | Args: 22 | channel (string): The specific API to return businesses of. 23 | Supported values are `send`, `checkout`, and `direct_charge`. 24 | """ 25 | try: 26 | response = httpx.get( 27 | f"{self.url}/businesses?channel={channel}", headers=dict(self.headers) 28 | ) 29 | return response.json() 30 | except Exception as e: 31 | raise SwitchErrorStates(e).switch() 32 | 33 | def generate_checkout(self, data: dict[str, Union[str, int]]): 34 | """ 35 | This is a checkout endpoint that you can use to generate a link for your customer to make a 36 | one-time payment with. 37 | Args: 38 | data (dict): The request payload which contains the `currency` (NGN), 39 | `amount`, `email`, and optionally `redirect_url` and `meta` fields. 40 | """ 41 | try: 42 | parsed_data = json.dumps(data) 43 | response = httpx.post( 44 | f"{self.url}/checkout", data=parsed_data, headers=dict(self.headers) 45 | ) 46 | return response.json() 47 | except Exception as e: 48 | raise SwitchErrorStates(e).switch() 49 | 50 | def validate_signature(self, data, signature: str): 51 | """helper method to validate the signature of the data received 52 | from thepeer's servers, it's usually used for validating webhook events 53 | coming from thepeer's servers 54 | Args: 55 | data (dict|Any): the payload to which the signature is applied 56 | signature (string): the signature to be validated 57 | 58 | Returns: 59 | _type_: boolean if the signature is valid or not 60 | """ 61 | SHA1 = hashlib.sha1 62 | return signature == hmac.new(self.secret.encode(), data.encode(), SHA1).hexdigest() 63 | 64 | def index_user(self, name: str, identifier: str, email: str): 65 | 66 | """this method helps identify the user on thepeer's servers in order to facilitate 67 | more usage of the SDK 68 | it is usually the first method called by the user 69 | 70 | Args: 71 | name (string): the name of the registered user 72 | identifier (string): the universal identifier of the registered user 73 | email (string): the email of the registered user 74 | 75 | Returns: 76 | dict: the json response from the server containing the user's id and 77 | other related information 78 | """ 79 | try: 80 | 81 | data = {"name": name, "identifier": identifier, "email": email} 82 | # convert the data to json 83 | parsed_data = json.dumps(data) 84 | response = httpx.post(f"{self.url}/users", data=parsed_data, headers=dict(self.headers)) 85 | return response.json() 86 | 87 | except Exception as e: 88 | raise SwitchErrorStates(e).switch() 89 | 90 | def view_user(self, reference: str): 91 | """this method helps view the user's information on thepeer's servers 92 | it is usually called after the user has indexed himself 93 | 94 | Args: 95 | reference (string): the reference of the indexed user 96 | 97 | Returns: 98 | dict: the json response from the server containing the user's id and 99 | other related information 100 | """ 101 | try: 102 | response = httpx.get(f"{self.url}/users/{reference}", headers=dict(self.headers)) 103 | return response.json() 104 | except Exception as e: 105 | raise SwitchErrorStates(e).switch() 106 | 107 | def all_users(self, page=1, per_page=15): 108 | """this method gets all the indexed users of a business 109 | 110 | Args: 111 | page (int): a specific page of pagination instances 112 | per_page (int): number of users to display in a single paginated instance page 113 | 114 | Returns: 115 | dict: a dict containing paginated lists of dicts of indexed users 116 | """ 117 | 118 | try: 119 | response = httpx.get( 120 | f"{self.url}/users?page={page}&perPage={per_page}", headers=dict(self.headers) 121 | ) 122 | return response.json() 123 | except Exception as e: 124 | raise SwitchErrorStates(e).switch() 125 | 126 | def update_user(self, reference: str, **data): 127 | """this method helps update the user's information on thepeer's servers 128 | it is usually called after the user has indexed himself 129 | 130 | Args: 131 | reference (string): the reference of the indexed user 132 | data (dict): the data to be updated which is a dictionary 133 | of at least one of the fields (name, identifier, identifier_type,email) 134 | 135 | Returns: 136 | dict: the json response from the server containing the user's id and 137 | other related information 138 | """ 139 | try: 140 | parsed_data = json.dumps(data) 141 | response = httpx.put( 142 | f"{self.url}/users/{reference}", data=parsed_data, headers=dict(self.headers) 143 | ) 144 | return response.json() 145 | except Exception as e: 146 | raise SwitchErrorStates(e).switch() 147 | 148 | def delete_user(self, reference: str): 149 | """this method helps delete the indexed user info on thepeer's servers 150 | a user can always reindex himself 151 | 152 | Args: 153 | reference (string): the reference of the indexed user 154 | """ 155 | try: 156 | response = httpx.delete(f"{self.url}/users/{reference}", headers=dict(self.headers)) 157 | return response.json() 158 | except Exception as e: 159 | raise SwitchErrorStates(e).switch() 160 | 161 | def get_user_links(self, reference: str): 162 | """This endpoint returns all linked accounts of a user, the user's account details, 163 | as well as the business the account is on. 164 | 165 | 166 | Args: 167 | reference (string): the reference of the indexed user 168 | 169 | Returns: 170 | dict: the json response from the server containing the linked accounts 171 | related to the user's id and other related information 172 | """ 173 | try: 174 | response = httpx.get(f"{self.url}/users/{reference}/links", headers=dict(self.headers)) 175 | return response.json() 176 | except Exception as e: 177 | raise SwitchErrorStates(e).switch() 178 | 179 | def get_single_link(self, link_id: str): 180 | """This endpoint returns a user's linked account's details. 181 | 182 | 183 | Args: 184 | link_id (string): the link ID 185 | 186 | Returns: 187 | dict: the json response from the server containing the payment link id and 188 | other related information 189 | """ 190 | try: 191 | response = httpx.get(f"{self.url}/link/{link_id}", headers=dict(self.headers)) 192 | return response.json() 193 | except Exception as e: 194 | raise SwitchErrorStates(e).switch() 195 | 196 | def charge_link(self, link_id: str, amount: Union[str, int], remark: str, currency="NGN"): 197 | """allows a business to charge a user via their linked account 198 | 199 | Args: 200 | link_id (string):the link ID 201 | data (dict): the request payload which contains the amount and remark and optionally, 202 | a currency (which could be NGN or USD default being NGN) 203 | 204 | Returns: 205 | dict: a transaction object generated from a webhook. more info about a 206 | transaction object here https://docs.thepeer.co/transaction/transaction-object 207 | """ 208 | try: 209 | data = json.dumps({"amount": amount, "remark": remark, "currency": currency}) 210 | response = httpx.post( 211 | f"{self.url}/link/{link_id}/charge", data=data, headers=dict(self.headers) 212 | ) 213 | return response.json() 214 | except Exception as e: 215 | raise SwitchErrorStates(e).switch() 216 | 217 | def authorize_charge(self, charge_reference: str, event: str): 218 | """allows a business to authorize a direct charge request made by a user 219 | 220 | Args: 221 | charge_reference (string): reference generated from the direct charge webhook event 222 | event (string): a string which contains the kind of webhook event 223 | 224 | Returns: 225 | dict: containing a key value pair of the event(key) and the type of event 226 | """ 227 | try: 228 | data = json.dumps({"event": event}) 229 | response = httpx.post( 230 | f"{self.url}/authorization/{charge_reference}", 231 | data=data, 232 | headers=dict(self.headers), 233 | ) 234 | return response.json() 235 | 236 | except Exception as e: 237 | raise SwitchErrorStates(e).switch() 238 | 239 | def get_transaction_detail(self, transaction_id: str): 240 | """Get the details about a transaction 241 | Args: 242 | transaction_id (string): the unique identifier of the transaction 243 | 244 | Returns: 245 | dict:containing the transaction object. more about a transaction object here 246 | https://docs.thepeer.co/transaction/transaction-object#anatomy-of-a-transaction-object 247 | """ 248 | try: 249 | response = httpx.get( 250 | f"{self.url}/transactions/{transaction_id}", headers=dict(self.headers) 251 | ) 252 | return response.json() 253 | except Exception as e: 254 | raise SwitchErrorStates(e).switch() 255 | 256 | def refund_transaction(self, transaction_id: str, reason: str): 257 | 258 | """This method allows a business to refund a transaction back to the user 259 | who made the transaction for obvious reasons. 260 | 261 | Args: 262 | transaction_id (string): the unique identifier of the transaction 263 | reason (string): a string explaining the reason for the refund 264 | 265 | Returns: 266 | dict: 267 | containing the transaction object. more about a transaction object can be found here 268 | https://docs.thepeer.co/transaction/transaction-object#anatomy-of-a-transaction-object 269 | """ 270 | try: 271 | data = json.dumps({"reason": reason}) 272 | response = httpx.post( 273 | f"{self.url}/transactions/{transaction_id}/refund", 274 | data=data, 275 | headers=dict(self.headers), 276 | ) 277 | return response.json() 278 | except Exception as e: 279 | raise SwitchErrorStates(e).switch() 280 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thepeer python-sdk 2 | 3 | ![example workflow](https://github.com/thepeerstack/python-sdk/actions/workflows/pytests.yml/badge.svg) ![PyPI - Downloads](https://img.shields.io/pypi/dm/pythepeer?style=flat-square) ![PyPI - License](https://img.shields.io/pypi/l/pythepeer) ![PyPI](https://img.shields.io/pypi/v/pythepeer) ![Codecov](https://img.shields.io/codecov/c/gh/E-wave112/py-thepeer?token=gYijsI9TCm) 4 | 5 | > Thepeer's official python sdk for developers to use in their python projects. 6 | 7 | - To start using this sdk, create an account at https://thepeer.co/ if you haven't already. 8 | - You can then retrieve your API keys from your [dashboard](https://dashboard.thepeer.co/) 9 | 10 | ## Installation 11 | To install this sdk, run the command: 12 | ```bash 13 | pip install pythepeer 14 | ``` 15 | 16 | ## Usage 17 | Instantiate ```Thepeer``` class like so: 18 | ```python 19 | from thepeer import Thepeer 20 | 21 | # create an instance of Thepeer class 22 | 23 | thepeer_instance = Thepeer("YOUR_API_KEY_HERE") 24 | 25 | ``` 26 | 27 | ## Available methods exposed by the sdk 28 | 29 | **Note:** 30 | - For more info about the exposed methods, please refer to the general [documentation](https://docs.thepeer.co/) 31 | - Be sure to keep your API Credentials securely in [environment variables](https://www.twilio.com/blog/environment-variables-python) 32 | 33 | ### Indexing a user 34 | This method describes how to index a user on your account (this is usually the first step before using other methods) 35 | 36 | ```python 37 | test = thepeer_instance.index_user("Osagie Iyayi", "iyayiemmanuel1@gmail.com", "iyayiemmanuel1@gmail.com") 38 | ``` 39 | 40 | #### Parameters supported 41 | 42 | | Parameters | Data type | Required | Description | 43 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 44 | | ```name``` | ```string``` | ```true``` | ```The name of user to be indexed```. 45 | | ```identifier``` | ```string``` | ```true``` | ```the identifier of the account(either email or username).``` 46 | | ```email``` | ```string``` | ```true``` | ```the email of the user``` | 47 | 48 | ### Validating a HMAC signature 49 | This method validates incoming an [hmac](https://www.okta.com/identity-101/hmac/) signature with the payload and credentials that was passed with it 50 | 51 | **Pro Tip:** it is used to verify that an incoming webhook event/response is coming from thepeer's servers 52 | 53 | ```python 54 | test = thepeer_instance.validate_signature(data,signature) 55 | ``` 56 | #### Parameters supported 57 | 58 | | Parameters | Data type | Required | Description | 59 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 60 | | ```data``` | ```dictionary``` | ```true``` | ```the payload containing the data to be authenticated``` 61 | | ```signature``` | ```string``` | ```true``` | ```The HMAC signature``` | 62 | 63 | ### Get an indexed user 64 | This method gets the information of an indexed user 65 | 66 | ```python 67 | test = thepeer_instance.view_user("3bbb0fbf-82fa-48a0-80eb-d2c0338fe7dd") 68 | ``` 69 | 70 | #### Parameters supported 71 | 72 | 73 | | Parameters | Data type | Required | Description | 74 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 75 | | ```reference``` | ```string``` | ```true``` | ```the unique reference returned when the user was indexed``` 76 | 77 | ### Get all indexed users 78 | This method returns all indexed users for a specific account 79 | 80 | ```python 81 | test = thepeer_instance.all_users(1,15) 82 | ``` 83 | 84 | #### Parameters supported 85 | 86 | | Parameters | Data type | Required | Description | 87 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 88 | | ```page``` | ```integer``` | ```false``` | ```the first page displaying all the indexed users. defaults to 1``` 89 | | ```per_page``` | ```integer``` | ```false``` | ```The number of users to display per page. defaults to 15 ``` | 90 | 91 | 92 | ### Update an indexed user 93 | This method helps to update the details of an indexed user 94 | 95 | ```python 96 | test = thepeer_instance.update_user(reference,**data) 97 | ``` 98 | #### Parameters supported 99 | 100 | | Parameters | Data type | Required | Description | 101 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 102 | | ```reference``` | ```string``` | ```true``` | ```the unique reference returned when the user was indexed``` 103 | | ```data``` | ```Any``` | ```true``` | ```A keyword argument which contains on or more of the indexed user's email, name or identifier``` | 104 | 105 | ### Sample 106 | ```python 107 | test = thepeer_instance.update_user("3bbb0fbf-82fa-48a0-80eb-d2c0338fe7dd", identifier="dwave101@yahoo.com", 108 | name="Edmond Kirsch", 109 | email="dwave101@gmail.com") 110 | ``` 111 | ### Remove an indexed user 112 | This method helps to remove the details of an indexed user from a specific account 113 | 114 | ```python 115 | test = thepeer_instance.delete_user("3bbb0fbf-82fa-48a0-80eb-d2c0338fe7dd") 116 | ``` 117 | 118 | #### Parameters supported 119 | 120 | | Parameters | Data type | Required | Description | 121 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 122 | | ```reference``` | ```string``` | ```true``` | ```the unique reference returned when the user was indexed``` 123 | 124 | 125 | 126 | 127 | ### Get Businesses 128 | This method returns businesses based on the API they integrated. 129 | 130 | ```python 131 | test = thepeer_instance.get_businesses("checkout") 132 | ``` 133 | 134 | #### Parameters supported 135 | 136 | 137 | | Parameters | Data type | Required | Description | 138 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 139 | | ```channel``` | ```string``` | ```true``` | ```The specific API to return businesses of. supported values are send, checkout, and direct_charge``` 140 | 141 | 142 | ### Generate a Checkout 143 | This method allows you to generate a link for your customer to make a one-time payment with 144 | ```python 145 | test = thepeer_instance.generate_checkout({ 146 | "amount": 1000000, 147 | "currency": "NGN", 148 | "redirect_url": "https://esportfolio.netlify.app", 149 | "email": "jevede6918@muzitp.com", 150 | "meta":{ 151 | "name": "Eddie Kirsch", 152 | "identifier": "eddiekirsch", 153 | } 154 | }) 155 | ``` 156 | 157 | #### Parameters required 158 | 159 | | Parameters | Data type | Required | Description | 160 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 161 | | ```redirect_url``` | ```string``` | ```false``` | ```The url Thepeer should redirect to after the customer completes payment.``` 162 | | ```amount``` | ```integer``` | ```true``` | ```The amount you are debiting the customer. This should be in kobo. The minimum value is 10000``` 163 | | ```email``` | ```string``` | ```true``` | ```The customer’s email address``` | 164 | | ```currency``` | ```string``` | ```true``` | ```The currency the transaction should be carried out in. The supported value is NGN.``` 165 | | ```meta``` | ```dictionary``` | ```false``` | ```An object containing additional attributes you will like to have in your transaction response.``` 166 | 167 | 168 | ### Get user links 169 | 170 | This method returns all linked accounts of a user, the user’s account details, as well as the business the account is on. 171 | ```python 172 | test = thepeer_instance.get_user_links("3bbb0fbf-82fa-48a0-80eb-d2c0338fe7dd") 173 | ``` 174 | #### Parameters required 175 | 176 | | Parameters | Data type | Required | Description | 177 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 178 | | ```reference``` | ```string``` | ```true``` | ```the unique reference returned when the user was indexed``` 179 | 180 | 181 | ### Get single link (linked account) 182 | 183 | This method returns a user's linked account's details. 184 | 185 | ```python 186 | test = thepeer_instance.get_single_link("da14a90c-61c2-4cf7-a837-e3112a2d0c3d") 187 | ``` 188 | 189 | #### Parameters required 190 | 191 | | Parameters | Data type | Required | Description | 192 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 193 | | ```link_id``` | ```string``` | ```true``` | ```The link’s identifier``` 194 | 195 | 196 | ### Charge a link 197 | This method allows a business to charge a user via their linked account 198 | ```python 199 | test = thepeer_instance.charge_link(link_id, amount, remark, currency) 200 | ``` 201 | 202 | #### Parameters required 203 | 204 | | Parameters | Data type | Required | Description | 205 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 206 | | ```link_id``` | ```string``` | ```true``` | ```The link’s identifier``` 207 | | ```amount``` | ```integer``` | ```true``` | ```the amount of the whole transaction``` 208 | | ```remark``` | ```string``` | ```true``` | ```short detail about the transaction``` | 209 | | ```currency``` | ```string``` | ```false``` | ```The denomination medium of paying (either one of NGN and USD). defaults to NGN``` 210 | 211 | ### Authorize charge 212 | This method allows a business to authorize a direct charge request made by a user 213 | 214 | ```python 215 | test = thepeer_instance.authorize_charge(charge_reference, event) 216 | ``` 217 | #### Parameters required 218 | 219 | | Parameters | Data type | Required | Description | 220 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 221 | | ```charge_reference``` | ```string``` | ```true``` | ```the reference associated to a pending charge request``` 222 | | ```event``` | ```string``` | ```true``` | ```the type of webhook event``` | 223 | 224 | **Pro Tip:** the various types of webhook events are available [here](https://docs.thepeer.co/webhooks/overview) 225 | 226 | 227 | ### Get transaction detail 228 | This method gets the details of a transaction 229 | ```python 230 | test = thepeer_instance.get_transaction_detail("eda58ee3-4f2c-4aa4-9da7-10a2b8ced453") 231 | ``` 232 | 233 | #### Parameters required 234 | 235 | | Parameters | Data type | Required | Description | 236 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 237 | | ```transaction_id``` | ```string``` | ```true``` | ```the unique transaction identifier``` 238 | 239 | ### Refund transaction 240 | This method allows a business to refund a transaction back to the user for obvious reasons 241 | ```python 242 | test = thepeer_instance.refund_transaction("28e52edf-16d9-4921-8a54-ef34d7029707", "possible threat actor"): 243 | ``` 244 | 245 | #### Parameters required 246 | | Parameters | Data type | Required | Description | 247 | |----------------------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 248 | | ```transaction_id``` | ```string``` | ```true``` | ```the unique transaction identifier``` 249 | | ```reason``` | ```string``` | ```false``` | ```a short sentence explaining reasons for the refund``` | 250 | 251 | 252 | 253 | 255 | --------------------------------------------------------------------------------