diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..d1b4fbc --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,15 @@ +version = 1 +test_patterns = [ + "test_*.py" +] + +[[analyzers]] +name = "python" +enabled = true + +[analyzers.meta] +runtime_version = "3.x.x" + +[[analyzers]] +name = "test-coverage" +enabled = true diff --git a/.github/workflows/setup.yml b/.github/workflows/setup.yml new file mode 100644 index 0000000..c087e7b --- /dev/null +++ b/.github/workflows/setup.yml @@ -0,0 +1,33 @@ +name: Distribution + +on: [push, pull_request] + +jobs: + setup: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-setup-${{ hashFiles('**/requirements*.txt') }} + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install --upgrade pip wheel + pip install -r requirements-test.txt + - name: build + run: python ./setup.py sdist bdist_wheel + - name: twine + run: twine check dist/* + - name: Publish package + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@v1.6.1 + with: + user: __token__ + password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..77c0b70 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,61 @@ +name: Test + +on: [push, pull_request] + +jobs: + test: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: [3.5, 3.6, 3.7, 3.8] + exclude: + - os: windows-latest + python-version: 3.5 + - os: windows-latest + python-version: 3.6 + - os: windows-latest + python-version: 3.7 + - os: macos-latest + python-version: 3.5 + - os: macos-latest + python-version: 3.6 + - os: macos-latest + python-version: 3.7 + name: ${{ matrix.os }}, Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v2 + - name: Cache pip + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements*.txt') }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install pip dependencies + run: | + python -m pip install --upgrade pip wheel + pip install -r requirements.txt -r requirements-test.txt + - name: Test + run: | + py.test --cov=thepay thepay README.rst + - name: Coverage + run: | + coverage xml + - uses: codecov/codecov-action@v1 + with: + token: ${{secrets.CODECOV_TOKEN}} + flags: unittests + name: Python ${{ matrix.python-version }}, ${{ matrix.os }} + - name: DeepSource + if: matrix.os == 'ubuntu-latest' + continue-on-error: true + env: + DEEPSOURCE_DSN: ${{secrets.DEEPSOURCE_DSN}} + run: | + curl https://deepsource.io/cli | sh + ./bin/deepsource report --analyzer test-coverage --key python --value-file ./coverage.xml diff --git a/.gitignore b/.gitignore index d9eddf8..5ae7f87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ __pycache__/ *.py[cod] -.idea \ No newline at end of file +.idea +.pytest_cache/ +.venv/ +thepay.egg-info/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0da4581..0000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: python -python: - - "2.7" - - "3.3" - - "3.4" - - "3.5" - - "3.6" - - "pypy" - -# command to install dependencies -install: - - pip install . - - pip install coverage - - pip install coveralls - -# command to run tests -script: nosetests --logging-level=INFO --with-coverage --cover-tests --cover-package=thepay -after_success: coveralls \ No newline at end of file diff --git a/README.rst b/README.rst index b3dcdae..5158663 100644 --- a/README.rst +++ b/README.rst @@ -4,13 +4,16 @@ ThePay Library for accesing ThePay payment gateway from python -.. image:: https://badge.fury.io/py/thepay.svg - :target: http://badge.fury.io/py/thepay -.. image:: https://travis-ci.org/cuchac/thepay.svg?branch=master - :target: https://travis-ci.org/cuchac/thepay -.. image:: https://coveralls.io/repos/cuchac/thepay/badge.svg?branch=master&service=github - :target: https://coveralls.io/github/cuchac/thepay?branch=master - +.. image:: https://github.com/nijel/thepay/workflows/Test/badge.svg + :target: https://github.com/nijel/thepay/actions?query=workflow%3ATest + +.. image:: https://img.shields.io/pypi/v/nijel-thepay.svg + :target: https://pypi.org/project/nijel-thepay/ + :alt: PyPI package + +.. image:: https://codecov.io/gh/nijel/thepay/branch/master/graph/badge.svg + :target: https://codecov.io/gh/nijel/thepay + ======== Example ======== diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..35df97f --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,4 @@ +pre-commit +pytest +pytest-cov +twine diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..475a05f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +suds-community +requests +six diff --git a/setup.py b/setup.py index f4ad744..3c7fd23 100755 --- a/setup.py +++ b/setup.py @@ -1,16 +1,30 @@ -from setuptools import setup +#!/usr/bin/env python3 + +import os +from setuptools import find_packages, setup + +with open(os.path.join(os.path.dirname(__file__), "README.rst")) as readme: + LONG_DESCRIPTION = readme.read() + +with open("requirements.txt") as handle: + REQUIRES = handle.read().splitlines() +with open("requirements-test.txt") as handle: + REQUIRES_TEST = handle.read().splitlines()[1:] setup( - name='thepay', - version='0.4', - packages=['thepay'], - url='https://github.com/cuchac/thepay', - license='LGPL', - author='cuchac', - author_email='cuchac@email.cz', + name='nijel-thepay', + python_requires=">=3.5", + version='0.5', + author="Michal Čihař", + author_email="michal@cihar.com", description='ThePay API library', - install_requires=( - 'suds_jurko', - 'six', - ) + long_description=LONG_DESCRIPTION, + long_description_content_type="text/x-rst", + license='LGPL', + keywords=["payment", "thepay"], + url='https://github.com/nijel/thepay', + packages=find_packages(), + install_requires=REQUIRES, + setup_requires=["pytest-runner"], + tests_require=REQUIRES_TEST, ) diff --git a/thepay/__init__.py b/thepay/__init__.py index 6654097..e69de29 100644 --- a/thepay/__init__.py +++ b/thepay/__init__.py @@ -1 +0,0 @@ -__author__ = 'cuchac@email.cz' diff --git a/thepay/config.py b/thepay/config.py index e5f6bb4..b6d5b03 100644 --- a/thepay/config.py +++ b/thepay/config.py @@ -3,7 +3,7 @@ class Config(object): # ThePay API gateUrl = 'https://www.thepay.cz/demo-gate/' merchantId = 1 - accountId = 1 + accountId = 3 password = 'my$up3rsecr3tp4$$word' # Data API diff --git a/thepay/dataApi.py b/thepay/dataApi.py index 1f3237e..af1540a 100644 --- a/thepay/dataApi.py +++ b/thepay/dataApi.py @@ -1,5 +1,4 @@ from collections import OrderedDict -from datetime import tzinfo import suds.client diff --git a/thepay/gateApi.py b/thepay/gateApi.py new file mode 100644 index 0000000..f2bec80 --- /dev/null +++ b/thepay/gateApi.py @@ -0,0 +1,47 @@ +from collections import OrderedDict +import hashlib + +from suds.client import Client + +from six.moves.urllib.parse import urlencode + +from thepay.utils import SignatureMixin + + +class GateError(Exception): + pass + + +class GateApi(SignatureMixin): + def __init__(self, config): + """ + + :param config: Config + """ + self.config = config + self.client = None + + self.connect() + + def connect(self): + self.client = Client(self.config.webServicesWsdl) + + def _hashParam(self, params): + # this interface is using deprecated md5 hashing + return hashlib.md5(params).hexdigest() + + def _buildQuery(self, params): + # this interface uses different way of encoding + return urlencode(params) + + def cardCreateRecurrentPayment(self, merchantData, newMerchantData, value): + params = self._signParams(OrderedDict(( + ('merchantId', self.config.merchantId), + ('accountId', self.config.accountId), + ('merchantData', merchantData), + ('newMerchantData', newMerchantData), + ('value', value), + )), self.config.password) + response = self.client.service.cardCreateRecurrentPaymentRequest(**params) + if not response.status: + raise GateError(response.errorDescription) diff --git a/thepay/payment.py b/thepay/payment.py index 01ee91f..a4300c9 100644 --- a/thepay/payment.py +++ b/thepay/payment.py @@ -318,12 +318,12 @@ def getCreateUrl(self): class ReturnPayment(SignatureMixin): required_data = ( - "value", "currency", "description", "merchantData", + "value", "currency", "methodId", "description", "merchantData", "status", "paymentId", "ipRating", "isOffline", "needConfirm" ) optional_data = ( - "isConfirm", "variableSymbol", "methodId", "specificSymbol", + "isConfirm", "variableSymbol", "specificSymbol", "deposit", "isRecurring", "customerAccountNumber", "customerAccountName" ) diff --git a/thepay/test_dataApi.py b/thepay/test_dataApi.py new file mode 100644 index 0000000..39b29ed --- /dev/null +++ b/thepay/test_dataApi.py @@ -0,0 +1,52 @@ +from __future__ import print_function + +import unittest +import datetime +from datetime import tzinfo, timedelta + +from thepay.config import Config +from thepay.dataApi import DataApi + + +class UTC(tzinfo): + """UTC""" + + def utcoffset(self, dt): + return timedelta(0) + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return timedelta(0) + + +class DataApiTests(unittest.TestCase): + def setUp(self): + super(DataApiTests, self).setUp() + self.config = Config() + self.dataApi = DataApi(self.config) + + def test_methods(self): + self.assertEqual(self.dataApi.getPaymentMethods()[0].name, 'Platba kartou') + + def test_payment_state(self): + self.assertEqual(self.dataApi.getPaymentState(1006402161), 1) + + def test_payment(self): + self.assertEqual(self.dataApi.getPayment(1006402161).id, '1006402161') + + def test_payment_info(self): + self.dataApi.getPaymentInstructions(1006402161) + + def test_payments(self): + self.dataApi.getPayments(finished_on_from=datetime.datetime.now(UTC()) - datetime.timedelta(days=1)) + self.dataApi.getPayments(state_ids=[2]) + + def test_credentials(self): + self.config.setCredentials(42, 43, 'test', 'test2') + + self.assertEqual(self.config.merchantId, 42) + self.assertEqual(self.config.accountId, 43) + self.assertEqual(self.config.password, 'test') + self.assertEqual(self.config.dataApiPassword, 'test2') diff --git a/thepay/test_gateApi.py b/thepay/test_gateApi.py new file mode 100644 index 0000000..9c3a804 --- /dev/null +++ b/thepay/test_gateApi.py @@ -0,0 +1,56 @@ +from __future__ import print_function +import requests +import unittest +import uuid + +from thepay.config import Config +from thepay.gateApi import GateApi, GateError +from thepay.dataApi import DataApi +from thepay.payment import Payment + + +class GateApiTests(unittest.TestCase): + def setUp(self): + self.config = Config() + self.gateApi = GateApi(self.config) + self.dataApi = DataApi(self.config) + + def test_invalid_cardCreateRecurrentPayment(self): + with self.assertRaises(GateError): + self.gateApi.cardCreateRecurrentPayment( + '4394c54e-27f1-411b-b5e0-3f4e1ecf3e2c', + uuid.uuid4(), + 10 + ) + + def test_cardCreateRecurrentPayment(self): + payment_id = str(uuid.uuid4()) + # Create new recurring payment + payment = Payment(self.config) + payment.setValue(123) + payment.setReturnUrl('http://test.te') + payment.setCustomerEmail('test@test.te') + payment.setDescription('Order 123 payment') + payment.setMethodId(31) + payment.setMerchantData(payment_id) + payment.setIsRecurring(1) + # Simulate user going through payment process + response = requests.get(payment.getCreateUrl()) + payment_url = response.url[:-1] + "p" + response = requests.get(payment_url) + body = response.content.decode() + assert "Číslo platby" in body + for line in body.splitlines(): + if '