From 5e66ae20ec8b599813897e8c1d590246d0041250 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Sat, 17 Nov 2018 16:22:47 +0300 Subject: [PATCH 01/60] fix encode error --- src/momoapi/client.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 81896d0..e2c4aa5 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -9,6 +9,11 @@ from requests.auth import HTTPBasicAuth from requests._internal_utils import to_native_string +try: + from json.decoder import JSONDecodeError +except ImportError: + JSONDecodeError = ValueError + class Response: @@ -60,7 +65,7 @@ def interpret_response(self, resp): try: rbody = resp.json() - except json.decoder.JSONDecodeError: + except JSONDecodeError: rbody = resp.text resp = Response(rbody, rcode, rheaders) From e2c95cc0b2d8c5d2ebe0e555539fed9498a1acf6 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Sat, 1 Dec 2018 15:21:35 +0300 Subject: [PATCH 02/60] Changes api endpoints --- src/momoapi/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index e2c4aa5..143fc41 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -99,7 +99,7 @@ def getAuthToken(self): "Ocp-Apim-Subscription-Key": "%s" % self.auth_key } - r = requests.post(self.base_url+"/colection/token/", + r = requests.post(self.base_url+"/collection/token/", auth=HTTPBasicAuth(self.user_id, self.api_secret), data=data, headers=headers) return r @@ -115,7 +115,7 @@ def requestToPay(self, mobile, amount, product_id, note="", message="", currency } - url = self.base_url+"/colection/v1_0/requesttopay" + url = self.base_url+"/collection/v1_0/requesttopay" res = self.request("POST", url, headers, data) return {"transaction_ref": ref} @@ -125,7 +125,7 @@ def getBalance(self, environment="sandbox"): "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": self.auth_key } - url = self.base_url+"/colection/v1_0/account/balance" + url = self.base_url+"/collection/v1_0/account/balance" res = self.request("GET", url, headers) return res.json() @@ -136,7 +136,7 @@ def getTransactionStatus(self, transaction_id, environment="sandbox"): "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": self.auth_key } - url = self.base_url+"/colection/v1_0/requesttopay/"+transaction_id + url = self.base_url+"/collection/v1_0/requesttopay/"+transaction_id res = self.request("GET", url, headers) return res.json() From 8954287b3c90fa71b8f8431761ad455e6f12a851 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 17 Jan 2019 14:47:20 +0300 Subject: [PATCH 03/60] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From ad3a697f249b51d42d955ba336ac10148e14f0f9 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 17 Jan 2019 14:48:04 +0300 Subject: [PATCH 04/60] Create pull_request_template.md --- .../ISSUE_TEMPLATE/pull_request_template.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/pull_request_template.md b/.github/ISSUE_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..ee2c216 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/pull_request_template.md @@ -0,0 +1,25 @@ +**IMPORTANT: Please do not create a Pull Request without creating an issue first.** + +*Any change needs to be discussed before proceeding. Failure to do so may result in the rejection of the pull request.* + +Please provide enough information so that others can review your pull request: + + + +Explain the **details** for making this change. What existing problem does the pull request solve? + + + +**Test plan (required)** + +Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. + + + +**Code formatting** + + + +**Closing issues** + +Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such). From 9cbf0452bd7989ec961bf7e709160a6685f32da1 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Wed, 20 Feb 2019 11:47:54 +0300 Subject: [PATCH 05/60] add initial travis conf --- .travis.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3052684 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: python +python: + - "2.5" + - "2.6" + - "2.7" + - "3.3" + - "3.4" + - "3.5" + - "3.5-dev" # 3.5 development branch + - "3.6" + - "3.6-dev" # 3.6 development branch + - "3.7-dev" # 3.7 development branch +# command to install dependencies +install: + - pip install -r requirements.txt +# command to run tests +script: + - pytest +after_success: + # call codecov from project root + - if [[ "$RUN_COVERAGE" == 1 ]]; then cd ../ && codecov; fi + \ No newline at end of file From 5235349cd5bafe78c8468fa71c469ef9d3d83ce0 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Wed, 20 Feb 2019 12:26:42 +0300 Subject: [PATCH 06/60] update travis conf --- .travis.yml | 76 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3052684..855b405 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,22 +1,66 @@ language: python -python: - - "2.5" - - "2.6" - - "2.7" - - "3.3" - - "3.4" - - "3.5" - - "3.5-dev" # 3.5 development branch - - "3.6" - - "3.6-dev" # 3.6 development branch - - "3.7-dev" # 3.7 development branch -# command to install dependencies +cache: + directories: + - "$HOME/.cache/pip" + - "$HOME/.pyenv" +matrix: + include: + - os: linux + dist: trusty + sudo: false + python: '2.6' + - os: linux + dist: trusty + sudo: false + python: '2.7' + - os: linux + dist: trusty + sudo: false + python: '3.3' + - os: linux + dist: trusty + sudo: false + python: '3.4' + - os: linux + dist: trusty + sudo: false + python: '3.5' + - os: linux + dist: trusty + sudo: false + python: '3.6' + - os: linux + dist: xenial + sudo: required + services: + - docker + python: '3.7' + env: BUILD_SDIST=true + - os: linux + sudo: false + python: pypy + - os: linux + sudo: false + python: pypy3 + - os: osx + language: objective-c + env: PYENV_VERSION=2.7.12 + - os: osx + language: objective-c + env: PYENV_VERSION=3.5.5 + - os: osx + language: objective-c + env: PYENV_VERSION=3.6.5 install: - - pip install -r requirements.txt -# command to run tests +- "./.travis/install.sh" script: - - pytest +- "./.travis/run.sh" + +deploy: + provider: pypi + user: mossplix + password: + secure: AjbgM0S84NnaPtkAI9JptI7lqtXU3HtcfNM8BLbSSxz/vUymPdU7vXCqwQM0UQWU4Ns5nC8kx6GOfXoikouOtQr7gLSlM7UQVwojsWGBUtVVi6D73ha2glhaaJ7Mf+9G3YMBYUHIUqtf7uVr9hKz5aADH0XIgRpHV4nNNamMduV4SvY4FZuvgQLaUrfwKtUY4t169vtHiReYWIOXEJHnA2PAsxNz99ZOVEvuxAX/Z4mFGkOuLtAvZnPxg3vinE8inQXcMNpLxwICFJM/sKAnMcICChHOErEIir4onOoodPVvVAvKfgb51wIM2q4YMyD8P3aTOILsWgk68EDJNO4w5T3R14eS9/ccRV7pW96lB/heaW92/vl5A59KJxuu6OEgk5gQaacFPv1IUccvc6U6vgHofGhjrsT1X8GI9ekl/M+gApRtL9ZYtT8lyRTIHBhayOKCITxFjivgUOcYZWiTFQCODSompgGdy4aoyIvDpmW2aztLcpr+modFWG/4GcJZxt14z7414LJvPvpyQj31KVEEo8jWdidJY/FNVXNCz8LS/3ri1Tz3p1Zof0sa4Zp3w6WpFxPeEXewyIS3b/R/Yv8w5n8f4byXtRGShvQe/i6sDy3oJAwiSyyjasYseE6WZpAajgk0dDODKZ0vLHkD1ZyHzkNOmqI7wo0pLdiyrKc= after_success: # call codecov from project root - if [[ "$RUN_COVERAGE" == 1 ]]; then cd ../ && codecov; fi - \ No newline at end of file From 289958088e98ea394d74628d0193bb62fdb5d39c Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Wed, 20 Feb 2019 13:10:59 +0300 Subject: [PATCH 07/60] more travis test conf --- .travis.yml | 11 ++++++++--- setup.cfg | 3 +++ setup.py | 15 ++++++++++----- tests/test_example.py | 5 +++++ tests/test_momoapi.py | 12 ------------ 5 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 tests/test_example.py delete mode 100644 tests/test_momoapi.py diff --git a/.travis.yml b/.travis.yml index 855b405..934d8f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,16 +51,21 @@ matrix: - os: osx language: objective-c env: PYENV_VERSION=3.6.5 -install: -- "./.travis/install.sh" + script: -- "./.travis/run.sh" + - python setup.py test +# command to install dependencies +install: + - python setup.py install deploy: provider: pypi user: mossplix password: secure: AjbgM0S84NnaPtkAI9JptI7lqtXU3HtcfNM8BLbSSxz/vUymPdU7vXCqwQM0UQWU4Ns5nC8kx6GOfXoikouOtQr7gLSlM7UQVwojsWGBUtVVi6D73ha2glhaaJ7Mf+9G3YMBYUHIUqtf7uVr9hKz5aADH0XIgRpHV4nNNamMduV4SvY4FZuvgQLaUrfwKtUY4t169vtHiReYWIOXEJHnA2PAsxNz99ZOVEvuxAX/Z4mFGkOuLtAvZnPxg3vinE8inQXcMNpLxwICFJM/sKAnMcICChHOErEIir4onOoodPVvVAvKfgb51wIM2q4YMyD8P3aTOILsWgk68EDJNO4w5T3R14eS9/ccRV7pW96lB/heaW92/vl5A59KJxuu6OEgk5gQaacFPv1IUccvc6U6vgHofGhjrsT1X8GI9ekl/M+gApRtL9ZYtT8lyRTIHBhayOKCITxFjivgUOcYZWiTFQCODSompgGdy4aoyIvDpmW2aztLcpr+modFWG/4GcJZxt14z7414LJvPvpyQj31KVEEo8jWdidJY/FNVXNCz8LS/3ri1Tz3p1Zof0sa4Zp3w6WpFxPeEXewyIS3b/R/Yv8w5n8f4byXtRGShvQe/i6sDy3oJAwiSyyjasYseE6WZpAajgk0dDODKZ0vLHkD1ZyHzkNOmqI7wo0pLdiyrKc= + on: + branch: master + after_success: # call codecov from project root - if [[ "$RUN_COVERAGE" == 1 ]]; then cd ../ && codecov; fi diff --git a/setup.cfg b/setup.cfg index ae9171f..77d8e2b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -80,6 +80,9 @@ coverage_flags = cover: true nocov: false +[aliases] +test=pytest + environment_variables = - diff --git a/setup.py b/setup.py index e584712..949536b 100644 --- a/setup.py +++ b/setup.py @@ -71,11 +71,16 @@ def read(*names, **kwargs): 'requests' # eg: 'aspectlib==1.1.1', 'six>=1.7', ], - extras_require={ - # eg: - # 'rst': ['docutils>=0.11'], - # ':python_version=="2.6"': ['argparse'], - }, + + setup_requires=["pytest-runner",], + + tests_require = [ + 'pytest', +], + + + + extras_require={'test': ['pytest', 'pytest-watch']}, entry_points={ 'console_scripts': [ 'momoapi = momoapi.cli:main', diff --git a/tests/test_example.py b/tests/test_example.py new file mode 100644 index 0000000..6d8e621 --- /dev/null +++ b/tests/test_example.py @@ -0,0 +1,5 @@ +def func(x): + return x + 1 + +def test_answer(): + assert func(3) == 4 \ No newline at end of file diff --git a/tests/test_momoapi.py b/tests/test_momoapi.py deleted file mode 100644 index e230dfc..0000000 --- a/tests/test_momoapi.py +++ /dev/null @@ -1,12 +0,0 @@ - -from click.testing import CliRunner - -from momoapi_python.cli import main - - -def test_main(): - runner = CliRunner() - result = runner.invoke(main, []) - - assert result.output == '()\n' - assert result.exit_code == 0 From 33e0ace6628a66e16cda95acfb53fb35402c3d37 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Wed, 20 Feb 2019 15:58:53 +0300 Subject: [PATCH 08/60] configure tox as the test runner --- setup.cfg | 5 ++++- setup.py | 7 +++++++ tox.ini | 22 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 77d8e2b..4c42e7b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,7 @@ max-line-length = 140 exclude = */migrations/* [tool:pytest] +pep8maxlinelength = 80 norecursedirs = .git .tox @@ -20,7 +21,8 @@ python_files = *_test.py tests.py addopts = - -ra + -ra -q -v -m slow + --doctest-modules --strict --ignore=docs/conf.py --ignore=setup.py @@ -29,6 +31,7 @@ addopts = --doctest-modules --doctest-glob=\*.rst --tb=short + [isort] force_single_line = True diff --git a/setup.py b/setup.py index 949536b..4a43797 100644 --- a/setup.py +++ b/setup.py @@ -76,6 +76,13 @@ def read(*names, **kwargs): tests_require = [ 'pytest', + 'tox', + 'pytest-cov', + 'pytest-pep8', + 'pytest-cov', + 'pytest-sugar', + 'pytest-instafail', + 'wltrace' ], diff --git a/tox.ini b/tox.ini index 44a1f16..bf8874d 100644 --- a/tox.ini +++ b/tox.ini @@ -83,6 +83,22 @@ skip_install = true commands = coveralls [] +[testenv:lint] +skip_install = true +basepython = python3.6 +commands = + flake8 colander setup.py + black --check --diff colander setup.py + python setup.py sdist --dist-dir {distdir} + twine check {distdir}/* + check-manifest +deps = + flake8 + black + readme_renderer[md] + check-manifest + twine + [testenv:report] @@ -109,6 +125,12 @@ commands = deps = {[testenv]deps} pytest-cov + pytest-pep8 + pytest-sugar + pytest-instafail + wltrace + pytest + coverage [testenv:py27-nocov] basepython = {env:TOXPYTHON:python2.7} From 314e75ecb66f6630af32c5c1da09c95b142749a1 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Wed, 20 Feb 2019 16:15:44 +0300 Subject: [PATCH 09/60] fix travis build --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 4a43797..4ede0d3 100644 --- a/setup.py +++ b/setup.py @@ -81,8 +81,7 @@ def read(*names, **kwargs): 'pytest-pep8', 'pytest-cov', 'pytest-sugar', - 'pytest-instafail', - 'wltrace' + 'pytest-instafail' ], From 0d8f63bab3dc04264463d9a4769824a72b22a58c Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Wed, 20 Feb 2019 16:58:35 +0300 Subject: [PATCH 10/60] fix setup.conf --- setup.cfg | 4 ++-- setup.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4c42e7b..f76cc30 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,9 +21,9 @@ python_files = *_test.py tests.py addopts = - -ra -q -v -m slow - --doctest-modules + -ra --strict + --cov --ignore=docs/conf.py --ignore=setup.py --ignore=ci diff --git a/setup.py b/setup.py index 4ede0d3..eff0a5d 100644 --- a/setup.py +++ b/setup.py @@ -81,7 +81,8 @@ def read(*names, **kwargs): 'pytest-pep8', 'pytest-cov', 'pytest-sugar', - 'pytest-instafail' + 'pytest-instafail', + 'pytest-bdd' ], From 40ec6a33d6661ddc4819c92bd00176a44608ae03 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 25 Feb 2019 14:27:01 +0300 Subject: [PATCH 11/60] setup auto-tagging --- .travis/semver.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .travis/semver.sh diff --git a/.travis/semver.sh b/.travis/semver.sh new file mode 100644 index 0000000..1eb43d8 --- /dev/null +++ b/.travis/semver.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +export SEMVER_LAST_TAG=$(git describe --abbrev=0 --tags 2>/dev/null) +export SEMVER_RELEASE_LEVEL=$(git log --oneline -1 --pretty=%B | cat | tr -d '\n' | cut -d "[" -f2 | cut -d "]" -f1) + +#curl -o /tmp/hub.tgz https://github.com/github/hub/releases/download/v2.2.9/hub-linux-arm64-2.2.9.tgz +#tar -xvzf /tmp/hub.tgz -C /tmp + +if [ -z $SEMVER_LAST_TAG ]; then + >&2 echo "No tags defined" + SEMVER_LAST_TAG="0.0.1" +fi + +if [ -n $SEMVER_RELEASE_LEVEL ]; then + git clone https://github.com/fsaintjacques/semver-tool /tmp/semver &> /dev/null + SEMVER_NEW_TAG=$(/tmp/semver/src/semver bump $SEMVER_RELEASE_LEVEL $SEMVER_LAST_TAG) + git tag $SEMVER_NEW_TAG &> /dev/null + git push origin --tags &> /dev/null + echo $SEMVER_NEW_TAG +else + >&2 echo "No release level defined" +fi + +exit 0 \ No newline at end of file From 99a9642bb3783791e1a4bcf01cc47d1da8e46c06 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 25 Feb 2019 14:45:45 +0300 Subject: [PATCH 12/60] add gh-token to travis --- .travis.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 934d8f3..c828855 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,7 @@ matrix: dist: xenial sudo: required services: - - docker + - docker python: '3.7' env: BUILD_SDIST=true - os: linux @@ -51,13 +51,10 @@ matrix: - os: osx language: objective-c env: PYENV_VERSION=3.6.5 - script: - - python setup.py test -# command to install dependencies +- python setup.py test install: - - python setup.py install - +- python setup.py install deploy: provider: pypi user: mossplix @@ -65,7 +62,8 @@ deploy: secure: AjbgM0S84NnaPtkAI9JptI7lqtXU3HtcfNM8BLbSSxz/vUymPdU7vXCqwQM0UQWU4Ns5nC8kx6GOfXoikouOtQr7gLSlM7UQVwojsWGBUtVVi6D73ha2glhaaJ7Mf+9G3YMBYUHIUqtf7uVr9hKz5aADH0XIgRpHV4nNNamMduV4SvY4FZuvgQLaUrfwKtUY4t169vtHiReYWIOXEJHnA2PAsxNz99ZOVEvuxAX/Z4mFGkOuLtAvZnPxg3vinE8inQXcMNpLxwICFJM/sKAnMcICChHOErEIir4onOoodPVvVAvKfgb51wIM2q4YMyD8P3aTOILsWgk68EDJNO4w5T3R14eS9/ccRV7pW96lB/heaW92/vl5A59KJxuu6OEgk5gQaacFPv1IUccvc6U6vgHofGhjrsT1X8GI9ekl/M+gApRtL9ZYtT8lyRTIHBhayOKCITxFjivgUOcYZWiTFQCODSompgGdy4aoyIvDpmW2aztLcpr+modFWG/4GcJZxt14z7414LJvPvpyQj31KVEEo8jWdidJY/FNVXNCz8LS/3ri1Tz3p1Zof0sa4Zp3w6WpFxPeEXewyIS3b/R/Yv8w5n8f4byXtRGShvQe/i6sDy3oJAwiSyyjasYseE6WZpAajgk0dDODKZ0vLHkD1ZyHzkNOmqI7wo0pLdiyrKc= on: branch: master - after_success: - # call codecov from project root - - if [[ "$RUN_COVERAGE" == 1 ]]; then cd ../ && codecov; fi +- .travis/semver.sh +env: + global: + secure: BfFLRnrlUwkU7FJui6XIrbN/nmxWkuDaGFrXLcqKSzWQmIwq8T/xIU2yje+wpjUx3mfErWXUgbf8EK/A83OwDgwDvZ4vlBojWA/aTKFUHtW7S7Ix1rTQbuxIzhEpBg2elIQzuKcH9hccGSzpcC6TqeuIWu/Soog7bofO6LT8SXqoRRPZnXgm07pKyJ/8wbUlJ34Ktoje5056zRsmEriFVf4/Qv6YciDzXtPbTRDCy+gXSAOFnr5y7d6Z8SQ3T5eki17X80Yn12nK0fymkTwbBi1F51dHkqSVYB/J6uBI5+K3c80gIw/sqhiHpk2WhfCB6ju/xAlHa9cSPKggJSPeX/v2Ujfk/7wDbH7mlBKsSisWKlQ/8oOd51aiXuHKP4yrj85URhxUNHoehYMg2JyQILTICFcSf4/AlbuEWFYtH34TBS+fF56kMxjHAdDCEX/i3VfAfAER3xsFTCTmQgvyZprPkekQOj5Cy1U+V5oPXRs94h0DqYbsMip1E1cbvevmjCzP3B2UZ/00mlp21a2OtNioxXaptgLsZjNS2kahc1yhDu6L3k2QZwxc58KdgL2vP+t7aQIcGO7gYimmn6GvXiDOjojXAVp0FPIHLnjNVHyqbQa6cma14sANKRZfyfuaGg3z763I7sdc5Su1iufZoD/+QRR4zrtyv3itfEvUBPo= From 94669241eac36a0df6d3af5577f7f0424badab5a Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 25 Feb 2019 15:04:46 +0300 Subject: [PATCH 13/60] [patch] test travis semver tagging --- .travis/semver.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis/semver.sh b/.travis/semver.sh index 1eb43d8..d563968 100644 --- a/.travis/semver.sh +++ b/.travis/semver.sh @@ -3,9 +3,6 @@ export SEMVER_LAST_TAG=$(git describe --abbrev=0 --tags 2>/dev/null) export SEMVER_RELEASE_LEVEL=$(git log --oneline -1 --pretty=%B | cat | tr -d '\n' | cut -d "[" -f2 | cut -d "]" -f1) -#curl -o /tmp/hub.tgz https://github.com/github/hub/releases/download/v2.2.9/hub-linux-arm64-2.2.9.tgz -#tar -xvzf /tmp/hub.tgz -C /tmp - if [ -z $SEMVER_LAST_TAG ]; then >&2 echo "No tags defined" SEMVER_LAST_TAG="0.0.1" From 2565bcad1fdb30a52a4290d9a140860fbc2d2214 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 25 Feb 2019 15:09:36 +0300 Subject: [PATCH 14/60] [patch] update perms --- .travis/semver.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .travis/semver.sh diff --git a/.travis/semver.sh b/.travis/semver.sh old mode 100644 new mode 100755 From f4c3af1da063cca79d2f3cc0fc210bb773689b12 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 25 Feb 2019 15:20:44 +0300 Subject: [PATCH 15/60] update origin --- .travis/semver.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 .travis/semver.sh diff --git a/.travis/semver.sh b/.travis/semver.sh old mode 100755 new mode 100644 index d563968..8bdcdd8 --- a/.travis/semver.sh +++ b/.travis/semver.sh @@ -12,7 +12,7 @@ if [ -n $SEMVER_RELEASE_LEVEL ]; then git clone https://github.com/fsaintjacques/semver-tool /tmp/semver &> /dev/null SEMVER_NEW_TAG=$(/tmp/semver/src/semver bump $SEMVER_RELEASE_LEVEL $SEMVER_LAST_TAG) git tag $SEMVER_NEW_TAG &> /dev/null - git push origin --tags &> /dev/null + git push https://${GH_TOKEN}@github.com/sparkplug/momoapi-python.git --tags &> /dev/null echo $SEMVER_NEW_TAG else >&2 echo "No release level defined" From df7f5a99403f236175a62ec4cfbc637d2f710e08 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 25 Feb 2019 15:25:22 +0300 Subject: [PATCH 16/60] [patch] update perms --- .travis/semver.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .travis/semver.sh diff --git a/.travis/semver.sh b/.travis/semver.sh old mode 100644 new mode 100755 From 1a48b6fa083386c9a7072cb83557cf39fd9458d8 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 25 Feb 2019 15:35:02 +0300 Subject: [PATCH 17/60] ensure tagging happens only on master --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c828855..187b9db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,7 +63,10 @@ deploy: on: branch: master after_success: -- .travis/semver.sh + - .travis/semver.sh +on: + branch: master + env: global: secure: BfFLRnrlUwkU7FJui6XIrbN/nmxWkuDaGFrXLcqKSzWQmIwq8T/xIU2yje+wpjUx3mfErWXUgbf8EK/A83OwDgwDvZ4vlBojWA/aTKFUHtW7S7Ix1rTQbuxIzhEpBg2elIQzuKcH9hccGSzpcC6TqeuIWu/Soog7bofO6LT8SXqoRRPZnXgm07pKyJ/8wbUlJ34Ktoje5056zRsmEriFVf4/Qv6YciDzXtPbTRDCy+gXSAOFnr5y7d6Z8SQ3T5eki17X80Yn12nK0fymkTwbBi1F51dHkqSVYB/J6uBI5+K3c80gIw/sqhiHpk2WhfCB6ju/xAlHa9cSPKggJSPeX/v2Ujfk/7wDbH7mlBKsSisWKlQ/8oOd51aiXuHKP4yrj85URhxUNHoehYMg2JyQILTICFcSf4/AlbuEWFYtH34TBS+fF56kMxjHAdDCEX/i3VfAfAER3xsFTCTmQgvyZprPkekQOj5Cy1U+V5oPXRs94h0DqYbsMip1E1cbvevmjCzP3B2UZ/00mlp21a2OtNioxXaptgLsZjNS2kahc1yhDu6L3k2QZwxc58KdgL2vP+t7aQIcGO7gYimmn6GvXiDOjojXAVp0FPIHLnjNVHyqbQa6cma14sANKRZfyfuaGg3z763I7sdc5Su1iufZoD/+QRR4zrtyv3itfEvUBPo= From 7e35a88e49db1727a5436f8ebd1e72a3ce4d08aa Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 25 Feb 2019 15:44:45 +0300 Subject: [PATCH 18/60] fix tagging of development branches --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 187b9db..61aae65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,9 +63,7 @@ deploy: on: branch: master after_success: - - .travis/semver.sh -on: - branch: master + - test $TRAVIS_BRANCH = "master" && .travis/semver.sh env: global: From 146dc6b47b96842d3327e496391effe50a536615 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 25 Feb 2019 17:21:40 +0300 Subject: [PATCH 19/60] only deploy tags --- .travis.yml | 17 +---------------- setup.cfg | 1 - setup.py | 12 +++--------- 3 files changed, 4 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index 61aae65..11a2ac1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,21 +36,6 @@ matrix: - docker python: '3.7' env: BUILD_SDIST=true - - os: linux - sudo: false - python: pypy - - os: linux - sudo: false - python: pypy3 - - os: osx - language: objective-c - env: PYENV_VERSION=2.7.12 - - os: osx - language: objective-c - env: PYENV_VERSION=3.5.5 - - os: osx - language: objective-c - env: PYENV_VERSION=3.6.5 script: - python setup.py test install: @@ -61,7 +46,7 @@ deploy: password: secure: AjbgM0S84NnaPtkAI9JptI7lqtXU3HtcfNM8BLbSSxz/vUymPdU7vXCqwQM0UQWU4Ns5nC8kx6GOfXoikouOtQr7gLSlM7UQVwojsWGBUtVVi6D73ha2glhaaJ7Mf+9G3YMBYUHIUqtf7uVr9hKz5aADH0XIgRpHV4nNNamMduV4SvY4FZuvgQLaUrfwKtUY4t169vtHiReYWIOXEJHnA2PAsxNz99ZOVEvuxAX/Z4mFGkOuLtAvZnPxg3vinE8inQXcMNpLxwICFJM/sKAnMcICChHOErEIir4onOoodPVvVAvKfgb51wIM2q4YMyD8P3aTOILsWgk68EDJNO4w5T3R14eS9/ccRV7pW96lB/heaW92/vl5A59KJxuu6OEgk5gQaacFPv1IUccvc6U6vgHofGhjrsT1X8GI9ekl/M+gApRtL9ZYtT8lyRTIHBhayOKCITxFjivgUOcYZWiTFQCODSompgGdy4aoyIvDpmW2aztLcpr+modFWG/4GcJZxt14z7414LJvPvpyQj31KVEEo8jWdidJY/FNVXNCz8LS/3ri1Tz3p1Zof0sa4Zp3w6WpFxPeEXewyIS3b/R/Yv8w5n8f4byXtRGShvQe/i6sDy3oJAwiSyyjasYseE6WZpAajgk0dDODKZ0vLHkD1ZyHzkNOmqI7wo0pLdiyrKc= on: - branch: master + tags: true after_success: - test $TRAVIS_BRANCH = "master" && .travis/semver.sh diff --git a/setup.cfg b/setup.cfg index f76cc30..e88a1af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,6 @@ python_files = addopts = -ra --strict - --cov --ignore=docs/conf.py --ignore=setup.py --ignore=ci diff --git a/setup.py b/setup.py index eff0a5d..92eb6b7 100644 --- a/setup.py +++ b/setup.py @@ -74,20 +74,14 @@ def read(*names, **kwargs): setup_requires=["pytest-runner",], - tests_require = [ - 'pytest', - 'tox', + + extras_require={'test': ['pytest', 'pytest-watch','tox', 'pytest-cov', 'pytest-pep8', 'pytest-cov', 'pytest-sugar', 'pytest-instafail', - 'pytest-bdd' -], - - - - extras_require={'test': ['pytest', 'pytest-watch']}, + 'pytest-bdd'],"dev":["semver"]}, entry_points={ 'console_scripts': [ 'momoapi = momoapi.cli:main', From 28888d5a85391f8a2ecb091d099429cd91d69798 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 26 Feb 2019 13:50:04 +0300 Subject: [PATCH 20/60] update keys --- .bumpversion.cfg | 20 ----- .cookiecutterrc | 54 ------------ AUTHORS.rst => AUTHORS.md | 0 CHANGELOG.rst => CHANGELOG.md | 0 CONTRIBUTING.rst | 90 -------------------- MANIFEST.in | 14 ++-- README.md | 10 +++ ci/templates/tox.ini | 119 --------------------------- docs/authors.rst | 1 - docs/changelog.rst | 1 - docs/conf.py | 54 ------------ docs/contributing.rst | 1 - docs/index.rst | 22 ----- docs/installation.rst | 7 -- docs/readme.rst | 1 - docs/reference/index.rst | 7 -- docs/reference/momoapi_python.rst | 9 -- docs/requirements.txt | 3 - docs/spelling_wordlist.txt | 11 --- docs/usage.rst | 7 -- setup.cfg | 1 - setup.py | 24 +++--- tests/__init__.py | 0 tests/features/account.feature | 0 tests/features/collections.feature | 0 tests/features/disbursements.feature | 0 tests/features/remittences.feature | 0 tests/features/sandbox.feature | 16 ++++ tests/test_example.py | 5 -- tests/test_sandbox.py | 57 +++++++++++++ tox.ini | 81 +----------------- 31 files changed, 104 insertions(+), 511 deletions(-) delete mode 100644 .bumpversion.cfg delete mode 100644 .cookiecutterrc rename AUTHORS.rst => AUTHORS.md (100%) rename CHANGELOG.rst => CHANGELOG.md (100%) delete mode 100644 CONTRIBUTING.rst delete mode 100644 ci/templates/tox.ini delete mode 100644 docs/authors.rst delete mode 100644 docs/changelog.rst delete mode 100644 docs/conf.py delete mode 100644 docs/contributing.rst delete mode 100644 docs/index.rst delete mode 100644 docs/installation.rst delete mode 100644 docs/readme.rst delete mode 100644 docs/reference/index.rst delete mode 100644 docs/reference/momoapi_python.rst delete mode 100644 docs/requirements.txt delete mode 100644 docs/spelling_wordlist.txt delete mode 100644 docs/usage.rst create mode 100644 tests/__init__.py create mode 100644 tests/features/account.feature create mode 100644 tests/features/collections.feature create mode 100644 tests/features/disbursements.feature create mode 100644 tests/features/remittences.feature create mode 100644 tests/features/sandbox.feature delete mode 100644 tests/test_example.py create mode 100644 tests/test_sandbox.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 7d2459b..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,20 +0,0 @@ -[bumpversion] -current_version = 0.1.0 -commit = True -tag = True - -[bumpversion:file:setup.py] -search = version='{current_version}' -replace = version='{new_version}' - -[bumpversion:file:README.rst] -search = v{current_version}. -replace = v{new_version}. - -[bumpversion:file:docs/conf.py] -search = version = release = '{current_version}' -replace = version = release = '{new_version}' - -[bumpversion:file:src/momoapi_python/__init__.py] -search = __version__ = '{current_version}' -replace = __version__ = '{new_version}' diff --git a/.cookiecutterrc b/.cookiecutterrc deleted file mode 100644 index b04ae28..0000000 --- a/.cookiecutterrc +++ /dev/null @@ -1,54 +0,0 @@ -# This file exists so you can easily regenerate your project. -# -# `cookiepatcher` is a convenient shim around `cookiecutter` -# for regenerating projects (it will generate a .cookiecutterrc -# automatically for any template). To use it: -# -# pip install cookiepatcher -# cookiepatcher gh:ionelmc/cookiecutter-pylibrary project-path -# -# See: -# https://pypi.python.org/pypi/cookiepatcher -# -# Alternatively, you can run: -# -# cookiecutter --overwrite-if-exists --config-file=project-path/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary - -default_context: - - _template: 'gh:ionelmc/cookiecutter-pylibrary' - appveyor: 'no' - c_extension_function: 'longest' - c_extension_module: '_momoapi_python' - c_extension_optional: 'yes' - c_extension_support: 'no' - codacy: 'no' - codeclimate: 'no' - codecov: 'no' - command_line_interface: 'click' - command_line_interface_bin_name: 'momoapi' - coveralls: 'yes' - distribution_name: 'momoapi-python' - email: 'mugisha@sparkpl.ug' - full_name: 'Moses Mugisha' - github_username: 'mossplix' - landscape: 'no' - license: 'MIT license' - linter: 'flake8' - package_name: 'momoapi_python' - project_name: 'momoapi-python' - project_short_description: 'An example package. Generated with cookiecutter-pylibrary.' - release_date: 'today' - repo_name: 'python-momoapi' - requiresio: 'no' - scrutinizer: 'no' - sphinx_docs: 'yes' - sphinx_doctest: 'no' - sphinx_theme: 'sphinx-py3doc-enhanced-theme' - test_matrix_configurator: 'yes' - test_matrix_separate_coverage: 'no' - test_runner: 'pytest' - travis: 'no' - version: '0.1.0' - website: 'http://sparkpl.ug' - year: 'now' diff --git a/AUTHORS.rst b/AUTHORS.md similarity index 100% rename from AUTHORS.rst rename to AUTHORS.md diff --git a/CHANGELOG.rst b/CHANGELOG.md similarity index 100% rename from CHANGELOG.rst rename to CHANGELOG.md diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index 28de1ce..0000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,90 +0,0 @@ -============ -Contributing -============ - -Contributions are welcome, and they are greatly appreciated! Every -little bit helps, and credit will always be given. - -Bug reports -=========== - -When `reporting a bug `_ please include: - - * Your operating system name and version. - * Any details about your local setup that might be helpful in troubleshooting. - * Detailed steps to reproduce the bug. - -Documentation improvements -========================== - -momoapi-python could always use more documentation, whether as part of the -official momoapi-python docs, in docstrings, or even on the web in blog posts, -articles, and such. - -Feature requests and feedback -============================= - -The best way to send feedback is to file an issue at https://github.com/mossplix/python-momoapi/issues. - -If you are proposing a feature: - -* Explain in detail how it would work. -* Keep the scope as narrow as possible, to make it easier to implement. -* Remember that this is a volunteer-driven project, and that code contributions are welcome :) - -Development -=========== - -To set up `python-momoapi` for local development: - -1. Fork `python-momoapi `_ - (look for the "Fork" button). -2. Clone your fork locally:: - - git clone git@github.com:your_name_here/python-momoapi.git - -3. Create a branch for local development:: - - git checkout -b name-of-your-bugfix-or-feature - - Now you can make your changes locally. - -4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: - - tox - -5. Commit your changes and push your branch to GitHub:: - - git add . - git commit -m "Your detailed description of your changes." - git push origin name-of-your-bugfix-or-feature - -6. Submit a pull request through the GitHub website. - -Pull Request Guidelines ------------------------ - -If you need some code review or feedback while you're developing the code just make the pull request. - -For merging, you should: - -1. Include passing tests (run ``tox``) [1]_. -2. Update documentation when there's new API, functionality etc. -3. Add a note to ``CHANGELOG.rst`` about the changes. -4. Add yourself to ``AUTHORS.rst``. - -.. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will - `run the tests `_ for each change you add in the pull request. - - It will be slower though ... - -Tips ----- - -To run a subset of tests:: - - tox -e envname -- pytest -k test_myfeature - -To run all the test environments in *parallel* (you need to ``pip install detox``):: - - detox diff --git a/MANIFEST.in b/MANIFEST.in index 3ae9b54..f68def1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,18 +1,14 @@ -graft docs + graft src graft ci graft tests -include .bumpversion.cfg -include .coveragerc -include .cookiecutterrc -include .editorconfig -include AUTHORS.rst -include CHANGELOG.rst -include CONTRIBUTING.rst +include AUTHORS.md +include CHANGELOG.md +include CONTRIBUTING.md include LICENSE -include README.rst +include README.md include tox.ini .travis.yml appveyor.yml diff --git a/README.md b/README.md index 573bbd4..2d9ca5a 100644 --- a/README.md +++ b/README.md @@ -72,3 +72,13 @@ We can then use this `Transaction Reference` to get the status of the `Transacti ``` Voila! + + + + readme + installation + usage + reference/index + contributing + authors + changelog diff --git a/ci/templates/tox.ini b/ci/templates/tox.ini deleted file mode 100644 index ef65edb..0000000 --- a/ci/templates/tox.ini +++ /dev/null @@ -1,119 +0,0 @@ -[tox] -envlist = - clean, - check, - docs, -{% for env in tox_environments|sort %} - {{ env }}, -{% endfor %} - report - -[testenv] -basepython = - {docs,spell}: {env:TOXPYTHON:python2.7} - {bootstrap,clean,check,report,coveralls}: {env:TOXPYTHON:python3} -setenv = - PYTHONPATH={toxinidir}/tests - PYTHONUNBUFFERED=yes -passenv = - * -deps = - pytest - pytest-travis-fold -commands = - {posargs:pytest -vv --ignore=src} - -[testenv:bootstrap] -deps = - jinja2 - matrix -skip_install = true -commands = - python ci/bootstrap.py - -[testenv:check] -deps = - docutils - check-manifest - flake8 - readme-renderer - pygments - isort -skip_install = true -commands = - python setup.py check --strict --metadata --restructuredtext - check-manifest {toxinidir} - flake8 src tests setup.py - isort --verbose --check-only --diff --recursive src tests setup.py - - -[testenv:spell] -setenv = - SPELLCHECK=1 -commands = - sphinx-build -b spelling docs dist/docs -skip_install = true -deps = - -r{toxinidir}/docs/requirements.txt - sphinxcontrib-spelling - pyenchant - -[testenv:docs] -deps = - -r{toxinidir}/docs/requirements.txt -commands = - sphinx-build {posargs:-E} -b html docs dist/docs - sphinx-build -b linkcheck docs dist/docs - -[testenv:coveralls] -deps = - coveralls -skip_install = true -commands = - coveralls [] - - - -[testenv:report] -deps = coverage -skip_install = true -usedevelop = false -commands = - coverage report - coverage html - -[testenv:clean] -commands = coverage erase -skip_install = true -usedevelop = false -deps = coverage - -{% for env, config in tox_environments|dictsort %} -[testenv:{{ env }}] -basepython = {env:TOXPYTHON:{{ env.split("-")[0] if env.startswith("pypy") else "python{0[2]}.{0[3]}".format(env) }}} -{% if config.cover or config.env_vars %} -setenv = - {[testenv]setenv} -{% endif %} -{% for var in config.env_vars %} - {{ var }} -{% endfor %} -{% if config.cover %} -usedevelop = true -commands = - {posargs:pytest --cov --cov-report=term-missing -vv} -{% endif %} -{% if config.cover or config.deps %} -deps = - {[testenv]deps} -{% endif %} -{% if config.cover %} - pytest-cov -{% endif %} -{% for dep in config.deps %} - {{ dep }} -{% endfor %} - -{% endfor %} - - diff --git a/docs/authors.rst b/docs/authors.rst deleted file mode 100644 index e122f91..0000000 --- a/docs/authors.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../AUTHORS.rst diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 100644 index 565b052..0000000 --- a/docs/changelog.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index c138046..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import os - - -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.coverage', - 'sphinx.ext.doctest', - 'sphinx.ext.extlinks', - 'sphinx.ext.ifconfig', - 'sphinx.ext.napoleon', - 'sphinx.ext.todo', - 'sphinx.ext.viewcode', -] -if os.getenv('SPELLCHECK'): - extensions += 'sphinxcontrib.spelling', - spelling_show_suggestions = True - spelling_lang = 'en_US' - -source_suffix = '.rst' -master_doc = 'index' -project = 'momoapi-python' -year = '2018' -author = 'Moses Mugisha' -copyright = '{0}, {1}'.format(year, author) -version = release = '0.1.0' - -pygments_style = 'trac' -templates_path = ['.'] -extlinks = { - 'issue': ('https://github.com/mossplix/python-momoapi/issues/%s', '#'), - 'pr': ('https://github.com/mossplix/python-momoapi/pull/%s', 'PR #'), -} -import sphinx_py3doc_enhanced_theme -html_theme = "sphinx_py3doc_enhanced_theme" -html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] -html_theme_options = { - 'githuburl': 'https://github.com/mossplix/python-momoapi/' -} - -html_use_smartypants = True -html_last_updated_fmt = '%b %d, %Y' -html_split_index = False -html_sidebars = { - '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], -} -html_short_title = '%s-%s' % (project, version) - -napoleon_use_ivar = True -napoleon_use_rtype = False -napoleon_use_param = False diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index e582053..0000000 --- a/docs/contributing.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CONTRIBUTING.rst diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 40f35b5..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,22 +0,0 @@ -======== -Contents -======== - -.. toctree:: - :maxdepth: 2 - - readme - installation - usage - reference/index - contributing - authors - changelog - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/installation.rst b/docs/installation.rst deleted file mode 100644 index 40917b1..0000000 --- a/docs/installation.rst +++ /dev/null @@ -1,7 +0,0 @@ -============ -Installation -============ - -At the command line:: - - pip install momoapi-python diff --git a/docs/readme.rst b/docs/readme.rst deleted file mode 100644 index 72a3355..0000000 --- a/docs/readme.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../README.rst diff --git a/docs/reference/index.rst b/docs/reference/index.rst deleted file mode 100644 index 209ab35..0000000 --- a/docs/reference/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Reference -========= - -.. toctree:: - :glob: - - momoapi_python* diff --git a/docs/reference/momoapi_python.rst b/docs/reference/momoapi_python.rst deleted file mode 100644 index aad0d9e..0000000 --- a/docs/reference/momoapi_python.rst +++ /dev/null @@ -1,9 +0,0 @@ -momoapi_python -============== - -.. testsetup:: - - from momoapi_python import * - -.. automodule:: momoapi_python - :members: diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index ef4a013..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx>=1.3 -sphinx-py3doc-enhanced-theme --e . diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt deleted file mode 100644 index f95eb78..0000000 --- a/docs/spelling_wordlist.txt +++ /dev/null @@ -1,11 +0,0 @@ -builtin -builtins -classmethod -staticmethod -classmethods -staticmethods -args -kwargs -callstack -Changelog -Indices diff --git a/docs/usage.rst b/docs/usage.rst deleted file mode 100644 index 85f3876..0000000 --- a/docs/usage.rst +++ /dev/null @@ -1,7 +0,0 @@ -===== -Usage -===== - -To use momoapi-python in a project:: - - import momoapi_python diff --git a/setup.cfg b/setup.cfg index e88a1af..7d1e88f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,6 @@ python_files = addopts = -ra --strict - --ignore=docs/conf.py --ignore=setup.py --ignore=ci --ignore=.eggs diff --git a/setup.py b/setup.py index 92eb6b7..7d3a6d2 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def read(*names, **kwargs): description='Python wrapper for the MTN MoMo API.', long_description='%s\n%s' % ( re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.md')), - re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) + re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.md')) ), author='Sparkplug', author_email='mugisha@sparkpl.ug', @@ -65,23 +65,25 @@ def read(*names, **kwargs): ], keywords=[ 'MoMo API', 'MoMo API Python Wrapper', 'MoMo API Python', + + + ], install_requires=[ - 'click', - 'requests' + 'click', 'requests' # eg: 'aspectlib==1.1.1', 'six>=1.7', ], - setup_requires=["pytest-runner",], + setup_requires=["pytest-runner", ], - extras_require={'test': ['pytest', 'pytest-watch','tox', - 'pytest-cov', - 'pytest-pep8', - 'pytest-cov', - 'pytest-sugar', - 'pytest-instafail', - 'pytest-bdd'],"dev":["semver"]}, + extras_require={'test': ['pytest', 'pytest-watch', 'tox', + 'pytest-cov', + 'pytest-pep8', + 'pytest-cov', + 'pytest-sugar', + 'pytest-instafail', + 'pytest-bdd'], "dev": ["semver"]}, entry_points={ 'console_scripts': [ 'momoapi = momoapi.cli:main', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/features/account.feature b/tests/features/account.feature new file mode 100644 index 0000000..e69de29 diff --git a/tests/features/collections.feature b/tests/features/collections.feature new file mode 100644 index 0000000..e69de29 diff --git a/tests/features/disbursements.feature b/tests/features/disbursements.feature new file mode 100644 index 0000000..e69de29 diff --git a/tests/features/remittences.feature b/tests/features/remittences.feature new file mode 100644 index 0000000..e69de29 diff --git a/tests/features/sandbox.feature b/tests/features/sandbox.feature new file mode 100644 index 0000000..fd28ca9 --- /dev/null +++ b/tests/features/sandbox.feature @@ -0,0 +1,16 @@ +Feature: As a new library User, I should be able to create an account on the MOMO sandbox + + Scenario: Adding new account + Given a user with the domain sparkpl.ug and subscription key 99e9cb10e8c04ea0b788334dc6346f13 + When I run the command "momoapi" + And I fill in the "providerCallBackHost" with "sparkpl.ug" + And I fill in the "Ocp-Apim-Subscription-Key" with "99e9cb10e8c04ea0b788334dc6346f13" + Then I should get back the apiKey + + Scenario: Wrong subscription Key + Given a user with the domain sparkpl.ug and subscription key 99e9cb10e8c04ea0b788334dc6346f13dg + When I run the command "momoapi" + And I fill in the "providerCallBackHost" with "sparkpl.ug" + And I fill in the "Ocp-Apim-Subscription-Key" with "f83xx8d8xx6749f19a26e2265aeadg" + Then I should get the message "Access denied due to invalid subscription key" + diff --git a/tests/test_example.py b/tests/test_example.py deleted file mode 100644 index 6d8e621..0000000 --- a/tests/test_example.py +++ /dev/null @@ -1,5 +0,0 @@ -def func(x): - return x + 1 - -def test_answer(): - assert func(3) == 4 \ No newline at end of file diff --git a/tests/test_sandbox.py b/tests/test_sandbox.py new file mode 100644 index 0000000..fd8db08 --- /dev/null +++ b/tests/test_sandbox.py @@ -0,0 +1,57 @@ +from pytest_bdd import scenario, given, when, then, parsers +import re +import pytest +from click.testing import CliRunner +from momoapi.cli import generateToken + +pytest.globalDict = {} + + +@scenario('features/sandbox.feature', 'Adding new account') +def test_sanbox_provisioning(): + pass + + +@given(parsers.re(r'a user with the domain (?P[a-zA-Z0-9\.]+) and subscription key (?P\w+)'), + converters=dict(domain=str, api_key=str)) +def provisioning_user(domain, api_key): + + pytest.globalDict['domain'] = domain + pytest.globalDict['api_key'] = api_key + return + + +@when('I run the command "momoapi"') +def run_command(): + return + + +@when('I fill in the "providerCallBackHost" with "sparkpl.ug"') +def fill_domain(): + return + + +@when('I fill in the "Ocp-Apim-Subscription-Key" with "99e9cb10e8c04ea0b788334dc6346f13"') +def fill_wrong_value(): + return + + +@when('I fill in the "Ocp-Apim-Subscription-Key" with "f83xx8d8xx6749f19a26e2265aeadbcdeg"') +def fill_key(): + return + + +@then('I should get the message "Access denied due to invalid subscription key"') +def wrong_key(key="f83xx8d8xx6749f19a26e2265aeadbcdeg"): + host = pytest.globalDict["domain"] + result = generateToken(host, key) + assert "Access denied due to invalid subscription key" in result + + +@then('I should get back the apiKey') +def get_Key(): + + host = pytest.globalDict["domain"] + key = pytest.globalDict["api_key"] + result = generateToken(host, key) + assert "Here is your User Id and API secret" in result diff --git a/tox.ini b/tox.ini index bf8874d..51d6186 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,6 @@ [tox] envlist = clean, - check, - docs, py27-cover, py27-nocov, py34-cover, @@ -13,16 +11,11 @@ envlist = py36-nocov, py37-cover, py37-nocov, - pypy-cover, - pypy-nocov, - pypy3-cover, - pypy3-nocov, report [testenv] basepython = - {docs,spell}: {env:TOXPYTHON:python2.7} - {bootstrap,clean,check,report,coveralls}: {env:TOXPYTHON:python3} + {clean,report,coveralls}: {env:TOXPYTHON:python3} setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes @@ -31,50 +24,10 @@ passenv = deps = pytest pytest-travis-fold + pytest-bdd commands = {posargs:pytest -vv --ignore=src} -[testenv:bootstrap] -deps = - jinja2 - matrix -skip_install = true -commands = - python ci/bootstrap.py - -[testenv:check] -deps = - docutils - check-manifest - flake8 - readme-renderer - pygments - isort -skip_install = true -commands = - python setup.py check --strict --metadata --restructuredtext - check-manifest {toxinidir} - flake8 src tests setup.py - isort --verbose --check-only --diff --recursive src tests setup.py - - -[testenv:spell] -setenv = - SPELLCHECK=1 -commands = - sphinx-build -b spelling docs dist/docs -skip_install = true -deps = - -r{toxinidir}/docs/requirements.txt - sphinxcontrib-spelling - pyenchant - -[testenv:docs] -deps = - -r{toxinidir}/docs/requirements.txt -commands = - sphinx-build {posargs:-E} -b html docs dist/docs - sphinx-build -b linkcheck docs dist/docs [testenv:coveralls] deps = @@ -99,8 +52,6 @@ deps = check-manifest twine - - [testenv:report] deps = coverage skip_install = true @@ -131,6 +82,7 @@ deps = wltrace pytest coverage + pytest-bdd [testenv:py27-nocov] basepython = {env:TOXPYTHON:python2.7} @@ -191,33 +143,6 @@ deps = [testenv:py37-nocov] basepython = {env:TOXPYTHON:python3.7} -[testenv:pypy-cover] -basepython = {env:TOXPYTHON:pypy} -setenv = - {[testenv]setenv} -usedevelop = true -commands = - {posargs:pytest --cov --cov-report=term-missing -vv} -deps = - {[testenv]deps} - pytest-cov - -[testenv:pypy-nocov] -basepython = {env:TOXPYTHON:pypy} - -[testenv:pypy3-cover] -basepython = {env:TOXPYTHON:pypy3} -setenv = - {[testenv]setenv} -usedevelop = true -commands = - {posargs:pytest --cov --cov-report=term-missing -vv} -deps = - {[testenv]deps} - pytest-cov - -[testenv:pypy3-nocov] -basepython = {env:TOXPYTHON:pypy3} From 138e3fd9ce40990fae7ec40c9d760baba08a1053 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Wed, 27 Feb 2019 14:52:37 +0300 Subject: [PATCH 21/60] setup unitest mocks --- .travis.yml | 73 ++++++++++--------- README.md | 34 ++++++--- .../features/account.feature | 0 .../integration/features/collections.feature | 8 ++ .../features/disbursements.feature | 0 .../features/remittences.feature | 0 .../features/sandbox.feature | 0 .../test_account.py} | 0 tests/integration/test_collections.py | 0 tests/integration/test_disbursements.py | 0 tests/integration/test_remittences.py | 0 tests/{ => integration}/test_sandbox.py | 0 tests/unitests/__init__.py | 0 tests/unitests/test_client.py | 33 +++++++++ tests/unitests/utils.py | 44 +++++++++++ 15 files changed, 145 insertions(+), 47 deletions(-) rename tests/{ => integration}/features/account.feature (100%) create mode 100644 tests/integration/features/collections.feature rename tests/{ => integration}/features/disbursements.feature (100%) rename tests/{ => integration}/features/remittences.feature (100%) rename tests/{ => integration}/features/sandbox.feature (100%) rename tests/{features/collections.feature => integration/test_account.py} (100%) create mode 100644 tests/integration/test_collections.py create mode 100644 tests/integration/test_disbursements.py create mode 100644 tests/integration/test_remittences.py rename tests/{ => integration}/test_sandbox.py (100%) create mode 100644 tests/unitests/__init__.py create mode 100644 tests/unitests/test_client.py create mode 100644 tests/unitests/utils.py diff --git a/.travis.yml b/.travis.yml index 11a2ac1..a554dde 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,45 +1,48 @@ language: python +branches: + only: + - master cache: directories: - - "$HOME/.cache/pip" - - "$HOME/.pyenv" + - "$HOME/.cache/pip" + - "$HOME/.pyenv" matrix: include: - - os: linux - dist: trusty - sudo: false - python: '2.6' - - os: linux - dist: trusty - sudo: false - python: '2.7' - - os: linux - dist: trusty - sudo: false - python: '3.3' - - os: linux - dist: trusty - sudo: false - python: '3.4' - - os: linux - dist: trusty - sudo: false - python: '3.5' - - os: linux - dist: trusty - sudo: false - python: '3.6' - - os: linux - dist: xenial - sudo: required - services: - - docker - python: '3.7' - env: BUILD_SDIST=true + - os: linux + dist: trusty + sudo: false + python: "2.6" + - os: linux + dist: trusty + sudo: false + python: "2.7" + - os: linux + dist: trusty + sudo: false + python: "3.3" + - os: linux + dist: trusty + sudo: false + python: "3.4" + - os: linux + dist: trusty + sudo: false + python: "3.5" + - os: linux + dist: trusty + sudo: false + python: "3.6" + - os: linux + dist: xenial + sudo: required + services: + - docker + python: "3.7" + env: BUILD_SDIST=true script: -- python setup.py test + - python setup.py test install: -- python setup.py install + - python setup.py install deploy: provider: pypi user: mossplix diff --git a/README.md b/README.md index 2d9ca5a..88e1c76 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,35 @@ -## Installation +# MTN Mobile Money API Client -First, ensure that the you have a virtual environment enabled before you proceed. You can create one and activate as follows: +MTN MoMo API Client for Python. -Creation of virtualenv: +[![Build Status](https://travis-ci.com/sparkplug/momoapi-python.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-node) +[![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) +[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=coverage)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) +[![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) -```bash -$ virtualenv -p python3 -``` -Activate the virtualenv: +# Installing -```bash -$ source /bin/activate -``` -Next, we install from source: +To install the latest version of Momo-api using pip:: ```bash -$ python setup.py install + $ pip install momoapi + ``` +Additional instructions for installing this software are in `the installation instructions `_. + + +# Unit Tests + +momo-api has a comprehensive test suite, which can be run by ``tox``:: + + $ tox -l # to view all test environments + $ tox -e py27-cover # to run the tests for Python 2.7 + $ tox -e py34-cover # to run the tests for Python 3.4 + + ## Requirements * Python 2.7+ or Python 3.4+ (PyPy supported) diff --git a/tests/features/account.feature b/tests/integration/features/account.feature similarity index 100% rename from tests/features/account.feature rename to tests/integration/features/account.feature diff --git a/tests/integration/features/collections.feature b/tests/integration/features/collections.feature new file mode 100644 index 0000000..1250d7e --- /dev/null +++ b/tests/integration/features/collections.feature @@ -0,0 +1,8 @@ +Feature: Get Payments + Scenario: Request a payment from a consumer (Payer) + Given I have a valid collection auth token + When I enter the following payment details + | note | amount | message | mobile | product_id | + | test payment | 600 | message | 0782631873 | 0001 | + + diff --git a/tests/features/disbursements.feature b/tests/integration/features/disbursements.feature similarity index 100% rename from tests/features/disbursements.feature rename to tests/integration/features/disbursements.feature diff --git a/tests/features/remittences.feature b/tests/integration/features/remittences.feature similarity index 100% rename from tests/features/remittences.feature rename to tests/integration/features/remittences.feature diff --git a/tests/features/sandbox.feature b/tests/integration/features/sandbox.feature similarity index 100% rename from tests/features/sandbox.feature rename to tests/integration/features/sandbox.feature diff --git a/tests/features/collections.feature b/tests/integration/test_account.py similarity index 100% rename from tests/features/collections.feature rename to tests/integration/test_account.py diff --git a/tests/integration/test_collections.py b/tests/integration/test_collections.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_disbursements.py b/tests/integration/test_disbursements.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_remittences.py b/tests/integration/test_remittences.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_sandbox.py b/tests/integration/test_sandbox.py similarity index 100% rename from tests/test_sandbox.py rename to tests/integration/test_sandbox.py diff --git a/tests/unitests/__init__.py b/tests/unitests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unitests/test_client.py b/tests/unitests/test_client.py new file mode 100644 index 0000000..24481b0 --- /dev/null +++ b/tests/unitests/test_client.py @@ -0,0 +1,33 @@ +import unittest +import pytest +from momoapi.client import MomoApi +try: + from unittest import mock +except ImportError: + import mock + +from .utils import mocked_requests_get, mocked_requests_post + + +class TestClient(unittest.TestCase): + def setUp(self): + pass + #self.widget = Widget('The widget') + + def tearDown(self): + pass + # self.widget.dispose() + #self.widget = None + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_client_instantiate(self, mock_get): + client = MomoApi("APIKEY", "USERID", "APISECRET") + + #request_mock.assert_requested("post", "/v1/accounts") + assert isinstance(client, MomoApi) + + @mock.patch('requests.post', side_effect=mocked_requests_post) + def test_request_to_pay(self, mock_get): + client = MomoApi("APIKEY", "USERID", "APISECRET") + ref = client.requestToPay("256772123456", "600", "123456789", note="dd", + message="dd", currency="EUR", environment="sandbox") diff --git a/tests/unitests/utils.py b/tests/unitests/utils.py new file mode 100644 index 0000000..e8f1c83 --- /dev/null +++ b/tests/unitests/utils.py @@ -0,0 +1,44 @@ +import requests +import unittest +try: + from unittest import mock +except ImportError: + import mock + + +def mocked_requests_get(*args, **kwargs): + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + import pdb + pdb.set_trace() + + if args[0] == 'http://someurl.com/test.json': + return MockResponse({"key1": "value1"}, 200) + elif args[0] == 'http://someotherurl.com/anothertest.json': + return MockResponse({"key2": "value2"}, 200) + + return MockResponse(None, 404) + + +def mocked_requests_post(*args, **kwargs): + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + import pdb + pdb.set_trace() + + if '/collection/token/' in args[0]: + return MockResponse({"key1": "value1"}, 200) + elif args[0] == 'http://someotherurl.com/anothertest.json': + return MockResponse({"key2": "value2"}, 200) + + return MockResponse(None, 404) From 80017b96efe209b0e15e5ba30099b819df4c4ea1 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Wed, 27 Feb 2019 15:55:49 +0300 Subject: [PATCH 22/60] add more mocks --- tests/unitests/test_client.py | 19 ++++++++++-------- tests/unitests/utils.py | 36 ++++++++++++++++++----------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/tests/unitests/test_client.py b/tests/unitests/test_client.py index 24481b0..97bb8e9 100644 --- a/tests/unitests/test_client.py +++ b/tests/unitests/test_client.py @@ -5,14 +5,17 @@ from unittest import mock except ImportError: import mock +from requests import Request, Session -from .utils import mocked_requests_get, mocked_requests_post +from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session class TestClient(unittest.TestCase): - def setUp(self): - pass - #self.widget = Widget('The widget') + + @mock.patch('requests.post', side_effect=mocked_requests_post) + def setUp(self, mock_get): + client = MomoApi("APIKEY", "USERID", "APISECRET") + self.client = client def tearDown(self): pass @@ -26,8 +29,8 @@ def test_client_instantiate(self, mock_get): #request_mock.assert_requested("post", "/v1/accounts") assert isinstance(client, MomoApi) - @mock.patch('requests.post', side_effect=mocked_requests_post) + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) def test_request_to_pay(self, mock_get): - client = MomoApi("APIKEY", "USERID", "APISECRET") - ref = client.requestToPay("256772123456", "600", "123456789", note="dd", - message="dd", currency="EUR", environment="sandbox") + + ref = self.client.requestToPay("256772123456", "600", "123456789", note="dd", + message="dd", currency="EUR", environment="sandbox") diff --git a/tests/unitests/utils.py b/tests/unitests/utils.py index e8f1c83..f8b0b71 100644 --- a/tests/unitests/utils.py +++ b/tests/unitests/utils.py @@ -6,14 +6,18 @@ import mock +class MockResponse: + def __init__(self, json_data, status_code, headers={}): + self.json_data = json_data + self.status_code = status_code + self.headers = headers + + def json(self): + return self.json_data + + def mocked_requests_get(*args, **kwargs): - class MockResponse: - def __init__(self, json_data, status_code): - self.json_data = json_data - self.status_code = status_code - def json(self): - return self.json_data import pdb pdb.set_trace() @@ -26,19 +30,17 @@ def json(self): def mocked_requests_post(*args, **kwargs): - class MockResponse: - def __init__(self, json_data, status_code): - self.json_data = json_data - self.status_code = status_code - - def json(self): - return self.json_data - import pdb - pdb.set_trace() if '/collection/token/' in args[0]: - return MockResponse({"key1": "value1"}, 200) - elif args[0] == 'http://someotherurl.com/anothertest.json': + return MockResponse({"access_token": "token"}, 200) + elif "/collection/v1_0/requesttopay" in args[0]: return MockResponse({"key2": "value2"}, 200) return MockResponse(None, 404) + + +def mocked_requests_session(*args, **kwargs): + if '/collection/token/' in args[1]: + return MockResponse({"access_token": "token"}, 200) + elif "/collection/v1_0/requesttopay" in args[1]: + return MockResponse({"key2": "value2"}, 200) From 11c47a85927d6b08d3017e7f7bf7659c09767814 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 12:05:31 +0300 Subject: [PATCH 23/60] update tox config --- .travis.yml | 18 +++++++---------- setup.cfg | 2 +- setup.py | 7 +++++-- tox.ini | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 66 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index a554dde..657dcb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,4 @@ language: python -branches: - only: - - master cache: directories: - "$HOME/.cache/pip" @@ -11,15 +8,13 @@ matrix: - os: linux dist: trusty sudo: false - python: "2.6" - - os: linux - dist: trusty - sudo: false + env: TOXENV=py27-cover python: "2.7" - os: linux dist: trusty sudo: false python: "3.3" + env: TOXENV=py33-cover - os: linux dist: trusty sudo: false @@ -28,19 +23,20 @@ matrix: dist: trusty sudo: false python: "3.5" + env: TOXENV=py35-cover - os: linux dist: trusty sudo: false python: "3.6" + env: TOXENV=py36-cover - os: linux dist: xenial sudo: required - services: - - docker python: "3.7" - env: BUILD_SDIST=true + env: TOXENV=py37-cover script: - - python setup.py test + - pip install tox + - tox install: - python setup.py install deploy: diff --git a/setup.cfg b/setup.cfg index 7d1e88f..4395759 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,7 +82,7 @@ coverage_flags = nocov: false [aliases] -test=pytest +test=pytest environment_variables = - diff --git a/setup.py b/setup.py index 7d3a6d2..7d171af 100644 --- a/setup.py +++ b/setup.py @@ -70,11 +70,13 @@ def read(*names, **kwargs): ], install_requires=[ - 'click', 'requests' + 'requests == 2.21.0', + 'Click==7.0' + # eg: 'aspectlib==1.1.1', 'six>=1.7', ], - setup_requires=["pytest-runner", ], + setup_requires=["pytest-runner", "pytest-cov"], extras_require={'test': ['pytest', 'pytest-watch', 'tox', @@ -82,6 +84,7 @@ def read(*names, **kwargs): 'pytest-pep8', 'pytest-cov', 'pytest-sugar', + 'mock', 'pytest-instafail', 'pytest-bdd'], "dev": ["semver"]}, entry_points={ diff --git a/tox.ini b/tox.ini index 51d6186..1d7dd55 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,10 @@ envlist = py36-nocov, py37-cover, py37-nocov, - report + report, + flake8 + +skip_missing_interpreters = {env:TOX_SKIP_MISSING_INTERPRETERS:True} [testenv] basepython = @@ -19,16 +22,22 @@ basepython = setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes -passenv = - * deps = pytest pytest-travis-fold pytest-bdd + mock commands = {posargs:pytest -vv --ignore=src} +passenv = + # See https://github.com/codecov/codecov-python/blob/5b9d539a6a09bc84501b381b563956295478651a/README.md#using-tox + codecov: TOXENV + codecov: CI + codecov: TRAVIS TRAVIS_* + + [testenv:coveralls] deps = coveralls @@ -52,6 +61,8 @@ deps = check-manifest twine + + [testenv:report] deps = coverage skip_install = true @@ -83,6 +94,7 @@ deps = pytest coverage pytest-bdd + mock [testenv:py27-nocov] basepython = {env:TOXPYTHON:python2.7} @@ -143,6 +155,44 @@ deps = [testenv:py37-nocov] basepython = {env:TOXPYTHON:python3.7} +[flake8] + +select = B,C,E,F,I,N,S,W + +disable-noqa = True +show-source = True +doctests = True + +# Codes: http://flake8.pycqa.org/en/latest/user/error-codes.html +ignore = + # multiple spaces before operator + E221, + + # too many blank lines + E302, + + # too many blank lines + E303, + + # expected 2 blank lines after class or function definition + E305, + + # function name should be lowercase + N802, + + # argument name should be lowercase + N803, + + # first argument of a method should be named 'self' + N805, + + # variable in function should be lowercase + N806, + + # lowercase imported as non lowercase + N812, + # line break after binary operator (W503 and W504 are opposites) + W504, From 41572731e6fbba5c5ad18dbfd7d7e5a8cf29a789 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 12:07:34 +0300 Subject: [PATCH 24/60] add missing env --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 657dcb6..7a4c7d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,7 @@ matrix: dist: trusty sudo: false python: "3.4" + env: TOXENV=py34-cover - os: linux dist: trusty sudo: false From a2d3b492f91ec4f946ae19de2612df98ab59bd53 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 12:20:36 +0300 Subject: [PATCH 25/60] exclude python 3.3 & 3.4 from tests --- .travis.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7a4c7d4..179ecc3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,16 +10,6 @@ matrix: sudo: false env: TOXENV=py27-cover python: "2.7" - - os: linux - dist: trusty - sudo: false - python: "3.3" - env: TOXENV=py33-cover - - os: linux - dist: trusty - sudo: false - python: "3.4" - env: TOXENV=py34-cover - os: linux dist: trusty sudo: false From 0a719450f4f1da412da1e7616d2f110dd6722708 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 14:09:54 +0300 Subject: [PATCH 26/60] unittest coverage --- src/momoapi/errors.py | 136 ++----------------------------- src/momoapi/utils.py | 145 ++++++++++++++++++++++++++++++++++ tests/unitests/test_client.py | 32 ++++++++ tests/unitests/utils.py | 47 ++++++++--- 4 files changed, 222 insertions(+), 138 deletions(-) create mode 100644 src/momoapi/utils.py diff --git a/src/momoapi/errors.py b/src/momoapi/errors.py index 42dde61..0efa3e8 100644 --- a/src/momoapi/errors.py +++ b/src/momoapi/errors.py @@ -1,4 +1,6 @@ +from .utils import ERROR_CODES + class MomoError(Exception): def __init__(self, message=None, http_body=None, http_status=None, @@ -40,13 +42,14 @@ def __repr__(self): self.request_id) - class APIError(MomoError): pass + class APIConnectionError(MomoError): pass + class AuthenticationError(MomoError): pass @@ -54,141 +57,18 @@ class AuthenticationError(MomoError): class PermissionError(MomoError): pass + class PreapprovalError(MomoError): pass + class RequestToPayError(MomoError): pass + class TransferError(MomoError): pass + class GeneralError(MomoError): pass - - -ERROR_CODES= [ - { - "http_code": 409, - "response_code":None, - "description": "Duplicated Reference Id. Cannot create new resource", - "error_type": "generic" - - }, - { - "http_code": 404, - "response_code":None, - "description": "Reference Id not found. Requested resource does not exist", - "error_type": "generic" - - }, - { - "http_code": 400, - "response_code":None, - "description": "Bad request. Request does not follow the specification.", - "error_type": "generic" - - }, - { - "http_code": 401, - "response_code":None, - "description": "Authentication failed. Credentials not valid", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"NOT_ALLOWED", - "description": "Authorization failed. User does not have permission.", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"NOT_ALLOWED_TARGET_ENVIRONMENT", - "description": "Not allowed target environment", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"INVALID_CALLBACK_URL_HOST", - "description": "Callback URL with different host name then configured for API User", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"INVALID_CURRENCY", - "description": "Currency not supported on the requested account", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"INTERNAL_PROCESSING_ERROR", - "description": "Default error code used when there is no specific error mapping.", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"SERVICE_UNAVAILABLE", - "description": "Service temporary unavailable, try again later", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"PAYER_NOT_FOUND", - "description": "Payer not found", - "error_type": "preapproval" - - }, - - { - "http_code": 500, - "response_code":"PAYEE_NOT_ALLOWED_TO_RECEIVE", - "description": "Payee cannot receive funds due to e.g. transfer limit.", - "error_type": "request_to_pay" - - }, - - { - "http_code": 500, - "response_code":"NOT_ENOUGH_FUNDS", - "description": "Not enough funds on payer account", - "error_type": "transfer" - - }, - { - "http_code": 500, - "response_code":"PAYER_LIMIT_REACHED", - "description": "Not allowed to end due to Payer limit reached", - "error_type": "transfer" - - }, - - { - "http_code": 500, - "response_code":"PAYEE_NOT_FOUND", - "description": "Payee not found. Account holder is not registered", - "error_type": "transfer" - - }, - - { - "http_code": 404, - "response_code":None, - "description": "Account holder is not found", - "error_type": "account" - - } -] diff --git a/src/momoapi/utils.py b/src/momoapi/utils.py new file mode 100644 index 0000000..4651c38 --- /dev/null +++ b/src/momoapi/utils.py @@ -0,0 +1,145 @@ +ERROR_CODES = [ + { + "http_code": 409, + "response_code": None, + "description": "Duplicated Reference Id. Cannot create new resource", + "error_type": "generic" + + }, + { + "http_code": 404, + "response_code": None, + "description": "Reference Id not found. Requested resource does not exist", + "error_type": "generic" + + }, + { + "http_code": 400, + "response_code": None, + "description": "Bad request. Request does not follow the specification.", + "error_type": "generic" + + }, + { + "http_code": 401, + "response_code": None, + "description": "Authentication failed. Credentials not valid", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "NOT_ALLOWED", + "description": "Authorization failed. User does not have permission.", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "NOT_ALLOWED_TARGET_ENVIRONMENT", + "description": "Not allowed target environment", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "INVALID_CALLBACK_URL_HOST", + "description": "Callback URL with different host name then configured for API User", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "INVALID_CURRENCY", + "description": "Currency not supported on the requested account", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "INTERNAL_PROCESSING_ERROR", + "description": "Default error code used when there is no specific error mapping.", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "SERVICE_UNAVAILABLE", + "description": "Service temporary unavailable, try again later", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "PAYER_NOT_FOUND", + "description": "Payer not found", + "error_type": "preapproval" + + }, + + { + "http_code": 500, + "response_code": "PAYEE_NOT_ALLOWED_TO_RECEIVE", + "description": "Payee cannot receive funds due to e.g. transfer limit.", + "error_type": "request_to_pay" + + }, + + { + "http_code": 500, + "response_code": "NOT_ENOUGH_FUNDS", + "description": "Not enough funds on payer account", + "error_type": "transfer" + + }, + { + "http_code": 500, + "response_code": "PAYER_LIMIT_REACHED", + "description": "Not allowed to end due to Payer limit reached", + "error_type": "transfer" + + }, + + { + "http_code": 500, + "response_code": "PAYEE_NOT_FOUND", + "description": "Payee not found. Account holder is not registered", + "error_type": "transfer" + + }, + + { + "http_code": 404, + "response_code": None, + "description": "Account holder is not found", + "error_type": "account" + + } +] + + +def requests_retry_session( + retries=3, + backoff_factor=0.3, + status_forcelist=(502, 504), + session=None, +): + session = session or requests.Session() + retry = Retry( + total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist, + ) + adapter = HTTPAdapter(max_retries=retry) + session.mount('http://', adapter) + session.mount('https://', adapter) + return session diff --git a/tests/unitests/test_client.py b/tests/unitests/test_client.py index 97bb8e9..7b1ceb0 100644 --- a/tests/unitests/test_client.py +++ b/tests/unitests/test_client.py @@ -1,6 +1,7 @@ import unittest import pytest from momoapi.client import MomoApi +import types try: from unittest import mock except ImportError: @@ -34,3 +35,34 @@ def test_request_to_pay(self, mock_get): ref = self.client.requestToPay("256772123456", "600", "123456789", note="dd", message="dd", currency="EUR", environment="sandbox") + + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_balance(self, mock_get): + balance = self.client.getBalance() + assert isinstance(balance, dict) + assert "availableBalance" in balance.keys() + assert "currency" in balance.keys() + + # @mock.patch('requests.get', side_effect=mocked_requests_get) + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_transaction_status(self, mock_get): + status = self.client.getTransactionStatus("dummy") + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_transfer(self, mock): + ref = self.client.transfer("600", "256772123456", note="dd", + message="dd", currency="EUR", environment="sandbox") + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + @mock.patch('requests.post', side_effect=mocked_requests_post) + def test_generate_token(self, mock): + res = MomoApi.generateToken("dummy_host", "dummy_user", "dummy_key", "dummy_base") + assert isinstance(res, dict) + assert "apiKey" in res.keys() diff --git a/tests/unitests/utils.py b/tests/unitests/utils.py index f8b0b71..59dd1f4 100644 --- a/tests/unitests/utils.py +++ b/tests/unitests/utils.py @@ -17,14 +17,18 @@ def json(self): def mocked_requests_get(*args, **kwargs): - - import pdb - pdb.set_trace() - - if args[0] == 'http://someurl.com/test.json': - return MockResponse({"key1": "value1"}, 200) - elif args[0] == 'http://someotherurl.com/anothertest.json': - return MockResponse({"key2": "value2"}, 200) + if "/requesttopay" in args[0]: + return MockResponse({ + "amount": 100, + "currency": "UGX", + "financialTransactionId": 23503452, + "externalId": 947354, + "payer": { + "partyIdType": "MSISDN", + "partyId": 4656473839 + }, + "status": "SUCCESSFUL" + }, 200) return MockResponse(None, 404) @@ -35,6 +39,10 @@ def mocked_requests_post(*args, **kwargs): return MockResponse({"access_token": "token"}, 200) elif "/collection/v1_0/requesttopay" in args[0]: return MockResponse({"key2": "value2"}, 200) + elif "apiuser" in args[0] and "apikey" in args[0]: + return MockResponse({ + "apiKey": "dummykey" + }, 200) return MockResponse(None, 404) @@ -42,5 +50,24 @@ def mocked_requests_post(*args, **kwargs): def mocked_requests_session(*args, **kwargs): if '/collection/token/' in args[1]: return MockResponse({"access_token": "token"}, 200) - elif "/collection/v1_0/requesttopay" in args[1]: - return MockResponse({"key2": "value2"}, 200) + elif '/account/balance' in args[1]: + return MockResponse({ + "availableBalance": "500", + "currency": "UGX" + }, 200) + elif "/requesttopay" in args[1] and args[0] == 'POST': + return MockResponse({}, 200) + elif "transfer" in args[1]: + return MockResponse({}, 200) + elif "/requesttopay" in args[1] and args[0] == 'GET': + return MockResponse({ + "amount": 100, + "currency": "UGX", + "financialTransactionId": 23503452, + "externalId": 947354, + "payer": { + "partyIdType": "MSISDN", + "partyId": 4656473839 + }, + "status": "SUCCESSFUL" + }, 200) From 2c9c522f6fe54dbede2465455fce50cc813f4506 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 14:16:23 +0300 Subject: [PATCH 27/60] [minor] add support for retries --- src/momoapi/client.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 143fc41..cbcb9be 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -14,6 +14,9 @@ except ImportError: JSONDecodeError = ValueError +from requests.packages.urllib3.util.retry import Retry +from .utils import requests_retry_session + class Response: @@ -34,7 +37,7 @@ def __init__(self, token): def __call__(self, r): # modify and return the request - r.headers['Authorization'] = "Bearer "+to_native_string(self.token) + r.headers['Authorization'] = "Bearer " + to_native_string(self.token) return r @@ -50,13 +53,14 @@ def __init__(self, auth_key, user_id, api_secret, base_url="https://ericssonbasi def request(self, method, url, headers, post_data=None): self.authToken = self.getAuthToken().json()["access_token"] - request = Request(method, url, data=json.dumps(post_data), headers=headers, auth=MoMoAuth("%s" % self.authToken)) + request = Request(method, url, data=json.dumps(post_data), + headers=headers, auth=MoMoAuth("%s" % self.authToken)) prepped = self._session.prepare_request(request) - resp = self._session.send(prepped, - verify=False - ) + resp = requests_retry_session(sesssion=self._session).send(prepped, + verify=False + ) return self.interpret_response(resp) def interpret_response(self, resp): @@ -99,11 +103,11 @@ def getAuthToken(self): "Ocp-Apim-Subscription-Key": "%s" % self.auth_key } - r = requests.post(self.base_url+"/collection/token/", + r = requests.post(self.base_url + "/collection/token/", auth=HTTPBasicAuth(self.user_id, self.api_secret), data=data, headers=headers) return r - def requestToPay(self, mobile, amount, product_id, note="", message="", currency="EUR", environment="sandbox"): + def requestToPay(self, mobile, amount, product_id, note="", message="", currency="EUR", environment="sandbox"): ref = str(uuid.uuid4()) data = {"payer": {"partyIdType": "MSISDN", "partyId": mobile}, "payeeNote": note, "payerMessage": message, "externalId": product_id, "currency": currency, "amount": amount} @@ -115,7 +119,7 @@ def requestToPay(self, mobile, amount, product_id, note="", message="", currency } - url = self.base_url+"/collection/v1_0/requesttopay" + url = self.base_url + "/collection/v1_0/requesttopay" res = self.request("POST", url, headers, data) return {"transaction_ref": ref} @@ -125,18 +129,18 @@ def getBalance(self, environment="sandbox"): "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": self.auth_key } - url = self.base_url+"/collection/v1_0/account/balance" + url = self.base_url + "/collection/v1_0/account/balance" res = self.request("GET", url, headers) return res.json() - def getTransactionStatus(self, transaction_id, environment="sandbox"): + def getTransactionStatus(self, transaction_id, environment="sandbox"): headers = { "X-Target-Environment": environment, "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": self.auth_key } - url = self.base_url+"/collection/v1_0/requesttopay/"+transaction_id + url = self.base_url + "/collection/v1_0/requesttopay/" + transaction_id res = self.request("GET", url, headers) return res.json() @@ -158,7 +162,7 @@ def transfer(self, amount, mobile, note="", message="", currency="EUR", environm "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": self.auth_key } - url = self.base_url+"/v1_0/transfer" + url = self.base_url + "/v1_0/transfer" res = self.request("POST", url, headers, data) return {"transaction_ref": external_ref} @@ -172,7 +176,7 @@ def generateToken(self, host, api_user, api_key, base_url, environment="sandbox" "X-Target-Environment": environment, } - url = base_url+"/v1_0/apiuser/%s/apikey" % api_user + url = base_url + "/v1_0/apiuser/%s/apikey" % api_user res = requests.post(url, data=json.dumps({}), headers=headers) print(res) From ed7f027e7f7f97c9a8aa8578c2ad5fc8de5f7914 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 15:33:58 +0300 Subject: [PATCH 28/60] configure mypy for static checks --- .coveralls.yml | 2 ++ .flake8 | 7 +++++ src/momoapi/client.py | 14 +++++++--- src/momoapi/utils.py | 28 +++++++++++++++++++ tox.ini | 63 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 .coveralls.yml create mode 100644 .flake8 diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..49800a0 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,2 @@ +service_name: travis-pro +repo_token: eYYZeUsJmXfN0lkaFJYh7waOrHLWMSJht diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..b730b16 --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +# E501 is the "Line too long" error. We disable it because we use Black for +# code formatting. Black makes a best effort to keep lines under the max +# length, but can go over in some cases. +# W503 goes against PEP8 rules. It's disabled by default, but must be disabled +# explicitly when using `ignore`. +ignore = E501, W503 \ No newline at end of file diff --git a/src/momoapi/client.py b/src/momoapi/client.py index cbcb9be..45171e6 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -1,3 +1,9 @@ +""" +Base implementation of the MTN API client + +@author: Moses Mugisha +""" + import requests import textwrap import json @@ -107,7 +113,7 @@ def getAuthToken(self): auth=HTTPBasicAuth(self.user_id, self.api_secret), data=data, headers=headers) return r - def requestToPay(self, mobile, amount, product_id, note="", message="", currency="EUR", environment="sandbox"): + def requestToPay(self, mobile, amount, product_id, note="", message="", currency="EUR", environment="sandbox", **kwargs): ref = str(uuid.uuid4()) data = {"payer": {"partyIdType": "MSISDN", "partyId": mobile}, "payeeNote": note, "payerMessage": message, "externalId": product_id, "currency": currency, "amount": amount} @@ -133,7 +139,7 @@ def getBalance(self, environment="sandbox"): res = self.request("GET", url, headers) return res.json() - def getTransactionStatus(self, transaction_id, environment="sandbox"): + def getTransactionStatus(self, transaction_id, environment="sandbox", **kwargs): headers = { "X-Target-Environment": environment, @@ -144,7 +150,7 @@ def getTransactionStatus(self, transaction_id, environment="sandbox"): res = self.request("GET", url, headers) return res.json() - def transfer(self, amount, mobile, note="", message="", currency="EUR", environment="sandbox"): + def transfer(self, amount, mobile, note="", message="", currency="EUR", environment="sandbox", **kwargs): external_ref = str(uuid.uuid4()) data = { "amount": amount, @@ -167,7 +173,7 @@ def transfer(self, amount, mobile, note="", message="", currency="EUR", environm return {"transaction_ref": external_ref} @classmethod - def generateToken(self, host, api_user, api_key, base_url, environment="sandbox"): + def generateToken(self, host, api_user, api_key, base_url, environment="sandbox", **kwargs): data = {"providerCallbackHost": host} headers = { diff --git a/src/momoapi/utils.py b/src/momoapi/utils.py index 4651c38..6a0da84 100644 --- a/src/momoapi/utils.py +++ b/src/momoapi/utils.py @@ -1,3 +1,7 @@ + +from phonenumbers import carrier +import phonenumbers + ERROR_CODES = [ { "http_code": 409, @@ -143,3 +147,27 @@ def requests_retry_session( session.mount('http://', adapter) session.mount('https://', adapter) return session + + +def validate_phone_number(number): + obj = phonenumbers.parse(number, "UG") + if (phonenumbers.is_valid_numbe(obj) == False): + raise Exception("Invalid Phone number %s" % number) + if (carrier.name_for_number(obj, "en") != "MTN"): + raise Exception("%s: Only MTN is supported at the moment" % number) + return "256" + obj.national_number + + +def validate_number(number): + number_types = (int, float) + if sys.version_info < (3, 0, 0): + number_types += (long,) + if not type(number) in number_types: + raise Exception("%s: Must be a number" % number) + return number + + +def validate_string(_string): + if not type(_string) == str: + raise Exception("%s: Must be a string" % _string) + return string diff --git a/tox.ini b/tox.ini index 1d7dd55..67ffec0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,8 @@ [tox] envlist = clean, + mypy, + flake8, py27-cover, py27-nocov, py34-cover, @@ -12,7 +14,7 @@ envlist = py37-cover, py37-nocov, report, - flake8 + skip_missing_interpreters = {env:TOX_SKIP_MISSING_INTERPRETERS:True} @@ -155,6 +157,30 @@ deps = [testenv:py37-nocov] basepython = {env:TOXPYTHON:python3.7} + +## +# Flake8 linting +## + +[testenv:flake8] + +skip_install = True + +deps = + flake8==3.6.0 + flake8-bugbear==18.8.0 + flake8-docstrings==1.3.0 + flake8-import-order==0.18 + flake8-mutable==1.2.0 + flake8-pep3101==1.2.1 + pep8-naming==0.7.0 + mccabe==0.6.1 + +basepython = python3.7 + +commands = + flake8 {posargs:src/momoapi} + [flake8] select = B,C,E,F,I,N,S,W @@ -196,3 +222,38 @@ ignore = W504, + +## +# Mypy linting +## + +[testenv:mypy] + +basepython = python3.7 + +skip_install = True + +deps = + mypy==0.650 + +commands = + + "mypy" --config-file="{toxinidir}/tox.ini" {posargs:src} + + +[mypy] + +# Global settings + +warn_redundant_casts = True +warn_unused_ignores = True +strict_optional = True +show_column_numbers = True + +# Module default settings +# disallow_untyped_calls = True +disallow_untyped_defs = True +# warn_return_any = True + +# Need some stub files to get rid of this +ignore_missing_imports = True From 7732ace7e3f1fed54ef1409e8006f6e6bc6fb450 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 16:41:32 +0300 Subject: [PATCH 29/60] configure flake8 travis & coveralls --- .flake8 | 7 --- .travis.yml | 14 +++-- setup.py | 3 +- src/momoapi/cli.py | 37 ++++++++----- src/momoapi/client.py | 125 ++++++++++++++++++++++++++++-------------- src/momoapi/errors.py | 4 +- src/momoapi/utils.py | 24 ++++---- tox.ini | 4 +- 8 files changed, 136 insertions(+), 82 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index b730b16..0000000 --- a/.flake8 +++ /dev/null @@ -1,7 +0,0 @@ -[flake8] -# E501 is the "Line too long" error. We disable it because we use Black for -# code formatting. Black makes a best effort to keep lines under the max -# length, but can go over in some cases. -# W503 goes against PEP8 rules. It's disabled by default, but must be disabled -# explicitly when using `ignore`. -ignore = E501, W503 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 179ecc3..0aaf548 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,23 +8,29 @@ matrix: - os: linux dist: trusty sudo: false - env: TOXENV=py27-cover + env: TOXENV=flake8 + python: "3.7" + + - os: linux + dist: trusty + sudo: false + env: TOXENV=py27-cover,coveralls python: "2.7" - os: linux dist: trusty sudo: false python: "3.5" - env: TOXENV=py35-cover + env: TOXENV=py35-cover,coveralls - os: linux dist: trusty sudo: false python: "3.6" - env: TOXENV=py36-cover + env: TOXENV=py36-cover,coveralls - os: linux dist: xenial sudo: required python: "3.7" - env: TOXENV=py37-cover + env: TOXENV=py37-cover,coveralls script: - pip install tox - tox diff --git a/setup.py b/setup.py index 7d171af..a3592aa 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,8 @@ def read(*names, **kwargs): ], install_requires=[ 'requests == 2.21.0', - 'Click==7.0' + 'Click==7.0', + 'phonenumbers' # eg: 'aspectlib==1.1.1', 'six>=1.7', ], diff --git a/src/momoapi/cli.py b/src/momoapi/cli.py index 09f14d2..6ee24a2 100644 --- a/src/momoapi/cli.py +++ b/src/momoapi/cli.py @@ -14,28 +14,33 @@ Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration """ +import json +import time +import uuid + import click + import requests -import uuid -import time -import json -def generateToken(host, key): +def generate_token(host, key): data = {"providerCallbackHost": host} - token = "%s" % uuid.uuid4() - + token = str(uuid.uuid4()) headers = { - "X-Reference-Id": "%s" % token, + "X-Reference-Id": token, "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": key } - r = requests.post("https://ericssonbasicapi2.azure-api.net/v1_0/apiuser", data=json.dumps(data), headers=headers) + requests.post( + "https://ericssonbasicapi2.azure-api.net/v1_0/apiuser", + data=json.dumps(data), + headers=headers) time.sleep(5) del headers["X-Reference-Id"] - url = "https://ericssonbasicapi2.azure-api.net/v1_0/apiuser/%s/apikey" % token + url = "https://ericssonbasicapi2.azure-api.net/v1_0/apiuser/{0}/apikey".format( + token) res = requests.post(url, data=json.dumps({}), headers=headers) @@ -46,11 +51,17 @@ def generateToken(host, key): ret["UserId"] = token ret["APISecret"] = rr["apiKey"] - return "Here is your User Id and API secret : %s" % ret + return "Here is your User Id and API secret : {0}".format(ret) @click.command() -@click.option('--provider', prompt="providerCallBackHost", help='providerCallBackHost') -@click.option('--key', prompt="Ocp-Apim-Subscription-Key", help='Ocp-Apim-Subscription-Key') +@click.option( + '--provider', + prompt="providerCallBackHost", + help='providerCallBackHost') +@click.option( + '--key', + prompt="Ocp-Apim-Subscription-Key", + help='Ocp-Apim-Subscription-Key') def main(provider, key): - click.echo(generateToken(provider, key)) + click.echo(generate_token(provider, key)) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 45171e6..717a537 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -4,23 +4,22 @@ @author: Moses Mugisha """ -import requests -import textwrap + import json import uuid -from .errors import APIError -from requests import Request, Session -from requests.auth import AuthBase -import base64 -from requests.auth import HTTPBasicAuth -from requests._internal_utils import to_native_string - try: from json.decoder import JSONDecodeError except ImportError: JSONDecodeError = ValueError -from requests.packages.urllib3.util.retry import Retry +import requests +from requests import Request, Session +from requests._internal_utils import to_native_string +from requests.auth import AuthBase +from requests.auth import HTTPBasicAuth + + +from .errors import APIError from .utils import requests_retry_session @@ -49,7 +48,13 @@ def __call__(self, r): class MomoApi(object): - def __init__(self, auth_key, user_id, api_secret, base_url="https://ericssonbasicapi2.azure-api.net", ** kwargs): + def __init__( + self, + auth_key, + user_id, + api_secret, + base_url="https://ericssonbasicapi2.azure-api.net", + ** kwargs): super(MomoApi, self).__init__(**kwargs) self._session = Session() self.api_secret = api_secret @@ -59,8 +64,12 @@ def __init__(self, auth_key, user_id, api_secret, base_url="https://ericssonbasi def request(self, method, url, headers, post_data=None): self.authToken = self.getAuthToken().json()["access_token"] - request = Request(method, url, data=json.dumps(post_data), - headers=headers, auth=MoMoAuth("%s" % self.authToken)) + request = Request( + method, + url, + data=json.dumps(post_data), + headers=headers, + auth=MoMoAuth(self.authToken)) prepped = self._session.prepare_request(request) @@ -85,15 +94,11 @@ def interpret_response(self, resp): return resp def handle_error_response(self, rbody, rcode, resp, rheaders): - try: - error_data = resp['error'] - except (KeyError, TypeError): - raise APIError( - "Invalid response object from API: %r (HTTP response code " - "was %d)" % (rbody, rcode), - rbody, rcode, resp) - raise "" + raise APIError( + "Invalid response object from API: {0} (HTTP response code " + "was {1})".format(rbody, rcode), + rbody, rcode, resp) def request_headers(self, api_key, method): headers = {} @@ -102,21 +107,40 @@ def request_headers(self, api_key, method): def getAuthToken(self): data = json.dumps({}) - auth = "%s:%s" % (self.user_id, self.api_secret) - bs64 = base64.b64encode(auth.encode()) headers = { "Content-Type": "application/json", - - "Ocp-Apim-Subscription-Key": "%s" % self.auth_key + "Ocp-Apim-Subscription-Key": self.auth_key } - r = requests.post(self.base_url + "/collection/token/", - auth=HTTPBasicAuth(self.user_id, self.api_secret), data=data, headers=headers) - return r - - def requestToPay(self, mobile, amount, product_id, note="", message="", currency="EUR", environment="sandbox", **kwargs): + response = requests.post( + + "{0}/collection/token/".format(self.base_url), + auth=HTTPBasicAuth( + self.user_id, + self.api_secret), + data=data, + headers=headers) + return response + + def requestToPay( + self, + mobile: str, + amount, + product_id, + note="", + message="", + currency="EUR", + environment="sandbox", + **kwargs): ref = str(uuid.uuid4()) - data = {"payer": {"partyIdType": "MSISDN", "partyId": mobile}, "payeeNote": note, - "payerMessage": message, "externalId": product_id, "currency": currency, "amount": amount} + data = { + "payer": { + "partyIdType": "MSISDN", + "partyId": mobile}, + "payeeNote": note, + "payerMessage": message, + "externalId": product_id, + "currency": currency, + "amount": amount} headers = { "X-Target-Environment": environment, "Content-Type": "application/json", @@ -125,8 +149,8 @@ def requestToPay(self, mobile, amount, product_id, note="", message="", currency } - url = self.base_url + "/collection/v1_0/requesttopay" - res = self.request("POST", url, headers, data) + url = "{0}/collection/v1_0/requesttopay".format(self.base_url) + self.request("POST", url, headers, data) return {"transaction_ref": ref} def getBalance(self, environment="sandbox"): @@ -135,11 +159,15 @@ def getBalance(self, environment="sandbox"): "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": self.auth_key } - url = self.base_url + "/collection/v1_0/account/balance" + url = "{0}/collection/v1_0/account/balance".format(self.base_url) res = self.request("GET", url, headers) return res.json() - def getTransactionStatus(self, transaction_id, environment="sandbox", **kwargs): + def getTransactionStatus( + self, + transaction_id, + environment="sandbox", + **kwargs): headers = { "X-Target-Environment": environment, @@ -150,7 +178,15 @@ def getTransactionStatus(self, transaction_id, environment="sandbox", **kwargs): res = self.request("GET", url, headers) return res.json() - def transfer(self, amount, mobile, note="", message="", currency="EUR", environment="sandbox", **kwargs): + def transfer( + self, + amount, + mobile, + note="", + message="", + currency="EUR", + environment="sandbox", + **kwargs): external_ref = str(uuid.uuid4()) data = { "amount": amount, @@ -169,11 +205,18 @@ def transfer(self, amount, mobile, note="", message="", currency="EUR", environm "Ocp-Apim-Subscription-Key": self.auth_key } url = self.base_url + "/v1_0/transfer" - res = self.request("POST", url, headers, data) + self.request("POST", url, headers, data) return {"transaction_ref": external_ref} @classmethod - def generateToken(self, host, api_user, api_key, base_url, environment="sandbox", **kwargs): + def generateToken( + cls, + host, + api_user, + api_key, + base_url, + environment="sandbox", + **kwargs): data = {"providerCallbackHost": host} headers = { @@ -182,9 +225,9 @@ def generateToken(self, host, api_user, api_key, base_url, environment="sandbox" "X-Target-Environment": environment, } - url = base_url + "/v1_0/apiuser/%s/apikey" % api_user + url = base_url + "/v1_0/apiuser/{0}/apikey".format(api_user) - res = requests.post(url, data=json.dumps({}), headers=headers) + res = requests.post(url, data=json.dumps(data), headers=headers) print(res) return res.json() diff --git a/src/momoapi/errors.py b/src/momoapi/errors.py index 0efa3e8..23ffa4e 100644 --- a/src/momoapi/errors.py +++ b/src/momoapi/errors.py @@ -1,6 +1,4 @@ -from .utils import ERROR_CODES - class MomoError(Exception): def __init__(self, message=None, http_body=None, http_status=None, @@ -35,7 +33,7 @@ def user_message(self): return self._message def __repr__(self): - return '%s(message=%r, http_status=%r, request_id=%r)' % ( + return '{0}(message={1}, http_status={2}, request_id={3})'.format( self.__class__.__name__, self._message, self.http_status, diff --git a/src/momoapi/utils.py b/src/momoapi/utils.py index 6a0da84..b454764 100644 --- a/src/momoapi/utils.py +++ b/src/momoapi/utils.py @@ -1,6 +1,9 @@ - -from phonenumbers import carrier import phonenumbers +from phonenumbers import carrier + +import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry ERROR_CODES = [ { @@ -151,23 +154,22 @@ def requests_retry_session( def validate_phone_number(number): obj = phonenumbers.parse(number, "UG") - if (phonenumbers.is_valid_numbe(obj) == False): - raise Exception("Invalid Phone number %s" % number) + if not phonenumbers.is_valid_numbe(obj): + raise Exception("Invalid Phone number {0}".format(number)) if (carrier.name_for_number(obj, "en") != "MTN"): - raise Exception("%s: Only MTN is supported at the moment" % number) + raise Exception( + "{0}: Only MTN is supported at the moment".format(number)) return "256" + obj.national_number def validate_number(number): number_types = (int, float) - if sys.version_info < (3, 0, 0): - number_types += (long,) if not type(number) in number_types: - raise Exception("%s: Must be a number" % number) + raise Exception("{0}: Must be a number".format(number)) return number def validate_string(_string): - if not type(_string) == str: - raise Exception("%s: Must be a string" % _string) - return string + if not isinstance(_string, str): + raise Exception("{0}: Must be a string".format(_string)) + return _string diff --git a/tox.ini b/tox.ini index 67ffec0..816370c 100644 --- a/tox.ini +++ b/tox.ini @@ -194,8 +194,8 @@ ignore = # multiple spaces before operator E221, - # too many blank lines - E302, + #camel case + N802, # too many blank lines E303, From d04262ac2864cf31baab57d4dc152d5ba033539c Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 16:52:32 +0300 Subject: [PATCH 30/60] cleanu[ --- .coveralls.yml | 2 -- src/momoapi/cli.py | 4 ++-- src/momoapi/client.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 .coveralls.yml diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 49800a0..0000000 --- a/.coveralls.yml +++ /dev/null @@ -1,2 +0,0 @@ -service_name: travis-pro -repo_token: eYYZeUsJmXfN0lkaFJYh7waOrHLWMSJht diff --git a/src/momoapi/cli.py b/src/momoapi/cli.py index 6ee24a2..c493f79 100644 --- a/src/momoapi/cli.py +++ b/src/momoapi/cli.py @@ -23,7 +23,7 @@ import requests -def generate_token(host, key): +def generateToken(host, key): data = {"providerCallbackHost": host} token = str(uuid.uuid4()) headers = { @@ -64,4 +64,4 @@ def generate_token(host, key): prompt="Ocp-Apim-Subscription-Key", help='Ocp-Apim-Subscription-Key') def main(provider, key): - click.echo(generate_token(provider, key)) + click.echo(generateToken(provider, key)) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 717a537..0df6158 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -123,7 +123,7 @@ def getAuthToken(self): def requestToPay( self, - mobile: str, + mobile, amount, product_id, note="", From 9f3e5a2274bbee01e17adf54db5c40c758394c3f Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 19:01:53 +0300 Subject: [PATCH 31/60] coveralls fix --- .travis.yml | 10 +++++----- README.md | 1 + src/momoapi/client.py | 1 + tox.ini | 7 +++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0aaf548..af1eb51 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,29 +8,29 @@ matrix: - os: linux dist: trusty sudo: false - env: TOXENV=flake8 + env: TOXENV=flake8,coveralls python: "3.7" - os: linux dist: trusty sudo: false - env: TOXENV=py27-cover,coveralls + env: TOXENV=py27-cover python: "2.7" - os: linux dist: trusty sudo: false python: "3.5" - env: TOXENV=py35-cover,coveralls + env: TOXENV=py35-cover - os: linux dist: trusty sudo: false python: "3.6" - env: TOXENV=py36-cover,coveralls + env: TOXENV=py36-cover - os: linux dist: xenial sudo: required python: "3.7" - env: TOXENV=py37-cover,coveralls + env: TOXENV=py37-cover script: - pip install tox - tox diff --git a/README.md b/README.md index 88e1c76..6aeb0d1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ MTN MoMo API Client for Python. [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) [![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=coverage)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) +[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=validations) # Installing diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 0df6158..01097f0 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -131,6 +131,7 @@ def requestToPay( currency="EUR", environment="sandbox", **kwargs): + # type: (String,String,String,String,String,String,String) -> json ref = str(uuid.uuid4()) data = { "payer": { diff --git a/tox.ini b/tox.ini index 816370c..f8ec37a 100644 --- a/tox.ini +++ b/tox.ini @@ -31,6 +31,7 @@ deps = mock commands = {posargs:pytest -vv --ignore=src} + passenv = @@ -42,10 +43,12 @@ passenv = [testenv:coveralls] deps = - coveralls + coveralls + PyYAML skip_install = true commands = - coveralls [] + coveralls +passenv = TRAVIS TRAVIS_* [testenv:lint] skip_install = true From f047e4a484dd17c7fab487011f548e504b703823 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 19:08:24 +0300 Subject: [PATCH 32/60] use python 3.6 for flake8 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index af1eb51..3f1ea7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ matrix: dist: trusty sudo: false env: TOXENV=flake8,coveralls - python: "3.7" + python: "3.6" - os: linux dist: trusty From 50a58eef0385115c0b47b945d4fcfeb31eb5bf33 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 5 Mar 2019 11:24:06 +0300 Subject: [PATCH 33/60] validation tests --- src/momoapi/client.py | 12 ++++++------ src/momoapi/errors.py | 4 ++++ src/momoapi/utils.py | 22 ++++++++++++++++------ tests/unitests/test_client.py | 21 +++++++++++++++++++-- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 01097f0..bb9cfab 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -20,7 +20,7 @@ from .errors import APIError -from .utils import requests_retry_session +from .utils import requests_retry_session, validate_phone_number, validate_uuid class Response: @@ -58,7 +58,7 @@ def __init__( super(MomoApi, self).__init__(**kwargs) self._session = Session() self.api_secret = api_secret - self.user_id = user_id + self.user_id = validate_uuid(user_id) self.auth_key = auth_key self.base_url = base_url @@ -136,12 +136,12 @@ def requestToPay( data = { "payer": { "partyIdType": "MSISDN", - "partyId": mobile}, + "partyId": validate_phone_number(mobile)}, "payeeNote": note, "payerMessage": message, "externalId": product_id, "currency": currency, - "amount": amount} + "amount": str(amount)} headers = { "X-Target-Environment": environment, "Content-Type": "application/json", @@ -190,12 +190,12 @@ def transfer( **kwargs): external_ref = str(uuid.uuid4()) data = { - "amount": amount, + "amount": str(amount), "currency": currency, "externalId": external_ref, "payee": { "partyIdType": "MSISDN", - "partyId": mobile + "partyId": validate_phone_number(mobile) }, "payerMessage": message, "payeeNote": note diff --git a/src/momoapi/errors.py b/src/momoapi/errors.py index 23ffa4e..b9cd63f 100644 --- a/src/momoapi/errors.py +++ b/src/momoapi/errors.py @@ -70,3 +70,7 @@ class TransferError(MomoError): class GeneralError(MomoError): pass + + +class ValidationError(Exception): + pass diff --git a/src/momoapi/utils.py b/src/momoapi/utils.py index b454764..1493a65 100644 --- a/src/momoapi/utils.py +++ b/src/momoapi/utils.py @@ -1,9 +1,11 @@ +from uuid import UUID import phonenumbers from phonenumbers import carrier import requests from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry +from .errors import ValidationError ERROR_CODES = [ { @@ -154,22 +156,30 @@ def requests_retry_session( def validate_phone_number(number): obj = phonenumbers.parse(number, "UG") - if not phonenumbers.is_valid_numbe(obj): - raise Exception("Invalid Phone number {0}".format(number)) + if not phonenumbers.is_valid_number(obj): + raise ValidationError("Invalid Phone number {0}".format(number)) if (carrier.name_for_number(obj, "en") != "MTN"): - raise Exception( + raise ValidationError( "{0}: Only MTN is supported at the moment".format(number)) - return "256" + obj.national_number + return "256{0}".format(obj.national_number) def validate_number(number): number_types = (int, float) if not type(number) in number_types: - raise Exception("{0}: Must be a number".format(number)) + raise ValidationError("{0}: Must be a number".format(number)) return number def validate_string(_string): if not isinstance(_string, str): - raise Exception("{0}: Must be a string".format(_string)) + raise ValidationError("{0}: Must be a string".format(_string)) + return _string + + +def validate_uuid(_string): + try: + _val = UUID(_string, version=4) + except ValueError: + raise ValidationError("{0}: Must be a valid uuid4 string".format(_string)) return _string diff --git a/tests/unitests/test_client.py b/tests/unitests/test_client.py index 7b1ceb0..ccdca9a 100644 --- a/tests/unitests/test_client.py +++ b/tests/unitests/test_client.py @@ -9,13 +9,14 @@ from requests import Request, Session from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session +from momoapi.errors import ValidationError class TestClient(unittest.TestCase): @mock.patch('requests.post', side_effect=mocked_requests_post) def setUp(self, mock_get): - client = MomoApi("APIKEY", "USERID", "APISECRET") + client = MomoApi("APIKEY", "0555e303-ae5b-4052-a77b-6d284cfc669c", "APISECRET") self.client = client def tearDown(self): @@ -25,11 +26,27 @@ def tearDown(self): @mock.patch('requests.get', side_effect=mocked_requests_get) def test_client_instantiate(self, mock_get): - client = MomoApi("APIKEY", "USERID", "APISECRET") + client = MomoApi("APIKEY", "0555e303-ae5b-4052-a77b-6d284cfc669c", "APISECRET") #request_mock.assert_requested("post", "/v1/accounts") assert isinstance(client, MomoApi) + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_uuid(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + client = MomoApi("APIKEY", "USERID", "APISECRET") + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_mobile(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + ref = self.client.requestToPay("256712123456", "600", "123456789", note="dd", + message="dd", currency="EUR", environment="sandbox") + with self.assertRaises(ValidationError): + ref = self.client.requestToPay("254712123456", "600", "123456789", note="dd", + message="dd", currency="EUR", environment="sandbox") + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) def test_request_to_pay(self, mock_get): From ee3845702105cacaa0b5d7a7a21f7421fbdd84b9 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Thu, 7 Mar 2019 12:04:02 +0300 Subject: [PATCH 34/60] [major] support all products --- README.md | 155 ++++++++++++++---- src/momoapi/client.py | 139 ++++++---------- src/momoapi/collection.py | 63 +++++++ src/momoapi/config.py | 100 +++++++++++ src/momoapi/disbursement.py | 63 +++++++ src/momoapi/errors.py | 4 + src/momoapi/remittance.py | 88 ++++++++++ src/momoapi/resources/account.py | 0 src/momoapi/utils.py | 6 +- tests/integration/features/account.feature | 0 .../integration/features/collections.feature | 36 +++- .../features/disbursements.feature | 9 + .../integration/features/remittances.feature | 9 + .../integration/features/remittences.feature | 0 tests/integration/test_account.py | 0 tests/integration/test_collection.py | 49 ++++++ tests/integration/test_collections.py | 0 tests/integration/test_disbursement.py | 50 ++++++ tests/integration/test_disbursements.py | 0 tests/integration/test_remittance.py | 50 ++++++ tests/integration/test_remittences.py | 0 tests/integration/test_sandbox.py | 2 +- .../{test_client.py => test_collection.py} | 46 ++++-- tests/unitests/test_disbursement.py | 84 ++++++++++ tests/unitests/test_remittance.py | 85 ++++++++++ tests/unitests/utils.py | 10 +- tox.ini | 12 +- 27 files changed, 910 insertions(+), 150 deletions(-) create mode 100644 src/momoapi/collection.py create mode 100644 src/momoapi/config.py create mode 100644 src/momoapi/disbursement.py create mode 100644 src/momoapi/remittance.py delete mode 100644 src/momoapi/resources/account.py delete mode 100644 tests/integration/features/account.feature create mode 100644 tests/integration/features/remittances.feature delete mode 100644 tests/integration/features/remittences.feature delete mode 100644 tests/integration/test_account.py create mode 100644 tests/integration/test_collection.py delete mode 100644 tests/integration/test_collections.py create mode 100644 tests/integration/test_disbursement.py delete mode 100644 tests/integration/test_disbursements.py create mode 100644 tests/integration/test_remittance.py delete mode 100644 tests/integration/test_remittences.py rename tests/unitests/{test_client.py => test_collection.py} (58%) create mode 100644 tests/unitests/test_disbursement.py create mode 100644 tests/unitests/test_remittance.py diff --git a/README.md b/README.md index 6aeb0d1..ebe92e3 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@ MTN MoMo API Client for Python. [![Build Status](https://travis-ci.com/sparkplug/momoapi-python.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-node) [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) -[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=coverage)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=master)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) -[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=validations) # Installing @@ -26,19 +25,35 @@ Additional instructions for installing this software are in `the installation in momo-api has a comprehensive test suite, which can be run by ``tox``:: - $ tox -l # to view all test environments - $ tox -e py27-cover # to run the tests for Python 2.7 - $ tox -e py34-cover # to run the tests for Python 3.4 +## to view all test environments + + ```bash + $ tox -l + ``` + ## to run the tests for Python 2.7 + + ```bash + $ tox -e py27-cover + + ``` + + ## to run the tests for Python 3.4 + + ```bash + $ tox -e py34-cover + ``` + ## Requirements * Python 2.7+ or Python 3.4+ (PyPy supported) +## Sandbox Environment -# Usage +# Creating a sandbox environment API user -Time to use the library. The goal is to create a `User ID` and `API Secret`. To do this, the API key from your profile on the MTN MoMo dashboard is needed. The library has a commandline app that helps you do just that when you enter the details as prompted on the commandline. +The library ships with a commandline application that helps to create sandbox credentials. It assumes you have created an account on `https://momodeveloper.mtn.com` and have your `Ocp-Apim-Subscription-Key` ```bash $ momoapi @@ -50,46 +65,128 @@ where `providerCallBackHost` is your callback host and `Ocp-Apim-Subscription-Ke ```bash Here is your User Id and API secret : {'apiKey': 'b0431db58a9b41faa8f5860230xxxxxx', 'UserId': '053c6dea-dd68-xxxx-xxxx-c830dac9f401'} - ``` -## Let's make calls. +## Configuration + +Each MOMO API product requires its own authentication details. i.e its own separate subscription key, user_id and api_secret. As such, we have to configure subscription keys for each product you will be using. + +In addition to this, In you may also need to specify your `BASE_URL`, `ENVIRONMENT` and `CALLBACK_HOST` if you are not using the defaults. +here is the full list of configuration options + + ```python + config={ + + "ENVIRONMENT": os.environ.get("ENVIRONMENT"),#Optional enviroment, either "sandbox" or "production". Default is 'sandbox' + "BASE_URL": os.environ.get("BASE_URL"),#An optional base url to the MTN Momo API. By default the staging base url will be used + "CALLBACK_HOST": os.environ.get("CALLBACK_HOST"),#The domain where you webhooks urls are hosted. + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"),# Primary Key for the `Collection` product. + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"),# User id of the collection product + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"),# API secret of the collection product + "REMITTANCE_USER_ID": os.environ.get("REMITTANCE_USER_ID"), # User id of the remittance product + "REMITTANCE_API_SECRET": os.environ.get("REMITTANCE_API_SECRET"),# API secret of the remittance product + "REMITTANCE_PRIMARY_KEY": os.envieon.get("REMITTANCE_PRIMARY_KEY"), #Primary Key for the 'Remittance' product. + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), # User id of the disbursement product + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENTS_API_SECRET"),# API secret of the Disbursemnet product + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), #Primary Key for the 'Disbursement' product. + } + ``` + +You will only need to configure the product(s) you will be using. + + +## Collections + +The collections client can be created with the following paramaters. Note that the `COLLECTION_USER_ID` and `COLLECTION_API_SECRET` for production are provided on the MTN OVA dashboard; + +- `COLLECTION_PRIMARY_KEY`: Primary Key for the `Collection` product. +- `COLLECTION_USER_ID`: For sandbox, use the one generated with the `momoapi` command. +- `COLLECTION_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. -We shall now import the library onto the commandline. Let us try to make a collection request. +You can create a collection client with the following ```python -from momoapi.client import MomoApi -client = MomoApi(APIKEY,USERID,APISECRET) -ref=client.requestToPay("256772123456", "600", "123456789", note="dd", message="dd", currency="EUR", environment="sandbox") +from momoapi.collection import Collection +import os +client = Collection({ + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), + }) ``` -So, what just happened? We create a client on the commandline, and made a `requestToPay` transaction. How do we know this happened? Still on the commandline, input `ref` +#### Methods + +1. `requestToPay: This operation is used to request a payment from a consumer (Payer). The payer will be asked to authorize the payment. The transaction is executed once the payer has authorized the payment. The transaction will be in status PENDING until it is authorized or declined by the payer or it is timed out by the system. Status of the transaction can be validated by using `getTransactionStatus` + +2. `getTransaction`: Retrieve transaction information using the `transactionId` returned by `requestToPay`. You can invoke it at intervals until the transaction fails or succeeds. If the transaction has failed, it will throw an appropriate error. + +3. `getBalance()`: Get the balance of the account. + +4. `isPayerActive: check if an account holder is registered and active in the system. + +#### Sample Code ```python ->>> ref +from momoapi.collection import Collection +import os +client = Collection({ + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), + }) + +client.requestToPay( + mobile="256772123456", amount="600", external_id="123456789", payee_note="dd", payer_message="dd", + currency="EUR") ``` -You should see a response similar to this: +## Disbursement + +The disbursements client can be created with the following paramaters. Note that the `DISBURSEMENT_USER_ID` and `DISBURSEMENT_API_SECRET` for production are provided on the MTN OVA dashboard; + +- `DISBURSEMENT_PRIMARY_KEY`: Primary Key for the `Disbursement` product. +- `DISBURSEMENT_USER_ID`: For sandbox, use the one generated with the `momoapi` command. +- `DISBURSEMENT_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. + +You can create a disbursements client with the following ```python ->>> ref -{'transaction_ref': '33a9d94b-6828-4879-xxxx-e0ecb946d465'} +from momoapi.collection import Disbursement +import os +client = Disbursement({ + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), + }) ``` -We can then use this `Transaction Reference` to get the status of the `Transaction` + +#### Methods + +1. `transfer` + +Used to transfer an amount from the owner’s account to a payee account. Status of the transaction can be validated by using the + +1. `getTransactionStatus`: Retrieve transaction information using the `transactionId` returned by `transfer`. You can invoke it at intervals until the transaction fails or succeeds. + +2. `getBalance()`: Get your account balance. + +3. `isPayerActive`: This method is used to check if an account holder is registered and active in the system. + +#### Sample Code ```python ->>> client.getTransactionStatus('33a9d94b-6828-4879-xxxx-e0ecb946d465') -{'financialTransactionId': '1854386795', 'externalId': '123456789', 'amount': '600', 'currency': 'EUR', 'payer': {'partyIdType': 'MSISDN', 'partyId': '256794631873'}, 'payerMessage': 'dd', 'payeeNote': 'dd', 'status': 'SUCCESSFUL'} +from momoapi.collection import Disbursement +import os +client = Disbursement({ + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), + }) + client.transfer(amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") ``` -Voila! - readme - installation - usage - reference/index - contributing - authors - changelog diff --git a/src/momoapi/client.py b/src/momoapi/client.py index bb9cfab..97c08a0 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -6,7 +6,6 @@ import json -import uuid try: from json.decoder import JSONDecodeError except ImportError: @@ -19,8 +18,9 @@ from requests.auth import HTTPBasicAuth +from .config import MomoConfig from .errors import APIError -from .utils import requests_retry_session, validate_phone_number, validate_uuid +from .utils import requests_retry_session class Response: @@ -46,21 +46,41 @@ def __call__(self, r): return r -class MomoApi(object): +class ClientInterface(): + def getAuthToken(self): + raise NotImplementedError + + def getBalance(self): + raise NotImplementedError + + def getTransactionStatus(self): + raise NotImplementedError + + +class Client(ClientInterface): + def getAuthToken(self): + return super().getAuthToken() + + def getBalance(self): + return super().getBalance() + + def getTransactionStatus(self): + return super().getTransactionStatus() + + +class MomoApi(ClientInterface): def __init__( self, - auth_key, - user_id, - api_secret, - base_url="https://ericssonbasicapi2.azure-api.net", + config, ** kwargs): super(MomoApi, self).__init__(**kwargs) self._session = Session() - self.api_secret = api_secret - self.user_id = validate_uuid(user_id) - self.auth_key = auth_key - self.base_url = base_url + self._config = MomoConfig(config) + + @property + def config(self): + return self._config def request(self, method, url, headers, post_data=None): self.authToken = self.getAuthToken().json()["access_token"] @@ -81,6 +101,7 @@ def request(self, method, url, headers, post_data=None): def interpret_response(self, resp): rcode = resp.status_code rheaders = resp.headers + print(resp) try: rbody = resp.json() @@ -105,110 +126,49 @@ def request_headers(self, api_key, method): return headers - def getAuthToken(self): + def getAuthToken(self, product, url, subscription_key): data = json.dumps({}) headers = { "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": self.auth_key + "Ocp-Apim-Subscription-Key": subscription_key } response = requests.post( - "{0}/collection/token/".format(self.base_url), + "{0}{1}".format(self.config.baseUrl, url), auth=HTTPBasicAuth( - self.user_id, - self.api_secret), + self.config.userId(product), + self.config.APISecret(product)), data=data, headers=headers) return response - def requestToPay( - self, - mobile, - amount, - product_id, - note="", - message="", - currency="EUR", - environment="sandbox", - **kwargs): - # type: (String,String,String,String,String,String,String) -> json - ref = str(uuid.uuid4()) - data = { - "payer": { - "partyIdType": "MSISDN", - "partyId": validate_phone_number(mobile)}, - "payeeNote": note, - "payerMessage": message, - "externalId": product_id, - "currency": currency, - "amount": str(amount)} - headers = { - "X-Target-Environment": environment, - "Content-Type": "application/json", - "X-Reference-Id": ref, - "Ocp-Apim-Subscription-Key": self.auth_key - - - } - url = "{0}/collection/v1_0/requesttopay".format(self.base_url) - self.request("POST", url, headers, data) - return {"transaction_ref": ref} - - def getBalance(self, environment="sandbox"): + def getBalance(self, url, subscription_key): headers = { - "X-Target-Environment": environment, + "X-Target-Environment": self.config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": self.auth_key + "Ocp-Apim-Subscription-Key": subscription_key } - url = "{0}/collection/v1_0/account/balance".format(self.base_url) + url = "{0}{1}".format(self.config.baseUrl, url) res = self.request("GET", url, headers) return res.json() def getTransactionStatus( self, transaction_id, - environment="sandbox", - **kwargs): + url, + subscription_key, + ** kwargs): headers = { - "X-Target-Environment": environment, + "X-Target-Environment": self.config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": self.auth_key + "Ocp-Apim-Subscription-Key": subscription_key } - url = self.base_url + "/collection/v1_0/requesttopay/" + transaction_id - res = self.request("GET", url, headers) + _url = self.config.baseUrl + url + transaction_id + print(_url) + res = self.request("GET", _url, headers) return res.json() - def transfer( - self, - amount, - mobile, - note="", - message="", - currency="EUR", - environment="sandbox", - **kwargs): - external_ref = str(uuid.uuid4()) - data = { - "amount": str(amount), - "currency": currency, - "externalId": external_ref, - "payee": { - "partyIdType": "MSISDN", - "partyId": validate_phone_number(mobile) - }, - "payerMessage": message, - "payeeNote": note - } - headers = { - "X-Target-Environment": environment, - "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": self.auth_key - } - url = self.base_url + "/v1_0/transfer" - self.request("POST", url, headers, data) - return {"transaction_ref": external_ref} - @classmethod def generateToken( cls, @@ -229,7 +189,6 @@ def generateToken( url = base_url + "/v1_0/apiuser/{0}/apikey".format(api_user) res = requests.post(url, data=json.dumps(data), headers=headers) - print(res) return res.json() diff --git a/src/momoapi/collection.py b/src/momoapi/collection.py new file mode 100644 index 0000000..efaa17e --- /dev/null +++ b/src/momoapi/collection.py @@ -0,0 +1,63 @@ +import uuid + +from .client import MomoApi +from .utils import validate_phone_number + + +class Collection(MomoApi): + def getAuthToken(self): + """ + Create an access token which can then be used + to authorize and authenticate towards the other end-points of the API + """ + url = "/collection/token/" + response = super().getAuthToken("COLLECTION", url, super().config.collectionsKey) + return response + + def getBalance(self): + url = "/collection/v1_0/account/balance" + + return super().getBalance(url, super().config.collectionsKey) + + def getTransactionStatus( + self, + transaction_id, + **kwargs): + url = "/collection/v1_0/requesttopay/" + + return super().getTransactionStatus( + transaction_id, url, super().config.collectionsKey) + + def requestToPay( + self, + mobile, + amount, + external_id, + payee_note="", + payer_message="", + currency="EUR", + **kwargs): + # type: (String,String,String,String,String,String,String) -> json + ref = str(uuid.uuid4()) + data = { + "payer": { + "partyIdType": "MSISDN", + "partyId": validate_phone_number(mobile)}, + "payeeNote": payee_note, + "payerMessage": payer_message, + "externalId": external_id, + "currency": currency, + "amount": str(amount)} + headers = { + "X-Target-Environment": super().config.environment, + "Content-Type": "application/json", + "X-Reference-Id": ref, + "Ocp-Apim-Subscription-Key": super().config.collectionsKey + + + } + if kwargs.get("callback_url"): + headers["X-Callback-Url"] = kwargs.get("callback_url") + url = "{0}/collection/v1_0/requesttopay".format(super().config.baseUrl) + self.request("POST", url, headers, data) + return {"transaction_ref": ref} diff --git a/src/momoapi/config.py b/src/momoapi/config.py new file mode 100644 index 0000000..7376814 --- /dev/null +++ b/src/momoapi/config.py @@ -0,0 +1,100 @@ +from .utils import validate_uuid +from .errors import ConfigurationError + + +class MomoConfig(object): + + def __init__(self, conf): + """ + + config={ + + ENVIRONMENT: os.environ.get("ENVIRONMENT"), + BASE_URL: os.environ.get("BASE_URL"), + CALLBACK_HOST: os.environ.get("CALLBACK_HOST"), + COLLECTION_PRIMARY_KEY: os.environ.get("COLLECTION_PRIMARY_KEY"), + COLLECTION_USER_ID: os.environ.get("COLLECTION_USER_ID"), + COLLECTION_API_SECRET: os.environ.get("COLLECTION_API_SECRET"), + + REMITTANCE_USER_ID: os.environ.get("REMITTANCE_USER_ID"), + REMITTANCE_API_SECRET: os.environ.get("REMITTANCE_API_SECRET"), + REMITTANCE_PRIMARY_KEY: os.envieon.get("REMITTANCE_PRIMARY_KEY") + + DISBURSEMENT_USER_ID: os.environ.get("DISBURSEMENT_USER_ID"), + DISBURSEMENT_API_SECRET: os.environ.get("DISBURSEMENTS_API_SECRET"), + DISBURSEMENT_PRIMARY_KEY: os.environ.get("DISBURSEMENT_PRIMARY_KEY"), + + + } + + + """ + self._config = conf + + def get_property(self, property_name): + if property_name not in self._config.keys(): + return None + return self._config[property_name] + + def userId(self, product): + key = self.get_property('{0}_USER_ID'.format(product)) + + if not key: + raise ConfigurationError( + "{0}_USER_ID is missing in the configuration".format(product)) + else: + return validate_uuid(key) + + def APISecret(self, product): + key = self.get_property('{0}_API_SECRET'.format(product)) + + if not key: + raise ConfigurationError( + "{0}_API_SECRET is missing in the configuration".format(product)) + else: + return key + + @property + def environment(self): + return self.get_property('ENVIRONMENT') or "sandbox" + + @property + def baseUrl(self): + return self.get_property( + 'BASE_URL') or "https://ericssonbasicapi2.azure-api.net" + + @property + def callbackHost(self): + key = self.get_property('CALLBACK_HOST') + if not key: + raise ConfigurationError( + "CALLBACK_HOST is missing in the configuration") + else: + return key + + @property + def collectionsKey(self): + key = self.get_property('COLLECTION_PRIMARY_KEY') + if not key: + raise ConfigurationError( + "COLLECTION_PRIMARY_KEY is missing in the configuration") + else: + return validate_uuid(key) + + @property + def disbursementsKey(self): + key = self.get_property('DISBURSEMENT_PRIMARY_KEY') + if not key: + raise ConfigurationError( + "DISBURSEMENT_PRIMARY_KEY is missing in the configuration") + else: + return validate_uuid(key) + + @property + def remittencesKey(self): + key = self.get_property('REMITTANCE_PRIMARY_KEY') + if not key: + raise ConfigurationError( + "REMITTANCE_PRIMARY_KEY is missing in the configuration") + else: + return validate_uuid(key) diff --git a/src/momoapi/disbursement.py b/src/momoapi/disbursement.py new file mode 100644 index 0000000..b427a0b --- /dev/null +++ b/src/momoapi/disbursement.py @@ -0,0 +1,63 @@ +from .client import MomoApi +import uuid +from .utils import validate_phone_number + + +class Disbursement(MomoApi): + + def getAuthToken(self): + """ + Create an access token which can then be used to authorize and authenticate towards the other end-points of the API. + """ + url = "/disbursement/token/" + response = super().getAuthToken( + "DISBURSEMENT", url, super().config.disbursementsKey) + return response + + def getBalance(self): + url = "/disbursement/v1_0/account/balance" + + return super().getBalance(url, super().config.disbursementsKey) + + def getTransactionStatus( + self, + transaction_id, + **kwargs): + url = "/disbursement/v1_0/transfer/" + + return super().getTransactionStatus( + transaction_id, url, super().config.disbursementsKey) + + def transfer( + self, + amount, + mobile, + external_id, + payee_note="", + payer_message="", + currency="EUR", + **kwargs): + ref = str(uuid.uuid4()) + data = { + "amount": str(amount), + "currency": currency, + "externalId": external_id, + "payee": { + "partyIdType": "MSISDN", + "partyId": validate_phone_number(mobile) + }, + "payerMessage": payer_message, + "payeeNote": payee_note + } + headers = { + "X-Target-Environment": super().config.environment, + "Content-Type": "application/json", + "Ocp-Apim-Subscription-Key": super().config.disbursementsKey, + "X-Reference-Id": ref, + } + if kwargs.get("callback_url"): + headers["X-Callback-Url"] = kwargs.get("callback_url") + url = super().config.baseUrl + "/disbursement/v1_0/transfer" + print(url) + self.request("POST", url, headers, data) + return {"transaction_ref": ref} diff --git a/src/momoapi/errors.py b/src/momoapi/errors.py index b9cd63f..19a7dc7 100644 --- a/src/momoapi/errors.py +++ b/src/momoapi/errors.py @@ -74,3 +74,7 @@ class GeneralError(MomoError): class ValidationError(Exception): pass + + +class ConfigurationError(Exception): + pass diff --git a/src/momoapi/remittance.py b/src/momoapi/remittance.py new file mode 100644 index 0000000..0648f1b --- /dev/null +++ b/src/momoapi/remittance.py @@ -0,0 +1,88 @@ +from .client import MomoApi +import uuid +from .utils import validate_phone_number + + +class Remittance(MomoApi): + def getAuthToken(self): + """Generate access token which can then be use to authorize and authenticate towards the other end-points of the API""" + + url = "/remittance/token/" + response = super().getAuthToken("REMITTANCE", url, super().config.remittencesKey) + return response + + def getBalance(self): + url = "/remittance/v1_0/account/balance" + return super().getBalance(url, super().config.remittencesKey) + + def getTransactionStatus( + self, + transaction_id, + **kwargs): + """ + get the status of a transfer + """ + url = "/remittance/v1_0/transfer/" + + return super().getTransactionStatus( + transaction_id, url, super().config.remittencesKey) + + def transfer( + self, + amount, + mobile, + external_id, + payer_message, + payee_note, + currency="EUR", + **kwargs): + """ + Transfer operation is used to transfer an amount from the own account to + a payee account + + """ + + ref = str(uuid.uuid4()) + data = { + "amount": str(amount), + "currency": currency, + "externalId": external_id, + "payee": { + "partyIdType": "MSISDN", + "partyId": validate_phone_number(mobile)}, + + "payerMessage": payer_message, + "payeeNote": payee_note + } + + headers = { + "X-Target-Environment": super().config.environment, + "Content-Type": "application/json", + "X-Reference-Id": ref, + "Ocp-Apim-Subscription-Key": super().config.remittencesKey + + } + + if kwargs.get("callback_url"): + headers["X-Callback-Url"] = kwargs.get("callback_url") + + url = "{0}/remittance/v1_0/transfer".format(super().config.baseUrl) + self.request("POST", url, headers, data) + return {"transaction_ref": ref} + + def isActive(self, mobile): + """ + Operation is used to check if an account holder is registered and + active in the system + + """ + + headers = { + "X-Target-Environment": self.config.environment, + "Content-Type": "application/json", + "Ocp-Apim-Subscription-Key": super().config.remittencesKey + } + url = "{0}/remittance/v1_0/accountholder/MSISDN/{1}/active".format( + super().config.baseUrl, mobile) + res = self.request("GET", url, headers) + return res.json() diff --git a/src/momoapi/resources/account.py b/src/momoapi/resources/account.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/momoapi/utils.py b/src/momoapi/utils.py index 1493a65..a34b0eb 100644 --- a/src/momoapi/utils.py +++ b/src/momoapi/utils.py @@ -139,6 +139,7 @@ def requests_retry_session( backoff_factor=0.3, status_forcelist=(502, 504), session=None, + **kwargs ): session = session or requests.Session() retry = Retry( @@ -179,7 +180,8 @@ def validate_string(_string): def validate_uuid(_string): try: - _val = UUID(_string, version=4) + UUID(_string, version=4) except ValueError: - raise ValidationError("{0}: Must be a valid uuid4 string".format(_string)) + raise ValidationError( + "{0}: Must be a valid uuid4 string".format(_string)) return _string diff --git a/tests/integration/features/account.feature b/tests/integration/features/account.feature deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/features/collections.feature b/tests/integration/features/collections.feature index 1250d7e..c1da4b1 100644 --- a/tests/integration/features/collections.feature +++ b/tests/integration/features/collections.feature @@ -1,8 +1,40 @@ -Feature: Get Payments +Feature: Collections Scenario: Request a payment from a consumer (Payer) - Given I have a valid collection auth token + Given I have a valid user_id, auth_secret, and collections subscription key + When I request for a payment with the following payment details + | note | amount | message | mobile | product_id | + | test payment | 600 | message | 0782631873 | 0001 | + + And I check for transaction Status + Then It should be successful + + Scenario: Failed Transfer + Given I have a valid user_id, auth_secret, and collections subscription key + When I enter the following payment details + | note | amount | message | mobile | product_id | + | test payment | 600 | message | 0782631873 | 0001 | + + And I check for transaction Status + Then It should be successful + + Scenario: Wrong Currency + Given I have a valid user_id, auth_secret, and collections subscription key When I enter the following payment details | note | amount | message | mobile | product_id | | test payment | 600 | message | 0782631873 | 0001 | + And I check for transaction Status + Then It should be successful + + Scenario: Non Mtn mobile + Given I have a valid user_id, auth_secret, and collections subscription key + When I enter the following payment details + | note | amount | message | mobile | product_id | + | test payment | 600 | message | 0782631873 | 0001 | + + And I check for transaction Status + Then It should be successful + + + diff --git a/tests/integration/features/disbursements.feature b/tests/integration/features/disbursements.feature index e69de29..1610f0a 100644 --- a/tests/integration/features/disbursements.feature +++ b/tests/integration/features/disbursements.feature @@ -0,0 +1,9 @@ +Feature: Disbursements + Scenario: Transfer Money to another account + Given I have a valid user_id, auth_secret, and disbursements subscription key + When I transfer with the following payment details + | note | amount | message | mobile | product_id | + | test payment | 600 | message | 0782631873 | 0001 | + + And I check for transaction Status + Then It should be successful \ No newline at end of file diff --git a/tests/integration/features/remittances.feature b/tests/integration/features/remittances.feature new file mode 100644 index 0000000..6d8871d --- /dev/null +++ b/tests/integration/features/remittances.feature @@ -0,0 +1,9 @@ +Feature: Remmittences + Scenario: Move money from one account to another + Given I have a valid user_id, auth_secret, and remittence subscription key + When I transfer with the following details + | note | amount | message | mobile | product_id | + | test payment | 600 | message | 0782631873 | 0001 | + + And I check for transaction Status + Then It should be successful \ No newline at end of file diff --git a/tests/integration/features/remittences.feature b/tests/integration/features/remittences.feature deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_account.py b/tests/integration/test_account.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_collection.py b/tests/integration/test_collection.py new file mode 100644 index 0000000..5e0a920 --- /dev/null +++ b/tests/integration/test_collection.py @@ -0,0 +1,49 @@ +from pytest_bdd import scenario, given, when, then, parsers +import re +import os +import pytest +from click.testing import CliRunner +from momoapi.collection import Collection + + +pytest.globalDict = {} + + +@scenario('features/collections.feature', 'Request a payment from a consumer (Payer)') +def test_collections(): + pass + + +@given("I have a valid user_id, auth_secret, and collections subscription key") +def user_credentials(): + config = { + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), + + } + client = Collection(config) + pytest.globalDict["client"] = client + + +@when("I request for a payment with the following payment details\n| note | amount | message | mobile | product_id |\n| test payment | 600 | message | 0782631873 | 0001 |") +def successful_request_to_pay(): + + ref = pytest.globalDict["client"].requestToPay( + mobile="256772123456", amount="600", external_id="123456789", payee_note="dd", payer_message="dd", + currency="EUR") + pytest.globalDict["ref"] = ref + + +@when("I check for transaction Status") +def check_transaction_status(): + status = pytest.globalDict["client"].getTransactionStatus(pytest.globalDict["ref"]["transaction_ref"]) + pytest.globalDict["status"] = status + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() + + +@then("It should be successful") +def successful_transaction(): + assert pytest.globalDict["status"]["status"] == "SUCCESSFUL" diff --git a/tests/integration/test_collections.py b/tests/integration/test_collections.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_disbursement.py b/tests/integration/test_disbursement.py new file mode 100644 index 0000000..a9db72d --- /dev/null +++ b/tests/integration/test_disbursement.py @@ -0,0 +1,50 @@ +from momoapi.disbursement import Disbursement +from pytest_bdd import scenario, given, when, then, parsers +import re +import os +import pytest +from click.testing import CliRunner +from momoapi.cli import generateToken + +pytest.globalDict = {} + + +@scenario('features/disbursements.feature', 'Transfer Money to another account') +def test_disbursements(): + pass + + +@given("I have a valid user_id, auth_secret, and disbursements subscription key") +def user_credentials(): + config = { + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), + } + client = Disbursement(config) + pytest.globalDict["client"] = client + + +@when("I transfer with the following payment details\n| note | amount | message | mobile | product_id |\n| test payment | 600 | message | 0782631873 | 0001 |") +def successful_transfer(): + ref = pytest.globalDict["client"].transfer( + amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", payer_message="dd", + currency="EUR") + pytest.globalDict["ref"] = ref + + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + +@when("I check for transaction Status") +def check_transaction_status(): + status = pytest.globalDict["client"].getTransactionStatus(pytest.globalDict["ref"]["transaction_ref"]) + pytest.globalDict["status"] = status + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() + + +@then("It should be successful") +def sucessful_transaction(): + assert pytest.globalDict["status"]["status"] == "SUCCESSFUL" diff --git a/tests/integration/test_disbursements.py b/tests/integration/test_disbursements.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_remittance.py b/tests/integration/test_remittance.py new file mode 100644 index 0000000..caa14ca --- /dev/null +++ b/tests/integration/test_remittance.py @@ -0,0 +1,50 @@ +from pytest_bdd import scenario, given, when, then, parsers +import re +import os +import pytest +from click.testing import CliRunner +from momoapi.cli import generateToken +from momoapi.remittance import Remittance + +pytest.globalDict = {} + + +@scenario('features/remittances.feature', 'Move money from one account to another') +def test_remittences(): + pass + + +@given("I have a valid user_id, auth_secret, and remittence subscription key") +def user_credentials(): + config = { + "REMITTANCE_USER_ID": os.environ.get("REMITTANCE_USER_ID"), + "REMITTANCE_API_SECRET": os.environ.get("REMITTANCE_API_SECRET"), + "REMITTANCE_PRIMARY_KEY": os.environ.get("REMITTANCE_PRIMARY_KEY"), + } + client = Remittance(config) + pytest.globalDict["client"] = client + + +@when("I transfer with the following details\n| note | amount | message | mobile | product_id |\n| test payment | 600 | message | 0782631873 | 0001 |") +def successful_transfer(): + ref = pytest.globalDict["client"].transfer( + amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", payer_message="dd", + currency="EUR") + pytest.globalDict["ref"] = ref + + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + +@when("I check for transaction Status") +def check_transaction_status(): + status = pytest.globalDict["client"].getTransactionStatus(pytest.globalDict["ref"]["transaction_ref"]) + pytest.globalDict["status"] = status + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() + + +@then("It should be successful") +def sucessful_transaction(): + assert pytest.globalDict["status"]["status"] == "SUCCESSFUL" diff --git a/tests/integration/test_remittences.py b/tests/integration/test_remittences.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_sandbox.py b/tests/integration/test_sandbox.py index fd8db08..f47288c 100644 --- a/tests/integration/test_sandbox.py +++ b/tests/integration/test_sandbox.py @@ -8,7 +8,7 @@ @scenario('features/sandbox.feature', 'Adding new account') -def test_sanbox_provisioning(): +def test_sandbox_provisioning(): pass diff --git a/tests/unitests/test_client.py b/tests/unitests/test_collection.py similarity index 58% rename from tests/unitests/test_client.py rename to tests/unitests/test_collection.py index ccdca9a..4af0a6c 100644 --- a/tests/unitests/test_client.py +++ b/tests/unitests/test_collection.py @@ -1,6 +1,7 @@ import unittest import pytest from momoapi.client import MomoApi +from momoapi.collection import Collection import types try: from unittest import mock @@ -12,11 +13,18 @@ from momoapi.errors import ValidationError -class TestClient(unittest.TestCase): +class TestCollections(unittest.TestCase): @mock.patch('requests.post', side_effect=mocked_requests_post) def setUp(self, mock_get): - client = MomoApi("APIKEY", "0555e303-ae5b-4052-a77b-6d284cfc669c", "APISECRET") + self.config = { + "COLLECTION_USER_ID": "0555e303-ae5b-4052-a77b-6d284cfc669c", + "COLLECTION_API_SECRET": "API_SECRET", + "COLLECTION_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + # "DISBURSEMENTS_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + # "REMITTENCES_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c" + } + client = Collection(self.config) self.client = client def tearDown(self): @@ -26,32 +34,36 @@ def tearDown(self): @mock.patch('requests.get', side_effect=mocked_requests_get) def test_client_instantiate(self, mock_get): - client = MomoApi("APIKEY", "0555e303-ae5b-4052-a77b-6d284cfc669c", "APISECRET") + + client = Collection(self.config) #request_mock.assert_requested("post", "/v1/accounts") - assert isinstance(client, MomoApi) + assert isinstance(client, Collection) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_invalid_uuid(self, mock_get): #client = MomoApi("APIKEY", "USERID", "APISECRET") with self.assertRaises(ValidationError): - client = MomoApi("APIKEY", "USERID", "APISECRET") + config = self.config + config["COLLECTION_PRIMARY_KEY"] = "invalid key" + client = Collection(config) + client.getAuthToken() @mock.patch('requests.get', side_effect=mocked_requests_get) def test_invalid_mobile(self, mock_get): #client = MomoApi("APIKEY", "USERID", "APISECRET") with self.assertRaises(ValidationError): - ref = self.client.requestToPay("256712123456", "600", "123456789", note="dd", - message="dd", currency="EUR", environment="sandbox") + ref = self.client.requestToPay(mobile="256712123456", amount="600", + external_id="123456789", payee_note="dd", payer_message="dd", currency="EUR") with self.assertRaises(ValidationError): - ref = self.client.requestToPay("254712123456", "600", "123456789", note="dd", - message="dd", currency="EUR", environment="sandbox") + ref = self.client.requestToPay(mobile="254712123456", amount="600", + external_id="123456789", payee_note="dd", payer_message="dd", currency="EUR") @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) def test_request_to_pay(self, mock_get): - ref = self.client.requestToPay("256772123456", "600", "123456789", note="dd", - message="dd", currency="EUR", environment="sandbox") + ref = self.client.requestToPay(mobile="256772123456", amount="600", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") assert isinstance(ref, dict) assert "transaction_ref" in ref.keys() @@ -71,12 +83,12 @@ def test_get_transaction_status(self, mock_get): assert "amount" in status.keys() assert "currency" in status.keys() - @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) - def test_transfer(self, mock): - ref = self.client.transfer("600", "256772123456", note="dd", - message="dd", currency="EUR", environment="sandbox") - assert isinstance(ref, dict) - assert "transaction_ref" in ref.keys() + # @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + # def test_transfer(self, mock): + # ref = self.client.transfer("600", "256772123456", note="dd", + # message="dd", currency="EUR", environment="sandbox") + #assert isinstance(ref, dict) + #assert "transaction_ref" in ref.keys() @mock.patch('requests.post', side_effect=mocked_requests_post) def test_generate_token(self, mock): diff --git a/tests/unitests/test_disbursement.py b/tests/unitests/test_disbursement.py new file mode 100644 index 0000000..ff4e359 --- /dev/null +++ b/tests/unitests/test_disbursement.py @@ -0,0 +1,84 @@ +import unittest +import pytest +from momoapi.client import MomoApi +from momoapi.disbursement import Disbursement +import types +try: + from unittest import mock +except ImportError: + import mock +from requests import Request, Session + +from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session +from momoapi.errors import ValidationError + + +class TestDisbursements(unittest.TestCase): + + @mock.patch('requests.post', side_effect=mocked_requests_post) + def setUp(self, mock_get): + self.config = { + "DISBURSEMENT_USER_ID": "USER_ID", + "DISBURSEMENT_API_SECRET": "API_SECRET", + # "COLLECTIONS_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + "DISBURSEMENT_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + # "REMITTENCES_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c" + } + client = Disbursement(self.config) + self.client = client + + def tearDown(self): + pass + # self.widget.dispose() + #self.widget = None + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_client_instantiate(self, mock_get): + + client = Disbursement(self.config) + + #request_mock.assert_requested("post", "/v1/accounts") + assert isinstance(client, Disbursement) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_uuid(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + config = self.config + config["COLLECTION_PRIMARY_KEY"] = "invalid key" + client = Disbursement(config) + client.getAuthToken() + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_mobile(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + ref = self.client.transfer(amount="600", mobile="2567721234569", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + with self.assertRaises(ValidationError): + ref = self.client.transfer(amount="600", mobile="256712123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_transfer(self, mock_get): + + ref = self.client.transfer(amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_balance(self, mock_get): + balance = self.client.getBalance() + assert isinstance(balance, dict) + assert "availableBalance" in balance.keys() + assert "currency" in balance.keys() + + # @mock.patch('requests.get', side_effect=mocked_requests_get) + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_transaction_status(self, mock_get): + status = self.client.getTransactionStatus("dummy") + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() diff --git a/tests/unitests/test_remittance.py b/tests/unitests/test_remittance.py new file mode 100644 index 0000000..4cba3a1 --- /dev/null +++ b/tests/unitests/test_remittance.py @@ -0,0 +1,85 @@ +import unittest +import pytest +from momoapi.client import MomoApi +from momoapi.remittance import Remittance +import types +try: + from unittest import mock +except ImportError: + import mock +from requests import Request, Session + +from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session +from momoapi.errors import ValidationError + + +class TestRemittences(unittest.TestCase): + + @mock.patch('requests.post', side_effect=mocked_requests_post) + def setUp(self, mock_get): + self.config = { + "REMITTANCE_USER_ID": "USER_ID", + "REMITTANCE_API_SECRET": "API_SECRET", + + # "COLLECTIONS_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + # "DISBURSEMENTS_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + "REMITTANCE_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c" + } + client = Remittance(self.config) + self.client = client + + def tearDown(self): + pass + # self.widget.dispose() + #self.widget = None + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_client_instantiate(self, mock_get): + + client = Remittance(self.config) + + #request_mock.assert_requested("post", "/v1/accounts") + assert isinstance(client, Remittance) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_uuid(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + config = self.config + config["REMITTANCE_PRIMARY_KEY"] = "invalid key" + client = Remittance(config) + client.getAuthToken() + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_mobile(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + ref = self.client.transfer(amount="600", mobile="256712123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + with self.assertRaises(ValidationError): + ref = self.client.transfer(amount="600", mobile="256712123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_transfer(self, mock_get): + + ref = self.client.transfer(amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_balance(self, mock_get): + balance = self.client.getBalance() + assert isinstance(balance, dict) + assert "availableBalance" in balance.keys() + assert "currency" in balance.keys() + + # @mock.patch('requests.get', side_effect=mocked_requests_get) + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_transaction_status(self, mock_get): + status = self.client.getTransactionStatus("dummy") + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() diff --git a/tests/unitests/utils.py b/tests/unitests/utils.py index 59dd1f4..d4eed6a 100644 --- a/tests/unitests/utils.py +++ b/tests/unitests/utils.py @@ -48,18 +48,18 @@ def mocked_requests_post(*args, **kwargs): def mocked_requests_session(*args, **kwargs): - if '/collection/token/' in args[1]: + if '/token/' in args[1]: return MockResponse({"access_token": "token"}, 200) - elif '/account/balance' in args[1]: + elif '/balance' in args[1]: return MockResponse({ "availableBalance": "500", "currency": "UGX" }, 200) elif "/requesttopay" in args[1] and args[0] == 'POST': return MockResponse({}, 200) - elif "transfer" in args[1]: + elif "transfer" in args[1] and args[0] == 'POST': return MockResponse({}, 200) - elif "/requesttopay" in args[1] and args[0] == 'GET': + elif ("/requesttopay" in args[1] or "/transfer" in args[1]) and args[0] == 'GET': return MockResponse({ "amount": 100, "currency": "UGX", @@ -71,3 +71,5 @@ def mocked_requests_session(*args, **kwargs): }, "status": "SUCCESSFUL" }, 200) + else: + return MockResponse({}, 200) diff --git a/tox.ini b/tox.ini index f8ec37a..34ca23d 100644 --- a/tox.ini +++ b/tox.ini @@ -31,14 +31,12 @@ deps = mock commands = {posargs:pytest -vv --ignore=src} - + passenv = - # See https://github.com/codecov/codecov-python/blob/5b9d539a6a09bc84501b381b563956295478651a/README.md#using-tox - codecov: TOXENV - codecov: CI - codecov: TRAVIS TRAVIS_* + COLLECTION_* REMITTANCE_* DISBURSEMENT_* TRAVIS TRAVIS_* + [testenv:coveralls] @@ -224,6 +222,10 @@ ignore = # line break after binary operator (W503 and W504 are opposites) W504, + I201, + + I100, + ## From eea0750a8ec99e86998147ee47cc0de28f287387 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 11 Mar 2019 17:55:43 +0300 Subject: [PATCH 35/60] fix python 2.7 compatibility issues --- src/momoapi/client.py | 10 ++++------ src/momoapi/collection.py | 17 +++++++++-------- src/momoapi/disbursement.py | 18 +++++++++--------- src/momoapi/remittance.py | 21 +++++++++++---------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 97c08a0..fdeee8f 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -3,8 +3,6 @@ @author: Moses Mugisha """ - - import json try: from json.decoder import JSONDecodeError @@ -59,16 +57,16 @@ def getTransactionStatus(self): class Client(ClientInterface): def getAuthToken(self): - return super().getAuthToken() + return super(Client, self).getAuthToken() def getBalance(self): - return super().getBalance() + return super(Client, self).getBalance() def getTransactionStatus(self): - return super().getTransactionStatus() + return super(Client, self).getTransactionStatus() -class MomoApi(ClientInterface): +class MomoApi(ClientInterface, object): def __init__( self, diff --git a/src/momoapi/collection.py b/src/momoapi/collection.py index efaa17e..ee52b29 100644 --- a/src/momoapi/collection.py +++ b/src/momoapi/collection.py @@ -4,20 +4,21 @@ from .utils import validate_phone_number -class Collection(MomoApi): +class Collection(MomoApi, object): def getAuthToken(self): """ Create an access token which can then be used to authorize and authenticate towards the other end-points of the API """ url = "/collection/token/" - response = super().getAuthToken("COLLECTION", url, super().config.collectionsKey) + response = super(Collection, self).getAuthToken( + "COLLECTION", url, super(Collection, self).config.collectionsKey) return response def getBalance(self): url = "/collection/v1_0/account/balance" - return super().getBalance(url, super().config.collectionsKey) + return super(Collection, self).getBalance(url, super(Collection, self).config.collectionsKey) def getTransactionStatus( self, @@ -25,8 +26,8 @@ def getTransactionStatus( **kwargs): url = "/collection/v1_0/requesttopay/" - return super().getTransactionStatus( - transaction_id, url, super().config.collectionsKey) + return super(Collection, self).getTransactionStatus( + transaction_id, url, super(Collection, self).config.collectionsKey) def requestToPay( self, @@ -49,15 +50,15 @@ def requestToPay( "currency": currency, "amount": str(amount)} headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Collection, self).config.environment, "Content-Type": "application/json", "X-Reference-Id": ref, - "Ocp-Apim-Subscription-Key": super().config.collectionsKey + "Ocp-Apim-Subscription-Key": super(Collection, self).config.collectionsKey } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = "{0}/collection/v1_0/requesttopay".format(super().config.baseUrl) + url = "{0}/collection/v1_0/requesttopay".format(super(Collection, self).config.baseUrl) self.request("POST", url, headers, data) return {"transaction_ref": ref} diff --git a/src/momoapi/disbursement.py b/src/momoapi/disbursement.py index b427a0b..6d71deb 100644 --- a/src/momoapi/disbursement.py +++ b/src/momoapi/disbursement.py @@ -3,21 +3,21 @@ from .utils import validate_phone_number -class Disbursement(MomoApi): +class Disbursement(MomoApi, object): def getAuthToken(self): """ Create an access token which can then be used to authorize and authenticate towards the other end-points of the API. """ url = "/disbursement/token/" - response = super().getAuthToken( - "DISBURSEMENT", url, super().config.disbursementsKey) + response = super(Disbursement, self).getAuthToken( + "DISBURSEMENT", url, super(Disbursement, self).config.disbursementsKey) return response def getBalance(self): url = "/disbursement/v1_0/account/balance" - return super().getBalance(url, super().config.disbursementsKey) + return super(Disbursement, self).getBalance(url, super(Disbursement, self).config.disbursementsKey) def getTransactionStatus( self, @@ -25,8 +25,8 @@ def getTransactionStatus( **kwargs): url = "/disbursement/v1_0/transfer/" - return super().getTransactionStatus( - transaction_id, url, super().config.disbursementsKey) + return super(Disbursement, self).getTransactionStatus( + transaction_id, url, super(Disbursement, self).config.disbursementsKey) def transfer( self, @@ -50,14 +50,14 @@ def transfer( "payeeNote": payee_note } headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Disbursement, self).config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": super().config.disbursementsKey, + "Ocp-Apim-Subscription-Key": super(Disbursement, self).config.disbursementsKey, "X-Reference-Id": ref, } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = super().config.baseUrl + "/disbursement/v1_0/transfer" + url = super(Disbursement, self).config.baseUrl + "/disbursement/v1_0/transfer" print(url) self.request("POST", url, headers, data) return {"transaction_ref": ref} diff --git a/src/momoapi/remittance.py b/src/momoapi/remittance.py index 0648f1b..c575fc8 100644 --- a/src/momoapi/remittance.py +++ b/src/momoapi/remittance.py @@ -3,17 +3,18 @@ from .utils import validate_phone_number -class Remittance(MomoApi): +class Remittance(MomoApi, object): def getAuthToken(self): """Generate access token which can then be use to authorize and authenticate towards the other end-points of the API""" url = "/remittance/token/" - response = super().getAuthToken("REMITTANCE", url, super().config.remittencesKey) + response = super(Remittance, self).getAuthToken( + "REMITTANCE", url, super(Remittance, self).config.remittencesKey) return response def getBalance(self): url = "/remittance/v1_0/account/balance" - return super().getBalance(url, super().config.remittencesKey) + return super(Remittance, self).getBalance(url, super(Remittance, self).config.remittencesKey) def getTransactionStatus( self, @@ -24,8 +25,8 @@ def getTransactionStatus( """ url = "/remittance/v1_0/transfer/" - return super().getTransactionStatus( - transaction_id, url, super().config.remittencesKey) + return super(Remittance, self).getTransactionStatus( + transaction_id, url, super(Remittance, self).config.remittencesKey) def transfer( self, @@ -56,17 +57,17 @@ def transfer( } headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Remittance, self).config.environment, "Content-Type": "application/json", "X-Reference-Id": ref, - "Ocp-Apim-Subscription-Key": super().config.remittencesKey + "Ocp-Apim-Subscription-Key": super(Remittance, self).config.remittencesKey } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = "{0}/remittance/v1_0/transfer".format(super().config.baseUrl) + url = "{0}/remittance/v1_0/transfer".format(super(Remittance, self).config.baseUrl) self.request("POST", url, headers, data) return {"transaction_ref": ref} @@ -80,9 +81,9 @@ def isActive(self, mobile): headers = { "X-Target-Environment": self.config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": super().config.remittencesKey + "Ocp-Apim-Subscription-Key": super(Remittance, self).config.remittencesKey } url = "{0}/remittance/v1_0/accountholder/MSISDN/{1}/active".format( - super().config.baseUrl, mobile) + super(Remittance, self).config.baseUrl, mobile) res = self.request("GET", url, headers) return res.json() From bbbc0b1e2f5aaafc3fdc0e24211101a743e91e86 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 11 Mar 2019 17:55:43 +0300 Subject: [PATCH 36/60] [major] fix python 2.7 compatibility issues --- src/momoapi/client.py | 10 ++++------ src/momoapi/collection.py | 17 +++++++++-------- src/momoapi/disbursement.py | 18 +++++++++--------- src/momoapi/remittance.py | 21 +++++++++++---------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 97c08a0..fdeee8f 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -3,8 +3,6 @@ @author: Moses Mugisha """ - - import json try: from json.decoder import JSONDecodeError @@ -59,16 +57,16 @@ def getTransactionStatus(self): class Client(ClientInterface): def getAuthToken(self): - return super().getAuthToken() + return super(Client, self).getAuthToken() def getBalance(self): - return super().getBalance() + return super(Client, self).getBalance() def getTransactionStatus(self): - return super().getTransactionStatus() + return super(Client, self).getTransactionStatus() -class MomoApi(ClientInterface): +class MomoApi(ClientInterface, object): def __init__( self, diff --git a/src/momoapi/collection.py b/src/momoapi/collection.py index efaa17e..ee52b29 100644 --- a/src/momoapi/collection.py +++ b/src/momoapi/collection.py @@ -4,20 +4,21 @@ from .utils import validate_phone_number -class Collection(MomoApi): +class Collection(MomoApi, object): def getAuthToken(self): """ Create an access token which can then be used to authorize and authenticate towards the other end-points of the API """ url = "/collection/token/" - response = super().getAuthToken("COLLECTION", url, super().config.collectionsKey) + response = super(Collection, self).getAuthToken( + "COLLECTION", url, super(Collection, self).config.collectionsKey) return response def getBalance(self): url = "/collection/v1_0/account/balance" - return super().getBalance(url, super().config.collectionsKey) + return super(Collection, self).getBalance(url, super(Collection, self).config.collectionsKey) def getTransactionStatus( self, @@ -25,8 +26,8 @@ def getTransactionStatus( **kwargs): url = "/collection/v1_0/requesttopay/" - return super().getTransactionStatus( - transaction_id, url, super().config.collectionsKey) + return super(Collection, self).getTransactionStatus( + transaction_id, url, super(Collection, self).config.collectionsKey) def requestToPay( self, @@ -49,15 +50,15 @@ def requestToPay( "currency": currency, "amount": str(amount)} headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Collection, self).config.environment, "Content-Type": "application/json", "X-Reference-Id": ref, - "Ocp-Apim-Subscription-Key": super().config.collectionsKey + "Ocp-Apim-Subscription-Key": super(Collection, self).config.collectionsKey } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = "{0}/collection/v1_0/requesttopay".format(super().config.baseUrl) + url = "{0}/collection/v1_0/requesttopay".format(super(Collection, self).config.baseUrl) self.request("POST", url, headers, data) return {"transaction_ref": ref} diff --git a/src/momoapi/disbursement.py b/src/momoapi/disbursement.py index b427a0b..6d71deb 100644 --- a/src/momoapi/disbursement.py +++ b/src/momoapi/disbursement.py @@ -3,21 +3,21 @@ from .utils import validate_phone_number -class Disbursement(MomoApi): +class Disbursement(MomoApi, object): def getAuthToken(self): """ Create an access token which can then be used to authorize and authenticate towards the other end-points of the API. """ url = "/disbursement/token/" - response = super().getAuthToken( - "DISBURSEMENT", url, super().config.disbursementsKey) + response = super(Disbursement, self).getAuthToken( + "DISBURSEMENT", url, super(Disbursement, self).config.disbursementsKey) return response def getBalance(self): url = "/disbursement/v1_0/account/balance" - return super().getBalance(url, super().config.disbursementsKey) + return super(Disbursement, self).getBalance(url, super(Disbursement, self).config.disbursementsKey) def getTransactionStatus( self, @@ -25,8 +25,8 @@ def getTransactionStatus( **kwargs): url = "/disbursement/v1_0/transfer/" - return super().getTransactionStatus( - transaction_id, url, super().config.disbursementsKey) + return super(Disbursement, self).getTransactionStatus( + transaction_id, url, super(Disbursement, self).config.disbursementsKey) def transfer( self, @@ -50,14 +50,14 @@ def transfer( "payeeNote": payee_note } headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Disbursement, self).config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": super().config.disbursementsKey, + "Ocp-Apim-Subscription-Key": super(Disbursement, self).config.disbursementsKey, "X-Reference-Id": ref, } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = super().config.baseUrl + "/disbursement/v1_0/transfer" + url = super(Disbursement, self).config.baseUrl + "/disbursement/v1_0/transfer" print(url) self.request("POST", url, headers, data) return {"transaction_ref": ref} diff --git a/src/momoapi/remittance.py b/src/momoapi/remittance.py index 0648f1b..c575fc8 100644 --- a/src/momoapi/remittance.py +++ b/src/momoapi/remittance.py @@ -3,17 +3,18 @@ from .utils import validate_phone_number -class Remittance(MomoApi): +class Remittance(MomoApi, object): def getAuthToken(self): """Generate access token which can then be use to authorize and authenticate towards the other end-points of the API""" url = "/remittance/token/" - response = super().getAuthToken("REMITTANCE", url, super().config.remittencesKey) + response = super(Remittance, self).getAuthToken( + "REMITTANCE", url, super(Remittance, self).config.remittencesKey) return response def getBalance(self): url = "/remittance/v1_0/account/balance" - return super().getBalance(url, super().config.remittencesKey) + return super(Remittance, self).getBalance(url, super(Remittance, self).config.remittencesKey) def getTransactionStatus( self, @@ -24,8 +25,8 @@ def getTransactionStatus( """ url = "/remittance/v1_0/transfer/" - return super().getTransactionStatus( - transaction_id, url, super().config.remittencesKey) + return super(Remittance, self).getTransactionStatus( + transaction_id, url, super(Remittance, self).config.remittencesKey) def transfer( self, @@ -56,17 +57,17 @@ def transfer( } headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Remittance, self).config.environment, "Content-Type": "application/json", "X-Reference-Id": ref, - "Ocp-Apim-Subscription-Key": super().config.remittencesKey + "Ocp-Apim-Subscription-Key": super(Remittance, self).config.remittencesKey } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = "{0}/remittance/v1_0/transfer".format(super().config.baseUrl) + url = "{0}/remittance/v1_0/transfer".format(super(Remittance, self).config.baseUrl) self.request("POST", url, headers, data) return {"transaction_ref": ref} @@ -80,9 +81,9 @@ def isActive(self, mobile): headers = { "X-Target-Environment": self.config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": super().config.remittencesKey + "Ocp-Apim-Subscription-Key": super(Remittance, self).config.remittencesKey } url = "{0}/remittance/v1_0/accountholder/MSISDN/{1}/active".format( - super().config.baseUrl, mobile) + super(Remittance, self).config.baseUrl, mobile) res = self.request("GET", url, headers) return res.json() From 54a2fd8167eb67c2dfea56a7d8dd9c083d05712d Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Mon, 18 Mar 2019 11:26:20 +0300 Subject: [PATCH 37/60] Update Coveralls badge to show coverage for validations branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ebe92e3..70aa924 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ MTN MoMo API Client for Python. [![Build Status](https://travis-ci.com/sparkplug/momoapi-python.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-node) [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) -[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=master)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=validations) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) From 9ed7b8340d71323fa3d20ef05abaf8aeb27fed00 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Mon, 18 Mar 2019 11:57:45 +0300 Subject: [PATCH 38/60] Update README --- README.md | 190 ++++++++++++++++++++++++++---------------------------- 1 file changed, 93 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 70aa924..48471b8 100644 --- a/README.md +++ b/README.md @@ -10,104 +10,80 @@ MTN MoMo API Client for Python. # Installing - -To install the latest version of Momo-api using pip:: +Add the latest version of the library to your project using pip: ```bash $ pip install momoapi - ``` -Additional instructions for installing this software are in `the installation instructions `_. - - -# Unit Tests - -momo-api has a comprehensive test suite, which can be run by ``tox``:: - -## to view all test environments - - ```bash - $ tox -l - ``` - ## to run the tests for Python 2.7 - - ```bash - $ tox -e py27-cover - - ``` - - ## to run the tests for Python 3.4 - - ```bash - $ tox -e py34-cover - ``` - +This library supports Python 2.7+ or Python 3.4+ (PyPy supported) +# Sandbox Environment -## Requirements +## Creating a sandbox environment API user -* Python 2.7+ or Python 3.4+ (PyPy supported) - -## Sandbox Environment - -# Creating a sandbox environment API user - -The library ships with a commandline application that helps to create sandbox credentials. It assumes you have created an account on `https://momodeveloper.mtn.com` and have your `Ocp-Apim-Subscription-Key` +Next, we need to get the `User ID` and `User Secret` and to do this we shall need to use the Primary Key for the Product to which we are subscribed, as well as specify a host. The library ships with a commandline application that helps to create sandbox credentials. It assumes you have created an account on `https://momodeveloper.mtn.com` and have your `Ocp-Apim-Subscription-Key`. ```bash +## within the project, on the command line. In this example, our domain is akabbo.ug $ momoapi $ providerCallBackHost: https://akabbo.ug $ Ocp-Apim-Subscription-Key: f83xx8d8xx6749f19a26e2265aeadbcdeg ``` -where `providerCallBackHost` is your callback host and `Ocp-Apim-Subscription-Key` is your API key for the specific product to which you are subscribed. The `API Key` is unique to the product and you will need an `API Key` for each product you use. You should get the following response. +The `providerCallBackHost` is your callback host and `Ocp-Apim-Subscription-Key` is your API key for the specific product to which you are subscribed. The `API Key` is unique to the product and you will need an `API Key` for each product you use. You should get a response similar to the following: ```bash Here is your User Id and API secret : {'apiKey': 'b0431db58a9b41faa8f5860230xxxxxx', 'UserId': '053c6dea-dd68-xxxx-xxxx-c830dac9f401'} ``` +These are the credentials we shall use for the sandbox environment. In production, these credentials are provided for you on the MTN OVA management dashboard after KYC requirements are met. + ## Configuration -Each MOMO API product requires its own authentication details. i.e its own separate subscription key, user_id and api_secret. As such, we have to configure subscription keys for each product you will be using. +Before we can fully utilize the library, we need to specify global configurations. The global configuration must contain the following: -In addition to this, In you may also need to specify your `BASE_URL`, `ENVIRONMENT` and `CALLBACK_HOST` if you are not using the defaults. -here is the full list of configuration options +* `BASE_URL`: An optional base url to the MTN Momo API. By default the staging base url will be used +* `ENVIRONMENT`: Optional enviroment, either "sandbox" or "production". Default is 'sandbox' +* `CALLBACK_HOST`: The domain where you webhooks urls are hosted. This is mandatory. + +Once you have specified the global variables, you can now provide the product-specific variables. Each MoMo API product requires its own authentication details i.e its own `Subscription Key`, `User ID` and `User Secret`, also sometimes refered to as the `API Secret`. As such, we have to configure subscription keys for each product you will be using. + +The full list of configuration options can be seen in the example below: ```python - config={ - - "ENVIRONMENT": os.environ.get("ENVIRONMENT"),#Optional enviroment, either "sandbox" or "production". Default is 'sandbox' - "BASE_URL": os.environ.get("BASE_URL"),#An optional base url to the MTN Momo API. By default the staging base url will be used - "CALLBACK_HOST": os.environ.get("CALLBACK_HOST"),#The domain where you webhooks urls are hosted. - "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"),# Primary Key for the `Collection` product. - "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"),# User id of the collection product - "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"),# API secret of the collection product - "REMITTANCE_USER_ID": os.environ.get("REMITTANCE_USER_ID"), # User id of the remittance product - "REMITTANCE_API_SECRET": os.environ.get("REMITTANCE_API_SECRET"),# API secret of the remittance product - "REMITTANCE_PRIMARY_KEY": os.envieon.get("REMITTANCE_PRIMARY_KEY"), #Primary Key for the 'Remittance' product. - "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), # User id of the disbursement product - "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENTS_API_SECRET"),# API secret of the Disbursemnet product - "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), #Primary Key for the 'Disbursement' product. - } - ``` - -You will only need to configure the product(s) you will be using. + config = { + "ENVIRONMENT": os.environ.get("ENVIRONMENT"), + "BASE_URL": os.environ.get("BASE_URL"), + "CALLBACK_HOST": os.environ.get("CALLBACK_HOST"), # Mandatory. + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), + "REMITTANCE_USER_ID": os.environ.get("REMITTANCE_USER_ID"), + "REMITTANCE_API_SECRET": os.environ.get("REMITTANCE_API_SECRET"), + "REMITTANCE_PRIMARY_KEY": os.envieon.get("REMITTANCE_PRIMARY_KEY"), + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENTS_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), +} +``` +You will only need to configure the variables for the product(s) you will be using. ## Collections The collections client can be created with the following paramaters. Note that the `COLLECTION_USER_ID` and `COLLECTION_API_SECRET` for production are provided on the MTN OVA dashboard; -- `COLLECTION_PRIMARY_KEY`: Primary Key for the `Collection` product. -- `COLLECTION_USER_ID`: For sandbox, use the one generated with the `momoapi` command. -- `COLLECTION_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. +* `COLLECTION_PRIMARY_KEY`: Primary Key for the `Collection` product on the developer portal. +* `COLLECTION_USER_ID`: For sandbox, use the one generated with the `momoapi` command. +* `COLLECTION_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. -You can create a collection client with the following +You can create a collection client with the following: ```python -from momoapi.collection import Collection import os +from momoapi.collection import Collection + client = Collection({ "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), @@ -115,78 +91,98 @@ client = Collection({ }) ``` -#### Methods +### Methods -1. `requestToPay: This operation is used to request a payment from a consumer (Payer). The payer will be asked to authorize the payment. The transaction is executed once the payer has authorized the payment. The transaction will be in status PENDING until it is authorized or declined by the payer or it is timed out by the system. Status of the transaction can be validated by using `getTransactionStatus` +1. `requestToPay`: This operation is used to request a payment from a consumer (Payer). The payer will be asked to authorize the payment. The transaction is executed once the payer has authorized the payment. The transaction will be in status PENDING until it is authorized or declined by the payer or it is timed out by the system. Status of the transaction can be validated by using `getTransactionStatus`. 2. `getTransaction`: Retrieve transaction information using the `transactionId` returned by `requestToPay`. You can invoke it at intervals until the transaction fails or succeeds. If the transaction has failed, it will throw an appropriate error. -3. `getBalance()`: Get the balance of the account. +3. `getBalance`: Get the balance of the account. -4. `isPayerActive: check if an account holder is registered and active in the system. +4. `isPayerActive`: check if an account holder is registered and active in the system. -#### Sample Code +### Sample Code ```python -from momoapi.collection import Collection import os +from momoapi.collection import Collection + client = Collection({ - "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), - "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), - "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), - }) + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), +}) client.requestToPay( - mobile="256772123456", amount="600", external_id="123456789", payee_note="dd", payer_message="dd", - currency="EUR") + mobile="256772123456", amount="600", external_id="123456789", payee_note="dd", payer_message="dd", currency="EUR") ``` ## Disbursement -The disbursements client can be created with the following paramaters. Note that the `DISBURSEMENT_USER_ID` and `DISBURSEMENT_API_SECRET` for production are provided on the MTN OVA dashboard; +The Disbursements client can be created with the following paramaters. Note that the `DISBURSEMENT_USER_ID` and `DISBURSEMENT_API_SECRET` for production are provided on the MTN OVA dashboard; -- `DISBURSEMENT_PRIMARY_KEY`: Primary Key for the `Disbursement` product. -- `DISBURSEMENT_USER_ID`: For sandbox, use the one generated with the `momoapi` command. -- `DISBURSEMENT_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. +* `DISBURSEMENT_PRIMARY_KEY`: Primary Key for the `Disbursement` product on the developer portal. +* `DISBURSEMENT_USER_ID`: For sandbox, use the one generated with the `momoapi` command. +* `DISBURSEMENT_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. You can create a disbursements client with the following ```python -from momoapi.collection import Disbursement import os +from momoapi.collection import Disbursement + client = Disbursement({ - "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), - "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), - "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), - }) + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), +}) ``` -#### Methods - -1. `transfer` +### Methods -Used to transfer an amount from the owner’s account to a payee account. Status of the transaction can be validated by using the +1. `transfer`: Used to transfer an amount from the owner’s account to a payee account. Status of the transaction can be validated by using the `getTransactionStatus` method. -1. `getTransactionStatus`: Retrieve transaction information using the `transactionId` returned by `transfer`. You can invoke it at intervals until the transaction fails or succeeds. +2. `getTransactionStatus`: Retrieve transaction information using the `transactionId` returned by `transfer`. You can invoke it at intervals until the transaction fails or succeeds. -2. `getBalance()`: Get your account balance. +2. `getBalance`: Get your account balance. 3. `isPayerActive`: This method is used to check if an account holder is registered and active in the system. #### Sample Code ```python -from momoapi.collection import Disbursement import os +from momoapi.collection import Disbursement + client = Disbursement({ - "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), - "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), - "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), - }) - client.transfer(amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", - payer_message="dd", currency="EUR") + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), +}) + +client.transfer(amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", payer_message="dd", currency="EUR") + ``` +# Unit Tests +This library has a comprehensive test suite, which can be run using the `tox` command: +## to view all test environments + +```bash +$ tox -l +``` +## to run the tests for Python 2.7 + +```bash +$ tox -e py27-cover +``` + +## to run the tests for Python 3.4 + +```bash +$ tox -e py34-cover +``` +Thank you. From 243e7223670864a2f9b8c90783eabe08567f255a Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Mon, 18 Mar 2019 12:08:33 +0300 Subject: [PATCH 39/60] Typo fixes --- src/momoapi/__main__.py | 2 +- src/momoapi/cli.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/momoapi/__main__.py b/src/momoapi/__main__.py index 572ff8d..4663176 100644 --- a/src/momoapi/__main__.py +++ b/src/momoapi/__main__.py @@ -1,5 +1,5 @@ """ -Entrypoint module, in case you use `python -mmomoapi_python`. +Entrypoint module, in case you use `python -momoapi_python`. Why does this file exist, and why __main__? For more info, read: diff --git a/src/momoapi/cli.py b/src/momoapi/cli.py index c493f79..09c9b65 100644 --- a/src/momoapi/cli.py +++ b/src/momoapi/cli.py @@ -19,7 +19,6 @@ import uuid import click - import requests From 123f6c6300bef44c71bd5efde9655ea89ead6230 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Thu, 21 Mar 2019 13:26:07 +0300 Subject: [PATCH 40/60] [major] bump up version --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3f1ea7d..8c31f70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ matrix: sudo: false env: TOXENV=flake8,coveralls python: "3.6" - - os: linux dist: trusty sudo: false From e0bf19cd49c42248152502985a0481dda9313fc2 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Thu, 21 Mar 2019 13:34:17 +0300 Subject: [PATCH 41/60] [minor] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a3592aa..38905a4 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ def read(*names, **kwargs): setup( name='momoapi', - version='0.1.0', + version='1.0.1', license='MIT license', description='Python wrapper for the MTN MoMo API.', long_description='%s\n%s' % ( From 114d66966c90d3d3c9aa4406c99ba952c0721829 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 26 Mar 2019 14:38:18 +0300 Subject: [PATCH 42/60] [major] rename momoapi to mtnmomo --- .travis.yml | 69 ++++++++--------- CONTRIBUTING.md | 90 ++++++++++++++++++++++ README.md | 20 ++--- setup.py | 6 +- src/{momoapi => mtnmomo}/__init__.py | 0 src/{momoapi => mtnmomo}/__main__.py | 4 +- src/{momoapi => mtnmomo}/cli.py | 0 src/{momoapi => mtnmomo}/client.py | 0 src/{momoapi => mtnmomo}/collection.py | 0 src/{momoapi => mtnmomo}/config.py | 0 src/{momoapi => mtnmomo}/disbursement.py | 0 src/{momoapi => mtnmomo}/errors.py | 0 src/{momoapi => mtnmomo}/remittance.py | 0 src/{momoapi => mtnmomo}/utils.py | 0 tests/integration/features/sandbox.feature | 4 +- tests/integration/test_collection.py | 2 +- tests/integration/test_disbursement.py | 4 +- tests/integration/test_remittance.py | 5 +- tests/integration/test_sandbox.py | 4 +- tests/unitests/test_collection.py | 7 +- tests/unitests/test_disbursement.py | 6 +- tests/unitests/test_remittance.py | 7 +- tox.ini | 2 +- 23 files changed, 161 insertions(+), 69 deletions(-) create mode 100644 CONTRIBUTING.md rename src/{momoapi => mtnmomo}/__init__.py (100%) rename src/{momoapi => mtnmomo}/__main__.py (75%) rename src/{momoapi => mtnmomo}/cli.py (100%) rename src/{momoapi => mtnmomo}/client.py (100%) rename src/{momoapi => mtnmomo}/collection.py (100%) rename src/{momoapi => mtnmomo}/config.py (100%) rename src/{momoapi => mtnmomo}/disbursement.py (100%) rename src/{momoapi => mtnmomo}/errors.py (100%) rename src/{momoapi => mtnmomo}/remittance.py (100%) rename src/{momoapi => mtnmomo}/utils.py (100%) diff --git a/.travis.yml b/.travis.yml index 8c31f70..6c076d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,50 +1,49 @@ language: python cache: directories: - - "$HOME/.cache/pip" - - "$HOME/.pyenv" + - "$HOME/.cache/pip" + - "$HOME/.pyenv" matrix: include: - - os: linux - dist: trusty - sudo: false - env: TOXENV=flake8,coveralls - python: "3.6" - - os: linux - dist: trusty - sudo: false - env: TOXENV=py27-cover - python: "2.7" - - os: linux - dist: trusty - sudo: false - python: "3.5" - env: TOXENV=py35-cover - - os: linux - dist: trusty - sudo: false - python: "3.6" - env: TOXENV=py36-cover - - os: linux - dist: xenial - sudo: required - python: "3.7" - env: TOXENV=py37-cover + - os: linux + dist: trusty + sudo: false + env: TOXENV=flake8,coveralls + python: '3.6' + - os: linux + dist: trusty + sudo: false + env: TOXENV=py27-cover + python: '2.7' + - os: linux + dist: trusty + sudo: false + python: '3.5' + env: TOXENV=py35-cover + - os: linux + dist: trusty + sudo: false + python: '3.6' + env: TOXENV=py36-cover + - os: linux + dist: xenial + sudo: required + python: '3.7' + env: TOXENV=py37-cover script: - - pip install tox - - tox +- pip install tox +- tox install: - - python setup.py install +- python setup.py install deploy: provider: pypi - user: mossplix - password: - secure: AjbgM0S84NnaPtkAI9JptI7lqtXU3HtcfNM8BLbSSxz/vUymPdU7vXCqwQM0UQWU4Ns5nC8kx6GOfXoikouOtQr7gLSlM7UQVwojsWGBUtVVi6D73ha2glhaaJ7Mf+9G3YMBYUHIUqtf7uVr9hKz5aADH0XIgRpHV4nNNamMduV4SvY4FZuvgQLaUrfwKtUY4t169vtHiReYWIOXEJHnA2PAsxNz99ZOVEvuxAX/Z4mFGkOuLtAvZnPxg3vinE8inQXcMNpLxwICFJM/sKAnMcICChHOErEIir4onOoodPVvVAvKfgb51wIM2q4YMyD8P3aTOILsWgk68EDJNO4w5T3R14eS9/ccRV7pW96lB/heaW92/vl5A59KJxuu6OEgk5gQaacFPv1IUccvc6U6vgHofGhjrsT1X8GI9ekl/M+gApRtL9ZYtT8lyRTIHBhayOKCITxFjivgUOcYZWiTFQCODSompgGdy4aoyIvDpmW2aztLcpr+modFWG/4GcJZxt14z7414LJvPvpyQj31KVEEo8jWdidJY/FNVXNCz8LS/3ri1Tz3p1Zof0sa4Zp3w6WpFxPeEXewyIS3b/R/Yv8w5n8f4byXtRGShvQe/i6sDy3oJAwiSyyjasYseE6WZpAajgk0dDODKZ0vLHkD1ZyHzkNOmqI7wo0pLdiyrKc= + user: sparkplug on: tags: true + password: + secure: AABEEwfWWBSb91c20ceFOUnriS9q+f+j/p0gSVsI8MvRES5lPsov9MGaiA6bf9hxLt9xIThpbY4AtwW4Y3U46tosPLW9kyeTgMINRAXmb/uU7h7hH9sTqmsZsSDwzJo/IdkqUszJmTlH/Km/6Ly3EDown/Hdd0FjTj/wJ3Z3PfkIR6GItz/h5QnIw8lx37PJhjE91keQqPxh6dvf8ewNz+f7QjxrFXy+O53pFwF2fpHmGTFa0MPBuRvBhi3fdn8dxKVZ5FI4tj6FBp8fPWoY5vOz6xRSDJ6W75IZPk24r7p4qH8oLRtCOcR+8kntftyrthaUacmFuWJWqlUSCU5p65qgJs4x3M1zzMutfCGPWHLnY/jhqpuAlu2QefBRc0vmVzs+Z/ieZSFHbjVuDrem9S1rt4HgCj7+VozQvLuz1LTWlOa/jh0phCK4+2BOeN20NNegjxNrLruz6kW9kneACvra+ppum0E8BRwknFkTmUPrLtnKd53h+lKZ7My7FRK8mjzXkj6RoOTjPoBZsDbvccXBc7ta8DlB07D9HGmYXwy2jCAAV3xdJy5Dwkhv2B7ck+g2xAHo/FdqmyLkhyh8BIZI6ixFbCGbwqN5sLFQ85+NDnfWA+HTh7UC1N6FNt4KBcnvx8Ho7cE/5piUPvhxw89nAH2beicnDz0ooWqKcn4= after_success: - - test $TRAVIS_BRANCH = "master" && .travis/semver.sh - +- test $TRAVIS_BRANCH = "master" && .travis/semver.sh env: global: secure: BfFLRnrlUwkU7FJui6XIrbN/nmxWkuDaGFrXLcqKSzWQmIwq8T/xIU2yje+wpjUx3mfErWXUgbf8EK/A83OwDgwDvZ4vlBojWA/aTKFUHtW7S7Ix1rTQbuxIzhEpBg2elIQzuKcH9hccGSzpcC6TqeuIWu/Soog7bofO6LT8SXqoRRPZnXgm07pKyJ/8wbUlJ34Ktoje5056zRsmEriFVf4/Qv6YciDzXtPbTRDCy+gXSAOFnr5y7d6Z8SQ3T5eki17X80Yn12nK0fymkTwbBi1F51dHkqSVYB/J6uBI5+K3c80gIw/sqhiHpk2WhfCB6ju/xAlHa9cSPKggJSPeX/v2Ujfk/7wDbH7mlBKsSisWKlQ/8oOd51aiXuHKP4yrj85URhxUNHoehYMg2JyQILTICFcSf4/AlbuEWFYtH34TBS+fF56kMxjHAdDCEX/i3VfAfAER3xsFTCTmQgvyZprPkekQOj5Cy1U+V5oPXRs94h0DqYbsMip1E1cbvevmjCzP3B2UZ/00mlp21a2OtNioxXaptgLsZjNS2kahc1yhDu6L3k2QZwxc58KdgL2vP+t7aQIcGO7gYimmn6GvXiDOjojXAVp0FPIHLnjNVHyqbQa6cma14sANKRZfyfuaGg3z763I7sdc5Su1iufZoD/+QRR4zrtyv3itfEvUBPo= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..28de1ce --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,90 @@ +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +Bug reports +=========== + +When `reporting a bug `_ please include: + + * Your operating system name and version. + * Any details about your local setup that might be helpful in troubleshooting. + * Detailed steps to reproduce the bug. + +Documentation improvements +========================== + +momoapi-python could always use more documentation, whether as part of the +official momoapi-python docs, in docstrings, or even on the web in blog posts, +articles, and such. + +Feature requests and feedback +============================= + +The best way to send feedback is to file an issue at https://github.com/mossplix/python-momoapi/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that code contributions are welcome :) + +Development +=========== + +To set up `python-momoapi` for local development: + +1. Fork `python-momoapi `_ + (look for the "Fork" button). +2. Clone your fork locally:: + + git clone git@github.com:your_name_here/python-momoapi.git + +3. Create a branch for local development:: + + git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: + + tox + +5. Commit your changes and push your branch to GitHub:: + + git add . + git commit -m "Your detailed description of your changes." + git push origin name-of-your-bugfix-or-feature + +6. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +If you need some code review or feedback while you're developing the code just make the pull request. + +For merging, you should: + +1. Include passing tests (run ``tox``) [1]_. +2. Update documentation when there's new API, functionality etc. +3. Add a note to ``CHANGELOG.rst`` about the changes. +4. Add yourself to ``AUTHORS.rst``. + +.. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will + `run the tests `_ for each change you add in the pull request. + + It will be slower though ... + +Tips +---- + +To run a subset of tests:: + + tox -e envname -- pytest -k test_myfeature + +To run all the test environments in *parallel* (you need to ``pip install detox``):: + + detox diff --git a/README.md b/README.md index 48471b8..9bb099b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ MTN MoMo API Client for Python. Add the latest version of the library to your project using pip: ```bash - $ pip install momoapi + $ pip install mtnmomo ``` This library supports Python 2.7+ or Python 3.4+ (PyPy supported) @@ -26,7 +26,7 @@ Next, we need to get the `User ID` and `User Secret` and to do this we shall nee ```bash ## within the project, on the command line. In this example, our domain is akabbo.ug -$ momoapi +$ mtnmomo $ providerCallBackHost: https://akabbo.ug $ Ocp-Apim-Subscription-Key: f83xx8d8xx6749f19a26e2265aeadbcdeg ``` @@ -75,14 +75,14 @@ You will only need to configure the variables for the product(s) you will be usi The collections client can be created with the following paramaters. Note that the `COLLECTION_USER_ID` and `COLLECTION_API_SECRET` for production are provided on the MTN OVA dashboard; * `COLLECTION_PRIMARY_KEY`: Primary Key for the `Collection` product on the developer portal. -* `COLLECTION_USER_ID`: For sandbox, use the one generated with the `momoapi` command. -* `COLLECTION_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. +* `COLLECTION_USER_ID`: For sandbox, use the one generated with the `mtnmomo` command. +* `COLLECTION_API_SECRET`: For sandbox, use the one generated with the `mtnmomo` command. You can create a collection client with the following: ```python import os -from momoapi.collection import Collection +from mtnmomo.collection import Collection client = Collection({ "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), @@ -105,7 +105,7 @@ client = Collection({ ```python import os -from momoapi.collection import Collection +from mtnmomo.collection import Collection client = Collection({ "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), @@ -122,14 +122,14 @@ client.requestToPay( The Disbursements client can be created with the following paramaters. Note that the `DISBURSEMENT_USER_ID` and `DISBURSEMENT_API_SECRET` for production are provided on the MTN OVA dashboard; * `DISBURSEMENT_PRIMARY_KEY`: Primary Key for the `Disbursement` product on the developer portal. -* `DISBURSEMENT_USER_ID`: For sandbox, use the one generated with the `momoapi` command. -* `DISBURSEMENT_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. +* `DISBURSEMENT_USER_ID`: For sandbox, use the one generated with the `mtnmomo` command. +* `DISBURSEMENT_API_SECRET`: For sandbox, use the one generated with the `mtnmomo` command. You can create a disbursements client with the following ```python import os -from momoapi.collection import Disbursement +from mtnmomo.collection import Disbursement client = Disbursement({ "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), @@ -152,7 +152,7 @@ client = Disbursement({ ```python import os -from momoapi.collection import Disbursement +from mtnmomo.collection import Disbursement client = Disbursement({ "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), diff --git a/setup.py b/setup.py index 38905a4..3ef3772 100644 --- a/setup.py +++ b/setup.py @@ -24,8 +24,8 @@ def read(*names, **kwargs): setup( - name='momoapi', - version='1.0.1', + name='mtnmomo', + version='2.0.0', license='MIT license', description='Python wrapper for the MTN MoMo API.', long_description='%s\n%s' % ( @@ -90,7 +90,7 @@ def read(*names, **kwargs): 'pytest-bdd'], "dev": ["semver"]}, entry_points={ 'console_scripts': [ - 'momoapi = momoapi.cli:main', + 'mtnmomo = mtnmomo.cli:main', ] }, ) diff --git a/src/momoapi/__init__.py b/src/mtnmomo/__init__.py similarity index 100% rename from src/momoapi/__init__.py rename to src/mtnmomo/__init__.py diff --git a/src/momoapi/__main__.py b/src/mtnmomo/__main__.py similarity index 75% rename from src/momoapi/__main__.py rename to src/mtnmomo/__main__.py index 4663176..203d83d 100644 --- a/src/momoapi/__main__.py +++ b/src/mtnmomo/__main__.py @@ -1,5 +1,5 @@ """ -Entrypoint module, in case you use `python -momoapi_python`. +Entrypoint module, in case you use `python -mtnmomo_python`. Why does this file exist, and why __main__? For more info, read: @@ -8,7 +8,7 @@ - https://docs.python.org/2/using/cmdline.html#cmdoption-m - https://docs.python.org/3/using/cmdline.html#cmdoption-m """ -from momoapi.cli import main +from mtnmomo.cli import main if __name__ == "__main__": main() diff --git a/src/momoapi/cli.py b/src/mtnmomo/cli.py similarity index 100% rename from src/momoapi/cli.py rename to src/mtnmomo/cli.py diff --git a/src/momoapi/client.py b/src/mtnmomo/client.py similarity index 100% rename from src/momoapi/client.py rename to src/mtnmomo/client.py diff --git a/src/momoapi/collection.py b/src/mtnmomo/collection.py similarity index 100% rename from src/momoapi/collection.py rename to src/mtnmomo/collection.py diff --git a/src/momoapi/config.py b/src/mtnmomo/config.py similarity index 100% rename from src/momoapi/config.py rename to src/mtnmomo/config.py diff --git a/src/momoapi/disbursement.py b/src/mtnmomo/disbursement.py similarity index 100% rename from src/momoapi/disbursement.py rename to src/mtnmomo/disbursement.py diff --git a/src/momoapi/errors.py b/src/mtnmomo/errors.py similarity index 100% rename from src/momoapi/errors.py rename to src/mtnmomo/errors.py diff --git a/src/momoapi/remittance.py b/src/mtnmomo/remittance.py similarity index 100% rename from src/momoapi/remittance.py rename to src/mtnmomo/remittance.py diff --git a/src/momoapi/utils.py b/src/mtnmomo/utils.py similarity index 100% rename from src/momoapi/utils.py rename to src/mtnmomo/utils.py diff --git a/tests/integration/features/sandbox.feature b/tests/integration/features/sandbox.feature index fd28ca9..21cd727 100644 --- a/tests/integration/features/sandbox.feature +++ b/tests/integration/features/sandbox.feature @@ -2,14 +2,14 @@ Feature: As a new library User, I should be able to create an account on the MOM Scenario: Adding new account Given a user with the domain sparkpl.ug and subscription key 99e9cb10e8c04ea0b788334dc6346f13 - When I run the command "momoapi" + When I run the command "mtnmomo" And I fill in the "providerCallBackHost" with "sparkpl.ug" And I fill in the "Ocp-Apim-Subscription-Key" with "99e9cb10e8c04ea0b788334dc6346f13" Then I should get back the apiKey Scenario: Wrong subscription Key Given a user with the domain sparkpl.ug and subscription key 99e9cb10e8c04ea0b788334dc6346f13dg - When I run the command "momoapi" + When I run the command "mtnmomo" And I fill in the "providerCallBackHost" with "sparkpl.ug" And I fill in the "Ocp-Apim-Subscription-Key" with "f83xx8d8xx6749f19a26e2265aeadg" Then I should get the message "Access denied due to invalid subscription key" diff --git a/tests/integration/test_collection.py b/tests/integration/test_collection.py index 5e0a920..0863bbc 100644 --- a/tests/integration/test_collection.py +++ b/tests/integration/test_collection.py @@ -3,7 +3,7 @@ import os import pytest from click.testing import CliRunner -from momoapi.collection import Collection +from mtnmomo.collection import Collection pytest.globalDict = {} diff --git a/tests/integration/test_disbursement.py b/tests/integration/test_disbursement.py index a9db72d..0d88508 100644 --- a/tests/integration/test_disbursement.py +++ b/tests/integration/test_disbursement.py @@ -1,10 +1,10 @@ -from momoapi.disbursement import Disbursement from pytest_bdd import scenario, given, when, then, parsers import re import os import pytest from click.testing import CliRunner -from momoapi.cli import generateToken +from mtnmomo.cli import generateToken +from mtnmomo.disbursement import Disbursement pytest.globalDict = {} diff --git a/tests/integration/test_remittance.py b/tests/integration/test_remittance.py index caa14ca..3b7c50c 100644 --- a/tests/integration/test_remittance.py +++ b/tests/integration/test_remittance.py @@ -3,8 +3,9 @@ import os import pytest from click.testing import CliRunner -from momoapi.cli import generateToken -from momoapi.remittance import Remittance + +from mtnmomo.cli import generateToken +from mtnmomo.remittance import Remittance pytest.globalDict = {} diff --git a/tests/integration/test_sandbox.py b/tests/integration/test_sandbox.py index f47288c..af93243 100644 --- a/tests/integration/test_sandbox.py +++ b/tests/integration/test_sandbox.py @@ -2,7 +2,7 @@ import re import pytest from click.testing import CliRunner -from momoapi.cli import generateToken +from mtnmomo.cli import generateToken pytest.globalDict = {} @@ -21,7 +21,7 @@ def provisioning_user(domain, api_key): return -@when('I run the command "momoapi"') +@when('I run the command "mtnmomo"') def run_command(): return diff --git a/tests/unitests/test_collection.py b/tests/unitests/test_collection.py index 4af0a6c..9da520e 100644 --- a/tests/unitests/test_collection.py +++ b/tests/unitests/test_collection.py @@ -1,16 +1,17 @@ import unittest import pytest -from momoapi.client import MomoApi -from momoapi.collection import Collection import types try: from unittest import mock except ImportError: import mock + from requests import Request, Session from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session -from momoapi.errors import ValidationError +from mtnmomo.errors import ValidationError +from mtnmomo.client import MomoApi +from mtnmomo.collection import Collection class TestCollections(unittest.TestCase): diff --git a/tests/unitests/test_disbursement.py b/tests/unitests/test_disbursement.py index ff4e359..c384b77 100644 --- a/tests/unitests/test_disbursement.py +++ b/tests/unitests/test_disbursement.py @@ -1,7 +1,5 @@ import unittest import pytest -from momoapi.client import MomoApi -from momoapi.disbursement import Disbursement import types try: from unittest import mock @@ -10,7 +8,9 @@ from requests import Request, Session from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session -from momoapi.errors import ValidationError +from mtnmomo.errors import ValidationError +from mtnmomo.client import MomoApi +from mtnmomo.disbursement import Disbursement class TestDisbursements(unittest.TestCase): diff --git a/tests/unitests/test_remittance.py b/tests/unitests/test_remittance.py index 4cba3a1..978e8a1 100644 --- a/tests/unitests/test_remittance.py +++ b/tests/unitests/test_remittance.py @@ -1,7 +1,5 @@ import unittest import pytest -from momoapi.client import MomoApi -from momoapi.remittance import Remittance import types try: from unittest import mock @@ -9,8 +7,11 @@ import mock from requests import Request, Session +from mtnmomo.client import MomoApi +from mtnmomo.remittance import Remittance +from mtnmomo.errors import ValidationError + from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session -from momoapi.errors import ValidationError class TestRemittences(unittest.TestCase): diff --git a/tox.ini b/tox.ini index 34ca23d..8583991 100644 --- a/tox.ini +++ b/tox.ini @@ -180,7 +180,7 @@ deps = basepython = python3.7 commands = - flake8 {posargs:src/momoapi} + flake8 {posargs:src/mtnmomo} [flake8] From b86a472d889a0c6d831bd244f5667e3debffa1fc Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 26 Mar 2019 15:52:18 +0300 Subject: [PATCH 43/60] [minor] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9bb099b..f5e35dc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ MTN MoMo API Client for Python. [![Build Status](https://travis-ci.com/sparkplug/momoapi-python.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-node) [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) -[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=validations) +[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) From d67798760e7dc7e929ceefbd4f1486a417ff6e70 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 26 Mar 2019 15:54:31 +0300 Subject: [PATCH 44/60] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5e35dc..4ce45fc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ MTN MoMo API Client for Python. [![Build Status](https://travis-ci.com/sparkplug/momoapi-python.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-node) [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) -[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=master)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) From c0659fcca8b9ba7bb727d0944d90da2ed40caae2 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 26 Mar 2019 15:58:52 +0300 Subject: [PATCH 45/60] [minor] fix pypi markdown rendering --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 38905a4..fc29a32 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ def read(*names, **kwargs): re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.md')), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.md')) ), + long_description_content_type='text/markdown', author='Sparkplug', author_email='mugisha@sparkpl.ug', url='https://github.com/sparkplug/momoapi-python', From ba2e10451034df8aa49d45cab74833166a8c5a7d Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 26 Mar 2019 16:04:31 +0300 Subject: [PATCH 46/60] [patch] configure markdown for pypi --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 27bd177..812252c 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ def read(*names, **kwargs): setup( name='mtnmomo', - version='2.0.0', + version='3.0.1', license='MIT license', description='Python wrapper for the MTN MoMo API.', long_description='%s\n%s' % ( From d1fc87b8dba5789d5811f69d71a7331cbdf23fa5 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:19:57 +0300 Subject: [PATCH 47/60] Move testing documentation to contributing guide --- README.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/README.md b/README.md index 4ce45fc..2db2bc6 100644 --- a/README.md +++ b/README.md @@ -164,25 +164,4 @@ client.transfer(amount="600", mobile="256772123456", external_id="123456789", pa ``` -# Unit Tests - -This library has a comprehensive test suite, which can be run using the `tox` command: - -## to view all test environments - -```bash -$ tox -l -``` -## to run the tests for Python 2.7 - -```bash -$ tox -e py27-cover -``` - -## to run the tests for Python 3.4 - -```bash -$ tox -e py34-cover -``` - Thank you. From 9a933f79ff8da8cd8f2b4fc908de4d9145a5f7d0 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:20:17 +0300 Subject: [PATCH 48/60] Add me to authors --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index 71ef76b..0da110b 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -3,3 +3,4 @@ Authors ======= * Moses Mugisha - http://sparkpl.ug +* Ray Besiga \ No newline at end of file From f22242ac96dd4b3d99f1b65d3272a5980568dfbd Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:20:43 +0300 Subject: [PATCH 49/60] Add resources file for future articles and links --- RESOURCES.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 RESOURCES.md diff --git a/RESOURCES.md b/RESOURCES.md new file mode 100644 index 0000000..e2899d7 --- /dev/null +++ b/RESOURCES.md @@ -0,0 +1,11 @@ +# Resources + +Here, we keep a list of resources for use of the library + +## Official Docs + +* [Developer Portal](https://momodeveloper.mtn.com/api-documentation) + +## Articles + + From c10d1af23869d774ec6a79541aaf818eab53f3b1 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:21:35 +0300 Subject: [PATCH 50/60] Update contributing guide --- CONTRIBUTING.md | 154 ++++++++++++++++++++++++++++++------------------ 1 file changed, 97 insertions(+), 57 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28de1ce..35792d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,90 +1,130 @@ -============ -Contributing -============ +# Contributing -Contributions are welcome, and they are greatly appreciated! Every -little bit helps, and credit will always be given. +First off, thank you for considering contributing to this Python MTN MoMo Library. It's people like you that make it such a great tool. Contributions are welcome, and they are greatly appreciated! -Bug reports -=========== +## Where do I go from here? -When `reporting a bug `_ please include: +If you've noticed a bug or have a question that doesn't belong on the +[Spectrum](https://spectrum.chat/momo-api-developers/) or [Stack Overflow](https://stackoverflow.com/), [search the issue tracker](https://github.com/sparkplug/momoapi-python/issues) to see if +someone else in the community has already created a ticket. If not, go ahead and +[make one](https://github.com/sparkplug/momoapi-python/issues/new/choose)! - * Your operating system name and version. - * Any details about your local setup that might be helpful in troubleshooting. - * Detailed steps to reproduce the bug. -Documentation improvements -========================== -momoapi-python could always use more documentation, whether as part of the -official momoapi-python docs, in docstrings, or even on the web in blog posts, -articles, and such. +## Fork & create a branch -Feature requests and feedback -============================= +If there is something you think you can fix, then fork the [repo](https://github.com/sparkplug/momoapi-python) and create a branch with a descriptive name. -The best way to send feedback is to file an issue at https://github.com/mossplix/python-momoapi/issues. +A good branch name would be (where issue #32 is the ticket you're working on): -If you are proposing a feature: +```sh +git checkout -b 32-add-swahili-translations +``` -* Explain in detail how it would work. -* Keep the scope as narrow as possible, to make it easier to implement. -* Remember that this is a volunteer-driven project, and that code contributions are welcome :) +## Get the test suite running -Development -=========== +This library has a comprehensive test suite, which can be run using the `tox` command: -To set up `python-momoapi` for local development: +To view all test environments -1. Fork `python-momoapi `_ - (look for the "Fork" button). -2. Clone your fork locally:: +```sh +$ tox -l +``` +To run the tests for Python 2.7 - git clone git@github.com:your_name_here/python-momoapi.git +```sh +$ tox -e py27-cover +``` -3. Create a branch for local development:: +To run the tests for Python 3.4 - git checkout -b name-of-your-bugfix-or-feature +```sh +$ tox -e py34-cover +``` - Now you can make your changes locally. +To run a subset of tests:: -4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: +```sh +tox -e envname -- pytest -k test_myfeature +``` - tox +To run all the test environments in *parallel*, you need to `pip install detox`: -5. Commit your changes and push your branch to GitHub:: +```sh +detox +``` - git add . - git commit -m "Your detailed description of your changes." - git push origin name-of-your-bugfix-or-feature +### Did you find a bug? -6. Submit a pull request through the GitHub website. +* **Ensure the bug was not already reported** by [searching all issues](https://github.com/sparkplug/momoapi-python/issues). -Pull Request Guidelines ------------------------ +* If you're unable to find an open issue addressing the problem, + [open a new one](https://github.com/sparkplug/momoapi-python/issues/new/choose). Be sure to include a **title and clear + description**, as much relevant information as possible, and a **code sample** + or an **executable test case** demonstrating the expected behavior that is not + occurring. -If you need some code review or feedback while you're developing the code just make the pull request. +* If possible, use the relevant bug report templates to create the issue. + Make the necessary changes to demonstrate the issue, and **paste the content into the + issue description** -For merging, you should: +### Implement your fix or feature -1. Include passing tests (run ``tox``) [1]_. -2. Update documentation when there's new API, functionality etc. -3. Add a note to ``CHANGELOG.rst`` about the changes. -4. Add yourself to ``AUTHORS.rst``. +At this point, you're ready to make your changes! Feel free to ask for help; +everyone is a beginner at first :smile_cat: -.. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will - `run the tests `_ for each change you add in the pull request. +If you are proposing a feature: - It will be slower though ... +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that code contributions are welcome :) -Tips ----- +If you would like to send us feedback, simply [file an issue](https://github.com/sparkplug/momoapi-python/issues/new/choose). -To run a subset of tests:: +## Local Development + +To set up `python-momoapi` for local development: + +1. Fork the repo. Look for the "Fork" button in the Github UI. +2. Clone your fork locally: + +```sh +git clone https://github.com/your_name_here/momoapi-python.git +``` + +3. Create a branch for local development: +```sh +git checkout -b name-of-your-bugfix-or-feature +``` + +Now you can make your changes locally. + +4. When you're done making changes, run all the checks, doc builder and spell checker with `tox`. +```sh +tox +``` +Make sure Tox is installed by following the instructions [here](http://tox.readthedocs.io/en/latest/install.html) + +5. Commit your changes and push your branch to GitHub:: + +```sh +git add . +git commit -m "Your detailed description of your changes." +git push origin name-of-your-bugfix-or-feature +``` + +6. Submit a pull request through the GitHub website. + +## Pull Request Guidelines + +Before you make a Pull Request, make sure of the following: + +1. Make sure your tests pass. Run `tox` beforehand. +2. Update documentation where necessary. +3. Note changes to `CHANGELOG.md`. +4. Add yourself to `AUTHORS.md`. - tox -e envname -- pytest -k test_myfeature +## Improvements -To run all the test environments in *parallel* (you need to ``pip install detox``):: +This library could always use more documentation, whether as part of the official docs, in docstrings, or even in blog posts and articles. We look forward to add them to our RESOURCES file. - detox From 8ed6eae218f2883e151d9e796d10261134a4049f Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:23:52 +0300 Subject: [PATCH 51/60] Add bugs and fixes header --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35792d5..f3a5c55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,8 @@ To run all the test environments in *parallel*, you need to `pip install detox`: detox ``` +## Bugs and Fixes + ### Did you find a bug? * **Ensure the bug was not already reported** by [searching all issues](https://github.com/sparkplug/momoapi-python/issues). From db36b67d7c683471cc23c0e5d2a7456408d8d631 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:40:20 +0300 Subject: [PATCH 52/60] Update guide for PR and maintainers --- CONTRIBUTING.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3a5c55..be573c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,6 +119,74 @@ git push origin name-of-your-bugfix-or-feature ## Pull Request Guidelines +### Make a Pull Request + +At this point, you should switch back to your master branch and make sure it's +up to date with `momoapi-python`'s master branch: + +```sh +git remote add upstream https://github.com/sparkplug/momoapi-python.git +git checkout master +git pull upstream master +``` + +Then update your feature branch from your local copy of master, and push it! + +```sh +git checkout 32-add-swahili-translations +git rebase master +git push --set-upstream origin 32-add-swahili-translations +``` + +Finally, go to GitHub and make a Pull Request :D + +TravisCI will run our test suite against all supported Python versions. We care +about quality, so your PR won't be merged until all tests pass. It's unlikely, +but it's possible that your changes pass tests in one Python version but fail in +another. In that case, you'll have to setup your development environment to use your Python version, and investigate what's going on! + +### Keeping your Pull Request updated + +If a maintainer asks you to "rebase" your PR, they're saying that a lot of code has changed, and that you need to update your branch so it's easier to merge. + +To learn more about rebasing in Git, there are a lot of [good](https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase) [resources](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) but here's the suggested workflow: + +```sh +git checkout 32-add-swahili-translations +git pull --rebase upstream master +git push --force-with-lease 32-add-swahili-translations +``` + +### Merging a PR (maintainers only) + +A PR can only be merged into master by a maintainer if: + +* It is passing CI. +* It has been approved by at least one maintainers. If it was a maintainer who opened the PR, only one extra approval is needed. +* It has no requested changes. +* It is up to date with current master. + +Any maintainer is allowed to merge a PR if all of these conditions are met. + +### Shipping a release (maintainers only) + +Maintainers need to do the following to push out a release: + +* Make sure all pull requests are in and that changelog is current +* Update version and changelog with new version number using semver +* If it's not a patch level release, create a stable branch for that release, + otherwise switch to the stable branch corresponding to the patch release you + want to ship: + + ```sh + git checkout master + git fetch momoapi-python + git rebase momoapi-python/master + # If the release is 2.1.x then this should be: 2-1-stable + git checkout -b N-N-stable + git push momoapi-python N-N-stable:N-N-stable + ``` + Before you make a Pull Request, make sure of the following: 1. Make sure your tests pass. Run `tox` beforehand. From 439c65aa6bf538fd006424c91ccb4fbac64d49df Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Tue, 7 May 2019 12:44:03 +0300 Subject: [PATCH 53/60] Update README --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2db2bc6..3394eb1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ -# MTN Mobile Money API Client +# MTN MoMo API Python Client + +Power your apps with our MTN MoMo API + +
+ Join our active, engaged community:
+ Website + | + Spectrum +

