Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-copr for openSUSE:Factory 
checked in at 2022-09-15 22:59:57
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-copr (Old)
 and      /work/SRC/openSUSE:Factory/.python-copr.new.2083 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-copr"

Thu Sep 15 22:59:57 2022 rev:4 rq:1003843 version:1.122

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-copr/python-copr.changes  2022-04-22 
21:56:18.930946921 +0200
+++ /work/SRC/openSUSE:Factory/.python-copr.new.2083/python-copr.changes        
2022-09-15 23:01:12.245556515 +0200
@@ -1,0 +2,7 @@
+Thu Sep 15 10:42:44 UTC 2022 - pgaj...@suse.com
+
+- version update to 1.122
+  - no upstream changelog file found
+- does not require python-six
+
+-------------------------------------------------------------------

Old:
----
  copr-1.119.tar.gz

New:
----
  copr-1.122.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-copr.spec ++++++
--- /var/tmp/diff_new_pack.Ihu8AB/_old  2022-09-15 23:01:12.725557871 +0200
+++ /var/tmp/diff_new_pack.Ihu8AB/_new  2022-09-15 23:01:12.733557893 +0200
@@ -18,7 +18,7 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-copr
-Version:        1.119
+Version:        1.122
 Release:        0
 Summary:        Python client for copr service
 License:        GPL-2.0-or-later
@@ -35,14 +35,12 @@
 BuildRequires:  %{python_module requests-gssapi}
 BuildRequires:  %{python_module requests-toolbelt}
 BuildRequires:  %{python_module requests}
-BuildRequires:  %{python_module six}
 # /SECTION
 BuildRequires:  fdupes
 Requires:       python-marshmallow
 Requires:       python-munch
 Requires:       python-requests
 Requires:       python-requests-toolbelt
-Requires:       python-six
 BuildArch:      noarch
 %python_subpackages
 

