Skip to content

Commit

Permalink
feat(SPT-27226): add python3.12 support and retry support (#200)
Browse files Browse the repository at this point in the history
* feat: add Python3.12 support and retry support for api requests
  • Loading branch information
koushik-kiran-kumar authored Jun 26, 2024
1 parent fb152e7 commit 426cdcf
Show file tree
Hide file tree
Showing 17 changed files with 140 additions and 66 deletions.
17 changes: 7 additions & 10 deletions .github/workflows/build_test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "BuildAndTest"
name: "Build And Test"

on:
push:
Expand All @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "CodeQL"
name: "CodeQL Analysis"

on:
push:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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: |
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,6 @@ functional_tests/pydriver-report.html

# virtual environment
venv3/
venv/
venv/

test-script.py
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 11 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------
Expand Down
45 changes: 41 additions & 4 deletions docs/examples/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -270,9 +310,6 @@ disabled by setting `verify_server_version=False`.
'password',
verify_server_version=False
)
Available Adapters
------------------

Expand Down
4 changes: 0 additions & 4 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^^^^^^^
Expand Down
2 changes: 1 addition & 1 deletion functional_tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Faker==0.8.15
pytest>=3.5.0
pytest-html>=1.22.1
pytest-html>=1.22.1
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
testpaths = tests
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 0 additions & 4 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,3 @@ test=pytest

[wheel]
universal=1

[tool:pytest]
python_paths = .
testpaths = tests
12 changes: 8 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ def parse_requirements(requirement_file):


setup(
version="10.14.0",
version="10.18.0",
name="swimlane",
author="Swimlane",
author_email="[email protected]",
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,
Expand All @@ -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"
]
)
60 changes: 48 additions & 12 deletions swimlane/core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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(
Expand Down Expand Up @@ -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

Expand Down
9 changes: 6 additions & 3 deletions swimlane/core/resources/usergroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 2 additions & 3 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
coverage
faker
mock
pytest==3.10.1
pytest-cov==2.8.1
pytest>=6.2.5
pytest-pythonpath
tox
pytz
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py27,py34,py35,py36
envlist = py36,py37,py38,py39,py310,py311,py312

[testenv]
passenv = LANG
Expand Down

0 comments on commit 426cdcf

Please sign in to comment.