Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-osc-tiny for openSUSE:Factory checked in at 2023-01-02 15:02:27 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-osc-tiny (Old) and /work/SRC/openSUSE:Factory/.python-osc-tiny.new.1563 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-osc-tiny" Mon Jan 2 15:02:27 2023 rev:26 rq:1046130 version:0.7.10 Changes: -------- --- /work/SRC/openSUSE:Factory/python-osc-tiny/python-osc-tiny.changes 2022-12-13 18:55:41.899277236 +0100 +++ /work/SRC/openSUSE:Factory/.python-osc-tiny.new.1563/python-osc-tiny.changes 2023-01-02 15:02:34.193486341 +0100 @@ -1,0 +2,9 @@ +Mon Jan 2 09:33:59 UTC 2023 - Andreas Hasenkopf <ahasenk...@suse.com> + +- Release 0.7.10 + * Include the original error message, when an SSH key cannot be read + * Added a link to the documentation of `HttpSignatureAuth` + * Allow `Package.exists` to raise exceptions + * Added methods to get/set the project config + +------------------------------------------------------------------- Old: ---- osc-tiny-0.7.9.tar.gz New: ---- osc-tiny-0.7.10.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-osc-tiny.spec ++++++ --- /var/tmp/diff_new_pack.IFYKUS/_old 2023-01-02 15:02:34.857490072 +0100 +++ /var/tmp/diff_new_pack.IFYKUS/_new 2023-01-02 15:02:34.861490095 +0100 @@ -18,7 +18,7 @@ %define skip_python2 1 Name: python-osc-tiny -Version: 0.7.9 +Version: 0.7.10 Release: 0 Summary: Client API for openSUSE BuildService License: MIT ++++++ osc-tiny-0.7.9.tar.gz -> osc-tiny-0.7.10.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.9/PKG-INFO new/osc-tiny-0.7.10/PKG-INFO --- old/osc-tiny-0.7.9/PKG-INFO 2022-12-12 13:21:46.650442600 +0100 +++ new/osc-tiny-0.7.10/PKG-INFO 2023-01-02 10:27:08.909208300 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: osc-tiny -Version: 0.7.9 +Version: 0.7.10 Summary: Client API for openSUSE BuildService Home-page: http://github.com/crazyscientist/osc-tiny Download-URL: http://github.com/crazyscientist/osc-tiny/tarball/master diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.9/osc_tiny.egg-info/PKG-INFO new/osc-tiny-0.7.10/osc_tiny.egg-info/PKG-INFO --- old/osc-tiny-0.7.9/osc_tiny.egg-info/PKG-INFO 2022-12-12 13:21:46.000000000 +0100 +++ new/osc-tiny-0.7.10/osc_tiny.egg-info/PKG-INFO 2023-01-02 10:27:08.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: osc-tiny -Version: 0.7.9 +Version: 0.7.10 Summary: Client API for openSUSE BuildService Home-page: http://github.com/crazyscientist/osc-tiny Download-URL: http://github.com/crazyscientist/osc-tiny/tarball/master diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.9/osctiny/__init__.py new/osc-tiny-0.7.10/osctiny/__init__.py --- old/osc-tiny-0.7.9/osctiny/__init__.py 2022-12-12 13:21:36.000000000 +0100 +++ new/osc-tiny-0.7.10/osctiny/__init__.py 2023-01-02 10:26:59.000000000 +0100 @@ -6,4 +6,4 @@ __all__ = ['Osc', 'bs_requests', 'buildresults', 'comments', 'packages', 'projects', 'search', 'users'] -__version__ = "0.7.9" +__version__ = "0.7.10" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.9/osctiny/extensions/packages.py new/osc-tiny-0.7.10/osctiny/extensions/packages.py --- old/osc-tiny-0.7.9/osctiny/extensions/packages.py 2022-12-12 13:21:36.000000000 +0100 +++ new/osc-tiny-0.7.10/osctiny/extensions/packages.py 2023-01-02 10:26:59.000000000 +0100 @@ -470,7 +470,7 @@ return parsed - def exists(self, project, package, filename=None): + def exists(self, project, package, filename=None) -> bool: """ Check whether package or file in package exists @@ -493,6 +493,12 @@ raise_for_status=False ) + if response.status_code == 404: + return False + + # 404 is the only acceptable HTTP error code, otherwise raise an exception + response.raise_for_status() + return response.status_code == 200 # pylint: disable=too-many-locals diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.9/osctiny/extensions/projects.py new/osc-tiny-0.7.10/osctiny/extensions/projects.py --- old/osc-tiny-0.7.9/osctiny/extensions/projects.py 2022-12-12 13:21:36.000000000 +0100 +++ new/osc-tiny-0.7.10/osctiny/extensions/projects.py 2023-01-02 10:26:59.000000000 +0100 @@ -358,6 +358,55 @@ return self.osc.get_objectified_xml(response) + def get_config(self, project, revision=None): + """ + Get project configuration + + .. versionadded:: 0.7.10 + + :param project: name of project + :param revision: optional revision of the config to get + :return: The project configuration as string + :rtype: str + """ + response = self.osc.request( + url=urljoin( + self.osc.url, + "{}/{}/_config".format(self.base_path, project) + ), + params={"rev":revision}, + method="GET" + ) + + return response.text + + def set_config(self, project, config=None, comment=None): + """ + Set project config data + + .. versionadded:: 0.7.10 + + :param project: name of project + :param config: Complete configuration to set + :type config: str + :param comment: Optional comment to use as commit message + :return: ``True``, if successful. Otherwise, API response + :rtype: bool or lxml.objectify.ObjectifiedElement + """ + response = self.osc.request( + url=urljoin(self.osc.url, + "/".join((self.base_path, project, "_config"))), + method="PUT", + data=config, + params={"comment": comment} + ) + + parsed = self.osc.get_objectified_xml(response) + if response.status_code == 200 and parsed.get("code") == "ok": + return True + + return parsed + def delete(self, project, force=False, comment=None): """ Delete project diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.9/osctiny/tests/test_packages.py new/osc-tiny-0.7.10/osctiny/tests/test_packages.py --- old/osc-tiny-0.7.9/osctiny/tests/test_packages.py 2022-12-12 13:21:36.000000000 +0100 +++ new/osc-tiny-0.7.10/osctiny/tests/test_packages.py 2023-01-02 10:26:59.000000000 +0100 @@ -3,6 +3,7 @@ import re from unittest import skip +from requests import HTTPError import responses from .base import OscTest, CallbackFactory @@ -520,3 +521,30 @@ self.assertEqual(expected, self.osc.packages.cleanup_params(view="verboseproductlist", expand=True)) + + @responses.activate + def test_exists(self): + data = ( + # status, expected + (200, True), + (404, False), + (401, None), + (500, None) + ) + + def callback(headers, params, request): + return status, headers, "" + + self.mock_request( + method=responses.HEAD, + url=re.compile(self.osc.url + '/source/.+'), + callback=CallbackFactory(callback) + ) + + for status, expected in data: + with self.subTest(status): + if expected is not None: + self.assertEqual(expected, self.osc.packages.exists("Some:Project", "package")) + else: + self.assertRaises(HTTPError, self.osc.packages.exists, "Some:Project", + "package") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.9/osctiny/tests/test_projects.py new/osc-tiny-0.7.10/osctiny/tests/test_projects.py --- old/osc-tiny-0.7.9/osctiny/tests/test_projects.py 2022-12-12 13:21:36.000000000 +0100 +++ new/osc-tiny-0.7.10/osctiny/tests/test_projects.py 2023-01-02 10:26:59.000000000 +0100 @@ -370,6 +370,80 @@ ) @responses.activate + def test_get_config(self): + def callback(headers, params, request): + status = 200 + body = "the_config" + self.assertTrue("/_config" in request.url) + if "Devel:ARM:Factory" not in request.url: + status = 404 + body = """ + <status code="unknown_project"> + <summary>Devel:ARM:Fbctory</summary> + </status> + """ + headers['request-id'] = '728d329e-0e86-11e4-a748-0c84dc037c13' + return status, headers, body + + self.mock_request( + method=responses.GET, + url=re.compile(self.osc.url + '/source/.*'), + callback=CallbackFactory(callback) + ) + + with self.subTest("existing project"): + response = self.osc.projects.get_config("Devel:ARM:Factory") + self.assertEqual(response, "the_config") + + with self.subTest("non-existing project"): + self.assertRaises( + HTTPError, self.osc.projects.get_config, "Devel:ARM:Fbctory" + ) + + with self.subTest("existing project with revision"): + response = self.osc.projects.get_config("Devel:ARM:Factory", revision=3) + self.assertEqual(response, "the_config") + self.assertEqual(responses.calls[-1].request.params["rev"], "3") + + @responses.activate + def test_set_config(self): + def callback(headers, params, request): + status = 500 + body = "<status code='error'></status>" + match = re.search("source/(?P<project>[^/]+)/_config", request.url) + if match and not "invalid" in request.url: + status = 200 + body = """<status code='ok'></status>""" + + headers['request-id'] = '728d329e-0e86-11e4-a748-0c84dc037c13' + return status, headers, body + + self.mock_request( + method=responses.PUT, + url=re.compile(self.osc.url + '/source/[^/]+/_config'), + callback=CallbackFactory(callback) + ) + + with self.subTest("Valid request"): + self.assertTrue(self.osc.projects.set_config( + project="test", + config="foo bar", + )) + + with self.subTest("Invalid request"): + self.assertRaises( + HTTPError, + self.osc.projects.set_config, + project="invalid:project", + config="test_config" + ) + + with self.subTest("Valid request with comment"): + self.assertTrue(self.osc.projects.set_config(project="project:foo", config="test conf", + comment="Test")) + self.assertEqual(responses.calls[-1].request.params["comment"], "Test") + + @responses.activate def test_delete(self): def callback(headers, params, request): if "existing" in request.url: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.9/osctiny/tests/test_utils.py new/osc-tiny-0.7.10/osctiny/tests/test_utils.py --- old/osc-tiny-0.7.9/osctiny/tests/test_utils.py 2022-12-12 13:21:36.000000000 +0100 +++ new/osc-tiny-0.7.10/osctiny/tests/test_utils.py 2023-01-02 10:26:59.000000000 +0100 @@ -396,7 +396,7 @@ @mock.patch("osctiny.utils.auth.time", return_value=123456) class TestAuth(TestCase): - @mock.patch("osctiny.utils.auth.is_ssh_key_readable", return_value=True) + @mock.patch("osctiny.utils.auth.is_ssh_key_readable", return_value=(True, None)) def setUp(self, *_): super().setUp() mocked_path = mock.MagicMock(spec=Path) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.9/osctiny/utils/auth.py new/osc-tiny-0.7.10/osctiny/utils/auth.py --- old/osc-tiny-0.7.9/osctiny/utils/auth.py 2022-12-12 13:21:36.000000000 +0100 +++ new/osc-tiny-0.7.10/osctiny/utils/auth.py 2023-01-02 10:26:59.000000000 +0100 @@ -54,7 +54,8 @@ return None -def is_ssh_key_readable(ssh_key_file: Path, password: typing.Optional[str]) -> bool: +def is_ssh_key_readable(ssh_key_file: Path, password: typing.Optional[str]) \ + -> typing.Tuple[bool, typing.Optional[str]]: """ Check whether SSH key can be read/unlocked @@ -67,17 +68,21 @@ .. versionchanged:: 0.7.8 * Moved from ``HttpSignatureAuth.is_ssh_agent_available`` + + .. versionchanged:: 0.7.10 + + * Return the error message, if key cannot be unlocked """ cmd = ['ssh-keygen', '-y', '-f', ssh_key_file.as_posix()] if password: cmd += ['-P', password] - with Popen(cmd, stdin=DEVNULL, stderr=DEVNULL, stdout=DEVNULL) as proc: - proc.communicate() + with Popen(cmd, stdin=DEVNULL, stderr=PIPE, stdout=DEVNULL) as proc: + _, error = proc.communicate() if proc.returncode == 0: - return True + return True, None - return False + return False, error.decode("utf-8") if isinstance(error, bytes) else error class HttpSignatureAuth(HTTPDigestAuth): @@ -88,13 +93,16 @@ This seems to be a variation of the `HTTP Message Signatures`_ specification. - See also the `reference implementation for osc`_ + See also the `blog post`_ describing the implementation and the + `reference implementation for osc`_. .. _HTTP Message Signatures: https://datatracker.ietf.org/doc/draft-ietf-httpbis-message-signatures/ .. _reference implementation for osc: https://github.com/openSUSE/osc/pull/1032 + .. _blog post: https://www.suse.com/c/multi-factor-authentication-on-suses-build-service/ + .. note:: 1. It is recommended to use SSH keys with a passphrase. @@ -110,8 +118,9 @@ super().__init__(username=username, password=password) if not ssh_key_file.is_file(): raise FileNotFoundError(f"SSH key at location does not exist: {ssh_key_file}") - if not is_ssh_key_readable(ssh_key_file=ssh_key_file, password=password): - raise RuntimeError("SSH signing impossible: Unable to decrypt key.") + readable, error = is_ssh_key_readable(ssh_key_file=ssh_key_file, password=password) + if not readable: + raise RuntimeError(f"SSH signing impossible because key cannot be decrypted: {error}.") self.ssh_key_file = ssh_key_file self.pattern = re.compile(r"(?<=\)) (?=\()") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.9/setup.py new/osc-tiny-0.7.10/setup.py --- old/osc-tiny-0.7.9/setup.py 2022-12-12 13:21:36.000000000 +0100 +++ new/osc-tiny-0.7.10/setup.py 2023-01-02 10:26:59.000000000 +0100 @@ -26,7 +26,7 @@ setup( name='osc-tiny', - version='0.7.9', + version='0.7.10', description='Client API for openSUSE BuildService', long_description=long_description, long_description_content_type="text/markdown",