++++++ copr-1.119.tar.gz -> copr-1.122.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/PKG-INFO new/copr-1.122/PKG-INFO
--- old/copr-1.119/PKG-INFO     2022-04-05 10:50:41.658881400 +0200
+++ new/copr-1.122/PKG-INFO     2022-08-18 15:02:18.373112200 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: copr
-Version: 1.119
+Version: 1.122
 Summary: Python client for copr service.
 Home-page: https://pagure.io/copr/copr
 Author: Valentin Gologuzov
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr/test/client_v3/test_auth.py 
new/copr-1.122/copr/test/client_v3/test_auth.py
--- old/copr-1.119/copr/test/client_v3/test_auth.py     1970-01-01 
01:00:00.000000000 +0100
+++ new/copr-1.122/copr/test/client_v3/test_auth.py     2022-08-18 
15:01:47.000000000 +0200
@@ -0,0 +1,153 @@
+import time
+import pytest
+from copr.v3.auth import ApiToken, Gssapi, auth_from_config, CoprAuthException
+from copr.v3.auth.base import BaseAuth
+from copr.test import mock
+
+
+class TestApiToken:
+    @staticmethod
+    def test_make_expensive():
+        """
+        Test that `ApiToken` load all necessary information from config
+        """
+        config = mock.MagicMock()
+        auth = ApiToken(config)
+        assert not auth.auth
+        assert not auth.username
+
+        # Make sure all auth values are loaded from the config
+        auth.make_expensive()
+        assert auth.auth
+        assert auth.username
+
+
+class TestGssApi:
+    @staticmethod
+    @mock.patch("requests.get")
+    def test_make_expensive(mock_get):
+        """
+        Test that `Gssapi` knows what to do with information from Kerberos
+        """
+        config = mock.MagicMock()
+        auth = Gssapi(config)
+        assert not auth.username
+        assert not auth.auth
+
+        # Fake a response from Kerberos
+        response = mock.MagicMock()
+        response.json.return_value = {"id": 123, "name": "jdoe"}
+        response.cookies = {"session": "foo-bar-hash"}
+        mock_get.return_value = response
+
+        # Make sure GSSAPI cookies are set
+        auth.make_expensive()
+        assert auth.username == "jdoe"
+        assert auth.cookies == {"session": "foo-bar-hash"}
+
+
+class TestBaseAuth:
+    @staticmethod
+    @mock.patch("copr.v3.auth.base.BaseAuth.expired")
+    @mock.patch("copr.v3.auth.base.BaseAuth.make_expensive")
+    def test_make_without_cache(make_expensive, _expired):
+        """
+        Test that auth classes remember and re-use the information from
+        `make_expensive()`
+        """
+        auth = BaseAuth(config=None)
+        auth.cache = mock.MagicMock()
+
+        # Even though we call make() multiple times,
+        # make_expensive() is called only once
+        for _ in range(5):
+            auth.make()
+        assert make_expensive.call_count == 1
+
+    @staticmethod
+    @mock.patch("copr.v3.auth.base.BaseAuth.make_expensive")
+    def test_make_reauth(make_expensive):
+        """
+        When reauth is requested, make sure we don't use any previous tokens
+        """
+        auth = BaseAuth(config=None)
+        auth.cache = mock.MagicMock()
+        for _ in range(5):
+            auth.make(reauth=True)
+        assert make_expensive.call_count == 5
+
+    @staticmethod
+    @mock.patch("copr.v3.auth.base.BaseAuth.make_expensive")
+    def test_make_from_cache(make_expensive):
+        """
+        If there is a cached session cookie that is still valid, use it and
+        don't make any expensive calls
+        """
+        auth = BaseAuth(config=None)
+        auth.cache = FakeCache(None)
+        for _ in range(5):
+            auth.make()
+        assert make_expensive.call_count == 0
+
+    @staticmethod
+    @mock.patch("copr.v3.auth.base.BaseAuth.make_expensive")
+    def test_make_from_cache_expired(make_expensive):
+        """
+        If there is a cached session cookie but it is expired, just ignore it
+        """
+        auth = BaseAuth(config=None)
+        auth.cache = FakeCacheExpired(None)
+        for _ in range(5):
+            auth.make()
+        assert make_expensive.call_count == 1
+
+
+class TestAuth:
+    @staticmethod
+    def test_auth_from_config():
+        """
+        Make sure we use the expected authentication method
+        """
+        # Use (login, token) authentication if there is enough information
+        auth = auth_from_config({
+            "copr_url": "http://copr";,
+            "login": "test",
+            "token": "test",
+            "username": "jdoe",
+        })
+        assert isinstance(auth, ApiToken)
+
+        # Otherwise use GSSAPI (if gssapi is enabled, which is by default)
+        auth = auth_from_config({
+            "copr_url": "http://copr";,
+            "gssapi": True,
+        })
+        assert isinstance(auth, Gssapi)
+
+        # There are no other authentication methods
+        config = {
+            "copr_url": "http://copr";,
+            "gssapi": False,
+        }
+        with pytest.raises(CoprAuthException):
+            auth = auth_from_config(config)
+
+
+class FakeCache(mock.MagicMock):
+    # pylint: disable=too-many-ancestors
+    def load_session(self):
+        # pylint: disable=no-self-use
+        return {
+            "name": "jdoe",
+            "session": "foo-bar-hash",
+            "expiration": time.time() + 1
+        }
+
+class FakeCacheExpired(FakeCache):
+    # pylint: disable=too-many-ancestors
+    def load_session(self):
+        return {
+            "name": "jdoe",
+            "session": "foo-bar-hash",
+            "expiration": time.time() - 1
+        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr/test/client_v3/test_modules.py 
new/copr-1.122/copr/test/client_v3/test_modules.py
--- old/copr-1.119/copr/test/client_v3/test_modules.py  2022-04-05 
10:49:51.000000000 +0200
+++ new/copr-1.122/copr/test/client_v3/test_modules.py  2022-08-18 
15:01:47.000000000 +0200
@@ -4,6 +4,7 @@
 import shutil
 from json import loads
 from copr.test import mock
+from copr.test.client_v3.test_auth import FakeCache
 from copr.v3 import ModuleProxy
 
 
@@ -27,6 +28,7 @@
     @mock.patch('copr.v3.requests.requests.Session.request')
     def test_module_dist_git_choice_url(self, request, distgit_opt):
         proxy = ModuleProxy(self.config_auth)
+        proxy.auth.cache = FakeCache(None)
         proxy.build_from_url('owner', 'project', 'http://test.yaml',
                              distgit=distgit_opt)
 
@@ -45,6 +47,7 @@
     @mock.patch('copr.v3.requests.requests.Session.request')
     def test_module_dist_git_choice_upload(self, request, distgit_opt):
         proxy = ModuleProxy(self.config_auth)
+        proxy.auth.cache = FakeCache(None)
         proxy.build_from_file('owner', 'project',
                               self.yaml_file,
                               distgit=distgit_opt)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr/v3/auth/__init__.py 
new/copr-1.122/copr/v3/auth/__init__.py
--- old/copr-1.119/copr/v3/auth/__init__.py     1970-01-01 01:00:00.000000000 
+0100
+++ new/copr-1.122/copr/v3/auth/__init__.py     2022-08-18 15:01:47.000000000 
+0200
@@ -0,0 +1,24 @@
+"""
+Authentication classes for usage within APIv3
+"""
+
+from copr.v3.exceptions import CoprAuthException
+from copr.v3.auth.token import ApiToken
+from copr.v3.auth.gssapi import Gssapi
+
+
+def auth_from_config(config):
+    """
+    Decide what authentication method to use and return an appropriate instance
+    """
+    if config.get("token"):
+        return ApiToken(config)
+
+    if config.get("gssapi"):
+        return Gssapi(config)
+
+    msg = "GSSAPI disabled and login:token is invalid ({0}:{1})".format(
+        config.get("login", "NOT_SET"),
+        config.get("token", "NOT_SET"),
+    )
+    raise CoprAuthException(msg)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr/v3/auth/base.py 
new/copr-1.122/copr/v3/auth/base.py
--- old/copr-1.119/copr/v3/auth/base.py 1970-01-01 01:00:00.000000000 +0100
+++ new/copr-1.122/copr/v3/auth/base.py 2022-08-18 15:01:47.000000000 +0200
@@ -0,0 +1,161 @@
+"""
+Base authentication interface
+"""
+
+import os
+import json
+import time
+import errno
+from filelock import FileLock
+
+try:
+    from urllib.parse import urlparse
+except ImportError:
+    from urlparse import urlparse
+
+
+class BaseAuth:
+    """
+    Base authentication class
+    There is a more standard way of implementing custom authentication classes,
+    please see https://docs.python-requests.org/en/latest/user/authentication/
+    We can eventually implement it using `requests.auth`.
+    """
+
+    def __init__(self, config):
+        self.config = config
+        self.cache = AuthCache(config)
+        self.username = None
+        self.expiration = None
+
+        # These attributes will be directly passed to the requests.request(...)
+        # calls. Various authentication mechanisms should set them accordingly.
+        self.auth = None
+        self.cookies = None
+
+    @property
+    def expired(self):
+        """
+        Are the authentication tokens, cookies, etc, expired?
+
+        We know this for example for cached cookies. It can be tricky because
+        it can be expired regardless of the expiration time when frontend
+        decides to e.g. revoke all tokens, but we get to know only when sending
+        a request.
+
+        But when expiration time is gone, we for sure know the cookie is
+        expired. That will help us avoid sending requests that we know will be
+        unsuccessful.
+        """
+        if not self.expiration:
+            return False
+        return self.expiration < time.time()
+
+    def make_expensive(self):
+        """
+        Perform the authentication process. This is the most expensive part.
+        The point is to set `self.username`, and some combination of
+        `self.auth` and `self.cookies`.
+        """
+        raise NotImplementedError
+
+    def make(self, reauth=False):
+        """
+        Perform the authentication process. It can be expensive, so we ensure
+        caching with locks, expiration checks, etc.
+        If `reauth=True` is set, then any cached cookies are ignored and the
+        authentication is done from scratch
+        """
+        # If we know an username, the authentication must have been done
+        # previously and there is no need to do it again.
+        # Unless we specifically request an re-auth
+        auth_done = bool(self.username) and not reauth
+        if auth_done:
+            return
+
+        with self.cache.lock:
+            # Try to load session data from cache
+            if not reauth and self._load_cache():
+                auth_done = True
+
+            # If we don't have any cached cookies or they are expired
+            if not auth_done or self.expired:
+                self.make_expensive()
+                self._save_cache()
+
+    def _load_cache(self):
+        session_data = self.cache.load_session()
+        if session_data:
+            token = session_data["session"]
+            self.username = session_data["name"]
+            self.cookies = {"session": token}
+            self.expiration = session_data["expiration"]
+        return bool(session_data)
+
+    def _save_cache(self):
+        # For now, we don't want to cache (login, token) information, only
+        # session cookies (for e.g. GSSAPI)
+        if not self.cookies:
+            return
+
+        data = {
+            "name": self.username,
+            "session": self.cookies["session"],
+            "expiration": self.expiration,
+        }
+        self.cache.save_session(data)
+
+
+class AuthCache:
+    """
+    Some authentication methods are expensive and we want to use them only
+    for an initial authentication, chache their value, and use until 
expiration.
+    """
+
+    def __init__(self, config):
+        self.config = config
+
+    @property
+    def session_file(self):
+        """
+        Path to the cached file for a given Copr instance
+        """
+        url = urlparse(self.config["copr_url"]).netloc
+        cachedir = os.path.join(os.path.expanduser("~"), ".cache", "copr")
+        try:
+            os.makedirs(cachedir)
+        except OSError as err:
+            if err.errno != errno.EEXIST:
+                raise
+        return os.path.join(cachedir, url + "-session")
+
+    @property
+    def lock_file(self):
+        """
+        Path to the lock file for a given Copr instance
+        """
+        return self.session_file + ".lock"
+
+    @property
+    def lock(self):
+        """
+        Allow the user to do `with cache.lock:`
+        """
+        return FileLock(self.lock_file)
+
+    def load_session(self):
+        """
+        Load the session data from a cache file
+        """
+        if not os.path.exists(self.session_file):
+            return None
+        with open(self.session_file, "r") as fp:
+            return json.load(fp)
+
+    def save_session(self, session_data):
+        """
+        Save the session data to a cache file
+        """
+        with open(self.session_file, "w") as file:
+            session_data["expiration"] = time.time() + 10 * 3600  # +10 hours
+            file.write(json.dumps(session_data, indent=4) + "\n")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr/v3/auth/gssapi.py 
new/copr-1.122/copr/v3/auth/gssapi.py
--- old/copr-1.119/copr/v3/auth/gssapi.py       1970-01-01 01:00:00.000000000 
+0100
+++ new/copr-1.122/copr/v3/auth/gssapi.py       2022-08-18 15:01:47.000000000 
+0200
@@ -0,0 +1,49 @@
+"""
+Authentication via GSSAPI
+"""
+
+import requests
+
+try:
+    import requests_gssapi
+except ImportError:
+    requests_gssapi = None
+
+from future.utils import raise_from
+from copr.v3.exceptions import CoprAuthException
+from copr.v3.requests import munchify, handle_errors
+from copr.v3.auth.base import BaseAuth
+
+
+class Gssapi(BaseAuth):
+    """
+    Authentication via GSSAPI (i.e. Kerberos)
+    """
+    def __init__(self, *args, **kwargs):
+        """
+        Gssapi class stub for the systems where requests_gssapi is not
+        installed (typically PyPI installations)
+        """
+        if not requests_gssapi:
+            # Raise an exception if any dependency is not installed
+            raise CoprAuthException(
+                "The 'requests_gssapi' package is not installed. "
+                "Please install it, or use the API token (config file)."
+            )
+        super(Gssapi, self).__init__(*args, **kwargs)
+
+    def make_expensive(self):
+        url = self.config["copr_url"] + "/api_3/gssapi_login/"
+        auth = requests_gssapi.HTTPSPNEGOAuth(opportunistic_auth=True)
+        try:
+            response = requests.get(url, auth=auth)
+        except requests_gssapi.exceptions.SPNEGOExchangeError as err:
+            msg = "Can not get session for {0} cookie via GSSAPI: {1}".format(
+                self.config["copr_url"], err)
+            raise_from(CoprAuthException(msg), err)
+
+        handle_errors(response)
+        data = munchify(response)
+        token = response.cookies.get("session")
+        self.username = data.name
+        self.cookies = {"session": token}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr/v3/auth/token.py 
new/copr-1.122/copr/v3/auth/token.py
--- old/copr-1.119/copr/v3/auth/token.py        1970-01-01 01:00:00.000000000 
+0100
+++ new/copr-1.122/copr/v3/auth/token.py        2022-08-18 15:01:47.000000000 
+0200
@@ -0,0 +1,14 @@
+"""
+Authentication via (login, token)
+"""
+
+from copr.v3.auth.base import BaseAuth
+
+
+class ApiToken(BaseAuth):
+    """
+    The standard authentication via `(login, token)` from `~/.config/copr`
+    """
+    def make_expensive(self):
+        self.auth = self.config["login"], self.config["token"]
+        self.username = self.config.get("username")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr/v3/helpers.py 
new/copr-1.122/copr/v3/helpers.py
--- old/copr-1.119/copr/v3/helpers.py   2022-04-05 10:49:51.000000000 +0200
+++ new/copr-1.122/copr/v3/helpers.py   2022-08-18 15:01:47.000000000 +0200
@@ -18,15 +18,20 @@
 
 def config_from_file(path=None):
     raw_config = configparser.ConfigParser()
-    path = os.path.expanduser(path or os.path.join("~", ".config", "copr"))
+
     config = {}
+    default_path = os.path.join("~", ".config", "copr")
 
     try:
-        exists = raw_config.read(path)
+        exists = raw_config.read(os.path.expanduser(path or default_path))
     except configparser.Error as ex:
         raise CoprConfigException(str(ex))
 
     if not exists:
+        if path:
+            # absence of the default_path is acceptable, but missing the
+            # explicitly specified path= argument deserves an exception.
+            raise CoprConfigException("File {0} is missing.".format(path))
         raw_config["copr-cli"] = {"copr_url": 
"https://copr.fedorainfracloud.org"}
 
     try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr/v3/proxies/__init__.py 
new/copr-1.122/copr/v3/proxies/__init__.py
--- old/copr-1.119/copr/v3/proxies/__init__.py  2022-04-05 10:49:51.000000000 
+0200
+++ new/copr-1.122/copr/v3/proxies/__init__.py  2022-08-18 15:01:47.000000000 
+0200
@@ -1,21 +1,8 @@
-import errno
-import json
-import time
 import os
-from filelock import FileLock
 
-try:
-    from urllib.parse import urlparse
-except ImportError:
-    from urlparse import urlparse
-
-from future.utils import raise_from
-
-import requests_gssapi
-
-from ..requests import Request, munchify, requests, handle_errors
+from copr.v3.auth import auth_from_config
+from copr.v3.requests import munchify, Request
 from ..helpers import for_all_methods, bind_proxy, config_from_file
-from ..exceptions import CoprAuthException
 
 
 @for_all_methods(bind_proxy)
@@ -26,9 +13,11 @@
 
     def __init__(self, config):
         self.config = config
-        self._auth_token_cached = None
-        self._auth_username = None
-        self.request = Request(api_base_url=self.api_base_url, 
connection_attempts=config.get("connection_attempts", 1))
+        self.request = Request(
+            api_base_url=self.api_base_url,
+            connection_attempts=config.get("connection_attempts", 1)
+        )
+        self._auth = None
 
     @classmethod
     def create_from_config_file(cls, path=None):
@@ -41,95 +30,9 @@
 
     @property
     def auth(self):
-        if self._auth_token_cached:
-            return self._auth_token_cached
-        if self.config.get("token"):
-            self._auth_token_cached = self.config["login"], 
self.config["token"]
-            self._auth_username = self.config.get("username")
-        elif self.config.get("gssapi"):
-            session_data = self._get_session_cookie_via_gssapi()
-            self._auth_token_cached = session_data["session"]
-            self._auth_username = session_data["name"]
-        else:
-            msg = "GSSAPI disabled and login:token is invalid 
({0}:{1})".format(
-                self.config.get("login", "NOT_SET"),
-                self.config.get("token", "NOT_SET"),
-            )
-            raise CoprAuthException(msg)
-        return self._auth_token_cached
-
-    def _get_session_cookie_via_gssapi(self):
-        """
-        Return the cached session for the configured username.  If not already
-        cached, new self.get_session_via_gssapi() is performed and result is
-        cached into ~/.config/copr/<session_file>.
-        """
-        url = urlparse(self.config["copr_url"]).netloc
-        cachedir = os.path.join(os.path.expanduser("~"), ".cache", "copr")
-
-        try:
-            os.makedirs(cachedir)
-        except OSError as err:
-            if err.errno != errno.EEXIST:
-                raise
-
-        session_file = os.path.join(cachedir, url + "-session")
-        session_data = self._load_or_download_session(session_file)
-        return session_data
-
-    @staticmethod
-    def _load_session_from_file(session_file):
-        session_data = None
-        if os.path.exists(session_file):
-            with open(session_file, "r") as file:
-                session_data = json.load(file)
-
-        if session_data and session_data["expiration"] > time.time():
-            return session_data
-        return None
-
-    def _load_or_download_session(self, session_file):
-        lock = FileLock(session_file + ".lock")
-        with lock:
-            session = BaseProxy._load_session_from_file(session_file)
-            if session:
-                return session
-            # TODO: create Munch sub-class that returns serializable dict, we
-            # have something like that in Cli: 
cli/copr_cli/util.py:serializable()
-            session_data = self.get_session_via_gssapi()
-            session_data = session_data.__dict__
-            session_data.pop("__response__", None)
-            session_data.pop("__proxy__", None)
-            BaseProxy._save_session(session_file, session_data)
-            return session_data
-
-    @staticmethod
-    def _save_session(session_file, session_data):
-        with open(session_file, "w") as file:
-            session_data["expiration"] = time.time() + 10 * 3600  # +10 hours
-            file.write(json.dumps(session_data, indent=4) + "\n")
-
-    def get_session_via_gssapi(self):
-        """
-        Obtain a _new_ session using GSSAPI route
-
-        :return: Munch, provides user's "id", "name", "session" cookie, and
-            "expiration".
-        """
-        url = self.config["copr_url"] + "/api_3/gssapi_login/"
-        session = requests.Session()
-        auth = requests_gssapi.HTTPSPNEGOAuth(opportunistic_auth=True)
-        try:
-            response = session.get(url, auth=auth)
-        except requests_gssapi.exceptions.SPNEGOExchangeError as err:
-            msg = "Can not get session for {0} cookie via GSSAPI: {1}".format(
-                self.config["copr_url"], err)
-            raise_from(CoprAuthException(msg), err)
-
-        handle_errors(response)
-        retval = munchify(response)
-        retval.session = response.cookies.get("session")
-        return retval
+        if not self._auth:
+            self._auth = auth_from_config(self.config)
+        return self._auth
 
     def home(self):
         """
@@ -156,7 +59,6 @@
         Return the username (string) assigned to this configuration.  May
         contact the server and authenticate if needed.
         """
