Hello community,
here is the log from the commit of package python-openqa_client for
openSUSE:Factory checked in at 2020-09-06 00:01:58
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-openqa_client (Old)
and /work/SRC/openSUSE:Factory/.python-openqa_client.new.3399 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-openqa_client"
Sun Sep 6 00:01:58 2020 rev:2 rq:832037 version:4.1.1
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-openqa_client/python-openqa_client.changes
2017-08-12 20:25:21.103590158 +0200
+++
/work/SRC/openSUSE:Factory/.python-openqa_client.new.3399/python-openqa_client.changes
2020-09-06 00:02:29.171256358 +0200
@@ -1,0 +2,42 @@
+Fri Sep 04 08:49:02 UTC 2020 - [email protected]
+
+- Update to version 4.1.1:
+- enable tests
+ * Fix use of 'latest' param when querying jobs
+ * Drop a rogue word from `do_request` docstring, rewrap
+ * Tweak release script to use 'pypi' repo
+ * Handle YAML responses as well as JSON (#12)
+ * Add a 'parse' argument for `do_request` to skip parsing
+ * Add toml to CI requires (for coverage to read TOML config)
+ * tox: run `coverage xml` explicitly
+ * Improve the ugly sed hack fix for the coverage vs. tox venv issue
+ * Update release.sh to use pep517
+ * Add pyproject.toml to comply with PEP-517 / PEP-518
+ * black-ify code and add black to CI config
+ * Move source under src/ , fix tox config to run tests on package
+ * Use f-strings for string formatting
+ * Drop Python 2 support, and some Python 2-specific workarounds
+ * Have MANIFEST.in exclude itself
+ * Add a MANIFEST.in to exclude some stuff we don't want
+ * Fix tests to run on ancient pytest (I hope)
+ * Fix more brokenness in setup.py
+ * Fix release.sh for no spaces in setup.py setup()
+ * Drop duplicated description line in setup.py
+ * Update release.sh to use Python 3
+ * Drop WaitError exception
+ * find_clones: don't edit list while iterating it
+ * _add_auth_headers: don't modify the original request
+ * setup.py: don't import os, we don't use it
+ * setup.py: Remove runtime dependency on setuptools (@jayvdb) (#9)
+ * setup.py: more cleanups based on sample project
+ * setup.py: we don't use find_packages, don't import it
+ * setup.py: no spaces for arg assignments
+ * setup.py: handle long_description as per pypa sample project
+ * Update release script to publish to PyPI
+ * Fix long description for pypi
+ * **API**: update constants to match upstream 4d89041
+ * Remove waiting state
+ * Add incomplete result "timeout_exceeded"
+ * Update job state constants for recent upstream changes
+
+-------------------------------------------------------------------
Old:
----
python-openqa_client-1.3.0.tar.xz
New:
----
python-openqa_client-4.1.1.tar.xz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-openqa_client.spec ++++++
--- /var/tmp/diff_new_pack.NYOAph/_old 2020-09-06 00:02:32.031257789 +0200
+++ /var/tmp/diff_new_pack.NYOAph/_new 2020-09-06 00:02:32.035257792 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-openqa_client
#
-# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2020 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -12,24 +12,29 @@
# license that conforms to the Open Source Definition (Version 1.9)
# published by the Open Source Initiative.
-# Please submit bugfixes or comments via http://bugs.opensuse.org/
+# Please submit bugfixes or comments via https://bugs.opensuse.org/
#
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
+%define skip_python2 1
Name: python-openqa_client
-Version: 1.3.0
+Version: 4.1.1
Release: 0
Summary: Python openQA client library
-License: GPL-2.0+
+License: GPL-2.0-or-later
Group: Development/Languages/Python
-Url: https://github.com/os-autoinst/openQA-python-client
+URL: https://github.com/os-autoinst/openQA-python-client
Source: %{name}-%{version}.tar.xz
+BuildRequires: %{python_module PyYAML}
+BuildRequires: %{python_module freezegun}
+BuildRequires: %{python_module pytest}
+BuildRequires: %{python_module requests}
BuildRequires: %{python_module setuptools}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
+Requires: python-PyYAML
Requires: python-requests
-Requires: python-six
BuildArch: noarch
%python_subpackages
@@ -46,6 +51,9 @@
%python_install
%python_expand %fdupes %{buildroot}%{$python_sitelib}
+%check
+%pytest
+
%files %{python_files}
%doc README.md
%license COPYING
++++++ _service ++++++
--- /var/tmp/diff_new_pack.NYOAph/_old 2020-09-06 00:02:32.063257806 +0200
+++ /var/tmp/diff_new_pack.NYOAph/_new 2020-09-06 00:02:32.063257806 +0200
@@ -4,7 +4,7 @@
<param name="versionformat">@PARENT_TAG@</param>
<param
name="url">git://github.com/os-autoinst/openQA-python-client.git</param>
<param name="scm">git</param>
- <param name="revision">1.3.0</param>
+ <param name="revision">4.1.1</param>
<param name="changesgenerate">enable</param>
</service>
<service mode="disabled" name="recompress">
++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.NYOAph/_old 2020-09-06 00:02:32.079257814 +0200
+++ /var/tmp/diff_new_pack.NYOAph/_new 2020-09-06 00:02:32.079257814 +0200
@@ -1,4 +1,4 @@
<servicedata>
<service name="tar_scm">
<param
name="url">git://github.com/os-autoinst/openQA-python-client.git</param>
- <param
name="changesrevision">184e6c8415ef3b0667caf8f8a440007d6b98ba93</param></service></servicedata>
\ No newline at end of file
+ <param
name="changesrevision">f227d83dd42c787579d95a03322004b283c02154</param></service></servicedata>
\ No newline at end of file
++++++ python-openqa_client-1.3.0.tar.xz -> python-openqa_client-4.1.1.tar.xz
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/.github/workflows/tox.yml
new/python-openqa_client-4.1.1/.github/workflows/tox.yml
--- old/python-openqa_client-1.3.0/.github/workflows/tox.yml 1970-01-01
01:00:00.000000000 +0100
+++ new/python-openqa_client-4.1.1/.github/workflows/tox.yml 2020-08-07
23:50:44.000000000 +0200
@@ -0,0 +1,24 @@
+name: Python package with Tox
+
+on: [pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ max-parallel: 4
+ matrix:
+ python-version: [3.6, 3.7, 3.8]
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v1
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install tox tox-gh-actions
+ - name: Test with tox
+ run: tox
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/CHANGELOG.md
new/python-openqa_client-4.1.1/CHANGELOG.md
--- old/python-openqa_client-1.3.0/CHANGELOG.md 2017-02-15 21:54:42.000000000
+0100
+++ new/python-openqa_client-4.1.1/CHANGELOG.md 2020-08-07 23:50:44.000000000
+0200
@@ -1,5 +1,75 @@
## Changelog
+### 4.1.1 - 2020-08-07
+
+1. Fix `latest` param when querying jobs to use value `1` not `true`
+
+### 4.1.0 - 2020-03-13
+
+1. Handle server sending us YAML (though we didn't ask for it)
+2. Add `parse` kwarg to `do_request` to allow skipping parsing
+
+This adds a dependency on pyyaml, unfortunately; can't see any way around that
short of
+just not parsing these responses at all.
+
+### 4.0.0 - 2020-02-28
+
+1. Drop Python 2 support, remove various Python 2-specific workarounds
+2. Move module source under `src/`
+3. Make tox build and test an sdist, not test the working directory
+4. Run [black](https://github.com/psf/black) on the source, add it to CI
+5. Add `pyproject.toml` compliant with PEP-517 and PEP-518
+6. Update `release.sh` to use `pep517`
+
+This is a modernization release to drop Python 2 support and align with
various shiny modern
+Best Practices. There should be no actual functional changes to the code at
all, but I'm gonna
+call it 4.0.0 due to the dropping of Python 2 support and the code being moved
within the
+git repo, which may disrupt some folks.
+
+### 3.0.4 - 2020-02-27
+
+1. OK, this time fix tests on ancient EPEL 7 for realz
+2. Tweak py27 tox environment to match EPEL 7
+
+### 3.0.3 - 2020-02-27
+
+1. Fix tests to run on ancient pytest in EPEL 7 (I hope)
+
+### 3.0.2 - 2020-02-27
+
+1. Fix more broken stuff in setup.py
+
+### 3.0.1 - 2020-02-27
+
+1. Drop duplicated description line in setup.py
+2. Fix release.sh for no spaces in setup.py setup()
+
+### 3.0.0 - 2020-02-27
+
+1. **API**: remove `WaitError` exception
+2. Update release script to use Python 3, publish to PyPI
+3. Update setup.py for current best practices
+4. Don't modify original request in `_add_auth_headers`
+5. Don't edit list while iterating it in `find_clones`
+6. Add a test suite, tox config and GitHub Actions-based CI
+
+### 2.0.1 - 2020-02-26
+
+1. Fix long description for PyPI
+
+### 2.0.0 - 2020-01-06
+
+1. Update constants to reflect upstream changes again, including
+ some additions and **REMOVAL** of JOB_INCOMPLETE_RESULTS
+
+### 1.3.2 - 2019-05-21
+
+1. Update constants to reflect upstream changes (again)
+
+### 1.3.1 - 2017-10-10
+
+1. Update constants to reflect upstream changes
+
### 1.3.0 - 2017-02-15
1. First proper release
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/MANIFEST.in
new/python-openqa_client-4.1.1/MANIFEST.in
--- old/python-openqa_client-1.3.0/MANIFEST.in 1970-01-01 01:00:00.000000000
+0100
+++ new/python-openqa_client-4.1.1/MANIFEST.in 2020-08-07 23:50:44.000000000
+0200
@@ -0,0 +1,3 @@
+exclude .gitignore
+exclude MANIFEST.in
+recursive-exclude .github *
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/README.md
new/python-openqa_client-4.1.1/README.md
--- old/python-openqa_client-1.3.0/README.md 2017-02-15 21:54:42.000000000
+0100
+++ new/python-openqa_client-4.1.1/README.md 2020-08-07 23:50:44.000000000
+0200
@@ -1,7 +1,8 @@
# openqa_client
This is a client for the [openQA](https://os-autoinst.github.io/openQA/)
-API, based on [requests](https://python-requests.org).
+API, based on [requests](https://python-requests.org). It requires Python
+3.6 or later.
## Usage
@@ -40,10 +41,9 @@
If you create an `OpenQA_Client` instance without passing the `server`
argument, it will use the first server listed in the configuration file
-if there is one (except with Python 2.6, where one server from the file
-will be used, but not necessarily the first), otherwise it will use
-'localhost'. Note: this is a difference in behaviour from the perl
-client, which *always* uses 'localhost' unless a server name is passed.
+if there is one, otherwise it will use 'localhost'. Note: this is a
+difference in behaviour from the perl client, which *always* uses 'localhost'
+unless a server name is passed.
TLS/SSL connections are the default (except for localhost). You can
pass the argument `scheme` to `OpenQA_Client` to force the use of
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/ci.requires
new/python-openqa_client-4.1.1/ci.requires
--- old/python-openqa_client-1.3.0/ci.requires 1970-01-01 01:00:00.000000000
+0100
+++ new/python-openqa_client-4.1.1/ci.requires 2020-08-07 23:50:44.000000000
+0200
@@ -0,0 +1,5 @@
+black
+coverage
+diff-cover
+pylint
+toml
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/install.requires
new/python-openqa_client-4.1.1/install.requires
--- old/python-openqa_client-1.3.0/install.requires 1970-01-01
01:00:00.000000000 +0100
+++ new/python-openqa_client-4.1.1/install.requires 2020-08-07
23:50:44.000000000 +0200
@@ -0,0 +1,2 @@
+pyyaml
+requests
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/openqa_client/__init__.py
new/python-openqa_client-4.1.1/openqa_client/__init__.py
--- old/python-openqa_client-1.3.0/openqa_client/__init__.py 2017-02-15
21:54:42.000000000 +0100
+++ new/python-openqa_client-4.1.1/openqa_client/__init__.py 1970-01-01
01:00:00.000000000 +0100
@@ -1,23 +0,0 @@
-# Copyright (C) Red Hat Inc.
-#
-# openqa_client is free software; you can redistribute it
-# and/or modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation, either version 3 of
-# the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-# Author: Adam Williamson <[email protected]>
-
-"""Python client library for openQA."""
-
-from __future__ import unicode_literals
-from __future__ import print_function
-
-__version__ = "1.3.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/openqa_client/client.py
new/python-openqa_client-4.1.1/openqa_client/client.py
--- old/python-openqa_client-1.3.0/openqa_client/client.py 2017-02-15
21:54:42.000000000 +0100
+++ new/python-openqa_client-4.1.1/openqa_client/client.py 1970-01-01
01:00:00.000000000 +0100
@@ -1,239 +0,0 @@
-# Copyright (C) 2015 Red Hat
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-# Authors: Adam Williamson <[email protected]>
-# Ludwig Nussel <[email protected]>
-# Jan Sedlak <[email protected]>
-
-"""Main client functionality."""
-
-import hashlib
-import hmac
-import os
-import logging
-import time
-
-from six.moves.urllib.parse import urlparse, urlunparse
-from six.moves import configparser
-import requests
-
-import openqa_client.exceptions
-import openqa_client.const as oqc
-
-logger = logging.getLogger(__name__)
-
-
-## MAIN CLIENT CLASS
-
-
-class OpenQA_Client(object):
- """A client for the OpenQA REST API; just handles API auth if
- needed and provides a couple of custom methods for convenience.
- """
- def __init__(self, server='', scheme=''):
- # Read in config files.
- config = configparser.ConfigParser()
- paths = ('/etc/openqa',
- '{0}/.config/openqa'.format(os.path.expanduser('~')))
- config.read('{0}/client.conf'.format(path)
- for path in paths)
-
- # If server not specified, default to the first one in the
- # configuration file. If no configuration file, default to
- # localhost. NOTE: this is different from the perl client, it
- # *always* defaults to localhost.
- if not server:
- try:
- server = config.sections()[0]
- except (configparser.MissingSectionHeaderError, IndexError):
- server = 'localhost'
-
- if server.startswith('http'):
- # Handle entries like [http://foo] or [https://foo]. The,
- # perl client does NOT handle these, so you shouldn't use
- # them. This client started out supporting this, though,
- # so it should continue to.
- if not scheme:
- scheme = urlparse(server).scheme
- server = urlparse(server).netloc
-
- if not scheme:
- if server in ('localhost', '127.0.0.1', '::1'):
- # Default to non-TLS for localhost; cert is unlikely to
- # be valid for 'localhost' and there's no MITM...
- scheme = 'http'
- else:
- scheme = 'https'
-
- self.baseurl = urlunparse((scheme, server, '', '', '', ''))
-
- # Get the API secrets from the config file.
- try:
- apikey = config.get(server, 'key')
- self.apisecret = config.get(server, 'secret')
- except configparser.Error:
- try:
- apikey = config.get(self.baseurl, 'key')
- self.apisecret = config.get(self.baseurl, 'secret')
- except configparser.Error:
- logger.debug("No API key for %s: only GET requests will be
allowed", server)
- apikey = ''
- self.apisecret = ''
-
- # Create a Requests session and ensure some standard headers
- # will be used for all requests run through the session.
- self.session = requests.Session()
- headers = {}
- headers['Accept'] = 'json'
- if apikey:
- headers['X-API-Key'] = apikey
- self.session.headers.update(headers)
-
- def _add_auth_headers(self, request):
- """Add authentication headers to a PreparedRequest. See
- openQA/lib/OpenQA/client.pm for the authentication design.
- """
- if not self.apisecret:
- # Can't auth without an API key.
- return request
- timestamp = time.time()
- path = request.path_url.replace('%20', '+').replace('~', '%7E')
- apihash = hmac.new(
- self.apisecret.encode(), '{0}{1}'.format(path,
timestamp).encode(), hashlib.sha1)
- headers = {}
- headers['X-API-Microtime'] = str(timestamp).encode()
- headers['X-API-Hash'] = apihash.hexdigest()
- request.headers.update(headers)
- return request
-
- def do_request(self, request, retries=5, wait=10):
- """Passed a requests.Request, prepare it with the necessary
- headers, submit it, and return the JSON output. You can use
- this directly instead of openqa_request() if you need to do
- something unusual. May raise ConnectionError or RequestError
- if the connection or the request fail in some way after
- 'retries' attempts. 'wait' determines how long we wait between
- retries: on the *first* retry we wait exactly 'wait' seconds,
- on each subsequent retry the wait time is doubled, up to a
- max of 60 seconds between attempts.
- """
- prepared = self.session.prepare_request(request)
- authed = self._add_auth_headers(prepared)
- # We can't use the nice urllib3 Retry stuff, because openSUSE
- # 13.2 has a sadly outdated version of python-requests. We'll
- # have to do it ourselves.
- try:
- resp = self.session.send(authed)
- if not resp.ok:
- raise openqa_client.exceptions.RequestError(
- request.method, resp.url, resp.status_code)
- return resp.json()
- except (requests.exceptions.ConnectionError,
- openqa_client.exceptions.RequestError) as err:
- if retries:
- logger.debug(
- "do_request: request failed! Retrying in %s seconds...",
- wait)
- logger.debug("Error: %s", err)
- time.sleep(wait)
- newwait = min(wait+wait, 60)
- return self.do_request(request, retries=retries-1,
wait=newwait)
- elif isinstance(err, openqa_client.exceptions.RequestError):
- raise err
- elif isinstance(err, requests.exceptions.ConnectionError):
- raise openqa_client.exceptions.ConnectionError(err)
-
- def openqa_request(self, method, path, params=None, retries=5, wait=10,
data=None):
- """Perform a typical openQA request, with an API path and some
- optional parameters. Use the data parameter instead of params if you
- need to pass lots of settings. It will post
- application/x-www-form-urlencoded data.
- """
- if not params:
- params = {}
- # As with the reference client, we assume relative paths are
- # relative to /api/v1.
- if not path.startswith('/'):
- path = '/api/v1/{0}'.format(path)
-
- method = method.upper()
- url = '{0}{1}'.format(self.baseurl, path)
- req = requests.Request(method=method, url=url, params=params,
data=data)
- return self.do_request(req, retries=retries, wait=wait)
-
- def find_clones(self, jobs):
- """Given an iterable of job dicts, this will see if any of the
- jobs were cloned, and replace any that were cloned with the dicts
- of their clones, returning a list. It recurses - so if 3 was
- cloned as 4 and 4 was cloned as 5, you'll wind up with 5. If both
- a job and its clone are already in the iterable, the original will
- be removed.
- """
- jobs = list(jobs)
- while any(job['clone_id'] for job in jobs):
- toget = []
- ids = [job['id'] for job in jobs]
- for job in jobs:
- if job['clone_id']:
- logger.debug("Replacing job %s with clone %s", job['id'],
job['clone_id'])
- if job['clone_id'] not in ids:
- toget.append(str(job['clone_id']))
- jobs.remove(job)
-
- if toget:
- toget = ','.join(toget)
- # Get clones and add them to the list
- clones = self.openqa_request('GET', 'jobs', params={'ids':
toget})['jobs']
- jobs.extend(clones)
- return jobs
-
- def get_jobs(self, jobs=None, build=None, filter_dupes=True):
- """Get job dicts. Either 'jobs' or 'build' must be specified.
- 'jobs' should be iterable of job IDs (string or int). 'build'
- should be an openQA BUILD to get all the jobs for. If both are
- specified, 'jobs' will be used and 'build' ignored. If
- filter_dupes is True, cloned jobs will be replaced by their
- clones (see find_clones docstring) and duplicate jobs will be
- filtered out (using the upstream 'latest' query param).
-
- Unlike all previous 'job get' methods in this module, this one
- will happily return results for running jobs. All it does is
- get the specified dicts, filter them if filter_dupes is set,
- and return. If you only want completed jobs, filter the result
- yourself, or just use fedmsg to make sure you only call this
- when all the jobs you want are done.
-
- This method requires the server to be at least version 4.3 to
- work correctly.
- """
- if not build and not jobs:
- raise TypeError("iterate_jobs: either 'jobs' or 'build' must be
specified")
- if jobs:
- jobs = [str(j) for j in jobs]
- # this gets all jobdicts with a single API query
- params = {'ids': ','.join(jobs)}
- else:
- params = {'build': build}
- if filter_dupes:
- params['latest'] = 'true'
- jobdicts = self.openqa_request('GET', 'jobs', params=params)['jobs']
- if filter_dupes:
- # sub out clones. when run on a BUILD this is superfluous
- # as 'latest' will always wind up finding the latest clone
- # but this is still useful if run on a jobs iterable and
- # the jobs in question have clones; 'latest' doesn't help
- # there as it only considers the jobs queried.
- jobdicts = self.find_clones(jobdicts)
- return jobdicts
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/openqa_client/const.py
new/python-openqa_client-4.1.1/openqa_client/const.py
--- old/python-openqa_client-1.3.0/openqa_client/const.py 2017-02-15
21:54:42.000000000 +0100
+++ new/python-openqa_client-4.1.1/openqa_client/const.py 1970-01-01
01:00:00.000000000 +0100
@@ -1,73 +0,0 @@
-# Copyright (C) 2016 Red Hat
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-# Authors: Adam Williamson <[email protected]>
-
-"""Important constants duplicated from openQA. We need to keep this in
-sync with upstream, but it's better to have it done just once here
-rather than every consumer of this library duplicating things like
-'these are the "running" states' on the fly. It is explicitly allowed
-to use 'from openqa_client.const import *'; this will only import
-sanely named 'constants'. You may prefer to do `from openqa_client
-import const as oqc` or similar.
-"""
-
-# we use 'bad' whitespace to align the definitions nicely.
-# pylint: disable=bad-whitespace
-
-# lib/OpenQA/Schema/Result/Jobs.pm
-
-# States
-JOB_STATE_SCHEDULED = "scheduled"
-JOB_STATE_RUNNING = "running"
-JOB_STATE_CANCELLED = "cancelled"
-JOB_STATE_WAITING = "waiting"
-JOB_STATE_DONE = "done"
-JOB_STATE_UPLOADING = "uploading"
-
-JOB_STATES = (JOB_STATE_SCHEDULED, JOB_STATE_RUNNING,
JOB_STATE_CANCELLED,
- JOB_STATE_WAITING, JOB_STATE_DONE, JOB_STATE_UPLOADING)
-JOB_PENDING_STATES = (JOB_STATE_SCHEDULED, JOB_STATE_RUNNING,
JOB_STATE_WAITING,
- JOB_STATE_UPLOADING)
-JOB_EXECUTION_STATES = (JOB_STATE_RUNNING, JOB_STATE_WAITING,
JOB_STATE_UPLOADING)
-JOB_FINAL_STATES = (JOB_STATE_DONE, JOB_STATE_CANCELLED)
-
-# Results
-JOB_RESULT_NONE = "none"
-JOB_RESULT_PASSED = "passed"
-JOB_RESULT_SOFTFAILED = "softfailed"
-JOB_RESULT_FAILED = "failed"
-JOB_RESULT_INCOMPLETE = "incomplete"
-JOB_RESULT_SKIPPED = "skipped"
-JOB_RESULT_OBSOLETED = "obsoleted"
-JOB_RESULT_PARALLEL_FAILED = "parallel_failed"
-JOB_RESULT_PARALLEL_RESTARTED = "parallel_restarted"
-JOB_RESULT_USER_CANCELLED = "user_cancelled"
-JOB_RESULT_USER_RESTARTED = "user_restarted"
-
-JOB_RESULTS = (JOB_RESULT_NONE, JOB_RESULT_PASSED,
JOB_RESULT_SOFTFAILED,
- JOB_RESULT_FAILED, JOB_RESULT_INCOMPLETE,
JOB_RESULT_SKIPPED,
- JOB_RESULT_OBSOLETED, JOB_RESULT_PARALLEL_FAILED,
- JOB_RESULT_PARALLEL_RESTARTED,
JOB_RESULT_USER_CANCELLED,
- JOB_RESULT_USER_RESTARTED)
-JOB_COMPLETE_RESULTS = (JOB_RESULT_PASSED, JOB_RESULT_SOFTFAILED,
JOB_RESULT_FAILED)
-JOB_OK_RESULTS = (JOB_RESULT_PASSED, JOB_RESULT_SOFTFAILED)
-JOB_INCOMPLETE_RESULTS = (JOB_RESULT_INCOMPLETE, JOB_RESULT_SKIPPED,
JOB_RESULT_OBSOLETED,
- JOB_RESULT_PARALLEL_FAILED,
JOB_RESULT_PARALLEL_RESTARTED,
- JOB_RESULT_USER_CANCELLED, JOB_RESULT_USER_RESTARTED)
-
-# Scenarios
-JOB_SCENARIO_KEYS = ('DISTRI', 'VERSION', 'FLAVOR', 'ARCH',
'TEST')
-JOB_SCENARIO_WITH_MACHINE_KEYS = JOB_SCENARIO_KEYS + ('MACHINE',)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-openqa_client-1.3.0/openqa_client/exceptions.py
new/python-openqa_client-4.1.1/openqa_client/exceptions.py
--- old/python-openqa_client-1.3.0/openqa_client/exceptions.py 2017-02-15
21:54:42.000000000 +0100
+++ new/python-openqa_client-4.1.1/openqa_client/exceptions.py 1970-01-01
01:00:00.000000000 +0100
@@ -1,41 +0,0 @@
-# Copyright (C) 2015 Red Hat
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-# Author: Adam Williamson <[email protected]>
-
-"""Custom exceptions used by openqa_client."""
-
-class OpenQAClientError(Exception):
- """Base class for openQA client errors."""
- pass
-
-class ConnectionError(OpenQAClientError):
- """Error raised when server connection fails. Just passed through
- requests.exceptions.ConnectionError.
- """
- pass
-
-class RequestError(OpenQAClientError):
- """Error raised when a request fails (after retries). 3-tuple of
- method, URL, and status code.
- """
- pass
-
-class WaitError(OpenQAClientError):
- """Error raised when some kind of wait has gone on too long."""
-
- def __init__(self, *args, **kwargs):
- super(WaitError, self).__init__(*args)
- self.unfinished_jobs = kwargs.get('unfinished_jobs', [])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/pyproject.toml
new/python-openqa_client-4.1.1/pyproject.toml
--- old/python-openqa_client-1.3.0/pyproject.toml 1970-01-01
01:00:00.000000000 +0100
+++ new/python-openqa_client-4.1.1/pyproject.toml 2020-08-07
23:50:44.000000000 +0200
@@ -0,0 +1,18 @@
+[build-system]
+requires = ["setuptools>=40.6.0", "setuptools-git", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.coverage.run]
+parallel = true
+branch = true
+source = ["openqa_client"]
+
+[tool.coverage.paths]
+source = ["src", ".tox/*/site-packages"]
+
+[tool.coverage.report]
+show_missing = true
+
+[tool.black]
+# don't @ me, Hynek
+line-length = 100
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/release.sh
new/python-openqa_client-4.1.1/release.sh
--- old/python-openqa_client-1.3.0/release.sh 2017-02-15 21:54:42.000000000
+0100
+++ new/python-openqa_client-4.1.1/release.sh 2020-08-07 23:50:44.000000000
+0200
@@ -2,8 +2,7 @@
baddeps=""
# check deps
-rpm -qi python2-setuptools > /dev/null 2>&1 || baddeps="python2-setuptools"
-rpm -qi python2-setuptools_git > /dev/null 2>&1 || baddeps="${baddeps}
python2-setuptools_git"
+python3 -m pep517.__init__ || baddeps="python3-pep517"
if [ -n "${baddeps}" ]; then
echo "${baddeps} must be installed!"
exit 1
@@ -16,12 +15,12 @@
version=$1
name=openqa_client
-sed -i -e "s,version = \".*\",version = \"$version\", g" setup.py
-sed -i -e "s,__version__ = \".*\",__version__ = \"${version}\", g"
${name}/__init__.py
-git add setup.py ${name}/__init__.py
+sed -i -e "s,version=\".*\",version=\"$version\", g" setup.py
+sed -i -e "s,__version__ = \".*\",__version__ = \"${version}\", g"
src/${name}/__init__.py
+git add setup.py src/${name}/__init__.py
git commit -s -m "Release $version"
git push
git tag -a -m "Release $version" $version
git push origin $version
-python ./setup.py sdist --formats=tar
-xz dist/$name-$version.tar
+python3 -m pep517.build .
+twine upload -r pypi dist/${name}-${version}*
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/setup.py
new/python-openqa_client-4.1.1/setup.py
--- old/python-openqa_client-1.3.0/setup.py 2017-02-15 21:54:42.000000000
+0100
+++ new/python-openqa_client-4.1.1/setup.py 2020-08-07 23:50:44.000000000
+0200
@@ -15,33 +15,39 @@
#
# Author: Adam Williamson <[email protected]>
-import os
-from setuptools import setup, find_packages
+from setuptools import setup
+from os import path
-# Utility function to read the README file.
-# Used for the long_description. It's nice, because now 1) we have a top level
-# README file and 2) it's easier to type in the README file than to put a raw
-# string in below. Stolen from
-# https://pythonhosted.org/an_example_pypi_project/setuptools.html
-def read(fname):
- return open(os.path.join(os.path.dirname(__file__), fname)).read()
+HERE = path.abspath(path.dirname(__file__))
+
+# Get the long description from the README file
+with open(path.join(HERE, 'README.md'), encoding='utf-8') as f:
+ LONGDESC = f.read()
setup(
- name = "openqa_client",
- version = "1.3.0",
- author = "Adam Williamson",
- author_email = "[email protected]",
- description = "openQA client",
- license = "GPLv2+",
- keywords = "openqa opensuse fedora client",
- url = "https://github.com/os-autoinst/openQA-python-client",
- packages = ["openqa_client"],
- install_requires = ['requests', 'setuptools', 'six'],
- long_description=read('README.md'),
+ name="openqa_client",
+ version="4.1.1",
+ description="Python client library for openQA API",
+ author="Adam Williamson",
+ author_email="[email protected]",
+ license="GPLv2+",
+ keywords="openqa opensuse fedora client",
+ url="https://github.com/os-autoinst/openQA-python-client",
+ packages=["openqa_client"],
+ package_dir={"": "src"},
+ install_requires=open('install.requires').read().splitlines(),
+ python_requires="!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4",
+ long_description=LONGDESC,
+ long_description_content_type='text/markdown',
classifiers=[
- "Development Status :: 3 - Alpha",
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
"Topic :: Utilities",
- "License :: OSI Approved :: GNU General Public License v2 or later "
- "(GPLv2+)",
+ "License :: OSI Approved :: GNU General Public License v2 or later
(GPLv2+)",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
],
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-openqa_client-1.3.0/src/openqa_client/__init__.py
new/python-openqa_client-4.1.1/src/openqa_client/__init__.py
--- old/python-openqa_client-1.3.0/src/openqa_client/__init__.py
1970-01-01 01:00:00.000000000 +0100
+++ new/python-openqa_client-4.1.1/src/openqa_client/__init__.py
2020-08-07 23:50:44.000000000 +0200
@@ -0,0 +1,20 @@
+# Copyright (C) Red Hat Inc.
+#
+# openqa_client is free software; you can redistribute it
+# and/or modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, either version 2 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Author: Adam Williamson <[email protected]>
+
+"""Python client library for openQA."""
+
+__version__ = "4.1.1"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-openqa_client-1.3.0/src/openqa_client/client.py
new/python-openqa_client-4.1.1/src/openqa_client/client.py
--- old/python-openqa_client-1.3.0/src/openqa_client/client.py 1970-01-01
01:00:00.000000000 +0100
+++ new/python-openqa_client-4.1.1/src/openqa_client/client.py 2020-08-07
23:50:44.000000000 +0200
@@ -0,0 +1,250 @@
+# Copyright (C) 2015 Red Hat
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors: Adam Williamson <[email protected]>
+# Ludwig Nussel <[email protected]>
+# Jan Sedlak <[email protected]>
+
+"""Main client functionality."""
+
+import hashlib
+import hmac
+import os
+import logging
+import time
+
+from urllib.parse import urlparse, urlunparse
+import configparser
+import requests
+import yaml
+
+import openqa_client.exceptions
+import openqa_client.const as oqc
+
+logger = logging.getLogger(__name__)
+
+
+## MAIN CLIENT CLASS
+
+
+class OpenQA_Client(object):
+ """A client for the OpenQA REST API; just handles API auth if
+ needed and provides a couple of custom methods for convenience.
+ """
+
+ def __init__(self, server="", scheme=""):
+ # Read in config files.
+ config = configparser.ConfigParser()
+ paths = ("/etc/openqa", f"{os.path.expanduser('~')}/.config/openqa")
+ config.read(f"{path}/client.conf" for path in paths)
+
+ # If server not specified, default to the first one in the
+ # configuration file. If no configuration file, default to
+ # localhost. NOTE: this is different from the perl client, it
+ # *always* defaults to localhost.
+ if not server:
+ try:
+ server = config.sections()[0]
+ except (configparser.MissingSectionHeaderError, IndexError):
+ server = "localhost"
+
+ if server.startswith("http"):
+ # Handle entries like [http://foo] or [https://foo]. The,
+ # perl client does NOT handle these, so you shouldn't use
+ # them. This client started out supporting this, though,
+ # so it should continue to.
+ if not scheme:
+ scheme = urlparse(server).scheme
+ server = urlparse(server).netloc
+
+ if not scheme:
+ if server in ("localhost", "127.0.0.1", "::1"):
+ # Default to non-TLS for localhost; cert is unlikely to
+ # be valid for 'localhost' and there's no MITM...
+ scheme = "http"
+ else:
+ scheme = "https"
+
+ self.baseurl = urlunparse((scheme, server, "", "", "", ""))
+
+ # Get the API secrets from the config file.
+ try:
+ apikey = config.get(server, "key")
+ self.apisecret = config.get(server, "secret")
+ except configparser.Error:
+ try:
+ apikey = config.get(self.baseurl, "key")
+ self.apisecret = config.get(self.baseurl, "secret")
+ except configparser.Error:
+ logger.debug("No API key for %s: only GET requests will be
allowed", server)
+ apikey = ""
+ self.apisecret = ""
+
+ # Create a Requests session and ensure some standard headers
+ # will be used for all requests run through the session.
+ self.session = requests.Session()
+ headers = {}
+ headers["Accept"] = "json"
+ if apikey:
+ headers["X-API-Key"] = apikey
+ self.session.headers.update(headers)
+
+ def _add_auth_headers(self, request):
+ """Add authentication headers to a PreparedRequest. See
+ openQA/lib/OpenQA/client.pm for the authentication design.
+ """
+ if not self.apisecret:
+ # Can't auth without an API key.
+ return request
+ # don't modify the original
+ request = request.copy()
+ timestamp = time.time()
+ path = request.path_url.replace("%20", "+").replace("~", "%7E")
+ apihash = hmac.new(self.apisecret.encode(),
f"{path}{timestamp}".encode(), hashlib.sha1)
+ headers = {}
+ headers["X-API-Microtime"] = str(timestamp).encode()
+ headers["X-API-Hash"] = apihash.hexdigest()
+ request.headers.update(headers)
+ return request
+
+ def do_request(self, request, retries=5, wait=10, parse=True):
+ """Passed a requests.Request, prepare it with the necessary
+ headers, submit it, and return the parsed output (unless parse
+ is False, in which case return the response for the caller to
+ do whatever it likes with). You can use this directly instead
+ of openqa_request() if you need to do something unusual. May
+ raise ConnectionError or RequestError if the connection or the
+ request fail in some way after 'retries' attempts. 'wait'
+ determines how long we wait between retries: on the *first*
+ retry we wait exactly 'wait' seconds, on each subsequent retry
+ the wait time is doubled, up to a max of 60 seconds between
+ attempts.
+ """
+ prepared = self.session.prepare_request(request)
+ authed = self._add_auth_headers(prepared)
+ # We can't use the nice urllib3 Retry stuff, because openSUSE
+ # 13.2 has a sadly outdated version of python-requests. We'll
+ # have to do it ourselves.
+ try:
+ resp = self.session.send(authed)
+ if not resp.ok:
+ raise openqa_client.exceptions.RequestError(
+ request.method, resp.url, resp.status_code
+ )
+ if not parse:
+ return resp
+ # check if the server sent us YAML when we asked for JSON
+ contype = resp.headers.get("content-type", "")
+ if contype.startswith("text/yaml"):
+ # FullLoader should also be fine as we trust the devs,
+ # but I doubt they're gonna put anything beyond
+ # SafeLoader's capacity in the responses
+ return yaml.load(resp.text, Loader=yaml.SafeLoader)
+ return resp.json()
+ except (requests.exceptions.ConnectionError,
openqa_client.exceptions.RequestError) as err:
+ if retries:
+ logger.debug("do_request: request failed! Retrying in %s
seconds...", wait)
+ logger.debug("Error: %s", err)
+ time.sleep(wait)
+ newwait = min(wait + wait, 60)
+ return self.do_request(request, retries=retries - 1,
wait=newwait)
+ elif isinstance(err, openqa_client.exceptions.RequestError):
+ raise err
+ elif isinstance(err, requests.exceptions.ConnectionError):
+ raise openqa_client.exceptions.ConnectionError(err)
+
+ def openqa_request(self, method, path, params=None, retries=5, wait=10,
data=None):
+ """Perform a typical openQA request, with an API path and some
+ optional parameters. Use the data parameter instead of params if you
+ need to pass lots of settings. It will post
+ application/x-www-form-urlencoded data.
+ """
+ if not params:
+ params = {}
+ # As with the reference client, we assume relative paths are
+ # relative to /api/v1.
+ if not path.startswith("/"):
+ path = f"/api/v1/{path}"
+
+ method = method.upper()
+ url = f"{self.baseurl}{path}"
+ req = requests.Request(method=method, url=url, params=params,
data=data)
+ return self.do_request(req, retries=retries, wait=wait)
+
+ def find_clones(self, jobs):
+ """Given an iterable of job dicts, this will see if any of the
+ jobs were cloned, and replace any that were cloned with the dicts
+ of their clones, returning a list. It recurses - so if 3 was
+ cloned as 4 and 4 was cloned as 5, you'll wind up with 5. If both
+ a job and its clone are already in the iterable, the original will
+ be removed.
+ """
+ jobs = list(jobs)
+ while any(job["clone_id"] for job in jobs):
+ toget = []
+ ids = [job["id"] for job in jobs]
+ # copy the list to iterate over it
+ for job in list(jobs):
+ if job["clone_id"]:
+ logger.debug("Replacing job %s with clone %s", job["id"],
job["clone_id"])
+ if job["clone_id"] not in ids:
+ toget.append(str(job["clone_id"]))
+ jobs.remove(job)
+
+ if toget:
+ toget = ",".join(toget)
+ # Get clones and add them to the list
+ clones = self.openqa_request("GET", "jobs", params={"ids":
toget})["jobs"]
+ jobs.extend(clones)
+ return jobs
+
+ def get_jobs(self, jobs=None, build=None, filter_dupes=True):
+ """Get job dicts. Either 'jobs' or 'build' must be specified.
+ 'jobs' should be iterable of job IDs (string or int). 'build'
+ should be an openQA BUILD to get all the jobs for. If both are
+ specified, 'jobs' will be used and 'build' ignored. If
+ filter_dupes is True, cloned jobs will be replaced by their
+ clones (see find_clones docstring) and duplicate jobs will be
+ filtered out (using the upstream 'latest' query param).
+
+ Unlike all previous 'job get' methods in this module, this one
+ will happily return results for running jobs. All it does is
+ get the specified dicts, filter them if filter_dupes is set,
+ and return. If you only want completed jobs, filter the result
+ yourself, or just use fedmsg to make sure you only call this
+ when all the jobs you want are done.
+
+ This method requires the server to be at least version 4.3 to
+ work correctly.
+ """
+ if not build and not jobs:
+ raise TypeError("iterate_jobs: either 'jobs' or 'build' must be
specified")
+ if jobs:
+ jobs = [str(j) for j in jobs]
+ # this gets all jobdicts with a single API query
+ params = {"ids": ",".join(jobs)}
+ else:
+ params = {"build": build}
+ if filter_dupes:
+ params["latest"] = "1"
+ jobdicts = self.openqa_request("GET", "jobs", params=params)["jobs"]
+ if filter_dupes:
+ # sub out clones. when run on a BUILD this is superfluous
+ # as 'latest' will always wind up finding the latest clone
+ # but this is still useful if run on a jobs iterable and
+ # the jobs in question have clones; 'latest' doesn't help
+ # there as it only considers the jobs queried.
+ jobdicts = self.find_clones(jobdicts)
+ return jobdicts
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-openqa_client-1.3.0/src/openqa_client/const.py
new/python-openqa_client-4.1.1/src/openqa_client/const.py
--- old/python-openqa_client-1.3.0/src/openqa_client/const.py 1970-01-01
01:00:00.000000000 +0100
+++ new/python-openqa_client-4.1.1/src/openqa_client/const.py 2020-08-07
23:50:44.000000000 +0200
@@ -0,0 +1,92 @@
+# Copyright (C) 2016 Red Hat
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors: Adam Williamson <[email protected]>
+
+"""Important constants duplicated from openQA. We need to keep this in
+sync with upstream, but it's better to have it done just once here
+rather than every consumer of this library duplicating things like
+'these are the "running" states' on the fly. It is explicitly allowed
+to use 'from openqa_client.const import *'; this will only import
+sanely named 'constants'. You may prefer to do `from openqa_client
+import const as oqc` or similar. For details on what each of these
+means, please refer to the openQA source, it has comments that explain
+them; it seems unnecessary to duplicate those here.
+"""
+
+# we use 'bad' whitespace to align the definitions nicely.
+# pylint: disable=bad-whitespace
+
+# lib/OpenQA/Schema/Result/Jobs.pm
+
+# States
+JOB_STATE_SCHEDULED = "scheduled"
+JOB_STATE_ASSIGNED = "assigned"
+JOB_STATE_SETUP = "setup"
+JOB_STATE_RUNNING = "running"
+JOB_STATE_UPLOADING = "uploading"
+JOB_STATE_CANCELLED = "cancelled"
+JOB_STATE_DONE = "done"
+
+JOB_STATES = (JOB_STATE_SCHEDULED, JOB_STATE_SETUP,
JOB_STATE_RUNNING,
+ JOB_STATE_CANCELLED, JOB_STATE_DONE,
JOB_STATE_UPLOADING,
+ JOB_STATE_ASSIGNED)
+JOB_PENDING_STATES = (JOB_STATE_SCHEDULED, JOB_STATE_ASSIGNED,
JOB_STATE_SETUP,
+ JOB_STATE_RUNNING, JOB_STATE_UPLOADING)
+JOB_EXECUTION_STATES = (JOB_STATE_ASSIGNED, JOB_STATE_SETUP,
+ JOB_STATE_RUNNING, JOB_STATE_UPLOADING)
+JOB_PRE_EXECUTION_STATES = (JOB_STATE_SCHEDULED,)
+JOB_FINAL_STATES = (JOB_STATE_DONE, JOB_STATE_CANCELLED)
+
+# These are referred to as 'meta' states upstream
+JOB_STATE_PRE_EXECUTION = "pre_execution"
+JOB_STATE_EXECUTION = "execution"
+JOB_STATE_FINAL = "final"
+
+# Results
+JOB_RESULT_NONE = "none"
+JOB_RESULT_PASSED = "passed"
+JOB_RESULT_SOFTFAILED = "softfailed"
+JOB_RESULT_FAILED = "failed"
+JOB_RESULT_INCOMPLETE = "incomplete"
+JOB_RESULT_SKIPPED = "skipped"
+JOB_RESULT_OBSOLETED = "obsoleted"
+JOB_RESULT_PARALLEL_FAILED = "parallel_failed"
+JOB_RESULT_PARALLEL_RESTARTED = "parallel_restarted"
+JOB_RESULT_USER_CANCELLED = "user_cancelled"
+JOB_RESULT_USER_RESTARTED = "user_restarted"
+JOB_RESULT_TIMEOUT_EXCEEDED = "timeout_exceeded"
+
+JOB_RESULTS = (JOB_RESULT_NONE, JOB_RESULT_PASSED,
JOB_RESULT_SOFTFAILED,
+ JOB_RESULT_FAILED, JOB_RESULT_INCOMPLETE,
JOB_RESULT_SKIPPED,
+ JOB_RESULT_OBSOLETED, JOB_RESULT_PARALLEL_FAILED,
+ JOB_RESULT_PARALLEL_RESTARTED,
JOB_RESULT_USER_CANCELLED,
+ JOB_RESULT_USER_RESTARTED,
JOB_RESULT_TIMEOUT_EXCEEDED)
+JOB_COMPLETE_RESULTS = (JOB_RESULT_PASSED, JOB_RESULT_SOFTFAILED,
JOB_RESULT_FAILED)
+JOB_OK_RESULTS = (JOB_RESULT_PASSED, JOB_RESULT_SOFTFAILED)
+JOB_NOT_COMPLETE_RESULTS = (JOB_RESULT_INCOMPLETE, JOB_RESULT_TIMEOUT_EXCEEDED)
+JOB_ABORTED_RESULTS = (JOB_RESULT_SKIPPED, JOB_RESULT_OBSOLETED,
JOB_RESULT_PARALLEL_FAILED,
+ JOB_RESULT_PARALLEL_RESTARTED,
JOB_RESULT_USER_CANCELLED,
+ JOB_RESULT_USER_RESTARTED)
+JOB_NOT_OK_RESULTS = (JOB_RESULT_FAILED,) + JOB_NOT_COMPLETE_RESULTS +
JOB_ABORTED_RESULTS
+
+# 'meta' results
+JOB_RESULT_COMPLETE = "complete"
+JOB_RESULT_NOT_COMPLETE = "not_complete"
+JOB_RESULT_ABORTED = "aborted"
+
+# Scenarios
+JOB_SCENARIO_KEYS = ('DISTRI', 'VERSION', 'FLAVOR', 'ARCH',
'TEST')
+JOB_SCENARIO_WITH_MACHINE_KEYS = JOB_SCENARIO_KEYS + ('MACHINE',)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-openqa_client-1.3.0/src/openqa_client/exceptions.py
new/python-openqa_client-4.1.1/src/openqa_client/exceptions.py
--- old/python-openqa_client-1.3.0/src/openqa_client/exceptions.py
1970-01-01 01:00:00.000000000 +0100
+++ new/python-openqa_client-4.1.1/src/openqa_client/exceptions.py
2020-08-07 23:50:44.000000000 +0200
@@ -0,0 +1,40 @@
+# Copyright (C) 2015 Red Hat
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Author: Adam Williamson <[email protected]>
+
+"""Custom exceptions used by openqa_client."""
+
+
+class OpenQAClientError(Exception):
+ """Base class for openQA client errors."""
+
+ pass
+
+
+class ConnectionError(OpenQAClientError):
+ """Error raised when server connection fails. Just passed through
+ requests.exceptions.ConnectionError.
+ """
+
+ pass
+
+
+class RequestError(OpenQAClientError):
+ """Error raised when a request fails (after retries). 3-tuple of
+ method, URL, and status code.
+ """
+
+ pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/tests/conftest.py
new/python-openqa_client-4.1.1/tests/conftest.py
--- old/python-openqa_client-1.3.0/tests/conftest.py 1970-01-01
01:00:00.000000000 +0100
+++ new/python-openqa_client-4.1.1/tests/conftest.py 2020-08-07
23:50:44.000000000 +0200
@@ -0,0 +1,97 @@
+# Copyright (C) 2016 Red Hat
+#
+# This file is part of openQA-python-client.
+#
+# openQA-python-client is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Author: Adam Williamson <[email protected]>
+
+# these are all kinda inappropriate for pytest patterns
+# pylint: disable=no-init, protected-access, no-self-use, unused-argument
+
+"""Test configuration and fixtures."""
+
+import os
+import shutil
+from unittest import mock
+
+import pytest
+
+
+def _config_teardown(datadir):
+ if os.path.exists(datadir):
+ shutil.rmtree(datadir)
+
+
+def _config_setup(hosts):
+ """Creates a config file in a fake user home directory, at
+ data/home/ under the tests directory. For each host in hosts we
+ write an entry with the same key and secret, unless the host has
+ 'nokey' in it, in which case we write an entry with no key or
+ secret. Before doing this, re-create the home dir.
+ """
+ datadir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data")
+ home = os.path.join(datadir, "home")
+ _config_teardown(datadir)
+ confpath = os.path.join(home, ".config", "openqa")
+ os.makedirs(confpath)
+ confpath = os.path.join(confpath, "client.conf")
+ content = []
+ for host in hosts:
+ if "nokey" in host:
+ # don't write a key and secret for this host
+ content.extend([f"[{host}]"])
+ else:
+ content.extend([f"[{host}]", "key = aaaaaaaaaaaaaaaa", "secret =
bbbbbbbbbbbbbbbb"])
+ content = "\n".join(content)
+ with open(confpath, "w") as conffh:
+ conffh.write(content)
+ return (datadir, home)
+
+
[email protected](scope="function")
+def config(config_hosts):
+ """Create config file via _config_setup, using list of hosts
+ passed in via arg (intended for parametrization). Patch
+ os.path.expanduser to return the home dir, then teardown on test
+ completion.
+ """
+ (datadir, home) = _config_setup(config_hosts)
+ with mock.patch("os.path.expanduser", return_value=home, autospec=True):
+ yield
+ _config_teardown(datadir)
+
+
[email protected](scope="function")
+def simple_config():
+ """Create config file via _config_setup, with a single host. Patch
+ os.path.expanduser to return the home dir, then teardown on test
+ completion.
+ """
+ (datadir, home) = _config_setup(["openqa.fedoraproject.org"])
+ with mock.patch("os.path.expanduser", return_value=home, autospec=True):
+ yield
+ _config_teardown(datadir)
+
+
[email protected](scope="function")
+def empty_config():
+ """Create empty config file via _config_setup. Patch
+ os.path.expanduser to return the home dir, then teardown on test
+ completion.
+ """
+ (datadir, home) = _config_setup([])
+ with mock.patch("os.path.expanduser", return_value=home, autospec=True):
+ yield
+ _config_teardown(datadir)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/tests/test_client.py
new/python-openqa_client-4.1.1/tests/test_client.py
--- old/python-openqa_client-1.3.0/tests/test_client.py 1970-01-01
01:00:00.000000000 +0100
+++ new/python-openqa_client-4.1.1/tests/test_client.py 2020-08-07
23:50:44.000000000 +0200
@@ -0,0 +1,273 @@
+# Copyright (C) 2020 Red Hat
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors: Adam Williamson <[email protected]>
+
+# these are all kinda inappropriate for pytest patterns
+# pylint: disable=old-style-class, no-init, protected-access, no-self-use,
unused-argument
+# pylint: disable=invalid-name, too-few-public-methods,
too-many-public-methods, too-many-lines
+
+"""Tests for the main client code."""
+
+from unittest import mock
+
+import freezegun
+import pytest
+import requests
+
+import openqa_client.client as oqc
+import openqa_client.exceptions as oqe
+
+
+class TestClient:
+ """Tests for the client library."""
+
+ @pytest.mark.parametrize(
+ "config_hosts",
+ [
+ ["localhost"],
+ ["openqa.fedoraproject.org"],
+ ["localhost", "openqa.fedoraproject.org"],
+ ["openqa.fedoraproject.org", "localhost"],
+ ["openqa.nokey.org", "localhost", "openqa.fedoraproject.org"],
+ ["http://openqa.fedoraproject.org", "openqa.fedoraproject.org"],
+ ["https://openqa.fedoraproject.org", "localhost"],
+ ],
+ )
+ def test_config_hosts(self, config, config_hosts):
+ """Test handling config files with various different hosts
+ specified (sometimes one, sometimes more).
+ """
+ client = oqc.OpenQA_Client()
+ # we expect default scheme 'http' for localhost, specified
+ # scheme if there is one, 'https' for all else
+ if config_hosts[0] == "localhost":
+ scheme = "http://"
+ elif config_hosts[0].startswith("http"):
+ scheme = ""
+ else:
+ scheme = "https://"
+ assert client.baseurl == f"{scheme}{config_hosts[0]}"
+ assert client.session.headers["Accept"] == "json"
+ # this should be set for all but the 'nokey' case
+ if "nokey" in config_hosts[0]:
+ assert "X-API-Key" not in client.session.headers
+ else:
+ assert client.session.headers["X-API-Key"] == "aaaaaaaaaaaaaaaa"
+ assert client.apisecret == "bbbbbbbbbbbbbbbb"
+ # check we override the config file priority but use the key
+ # if server and scheme specified
+ client = oqc.OpenQA_Client(server="openqa.fedoraproject.org",
scheme="http")
+ assert client.baseurl == "http://openqa.fedoraproject.org"
+ if "openqa.fedoraproject.org" in config_hosts:
+ assert client.session.headers["X-API-Key"] == "aaaaaaaaaaaaaaaa"
+ assert client.apisecret == "bbbbbbbbbbbbbbbb"
+ else:
+ assert "X-API-Key" not in client.session.headers
+
+ def test_noconfig_host(self, empty_config):
+ """Test with empty config file (should use localhost)."""
+ client = oqc.OpenQA_Client()
+ assert client.baseurl == "http://localhost"
+ assert "X-API-Key" not in client.session.headers
+
+ @freezegun.freeze_time("2020-02-27")
+ def test_add_auth_headers(self, simple_config):
+ """Test _add_auth_headers."""
+ client = oqc.OpenQA_Client()
+ # this weird build value tests tilde substitution in hash
+ params = {"build": "foo~", "latest": "1"}
+ # this (incorrect) URL tests space substitution in hash
+ request = requests.Request(
+ url=client.baseurl + "/api/v1/jobs ", method="GET", params=params
+ )
+ prepared = client.session.prepare_request(request)
+ authed = client._add_auth_headers(prepared)
+ assert prepared.headers != authed.headers
+ assert authed.headers["X-API-Hash"] ==
"71373f0a57118b120d1915ccc0a24ae2cc112ad3"
+ assert authed.headers["X-API-Microtime"] == b"1582761600.0"
+ # with no key/secret, request should be returned unmodified
+ client = oqc.OpenQA_Client("localhost")
+ request = requests.Request(
+ url=client.baseurl + "/api/v1/jobs ", method="GET", params=params
+ )
+ prepared = client.session.prepare_request(request)
+ authed = client._add_auth_headers(prepared)
+ assert prepared.headers == authed.headers
+
+ @mock.patch("requests.sessions.Session.send", autospec=True)
+ def test_do_request_ok(self, fakesend, simple_config):
+ """Test do_request (normal, success case)."""
+ # we have to set up a proper headers dict or mock gets lost in
+ # infinite recursion and eats all our RAM...
+ fakeresp = fakesend.return_value
+ fakeresp.headers = {"content-type": "text/json,encoding=utf-8"}
+ client = oqc.OpenQA_Client()
+ params = {"id": "1"}
+ request = requests.Request(url=client.baseurl + "/api/v1/jobs",
method="GET", params=params)
+ client.do_request(request)
+ # check request was authed. Note: [0][0] is self
+ assert "X-API-Key" in fakesend.call_args[0][1].headers
+ assert "X-API-Hash" in fakesend.call_args[0][1].headers
+ assert "X-API-Microtime" in fakesend.call_args[0][1].headers
+ # check URL looks right
+ assert fakesend.call_args[0][1].url ==
"https://openqa.fedoraproject.org/api/v1/jobs?id=1"
+ # check we called .json() on the response
+ fakeresp = fakesend.return_value
+ assert len(fakeresp.method_calls) == 1
+ (callname, callargs, callkwargs) = fakeresp.method_calls[0]
+ assert callname == "json"
+ assert not callargs
+ assert not callkwargs
+
+ @mock.patch("requests.sessions.Session.send", autospec=True)
+ def test_do_request_ok_no_parse(self, fakesend, simple_config):
+ """Test do_request (normal, success case, with parse=False)."""
+ client = oqc.OpenQA_Client()
+ params = {"id": "1"}
+ request = requests.Request(url=client.baseurl + "/api/v1/jobs",
method="GET", params=params)
+ client.do_request(request, parse=False)
+ # check request was authed. Note: [0][0] is self
+ assert "X-API-Key" in fakesend.call_args[0][1].headers
+ assert "X-API-Hash" in fakesend.call_args[0][1].headers
+ assert "X-API-Microtime" in fakesend.call_args[0][1].headers
+ # check URL looks right
+ assert fakesend.call_args[0][1].url ==
"https://openqa.fedoraproject.org/api/v1/jobs?id=1"
+ # check we did not call .json() (or anything else) on response
+ fakeresp = fakesend.return_value
+ assert len(fakeresp.method_calls) == 0
+
+ @mock.patch("requests.sessions.Session.send", autospec=True)
+ def test_do_request_ok_yaml(self, fakesend, simple_config):
+ """Test do_request (with YAML response)."""
+ # set up the response to return YAML and correct
+ # content-type header
+ fakeresp = fakesend.return_value
+ fakeresp.headers = {"content-type": "text/yaml,encoding=utf-8"}
+ fakeresp.text = "defaults:\n arm:\n machine: ARM"
+ client = oqc.OpenQA_Client()
+ request = requests.Request(
+ url=client.baseurl + "/api/v1/job_templates_scheduling/1",
method="GET"
+ )
+ ret = client.do_request(request)
+ # check we did not call .json() on response
+ assert len(fakeresp.method_calls) == 0
+ # check we parsed the response
+ assert ret == {"defaults": {"arm": {"machine": "ARM"}}}
+
+ @mock.patch("time.sleep", autospec=True)
+ @mock.patch("requests.sessions.Session.send", autospec=True)
+ def test_do_request_not_ok(self, fakesend, fakesleep, simple_config):
+ """Test do_request (response not OK, default retries)."""
+ fakesend.return_value.ok = False
+ client = oqc.OpenQA_Client()
+ params = {"id": "1"}
+ request = requests.Request(url=client.baseurl + "/api/v1/jobs",
method="GET", params=params)
+ # if response is not OK, we should raise RequestError
+ with pytest.raises(oqe.RequestError):
+ client.do_request(request)
+ # we should also have retried 5 times, with a wait based on 10
+ assert fakesend.call_count == 6
+ assert fakesleep.call_count == 5
+ sleeps = [call[0][0] for call in fakesleep.call_args_list]
+ assert sleeps == [10, 20, 40, 60, 60]
+
+ @mock.patch("time.sleep", autospec=True)
+ @mock.patch(
+ "requests.sessions.Session.send",
+ autospec=True,
+ side_effect=requests.exceptions.ConnectionError("foo"),
+ )
+ def test_do_request_error(self, fakesend, fakesleep, simple_config):
+ """Test do_request (send raises exception, custom retries)."""
+ client = oqc.OpenQA_Client()
+ params = {"id": "1"}
+ request = requests.Request(url=client.baseurl + "/api/v1/jobs",
method="GET", params=params)
+ # if send raises ConnectionError, we should raise ours
+ with pytest.raises(oqe.ConnectionError):
+ client.do_request(request, retries=2, wait=5)
+ # we should also have retried 2 times, with a wait based on 5
+ assert fakesend.call_count == 3
+ assert fakesleep.call_count == 2
+ sleeps = [call[0][0] for call in fakesleep.call_args_list]
+ assert sleeps == [5, 10]
+
+ @mock.patch("openqa_client.client.OpenQA_Client.do_request", autospec=True)
+ def test_openqa_request(self, fakedo, simple_config):
+ """Test openqa_request."""
+ client = oqc.OpenQA_Client()
+ params = {"id": "1"}
+ client.openqa_request("get", "jobs", params=params, retries=2, wait=5)
+ # check we called do_request right. Note: [0][0] is self
+ assert fakedo.call_args[0][1].url ==
"https://openqa.fedoraproject.org/api/v1/jobs"
+ assert fakedo.call_args[0][1].params == {"id": "1"}
+ assert fakedo.call_args[1]["retries"] == 2
+ assert fakedo.call_args[1]["wait"] == 5
+ # check requests with no params work
+ fakedo.reset_mock()
+ client.openqa_request("get", "jobs", retries=2, wait=5)
+ assert fakedo.call_args[0][1].url ==
"https://openqa.fedoraproject.org/api/v1/jobs"
+ assert fakedo.call_args[0][1].params == {}
+ assert fakedo.call_args[1]["retries"] == 2
+ assert fakedo.call_args[1]["wait"] == 5
+
+ @mock.patch("openqa_client.client.OpenQA_Client.openqa_request",
autospec=True)
+ def test_find_clones(self, fakerequest, simple_config):
+ """Test find_clones."""
+ client = oqc.OpenQA_Client()
+ # test data: three jobs with clones, one included in the data,
+ # two not
+ jobs = [
+ {"id": 1, "name": "foo", "result": "failed", "clone_id": 2},
+ {"id": 2, "name": "foo", "result": "passed", "clone_id": None},
+ {"id": 3, "name": "bar", "result": "failed", "clone_id": 4},
+ {"id": 5, "name": "moo", "result": "failed", "clone_id": 6},
+ ]
+ # set the mock to return the additional jobs when we ask
+ fakerequest.return_value = {
+ "jobs": [
+ {"id": 4, "name": "bar", "result": "passed", "clone_id": None},
+ {"id": 6, "name": "moo", "result": "passed", "clone_id": None},
+ ]
+ }
+ ret = client.find_clones(jobs)
+ assert ret == [
+ {"id": 2, "name": "foo", "result": "passed", "clone_id": None},
+ {"id": 4, "name": "bar", "result": "passed", "clone_id": None},
+ {"id": 6, "name": "moo", "result": "passed", "clone_id": None},
+ ]
+ # check we actually requested the additional job correctly
+ assert fakerequest.call_count == 1
+ assert fakerequest.call_args[0][1] == "GET"
+ assert fakerequest.call_args[0][2] == "jobs"
+ assert fakerequest.call_args[1]["params"] == {"ids": "4,6"}
+
+ @mock.patch("openqa_client.client.OpenQA_Client.find_clones",
autospec=True)
+ @mock.patch("openqa_client.client.OpenQA_Client.openqa_request",
autospec=True)
+ def test_get_jobs(self, fakerequest, fakeclones, simple_config):
+ """Test get_jobs."""
+ client = oqc.OpenQA_Client()
+ with pytest.raises(TypeError):
+ client.get_jobs()
+ client.get_jobs(jobs=[1, 2])
+ assert fakerequest.call_args[0][1] == "GET"
+ assert fakerequest.call_args[0][2] == "jobs"
+ assert fakerequest.call_args[1]["params"] == {"ids": "1,2", "latest":
"1"}
+ assert fakeclones.call_count == 1
+ client.get_jobs(build="foo", filter_dupes=False)
+ assert fakerequest.call_args[0][1] == "GET"
+ assert fakerequest.call_args[0][2] == "jobs"
+ assert fakerequest.call_args[1]["params"] == {"build": "foo"}
+ assert fakeclones.call_count == 1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/tests.requires
new/python-openqa_client-4.1.1/tests.requires
--- old/python-openqa_client-1.3.0/tests.requires 1970-01-01
01:00:00.000000000 +0100
+++ new/python-openqa_client-4.1.1/tests.requires 2020-08-07
23:50:44.000000000 +0200
@@ -0,0 +1,3 @@
+freezegun
+mock
+pytest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-openqa_client-1.3.0/tox.ini
new/python-openqa_client-4.1.1/tox.ini
--- old/python-openqa_client-1.3.0/tox.ini 1970-01-01 01:00:00.000000000
+0100
+++ new/python-openqa_client-4.1.1/tox.ini 2020-08-07 23:50:44.000000000
+0200
@@ -0,0 +1,26 @@
+[tox]
+envlist = py{36,37,38,39}-ci
+skip_missing_interpreters = true
+isolated_build = true
+
+[gh-actions]
+python =
+ 3.6: py36-ci
+ 3.7: py37-ci
+ 3.8: py38-ci
+
+[testenv]
+deps =
+ -r{toxinidir}/install.requires
+ -r{toxinidir}/tests.requires
+ ci: -r{toxinidir}/ci.requires
+
+commands =
+ py.test
+ ci: coverage run -m pytest {posargs}
+ ci: coverage combine
+ ci: coverage report
+ ci: coverage xml
+ ci: diff-cover coverage.xml --fail-under=90
+ ci: diff-quality --violations=pylint --fail-under=90
+ ci: black src/ tests/ --check --exclude const.py