+
-MTN MoMo API Client for Python. [![Build Status](https://travis-ci.com/sparkplug/momoapi-python.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-node) [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) @@ -8,7 +17,9 @@ MTN MoMo API Client for Python. [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) -# Installing +# Usage + +## Installation Add the latest version of the library to your project using pip: From 8b93092f03594dfa3012e9d7c73cf7f15e075122 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Tue, 7 May 2019 13:07:12 +0300 Subject: [PATCH 54/60] Update Authors --- AUTHORS.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 0da110b..0343a4e 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -2,5 +2,7 @@ Authors ======= -* Moses Mugisha - http://sparkpl.ug -* Ray Besiga \ No newline at end of file +* Moses Mugisha +* Ray Besiga + +For [Sparkplug](http://sparkpl.ug) \ No newline at end of file From a5fa464e36ff5c34e163722e57ffa1de53acfaac Mon Sep 17 00:00:00 2001 From: Phillip Ahereza Date: Sat, 22 Jun 2019 08:32:06 +0300 Subject: [PATCH 55/60] fixed typo in config.py --- src/mtnmomo/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mtnmomo/config.py b/src/mtnmomo/config.py index 7376814..60c6912 100644 --- a/src/mtnmomo/config.py +++ b/src/mtnmomo/config.py @@ -18,7 +18,7 @@ def __init__(self, conf): REMITTANCE_USER_ID: os.environ.get("REMITTANCE_USER_ID"), REMITTANCE_API_SECRET: os.environ.get("REMITTANCE_API_SECRET"), - REMITTANCE_PRIMARY_KEY: os.envieon.get("REMITTANCE_PRIMARY_KEY") + REMITTANCE_PRIMARY_KEY: os.environ.get("REMITTANCE_PRIMARY_KEY") DISBURSEMENT_USER_ID: os.environ.get("DISBURSEMENT_USER_ID"), DISBURSEMENT_API_SECRET: os.environ.get("DISBURSEMENTS_API_SECRET"), From 9728556bd6beb55b407d4c694dc048bec33ff72c Mon Sep 17 00:00:00 2001 From: okellogabrielinnocent Date: Tue, 10 Sep 2019 13:57:51 +0300 Subject: [PATCH 56/60] update README.md to match implemented function - change getTransaction to getTransactionStatus on readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3394eb1..fc92547 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ client = Collection({ 1. `requestToPay`: This operation is used to request a payment from a consumer (Payer). The payer will be asked to authorize the payment. The transaction is executed once the payer has authorized the payment. The transaction will be in status PENDING until it is authorized or declined by the payer or it is timed out by the system. Status of the transaction can be validated by using `getTransactionStatus`. -2. `getTransaction`: Retrieve transaction information using the `transactionId` returned by `requestToPay`. You can invoke it at intervals until the transaction fails or succeeds. If the transaction has failed, it will throw an appropriate error. +2. `getTransactionStatus`: Retrieve transaction information using the `transactionId` returned by `requestToPay`. You can invoke it at intervals until the transaction fails or succeeds. If the transaction has failed, it will throw an appropriate error. 3. `getBalance`: Get the balance of the account. From 70100d9e280291e98f3d084e1ebf12f9ac4e446c Mon Sep 17 00:00:00 2001 From: arthurarty Date: Tue, 14 Jan 2020 12:09:45 +0300 Subject: [PATCH 57/60] ch(README) - Correct importation in sample code. - Change import for Disbursement class in sample code. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3394eb1..8e192a6 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ You can create a disbursements client with the following ```python import os -from mtnmomo.collection import Disbursement +from mtnmomo.disbursement import Disbursement client = Disbursement({ "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), @@ -163,7 +163,7 @@ client = Disbursement({ ```python import os -from mtnmomo.collection import Disbursement +from mtnmomo.disbursement import Disbursement client = Disbursement({ "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), From c978d594131fb770905d972d97113282d9510c76 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 27 May 2020 13:28:42 +0300 Subject: [PATCH 58/60] remove print statements --- src/mtnmomo/client.py | 3 --- src/mtnmomo/disbursement.py | 1 - 2 files changed, 4 deletions(-) diff --git a/src/mtnmomo/client.py b/src/mtnmomo/client.py index fdeee8f..2740009 100644 --- a/src/mtnmomo/client.py +++ b/src/mtnmomo/client.py @@ -99,7 +99,6 @@ def request(self, method, url, headers, post_data=None): def interpret_response(self, resp): rcode = resp.status_code rheaders = resp.headers - print(resp) try: rbody = resp.json() @@ -163,7 +162,6 @@ def getTransactionStatus( "Ocp-Apim-Subscription-Key": subscription_key } _url = self.config.baseUrl + url + transaction_id - print(_url) res = self.request("GET", _url, headers) return res.json() @@ -192,5 +190,4 @@ def generateToken( def close(self): if self._session is not None: - print("closing!") self._session.close() diff --git a/src/mtnmomo/disbursement.py b/src/mtnmomo/disbursement.py index 6d71deb..4472b95 100644 --- a/src/mtnmomo/disbursement.py +++ b/src/mtnmomo/disbursement.py @@ -58,6 +58,5 @@ def transfer( if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") url = super(Disbursement, self).config.baseUrl + "/disbursement/v1_0/transfer" - print(url) self.request("POST", url, headers, data) return {"transaction_ref": ref} From 06e2abd2ef5c4e8377c0337182ea93a3254781dc Mon Sep 17 00:00:00 2001 From: Peter Thaleikis Date: Wed, 25 Nov 2020 16:50:35 +0400 Subject: [PATCH 59/60] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36deeef..1a89e95 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ These are the credentials we shall use for the sandbox environment. In productio Before we can fully utilize the library, we need to specify global configurations. The global configuration must contain the following: * `BASE_URL`: An optional base url to the MTN Momo API. By default the staging base url will be used -* `ENVIRONMENT`: Optional enviroment, either "sandbox" or "production". Default is 'sandbox' +* `ENVIRONMENT`: Optional environment, either "sandbox" or "production". Default is 'sandbox' * `CALLBACK_HOST`: The domain where you webhooks urls are hosted. This is mandatory. Once you have specified the global variables, you can now provide the product-specific variables. Each MoMo API product requires its own authentication details i.e its own `Subscription Key`, `User ID` and `User Secret`, also sometimes refered to as the `API Secret`. As such, we have to configure subscription keys for each product you will be using. From 5914667d6e393d2879315190a1862fa715e9567f Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Mon, 14 Jul 2025 14:34:56 +0300 Subject: [PATCH 60/60] Update README.md --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a89e95..5e46eed 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,21 @@ # MTN MoMo API Python Client -Power your apps with our MTN MoMo API + +**🛑 This repository is no longer actively maintained.** + +As of July 14, 2025, this project is no longer under active development. This means: +* No new features will be added. +* Bugs will not be fixed. +* Pull requests will not be reviewed or merged. +* Issues will not be addressed. + +We appreciate your interest and contributions. +**Thank you.** + +
+ +Power your apps with our MTN MoMo API
Join our active, engaged community:
Website