-        if not self._auth_username:
-            # perform authentication as a side effect
-            _ = self.auth
-        return self._auth_username
+        if not self.auth.username:
+            self.auth.make()
+        return self.auth.username
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr/v3/proxies/build.py 
new/copr-1.122/copr/v3/proxies/build.py
--- old/copr-1.119/copr/v3/proxies/build.py     2022-04-05 10:49:51.000000000 
+0200
+++ new/copr-1.122/copr/v3/proxies/build.py     2022-08-18 15:01:47.000000000 
+0200
@@ -212,8 +212,10 @@
         return self._create(endpoint, data, buildopts=buildopts)
 
 
-    def create_from_pypi(self, ownername, projectname, pypi_package_name, 
pypi_package_version=None,
-                         spec_template='', python_versions=None, 
buildopts=None, project_dirname=None):
+    def create_from_pypi(self, ownername, projectname, pypi_package_name,
+                         pypi_package_version=None, spec_template='',
+                         python_versions=None, buildopts=None,
+                         project_dirname=None, spec_generator=None):
         """
         Create a build from PyPI - https://pypi.org/
 
@@ -225,6 +227,7 @@
         :param list python_versions: list of python versions to build for
         :param buildopts: 
http://python-copr.readthedocs.io/en/latest/client_v3/build_options.html
         :param str project_dirname:
+        :param str spec_generator: what tool should be used for spec generation
         :return: Munch
         """
         endpoint = "/build/create/pypi"
