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


Reply via email to