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__,