@@ -233,6 +236,7 @@
             "projectname": projectname,
             "pypi_package_name": pypi_package_name,
             "pypi_package_version": pypi_package_version,
+            "spec_generator": spec_generator,
             "spec_template": spec_template,
             "python_versions": python_versions or [3, 2],
             "project_dirname": project_dirname,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr/v3/proxies/project.py 
new/copr-1.122/copr/v3/proxies/project.py
--- old/copr-1.119/copr/v3/proxies/project.py   2022-04-05 10:49:51.000000000 
+0200
+++ new/copr-1.122/copr/v3/proxies/project.py   2022-08-18 15:01:47.000000000 
+0200
@@ -14,6 +14,7 @@
     warnings.warn("The 'use_bootstrap_container' argument is obsoleted by "
                   "'bootstrap' and 'bootstrap_image'")
 
+
 @for_all_methods(bind_proxy)
 class ProjectProxy(BaseProxy):
 
@@ -66,11 +67,11 @@
         return munchify(response)
 
     def add(self, ownername, projectname, chroots, description=None, 
instructions=None, homepage=None,
-            contact=None, additional_repos=None, unlisted_on_hp=False, 
enable_net=True, persistent=False,
+            contact=None, additional_repos=None, unlisted_on_hp=False, 
enable_net=False, persistent=False,
             auto_prune=True, use_bootstrap_container=None, devel_mode=False,
             delete_after_days=None, multilib=False, module_hotfixes=False,
             bootstrap=None, bootstrap_image=None, isolation=None,
-            fedora_review=None, appstream=True):
+            fedora_review=None, appstream=True, runtime_dependencies=None, 
packit_forge_projects_allowed=None):
         """
         Create a project
 
@@ -102,6 +103,11 @@
         :param bool fedora_review: Run fedora-review tool for packages
                                    in this project
         :param bool appstream: Disable or enable generating the appstream 
metadata
+        :param string runtime_dependencies: List of external repositories
+            (== dependencies, specified as baseurls) that will be automatically
+            enabled together with this project repository.
+        :param list packit_forge_projects_allowed: List of forge projects that
+            will be allowed to build in the project via Packit
         :return: Munch
         """
         endpoint = "/project/add/{ownername}"
