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
 ========
 
-[![Build 
Status](https://travis-ci.com/crazyscientist/osc-tiny.svg?branch=master)](https://travis-ci.com/crazyscientist/osc-tiny)
-[![Coverage 
Status](https://coveralls.io/repos/github/crazyscientist/osc-tiny/badge.svg)](https://coveralls.io/github/crazyscientist/osc-tiny)
+![Build 
Status](https://github.com/crazyscientist/osc-tiny/actions/workflows/default.yml/badge.svg?branch=master)]
+![Publish 
Status](https://github.com/crazyscientist/osc-tiny/actions/workflows/publish.yml/badge.svg)
 [![PyPI 
version](https://badge.fury.io/py/osc-tiny.svg)](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
 ========
 
-[![Build 
Status](https://travis-ci.com/crazyscientist/osc-tiny.svg?branch=master)](https://travis-ci.com/crazyscientist/osc-tiny)
-[![Coverage 
Status](https://coveralls.io/repos/github/crazyscientist/osc-tiny/badge.svg)](https://coveralls.io/github/crazyscientist/osc-tiny)
+![Build 
Status](https://github.com/crazyscientist/osc-tiny/actions/workflows/default.yml/badge.svg?branch=master)]
+![Publish 
Status](https://github.com/crazyscientist/osc-tiny/actions/workflows/publish.yml/badge.svg)
 [![PyPI 
version](https://badge.fury.io/py/osc-tiny.svg)](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
 ========
 
-[![Build 
Status](https://travis-ci.com/crazyscientist/osc-tiny.svg?branch=master)](https://travis-ci.com/crazyscientist/osc-tiny)
-[![Coverage 
Status](https://coveralls.io/repos/github/crazyscientist/osc-tiny/badge.svg)](https://coveralls.io/github/crazyscientist/osc-tiny)
+![Build 
Status](https://github.com/crazyscientist/osc-tiny/actions/workflows/default.yml/badge.svg?branch=master)]
+![Publish 
Status](https://github.com/crazyscientist/osc-tiny/actions/workflows/publish.yml/badge.svg)
 [![PyPI 
version](https://badge.fury.io/py/osc-tiny.svg)](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",

Reply via email to