diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 6ba704ab..e4eb40c1 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -1,4 +1,4 @@ -name: "BuildAndTest" +name: "Build And Test" on: push: @@ -8,16 +8,16 @@ on: jobs: build: - name: Build and test driver + name: Build and Test Driver runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.6] + python-version: [3.6, 3.7, 3.12] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -32,9 +32,6 @@ jobs: # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Run Tests - run: py.test -v --cov=swimlane --cov-report=xml - - name: Run Codacy Coverage - env: - CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} - run: python-codacy-coverage -r coverage.xml + run: | + python -m pytest diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 209ef1c9..2aafd983 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,4 +1,4 @@ -name: "CodeQL" +name: "CodeQL Analysis" on: push: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be218891..58f9d776 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,7 @@ # This workflow is responsible for publishing packages to PyPi # and creating offline packages -name: "Release new version" +name: "Release New Version" on: - workflow_call @@ -12,12 +12,12 @@ jobs: name: Publish to PyPi runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: 3.12 - name: Update PIP tools run: | diff --git a/.gitignore b/.gitignore index fbc86700..2b05feb3 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,6 @@ functional_tests/pydriver-report.html # virtual environment venv3/ -venv/ \ No newline at end of file +venv/ + +test-script.py \ No newline at end of file diff --git a/Makefile b/Makefile index 6ef49ca5..44d660b2 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ docs: cd docs/ && make html release: - python setup.py sdist bdist_wheel upload -r swimlane + python3 setup.py sdist bdist_wheel upload -r swimlane build-offline-installer: - python2.7 offline_installer/build_installer.py + python3 offline_installer/build_installer.py diff --git a/README.rst b/README.rst index e9a3ad26..973a89ad 100644 --- a/README.rst +++ b/README.rst @@ -63,27 +63,28 @@ Install the required PIP packages with the command ## Executing The test suite allows for overriding the target server and user parameters via the following arguments: - ---url default="https://localhost" ---user default="admin" ---pass This is the password for the user defined above. ---skipverify This is for allowing the version of PyDriver to not match the version of Swimlane. + :: + pytest + --url default="https://localhost" + --user default="admin" + --pass This is the password for the user defined above. + --skipverify This is for allowing the version of PyDriver to not match the version of Swimlane. To run a specific test and skip the version verification: -pytest driver_tests/test_app_adaptor.py --skipverify +:: + pytest driver_tests/test_app_adaptor.py --skipverify To run all the tests against 10.20.30.40: - -pytest --url "https://10.20.30.40" - + :: + pytest --url "https://10.20.30.40" .. NOTE:: All of the data created for testing purposes is cleaned up. No preset data is needed beyond the base user. - These tests are Python 2 and 3 compatible. + These tests are Python 3.6+ compatible. Issues ------ diff --git a/docs/examples/client.rst b/docs/examples/client.rst index 3c7820a5..64d4c468 100644 --- a/docs/examples/client.rst +++ b/docs/examples/client.rst @@ -97,6 +97,46 @@ additional requests made by using the client automatically. The `verify_ssl` parameter is ignored when connecting over HTTP. +Retry Requests +^^^^^^^^^^^^^ + +Initial client connection and all failed requests are retried upon recieving :class:`HTTP 5XX` errors (server errors) if the :class:`retry` parameter is enabled. +The default retry options are set as following: + - retry = True + - max_retries = 5 + - retry_interval = 5 (in seconds) + +To override the default retry options used by all library methods, provide them during client instantiation. + +.. code-block:: python + + from swimlane import Swimlane + + swimlane = Swimlane( + '192.168.1.1', + 'username', + 'password', + retry=True, + max_retries=3, + retry_interval=10 # in seconds + ) +The :meth:`swimlane.Swimlane.request` method can also accept the optional retry options that will override the +global defaults for the single request. + +.. code-block:: python + + from swimlane import Swimlane + + swimlane = Swimlane('192.168.1.1', 'username', 'password') + + response = swimlane.request( + 'post', + 'some/endpoint', + ..., + retry=True, + max_retries=3, + retry_interval=10 # in seconds + ) Resource Caching ^^^^^^^^^^^^^^^^ @@ -200,7 +240,7 @@ All provided keyword arguments will be passed to the underlying :meth:`requests. .. note:: - Any 400/500 responses will raise :class:`requests.HTTPError` automatically. + Any 400/500 responses will raise :class:`requests.HTTPError` automatically after Max Retry attempts are exceeded. Request Timeouts @@ -270,9 +310,6 @@ disabled by setting `verify_server_version=False`. 'password', verify_server_version=False ) - - - Available Adapters ------------------ diff --git a/docs/index.rst b/docs/index.rst index 4a599ec2..af107d63 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -50,10 +50,6 @@ Install/upgrade to latest release:: pip install -U swimlane -Install/upgrade to latest release for platform v2.x:: - - pip install -U "swimlane>=2,<3" - Offline Installer ^^^^^^^^^^^^^^^^^ diff --git a/functional_tests/requirements.txt b/functional_tests/requirements.txt index 04de3b14..0af0bfb7 100644 --- a/functional_tests/requirements.txt +++ b/functional_tests/requirements.txt @@ -1,3 +1,3 @@ Faker==0.8.15 pytest>=3.5.0 -pytest-html>=1.22.1 +pytest-html>=1.22.1 \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..5ee64771 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests diff --git a/requirements.txt b/requirements.txt index 16124f20..19f28794 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -cachetools>=2.0,<2.1 +cachetools>=4.2.4 certifi>=2017 -pendulum==2.1.2 +pendulum==2.1.2; python_version<='3.7' +pendulum==3.0.0; python_version>='3.8' pyjwt>=2.4.0 pyuri>=0.3,<0.4 requests[security]>=2,<3 diff --git a/setup.cfg b/setup.cfg index 3debf7ee..06f8414f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,3 @@ test=pytest [wheel] universal=1 - -[tool:pytest] -python_paths = . -testpaths = tests diff --git a/setup.py b/setup.py index 0df8916c..d9f06fbd 100644 --- a/setup.py +++ b/setup.py @@ -11,11 +11,12 @@ def parse_requirements(requirement_file): setup( - version="10.14.0", + version="10.18.0", name="swimlane", author="Swimlane", author_email="info@swimlane.com", url="https://github.com/swimlane/swimlane-python", + python_requires=">=3.6", packages=find_packages(exclude=('tests', 'tests.*')), description="Python driver for the Swimlane API", long_description=long_description, @@ -29,9 +30,12 @@ def parse_requirements(requirement_file): "License :: OSI Approved :: GNU Affero General Public License v3", "Development Status :: 5 - Production/Stable", "Programming Language :: Python", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7" + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12" ] ) diff --git a/swimlane/core/client.py b/swimlane/core/client.py index d3e2238c..13df8885 100644 --- a/swimlane/core/client.py +++ b/swimlane/core/client.py @@ -5,10 +5,12 @@ import jwt import pendulum import requests +import time from pyuri import URI from requests.compat import json from requests.packages import urllib3 from requests.structures import CaseInsensitiveDict +from requests.exceptions import ConnectionError from six.moves.urllib.parse import urljoin from swimlane.core.adapters import GroupAdapter, UserAdapter, AppAdapter, HelperAdapter @@ -46,6 +48,9 @@ class Swimlane(object): caching. Disabled by default access_token (str): Authentication token, used in lieu of a username and password write_to_read_only (bool): Enable the ability to write to Read-only fields + retry (bool): Retry request when error code is >= 500 + max_retries (int): Maximum number of retry attempts + retry_interval (int): Time interval (in seconds) between two retry attempts Attributes: host (pyuri.URI): Full RFC-1738 URL pointing to Swimlane host @@ -92,7 +97,10 @@ def __init__( verify_server_version=True, resource_cache_size=0, access_token=None, - write_to_read_only=False + write_to_read_only: bool=False, + retry: bool=True, + max_retries: int=5, + retry_interval: int=5 ): self.__verify_auth_params(username, password, access_token) @@ -111,6 +119,10 @@ def __init__( self._session = WrappedSession() self._session.verify = verify_ssl + + self.retry = retry + self.max_retries = max_retries + self.retry_interval = retry_interval if username is not None and password is not None: self._session.auth = SwimlaneJwtAuth( @@ -215,17 +227,41 @@ def request(self, method, api_endpoint, **kwargs): kwargs['headers'] = headers kwargs['data'] = json.dumps(json_data, sort_keys=True, separators=(',', ':')) - - response = self._session.request(method, urljoin(str(self.host) + self._api_root, api_endpoint), **kwargs) - - # Roll 400 errors up into SwimlaneHTTP400Errors with specific Swimlane error code support - try: - response.raise_for_status() - except requests.HTTPError as error: - if error.response.status_code == 400: - raise SwimlaneHTTP400Error(error) - else: - raise error + + # Retry logic + req_retry = kwargs.pop('retry', self.retry) + + req_max_retries = kwargs.pop('max_retries', self.max_retries) + if not isinstance(req_max_retries, int): + raise TypeError('max_retries should be an integer') + if req_max_retries <= 0: + raise ValueError('max_retries should be a positive integer') + + req_retry_interval = kwargs.pop('retry_interval', self.retry_interval) + if not isinstance(req_retry_interval, int): + raise TypeError('retry_interval should be an integer') + if req_retry_interval <= 0: + raise ValueError('retry_interval should be a positive integer') + + while not req_max_retries<0: + response = self._session.request(method, urljoin(str(self.host) + self._api_root, api_endpoint), **kwargs) + + # Roll 400 errors up into SwimlaneHTTP400Errors with specific Swimlane error code support + try: + response.raise_for_status() + # Exit loop on successful request + req_max_retries = -1 + except requests.HTTPError as error: + if error.response.status_code == 400: + raise SwimlaneHTTP400Error(error) + else: + if req_retry and req_max_retries>0 and error.response.status_code>=500: + req_max_retries -= 1 + time.sleep(req_retry_interval) + continue + elif req_max_retries == 0: + raise ConnectionError(f'Max retries exceeded. Caused by ({error})') + raise error return response diff --git a/swimlane/core/resources/usergroup.py b/swimlane/core/resources/usergroup.py index 4d19dcbe..c4984049 100644 --- a/swimlane/core/resources/usergroup.py +++ b/swimlane/core/resources/usergroup.py @@ -158,6 +158,9 @@ def _evaluate(self): yield element else: for user_id in self.__user_ids: - element = self._swimlane.users.get(id=user_id) - self._elements.append(element) - yield element + try: + element = self._swimlane.users.get(id=user_id) + self._elements.append(element) + yield element + except StopIteration: + return diff --git a/test-requirements.txt b/test-requirements.txt index d05620e1..cdf6caac 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,6 @@ -coverage +faker mock -pytest==3.10.1 -pytest-cov==2.8.1 +pytest>=6.2.5 pytest-pythonpath tox pytz diff --git a/tox.ini b/tox.ini index 633d2a96..dacf525b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36 +envlist = py36,py37,py38,py39,py310,py311,py312 [testenv] passenv = LANG