@@ -129,6 +135,8 @@
             "module_hotfixes": module_hotfixes,
             "fedora_review": fedora_review,
             "appstream": appstream,
+            "runtime_dependencies": runtime_dependencies,
+            "packit_forge_projects_allowed": packit_forge_projects_allowed,
         }
 
         _compat_use_bootstrap_container(data, use_bootstrap_container)
@@ -147,7 +155,7 @@
              auto_prune=None, use_bootstrap_container=None, devel_mode=None,
              delete_after_days=None, multilib=None, module_hotfixes=None,
              bootstrap=None, bootstrap_image=None, isolation=None,
-             fedora_review=None, appstream=None):
+             fedora_review=None, appstream=None, runtime_dependencies=None, 
packit_forge_projects_allowed=None):
         """
         Edit a project
 
@@ -178,6 +186,8 @@
         :param bool fedora_review: Run fedora-review tool for packages
                                    in this project
         :param bool appstream: Disable or enable generating the appstream 
metadata
+        :param list packit_forge_projects_allowed: List of forge projects that
+            will be allowed to build in the project via Packit
         :return: Munch
         """
         endpoint = "/project/edit/{ownername}/{projectname}"
@@ -204,6 +214,8 @@
             "module_hotfixes": module_hotfixes,
             "fedora_review": fedora_review,
             "appstream": appstream,
