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 2022-08-17 18:17:11 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-osc-tiny (Old) and /work/SRC/openSUSE:Factory/.python-osc-tiny.new.1521 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-osc-tiny" Wed Aug 17 18:17:11 2022 rev:20 rq:997538 version:0.7.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-osc-tiny/python-osc-tiny.changes 2022-08-06 22:08:43.906756610 +0200 +++ /work/SRC/openSUSE:Factory/.python-osc-tiny.new.1521/python-osc-tiny.changes 2022-08-17 18:25:19.387461136 +0200 @@ -1,0 +2,15 @@ +Tue Aug 16 12:34:34 UTC 2022 - Andreas Hasenkopf <ahasenk...@suse.com> + +- Release 0.7.1 + * Make distinction between "true" booleans and pretenders + +------------------------------------------------------------------- +Tue Aug 16 07:51:59 UTC 2022 - Andreas Hasenkopf <ahasenk...@suse.com> + +- Release 0.7.0 + * Support setting of multiple values on attribute + * Added feature to download binaries + * Handle boolean query params + * Convert relative paths to SSH keys to absolute paths + +------------------------------------------------------------------- Old: ---- osc-tiny-0.6.6.tar.gz New: ---- osc-tiny-0.7.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-osc-tiny.spec ++++++ --- /var/tmp/diff_new_pack.4xpRYF/_old 2022-08-17 18:25:19.855462291 +0200 +++ /var/tmp/diff_new_pack.4xpRYF/_new 2022-08-17 18:25:19.863462310 +0200 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-osc-tiny -Version: 0.6.6 +Version: 0.7.1 Release: 0 Summary: Client API for openSUSE BuildService License: MIT @@ -41,7 +41,6 @@ Requires: python-python-dateutil Requires: python-pytz Requires: python-requests -Requires: python-responses Suggests: openssh BuildArch: noarch # Using 'if' instead of 'with' because the latter requires rpm >= 4.14 ++++++ osc-tiny-0.6.6.tar.gz -> osc-tiny-0.7.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/PKG-INFO new/osc-tiny-0.7.1/PKG-INFO --- old/osc-tiny-0.6.6/PKG-INFO 2022-07-21 16:13:15.865302800 +0200 +++ new/osc-tiny-0.7.1/PKG-INFO 2022-08-16 14:32:49.240917400 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: osc-tiny -Version: 0.6.6 +Version: 0.7.1 Summary: Client API for openSUSE BuildService Home-page: http://github.com/crazyscientist/osc-tiny Download-URL: http://github.com/crazyscientist/osc-tiny/tarball/master @@ -23,8 +23,8 @@ OSC Tiny ======== -[](https://travis-ci.com/crazyscientist/osc-tiny) -[](https://coveralls.io/github/crazyscientist/osc-tiny) +] + [](https://badge.fury.io/py/osc-tiny) This project aims to provide a minimalistic and transparent client for accessing diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/README.md new/osc-tiny-0.7.1/README.md --- old/osc-tiny-0.6.6/README.md 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/README.md 2022-08-16 14:32:35.000000000 +0200 @@ -1,8 +1,8 @@ OSC Tiny ======== -[](https://travis-ci.com/crazyscientist/osc-tiny) -[](https://coveralls.io/github/crazyscientist/osc-tiny) +] + [](https://badge.fury.io/py/osc-tiny) This project aims to provide a minimalistic and transparent client for accessing diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osc_tiny.egg-info/PKG-INFO new/osc-tiny-0.7.1/osc_tiny.egg-info/PKG-INFO --- old/osc-tiny-0.6.6/osc_tiny.egg-info/PKG-INFO 2022-07-21 16:13:15.000000000 +0200 +++ new/osc-tiny-0.7.1/osc_tiny.egg-info/PKG-INFO 2022-08-16 14:32:49.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: osc-tiny -Version: 0.6.6 +Version: 0.7.1 Summary: Client API for openSUSE BuildService Home-page: http://github.com/crazyscientist/osc-tiny Download-URL: http://github.com/crazyscientist/osc-tiny/tarball/master @@ -23,8 +23,8 @@ OSC Tiny ======== -[](https://travis-ci.com/crazyscientist/osc-tiny) -[](https://coveralls.io/github/crazyscientist/osc-tiny) +] + [](https://badge.fury.io/py/osc-tiny) This project aims to provide a minimalistic and transparent client for accessing diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/__init__.py new/osc-tiny-0.7.1/osctiny/__init__.py --- old/osc-tiny-0.6.6/osctiny/__init__.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/__init__.py 2022-08-16 14:32:35.000000000 +0200 @@ -6,4 +6,4 @@ __all__ = ['Osc', 'bs_requests', 'buildresults', 'comments', 'packages', 'projects', 'search', 'users'] -__version__ = "0.6.6" +__version__ = "0.7.1" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/extensions/buildresults.py new/osc-tiny-0.7.1/osctiny/extensions/buildresults.py --- old/osc-tiny-0.6.6/osctiny/extensions/buildresults.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/extensions/buildresults.py 2022-08-16 14:32:35.000000000 +0200 @@ -115,19 +115,26 @@ return self.osc.get_objectified_xml(response) # pylint: disable=too-many-arguments - def get_binary(self, project, repo, arch, package, filename): + def get_binary(self, project, repo, arch, package, filename, raw=False): """ - Get the build binary file + Get the content of file + + .. note:: This method decodes the content of the file and returns a Python string by + default. :param project: Project name :param repo: Repository name :param arch: Architecture name :param package: Package name :param filename: File name - :return: Raw response - :rtype: str + :param raw: If ``True``, return a byte string. Otherwise, a string is returned + :return: Content of binary file + :rtype: str or bytes .. versionadded:: 0.2.4 + + .. versionchanged:: 0.7.0 + Added the ``raw`` parameter """ response = self.osc.request( method="GET", @@ -136,7 +143,34 @@ )), ) - return response.text + return response.content if raw else response.text + + def download_binary(self, project, repo, arch, package, filename, destdir, destfile=None, + overwrite=False): + """ + Download binary file to disk + + :param project: Project name + :param repo: Repository name + :param arch: Architecture name + :param package: Package name + :param filename: File name + :param pathlib.Path destdir: Destination directory + :param str destfile: Target file name. If not specified, it will be taken from the URL + :param bool overwrite: switch to overwrite existing downloaded file + :return: Path of downloaded file + :rtype: pathlib.Path + + .. versionadded:: 0.7.0 + """ + return self.osc.download( + url=urljoin(self.osc.url, "{}/{}/{}/{}/{}/{}".format( + self.base_path, project, repo, arch, package, filename + )), + destdir=destdir, + destfile=destfile, + overwrite=overwrite + ) def cmd(self, project, cmd, **params): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/extensions/issues.py new/osc-tiny-0.7.1/osctiny/extensions/issues.py --- old/osc-tiny-0.6.6/osctiny/extensions/issues.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/extensions/issues.py 2022-08-16 14:32:35.000000000 +0200 @@ -49,14 +49,14 @@ ) return self.osc.get_objectified_xml(response) - def get(self, tracker, name, force_update=None): + def get(self, tracker, name, force_update=False): """ Get details for an issue :param str tracker: issue tracker name :param str name: issue name - :param force_update: If ``True``, BuildService will update the issue - details internally prior to returning the response + :param bool force_update: If ``True``, BuildService will update the issue + details internally prior to returning the response :return: Objectified XML element :rtype: lxml.objectify.ObjectifiedElement """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/extensions/origin.py new/osc-tiny-0.7.1/osctiny/extensions/origin.py --- old/osc-tiny-0.6.6/osctiny/extensions/origin.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/extensions/origin.py 2022-08-16 14:32:35.000000000 +0200 @@ -497,7 +497,8 @@ return None if resolve_inheritance: - response = self.osc.packages.get_files(package=package, project=project, withlinked=1) + response = self.osc.packages.get_files(package=package, project=project, + withlinked=True) links = response.xpath("linkinfo/linked") if len(links) > 0: project = links[-1].get("project") @@ -535,7 +536,7 @@ warn("Project {} has no origin definition".format(project)) return - packages = self.osc.projects.get_files(project, expand='1') + packages = self.osc.projects.get_files(project, expand=True) for package in getattr(packages, "entry", []): name = package.get("name") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/extensions/packages.py new/osc-tiny-0.7.1/osctiny/extensions/packages.py --- old/osc-tiny-0.6.6/osctiny/extensions/packages.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/extensions/packages.py 2022-08-16 14:32:35.000000000 +0200 @@ -3,7 +3,6 @@ ------------------ """ from __future__ import unicode_literals -import errno import os from six.moves.urllib.parse import urljoin from six import text_type @@ -22,19 +21,23 @@ base_path = "/source" new_package_meta_templ = "<package><title/><description/></package>" - def get_list(self, project, deleted=False): + def get_list(self, project: str, deleted: bool = False, expand: bool = True): """ Get packages from project .. versionadded:: 0.1.7 Parameter ``deleted`` + .. versionadded:: 0.7.0 + Parameter ``expand`` + :param project: name of project :param deleted: Show deleted packages instead + :param expand: Include inherited packages and their project of origin :return: Objectified XML element :rtype: lxml.objectify.ObjectifiedElement """ - params = {"deleted": deleted} + params = {"deleted": deleted, "expand": expand} response = self.osc.request( url=urljoin(self.osc.url, "{}/{}".format(self.base_path, project)), method="GET", @@ -148,7 +151,7 @@ # pylint: disable=too-many-arguments def get_file(self, project, package, filename, meta=False, rev=None, - expand=0): + expand=False): """ Get a source file @@ -207,26 +210,20 @@ .. versionchanged:: 0.3.3 Added the parameter ``expand`` - """ - abspath_filename = os.path.abspath(os.path.join(destdir, filename)) - if os.path.isfile(destdir): - raise OSError( - errno.EEXIST, "Target directory is a file", destdir - ) - if not overwrite and os.path.exists(abspath_filename): - raise OSError( - errno.EEXIST, "File already exists", abspath_filename - ) - if not os.path.exists(destdir): - os.makedirs(destdir) - - response = self.get_file(project, package, filename, meta=meta, rev=rev, expand=expand) - - with open(abspath_filename, "wb") as handle: - for chunk in response.iter_content(1024): - handle.write(chunk) - return abspath_filename + .. versionchanged:: 0.7.0 + Moved some logic to :py:meth:`osctiny.osc.Osc.download` + """ + return self.osc.download( + url=urljoin(self.osc.url, + "{}/{}/{}/{}".format(self.base_path, project, package, filename)), + destdir=destdir, + destfile=filename, + overwrite=overwrite, + meta=meta, + rev=rev, + expand=expand + ) def push_file(self, project, package, filename, data, comment=None): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/extensions/projects.py new/osc-tiny-0.7.1/osctiny/extensions/projects.py --- old/osc-tiny-0.6.6/osctiny/extensions/projects.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/extensions/projects.py 2022-08-16 14:32:35.000000000 +0200 @@ -8,13 +8,12 @@ from six import text_type from lxml.etree import tounicode -from lxml.objectify import fromstring +from lxml.objectify import fromstring, SubElement from ..utils.base import ExtensionBase -TEMPLATE_CREATE_ATTR = "<attributes><attribute namespace='' name=''>" \ - "<value></value></attribute></attributes>" +TEMPLATE_CREATE_ATTR = "<attributes><attribute namespace='' name=''></attribute></attributes>" TEMPLATE_META = "<project name=''><title></title><description></description>" \ "<person userid='' role='bugowner'/>" \ "<person userid='' role='maintainer'/>" \ @@ -133,8 +132,7 @@ :return: Objectified XML element :rtype: lxml.objectify.ObjectifiedElement """ - if meta: - kwargs["meta"] = '1' + kwargs["meta"] = meta if rev: kwargs["rev"] = text_type(rev) response = self.osc.request( @@ -185,9 +183,12 @@ :param project: project name :param attribute: attribute name (can include prefix separated by colon) - :param value: attribute value + :param value: attribute value or list of values :return: ``True``, if successful. Otherwise API response :rtype: bool or lxml.objectify.ObjectifiedElement + + .. versionchanged:: 0.7.0 + Support attributes with multiple values """ url = urljoin( self.osc.url, @@ -199,11 +200,14 @@ if match is None: raise ValueError("Invalid attribute format: {}".format(attribute)) + value = value if isinstance(value, (list, tuple, set)) else [value] attr_xml = fromstring(TEMPLATE_CREATE_ATTR) attr_xml.attribute.set('namespace', match.group("prefix")) attr_xml.attribute.set('name', match.group("name")) - # pylint: disable=protected-access - attr_xml.attribute.value._setText(text_type(value)) + for val in value: + elem = SubElement(attr_xml.attribute, "value") + # pylint: disable=protected-access + elem._setText(text_type(val)) response = self.osc.request( url=url, @@ -297,7 +301,8 @@ """ if rev: kwargs["rev"] = rev - kwargs["meta"] = "1" if meta else "0" + + kwargs["meta"] = meta response = self.osc.request( url=urljoin(self.osc.url, "{}/{}/_project/_history".format( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/osc.py new/osc-tiny-0.7.1/osctiny/osc.py --- old/osc-tiny-0.6.6/osctiny/osc.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/osc.py 2022-08-16 14:32:35.000000000 +0200 @@ -6,6 +6,7 @@ from base64 import b64encode import typing +import errno from io import BufferedReader, BytesIO, StringIO import gc import logging @@ -14,7 +15,7 @@ from ssl import get_default_verify_paths import time import threading -from urllib.parse import quote +from urllib.parse import quote, parse_qs, urlparse import warnings # pylint: disable=no-name-in-module @@ -35,7 +36,7 @@ from .extensions.search import Search from .extensions.users import Group, Person from .utils.auth import HttpSignatureAuth -from .utils.conf import get_credentials +from .utils.conf import BOOLEAN_PARAMS, get_credentials from .utils.errors import OscError try: @@ -321,7 +322,11 @@ "\n".join(f"{k}: {v}" for k, v in req.data.items()) if isinstance(req.data, dict) else req.data) logger.debug("Sent parameters:\n%s\n---", - "\n".join(f"{k}: {v}" for k, v in req.params.items())) + "\n".join(f"{k}: {v}" for k, v in ( + req.params + if isinstance(req.params, dict) + else parse_qs(req.params, keep_blank_values=True) + ).items())) try: response = session.send(prepped_req, **settings) except _ConnectionError as error: @@ -349,7 +354,15 @@ Translate request parameters to API conform format .. note:: The build service does not handle URL-encoded Unicode well. - Therefore parameters are encoded as ``bytes``. + Therefore, parameters are encoded as ``bytes``. + + .. warning:: The build service does not declare its parameters properly and developers do + `not intend to fix`_ this server-side. If you want to use _boolean_ parameters, + make sure to use ``True`` and ``False``. If you use ``0`` or ``1`` instead, you + might receive unexpected results. + + .. _not intend to fix: https://github.com/openSUSE/open-build-service/issues + /9715 :param params: Request parameter :type params: dict or str or io.BufferedReader @@ -373,13 +386,65 @@ if not isinstance(params, dict): return {} - for key in params: - if isinstance(params[key], bool): + # The OBS API has a weird expectation regarding boolean parameters and the maintainers have + # made it clear, that they are not going to clean up the API :( + # See: https://github.com/openSUSE/open-build-service/issues/9715 + # Also, there are parameters giving the impression that they are boolean, but actually are + # not. + unexpected_bools = {key for key, value in params.items() + if isinstance(value, bool) and key not in BOOLEAN_PARAMS} + if unexpected_bools: + warnings.warn(f"Received boolean query params, which are not expected to be: " + f"{', '.join(unexpected_bools)}") + for key in unexpected_bools: params[key] = '1' if params[key] else '0' - return {key.encode(): str(value).encode() + return "&".join( + quote(str(key)) + if key in BOOLEAN_PARAMS + else f"{quote(str(key))}={quote(str(value))}" + for key, value in ( + (key, value) for key, value in params.items() - if value is not None} + if not (key in BOOLEAN_PARAMS and value in [False, "0", 0, None, ""]) + ) + if value is not None + ).encode() + + def download(self, url, destdir, destfile=None, overwrite=False, **params): + """ + Shortcut for a streaming GET request + + :param str url: Download URL + :param pathlib.Path destdir: Destination directory + :param str destfile: Target file name. If not specified, it will be taken from the URL + :param bool overwrite: switch to overwrite existing downloaded file + :param params: Additional query params + :return: absolute path to file or ``None`` + + .. versionadded:: 0.7.0 + """ + destdir = destdir if isinstance(destdir, Path) else Path(destdir) + if not destfile: + parsed = urlparse(url) + destfile = Path(parsed.path).name + + if destdir.is_file(): + raise OSError(errno.EEXIST, "Target directory is a file", destdir) + + target = destdir.joinpath(destfile) + if not overwrite and target.exists(): + raise OSError(errno.EEXIST, "File already exists", target) + if not destdir.exists(): + destdir.mkdir(parents=True, exist_ok=True) + + response = self.request(url=url, method="GET", stream=True, params=params) + + with target.open("wb") as handle: + for chunk in response.iter_content(1024): + handle.write(chunk) + + return target def get_objectified_xml(self, response): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/tests/test_basic.py new/osc-tiny-0.7.1/osctiny/tests/test_basic.py --- old/osc-tiny-0.6.6/osctiny/tests/test_basic.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/tests/test_basic.py 2022-08-16 14:32:35.000000000 +0200 @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- +import pathlib import re -from urllib.parse import unquote_plus +import tempfile +from urllib.parse import unquote_plus, parse_qs import responses @@ -9,6 +11,43 @@ class BasicTest(OscTest): + @responses.activate + def test_download(self): + filename = 'test-file.bin' + url = self.osc.url + '/' + filename + content = "L??rem ????sum do??or si?? a??et ..." + self.mock_request( + method=responses.GET, + url=url, + body=content.encode() + ) + + tmpfile1 = pathlib.Path(tempfile.mkstemp()[1]) + kwargs = {"url": url, "destdir": tmpfile1.parent, "destfile": tmpfile1.name} + + with self.subTest("overwrite=False"): + self.assertRaises(OSError, self.osc.download, **kwargs) + + with self.subTest("overwrite=True"): + kwargs2 = kwargs.copy() + kwargs2["overwrite"] = True + tmpfile2 = self.osc.download(**kwargs2) + self.assertEqual(tmpfile1, tmpfile2) + with tmpfile2.open("r") as handle: + self.assertEqual(content, handle.read()) + + with self.subTest("No destfile"): + kwargs2 = kwargs.copy() + del kwargs2["destfile"] + tmpfile2 = self.osc.download(**kwargs2) + try: + self.assertEqual(tmpfile1.parent, tmpfile2.parent) + self.assertEqual(tmpfile2.name, filename) + with tmpfile2.open("r") as handle: + self.assertEqual(content, handle.read()) + finally: + tmpfile2.unlink() + def test_handle_params(self): def _run(data, expected): handled = self.osc.handle_params(data) @@ -19,25 +58,51 @@ (1, {}), ("hello world", b"hello world"), ("f???? b??r", b"f\xc3\xb8\xc3\xb8 b\xc3\xa6r"), + # 'withissues' is not a boolean param in the API ( {"view": "xml", "withissues": 1}, - {b"view": b"xml", b"withissues": b"1"} + b"view=xml&withissues=1" ), ( {"view": "xml", "withissues": True}, - {b"view": b"xml", b"withissues": b"1"} + b"view=xml&withissues=1" ), ( {"view": "xml", "withissues": 0}, - {b"view": b"xml", b"withissues": b"0"} + b"view=xml&withissues=0" ), ( {"view": "xml", "withissues": False}, - {b"view": b"xml", b"withissues": b"0"} + b"view=xml&withissues=0" ), ( {"view": "xml", "withissues": None}, - {b"view": b"xml"} + b"view=xml" + ), + # 'deleted' is a boolean param in the API + ( + {"view": "xml", "deleted": 1}, + b"view=xml&deleted" + ), + ( + {"view": "xml", "deleted": 0}, + b"view=xml" + ), + ( + {"view": "xml", "deleted": "1"}, + b"view=xml&deleted" + ), + ( + {"view": "xml", "deleted": "0"}, + b"view=xml" + ), + ( + {"view": "xml", "deleted": False}, + b"view=xml" + ), + ( + {"view": "xml", "deleted": True}, + b"view=xml&deleted" ), ) @@ -89,3 +154,30 @@ for name, filename in data: with self.subTest(name): self.osc.request(f"{self.osc.url}file/{filename}") + + @responses.activate + def test_request_boolean_params(self): + pattern = re.compile(self.osc.url + r'/\?(?P<query>.*)') + + def callback(headers, params, request): + match = pattern.match(request.url) + + parsed = parse_qs(match.group("query"), keep_blank_values=True) + self.assertEqual(parsed, expected) + return 200, headers, "" + + self.mock_request( + method=responses.GET, + url=pattern, + callback=CallbackFactory(callback) + ) + + for path, expected in ( + (b"foo", {"foo": [""]}), + (b"foo=bar", {"foo": ["bar"]}), + (b"foo=foo&bar", {"foo": ["foo"], "bar": [""]}), + (b"foo=foo?bar", {"foo": ["foo?bar"]}), + (b"foo=foo=bar", {"foo": ["foo=bar"]}) + ): + with self.subTest(path): + self.osc.request(self.osc.url, params=path) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/tests/test_build.py new/osc-tiny-0.7.1/osctiny/tests/test_build.py --- old/osc-tiny-0.6.6/osctiny/tests/test_build.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/tests/test_build.py 2022-08-16 14:32:35.000000000 +0200 @@ -15,7 +15,7 @@ status = 500 body = "" parsed = urlparse(request.url) - params.update(parse_qs(parsed.query)) + params.update(parse_qs(parsed.query, keep_blank_values=True)) if not params: status = 200 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/tests/test_issues.py new/osc-tiny-0.7.1/osctiny/tests/test_issues.py --- old/osc-tiny-0.6.6/osctiny/tests/test_issues.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/tests/test_issues.py 2022-08-16 14:32:35.000000000 +0200 @@ -1,5 +1,6 @@ # -*- coding: utf8 -*- import re +from urllib.parse import parse_qs, urlparse from lxml.objectify import ObjectifiedElement from requests.exceptions import HTTPError @@ -102,7 +103,9 @@ @responses.activate def test_get(self): def callback(headers, params, request): - if params.get("force_update", ["0"]) == ["1"]: + parsed = urlparse(request.url) + query_params = parse_qs(parsed.query, keep_blank_values=True) + if query_params.get("force_update", ["0"]) == ["1"]: status, body = 200, u""" <issue> <created_at>2020-01-04 14:12:00 UTC</created_at> @@ -131,17 +134,16 @@ self.mock_request( method=responses.GET, - url=re.compile(self.osc.url + - r'/issue_trackers/bnc/issues/1160086/?.*'), + url=re.compile(self.osc.url + r'/issue_trackers/bnc/issues/1160086/?.*'), callback=CallbackFactory(callback) ) - with self.subTest("Manual force update"): + with self.subTest("Force update"): response = self.osc.issues.get("bnc", 1160086, True) self.assertTrue(hasattr(response, "summary")) self.assertEqual(len(responses.calls), 2) - with self.subTest("Manual force update"): + with self.subTest("Update"): response = self.osc.issues.get("bnc", 1160086, False) self.assertTrue(hasattr(response, "summary")) # to whom it may concern: `responses.calls` does not get reset diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/tests/test_projects.py new/osc-tiny-0.7.1/osctiny/tests/test_projects.py --- old/osc-tiny-0.6.6/osctiny/tests/test_projects.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/tests/test_projects.py 2022-08-16 14:32:35.000000000 +0200 @@ -24,8 +24,8 @@ </directory> """ parsed = urlparse(request.url) - params.update(parse_qs(parsed.query)) - if params.get("deleted", ["0"]) == ["1"]: + params.update(parse_qs(parsed.query, keep_blank_values=True)) + if "deleted" in params: status = 403 body = """<status code="no_permission_for_deleted"> <summary>only admins can see deleted projects</summary> @@ -165,9 +165,9 @@ </directory> """ parsed = urlparse(request.url) - params.update(parse_qs(parsed.query)) + params.update(parse_qs(parsed.query, keep_blank_values=True)) - if params.get("meta", ['0']) == ['1']: + if "meta" in params: status = 200 body = """ <directory name="_project" rev="41" vrev="" @@ -267,6 +267,7 @@ @responses.activate def test_set_attribute(self): def callback(headers, params, request): + self.assertEqual(request.body, expected) status, body = 200, "<status code='ok'></status>" return status, headers, body @@ -278,13 +279,27 @@ callback=CallbackFactory(callback) ) - self.assertTrue( - self.osc.projects.set_attribute( - project="test:project", - attribute="namespace:attr", - value="value" + with self.subTest("Single value"): + expected = b'<attributes><attribute namespace="namespace" name="attr">' \ + b'<value>value</value></attribute></attributes>' + self.assertTrue( + self.osc.projects.set_attribute( + project="test:project", + attribute="namespace:attr", + value="value" + ) + ) + + with self.subTest("Two values"): + expected = b'<attributes><attribute namespace="namespace" name="attr">' \ + b'<value>value1</value><value>value2</value></attribute></attributes>' + self.assertTrue( + self.osc.projects.set_attribute( + project="test:project", + attribute="namespace:attr", + value=["value1", "value2"] + ) ) - ) @responses.activate def test_delete_attribute(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/tests/test_requests.py new/osc-tiny-0.7.1/osctiny/tests/test_requests.py --- old/osc-tiny-0.6.6/osctiny/tests/test_requests.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/tests/test_requests.py 2022-08-16 14:32:35.000000000 +0200 @@ -12,7 +12,7 @@ status = 500 body = "" parsed = urlparse(request.url) - params.update(parse_qs(parsed.query)) + params.update(parse_qs(parsed.query, keep_blank_values=True)) if re.search("comments/request/30902", request.url): status = 200 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/osctiny/utils/conf.py new/osc-tiny-0.7.1/osctiny/utils/conf.py --- old/osc-tiny-0.6.6/osctiny/utils/conf.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/osctiny/utils/conf.py 2022-08-16 14:32:35.000000000 +0200 @@ -22,6 +22,30 @@ _conf = None +# Query parameters that are considered to be boolean by the build service +BOOLEAN_PARAMS = ( + "add_repositories", + "deleted", + "emptylink", + "expand", + "extend_package_names", + "extend_package_names", + "ignoredevel", + "keeplink", + "lastbuild", + "lastworking", + "locallink", + "meta", + "multibuild", + "noaccess", + "parse", + "repairlink", + "update_path_elements", + "withdownloadurl", + "withlinked", +) + + def get_config_path() -> Path: """ Return path of ``osc`` configuration file @@ -157,7 +181,11 @@ if sshkey is not None: if not sshkey.exists(): - raise ValueError(f"SSH key from config does not exist: {sshkey}") + # if it is just a key file name, look at the default SSH dir (which is the most + # common case) + sshkey = Path.home() / ".ssh" / sshkey + if not sshkey.exists(): + raise ValueError(f"SSH key from config does not exist: {sshkey}") if not password and not sshkey: raise ValueError(f"`osc` config provides no password or SSH key for URL {url}") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.6.6/setup.py new/osc-tiny-0.7.1/setup.py --- old/osc-tiny-0.6.6/setup.py 2022-07-21 16:13:03.000000000 +0200 +++ new/osc-tiny-0.7.1/setup.py 2022-08-16 14:32:35.000000000 +0200 @@ -26,7 +26,7 @@ setup( name='osc-tiny', - version='0.6.6', + version='0.7.1', description='Client API for openSUSE BuildService', long_description=long_description, long_description_content_type="text/markdown",