+            "runtime_dependencies": runtime_dependencies,
+            "packit_forge_projects_allowed": packit_forge_projects_allowed,
         }
 
         _compat_use_bootstrap_container(data, use_bootstrap_container)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr/v3/requests.py 
new/copr-1.122/copr/v3/requests.py
--- old/copr-1.119/copr/v3/requests.py  2022-04-05 10:49:51.000000000 +0200
+++ new/copr-1.122/copr/v3/requests.py  2022-08-18 15:01:47.000000000 +0200
@@ -4,10 +4,8 @@
 import json
 import time
 import requests
-import requests_gssapi
 from copr.v3.helpers import List
 from munch import Munch
-from future.utils import raise_from
 from requests_toolbelt.multipart.encoder import MultipartEncoder, 
MultipartEncoderMonitor
 from .exceptions import CoprRequestException, CoprNoResultException, 
CoprTimeoutException, CoprAuthException
 
@@ -39,33 +37,33 @@
 
     def send(self, endpoint, method=GET, data=None, params=None, headers=None,
              auth=None):
-        session = requests.Session()
-        if not isinstance(auth, tuple):
-            # api token not available, set session cookie obtained via gssapi
-            session.cookies.set("session", auth)
 
         request_params = self._request_params(
             endpoint, method, data, params, headers, auth)
 
-        response = self._send_request_repeatedly(session, request_params)
+        response = self._send_request_repeatedly(request_params, auth)
+
         handle_errors(response)
         return response
 
-    def _send_request_repeatedly(self, session, request_params):
+    def _send_request_repeatedly(self, request_params, auth):
         """
         Repeat the request until it succeeds, or connection retry reaches its 
limit.
         """
         sleep = 5
         for i in range(1, self.connection_attempts + 1):
             try:
-                response = session.request(**request_params)
-            except requests_gssapi.exceptions.SPNEGOExchangeError as e:
-                raise_from(CoprAuthException("GSSAPI authentication failed."), 
e)
+                response = requests.request(**request_params)
+                if response.status_code == 401 and i < 
self.connection_attempts:
+                    # try to authenticate again, don't sleep!
+                    self._update_auth_params(request_params, auth, reauth=True)
+                    continue
+                # Return the response object (even for non-200 status codes!)
+                return response
             except requests.exceptions.ConnectionError:
                 if i < self.connection_attempts:
                     time.sleep(sleep)
-            else:
-                return response
+
         raise CoprRequestException("Unable to connect to 
{0}.".format(self.api_base_url))
 
     def _request_params(self, endpoint, method=GET, data=None, params=None,
@@ -77,12 +75,20 @@
             "params": params,
             "headers": headers,
         }
-        # We usually use a tuple (login, token). If this is not available,
-        # we use gssapi auth, which works with cookies.
-        if isinstance(auth, tuple):
-            params["auth"] = auth
+        self._update_auth_params(params, auth)
         return params
 
+    def _update_auth_params(self, request_params, auth, reauth=False):
+        # pylint: disable=no-self-use
+        if not auth:
+            return
+
+        auth.make(reauth)
+        request_params.update({
+            "auth": auth.auth,
+            "cookies": auth.cookies,
+        })
+
 
 class FileRequest(Request):
     def __init__(self, files=None, progress_callback=None, **kwargs):
@@ -139,5 +145,5 @@
             # pylint: disable=raise-missing-from
             raise CoprTimeoutException(message, response=response)
 
-        raise CoprRequestException("Request is not in JSON format, there is 
probably a bug in the API code.",
+        raise CoprRequestException("Response is not in JSON format, there is 
probably a bug in the API code.",
                                    response=response)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr.egg-info/PKG-INFO 
new/copr-1.122/copr.egg-info/PKG-INFO
--- old/copr-1.119/copr.egg-info/PKG-INFO       2022-04-05 10:50:41.000000000 
+0200
+++ new/copr-1.122/copr.egg-info/PKG-INFO       2022-08-18 15:02:18.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: copr
-Version: 1.119
+Version: 1.122
 Summary: Python client for copr service.
 Home-page: https://pagure.io/copr/copr
 Author: Valentin Gologuzov
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr.egg-info/SOURCES.txt 
new/copr-1.122/copr.egg-info/SOURCES.txt
--- old/copr-1.119/copr.egg-info/SOURCES.txt    2022-04-05 10:50:41.000000000 
+0200
+++ new/copr-1.122/copr.egg-info/SOURCES.txt    2022-08-18 15:02:18.000000000 
+0200
@@ -16,6 +16,8 @@
 copr.egg-info/requires.txt
 copr.egg-info/top_level.txt
 copr/test/__init__.py
+copr/test/client_v3/__init__.py
+copr/test/client_v3/test_auth.py
 copr/test/client_v3/test_builds.py
 copr/test/client_v3/test_general.py
 copr/test/client_v3/test_helpers.py
@@ -32,6 +34,10 @@
 copr/v3/helpers.py
 copr/v3/pagination.py
 copr/v3/requests.py
+copr/v3/auth/__init__.py
+copr/v3/auth/base.py
+copr/v3/auth/gssapi.py
+copr/v3/auth/token.py
 copr/v3/proxies/__init__.py
 copr/v3/proxies/build.py
 copr/v3/proxies/build_chroot.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/copr.egg-info/requires.txt 
new/copr-1.122/copr.egg-info/requires.txt
--- old/copr-1.119/copr.egg-info/requires.txt   2022-04-05 10:50:41.000000000 
+0200
+++ new/copr-1.122/copr.egg-info/requires.txt   2022-08-18 15:02:18.000000000 
+0200
@@ -5,5 +5,4 @@
 setuptools
 six
 munch
-requests-gssapi
 future
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/docs/client_v3/client_initialization.rst 
new/copr-1.122/docs/client_v3/client_initialization.rst
--- old/copr-1.119/docs/client_v3/client_initialization.rst     2019-07-17 
10:59:09.000000000 +0200
+++ new/copr-1.122/docs/client_v3/client_initialization.rst     2022-08-18 
15:01:47.000000000 +0200
@@ -8,6 +8,7 @@
 ::
 
     from copr.v3 import Client
+    from pprint import pprint
     client = Client.create_from_config_file()
     pprint(client.config)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/docs/client_v3/data_structures.rst 
new/copr-1.122/docs/client_v3/data_structures.rst
--- old/copr-1.119/docs/client_v3/data_structures.rst   2022-04-05 
10:49:51.000000000 +0200
+++ new/copr-1.122/docs/client_v3/data_structures.rst   2022-08-18 
15:01:47.000000000 +0200
@@ -1,7 +1,7 @@
 Data structures
 ===============
 
-A data returned from successful API calls are transformed and presented to you 
as a Munch (it is a subclass of a
+A data returned from successful API calls are transformed and presented to you 
as a [Munch](https://github.com/Infinidat/munch) (it is a subclass of a
 ``dict`` with all its features, with an additional support of accessing its 
attributes like object properties, etc).
 This page shows how to work with the results, how to access the original 
responses from frontend and what are the
 specifics for lists of results.
@@ -12,12 +12,13 @@
 ::
 
     from copr.v3 import Client
+    from pprint import pprint
     client = Client.create_from_config_file()
     build = client.build_proxy.get(2545)
     pprint(build)
 
 
-As advertised, the data is represented as a Munch.
+As advertised, the data is represented as a 
[Munch](https://github.com/Infinidat/munch).
 
 ::
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/docs/client_v3/error_handling.rst 
new/copr-1.122/docs/client_v3/error_handling.rst
--- old/copr-1.119/docs/client_v3/error_handling.rst    2019-07-17 
10:59:09.000000000 +0200
+++ new/copr-1.122/docs/client_v3/error_handling.rst    2022-08-18 
15:01:47.000000000 +0200
@@ -1,7 +1,7 @@
 Error handling
 ==============
 
-All methods from proxy classes return Munch with data only when the API call 
succeeds. Otherwise, an exception is raised.
+All methods from proxy classes return 
[Munch](https://github.com/Infinidat/munch) with data only when the API call 
succeeds. Otherwise, an exception is raised.
 
 This example code tries to cancel a build. Such thing is possible only when 
the build is not already finished.
 
@@ -35,7 +35,7 @@
 ---------
 
 Sometimes it is useful to dig deeper and examine the failure. Exceptions 
contain a ``result`` attribute
-returning a Munch with additional information.
+returning a [Munch](https://github.com/Infinidat/munch) with additional 
information.
 
 ::
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/docs/client_v3/pagination.rst 
new/copr-1.122/docs/client_v3/pagination.rst
--- old/copr-1.119/docs/client_v3/pagination.rst        2019-07-17 
10:59:09.000000000 +0200
+++ new/copr-1.122/docs/client_v3/pagination.rst        2022-08-18 
15:01:47.000000000 +0200
@@ -48,7 +48,7 @@
 
 .. code-block:: python
 
-    from copr.v3 import next_page
+    from copr.v3.pagination import next_page
 
     package_page = client.package_proxy.get_list("@copr", "copr", 
pagination={"limit": 3})
     while package_page:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/python-copr.spec 
new/copr-1.122/python-copr.spec
--- old/copr-1.119/python-copr.spec     2022-04-05 10:49:51.000000000 +0200
+++ new/copr-1.122/python-copr.spec     2022-08-18 15:01:47.000000000 +0200
@@ -9,7 +9,7 @@
 %endif
 
 Name:       python-copr
-Version:    1.119
+Version:    1.122
 Release:    1%{?dist}
 Summary:    Python interface for Copr
 
@@ -86,6 +86,7 @@
 Requires: python-filelock
 Requires: python-requests
 Requires: python-requests-toolbelt
+Requires: python-requests-gssapi
 Requires: python-setuptools
 Requires: python-six >= 1.9.0
 Requires: python-future
@@ -143,6 +144,9 @@
 %{?python_provide:%python_provide python3-copr}
 
 %if 0%{?fedora} > 30 || 0%{?rhel} > 8
+# These are not in requirements.txt
+Requires: python3-requests-gssapi
+
 BuildRequires: python3-devel
 BuildRequires: python3-sphinx
 BuildRequires: python3-pytest
@@ -240,6 +244,19 @@
 %doc %{_pkgdocdir}
 
 %changelog
+* Tue Aug 16 2022 Jiri Kyjovsky <j1.kyjov...@gmail.com> 1.122-1
+- add packit_forge_projects_allowed for Copr projects
+
+* Tue Jul 26 2022 Jakub Kadlcik <fros...@email.cz> 1.121-1
+- Add support for pyp2spec generator
+- Make requests-gssapi an opt-in dep in PyPI
+- Return the correct message when all request attempts fail
+- Add API support for runtime_dependencies
+- The auth_username() needs to trigger authentication
+
+* Tue Jun 21 2022 Jakub Kadlcik <fros...@email.cz> 1.120-1
+- Disable network on builders by default
+
 * Mon Apr 04 2022 Pavel Raiskup <prais...@redhat.com> 1.119-1
 - really depend on filelock component
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copr-1.119/setup.py new/copr-1.122/setup.py
--- old/copr-1.119/setup.py     2022-04-05 10:49:51.000000000 +0200
+++ new/copr-1.122/setup.py     2022-08-18 15:01:47.000000000 +0200
@@ -21,7 +21,6 @@
     'setuptools',
     'six',
     'munch',
-    'requests-gssapi',
     'future',
 ]
 
@@ -33,7 +32,7 @@
 
 setup(
     name='copr',
-    version="1.119",
+    version="1.122",
     description=__description__,
     long_description=long_description,
     author=__author__,

Reply via email to