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 2024-05-13 17:58:32
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-osc-tiny (Old)
 and      /work/SRC/openSUSE:Factory/.python-osc-tiny.new.1880 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-osc-tiny"

Mon May 13 17:58:32 2024 rev:32 rq:1173655 version:0.9.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-osc-tiny/python-osc-tiny.changes  
2024-03-25 21:15:53.994149926 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-osc-tiny.new.1880/python-osc-tiny.changes    
    2024-05-13 17:59:07.836192877 +0200
@@ -1,0 +2,12 @@
+Mon May 13 11:49:37 UTC 2024 - Gabriel Niebler <gnieb...@suse.com>
+
+- Release 0.9.0
+  * Staging extension (closes #151) (#154)
+  * Type annotations and models
+
+-------------------------------------------------------------------
+Mon May 13 10:16:52 UTC 2024 - Robert Frohl <rfr...@suse.com>
+
+- Use sle15allpythons to build for all SLE/Leap python versions
+
+-------------------------------------------------------------------

Old:
----
  osc-tiny-0.8.2.tar.gz

New:
----
  osc-tiny-0.9.0.tar.gz

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

Other differences:
------------------
++++++ python-osc-tiny.spec ++++++
--- /var/tmp/diff_new_pack.Y199LL/_old  2024-05-13 17:59:08.284209223 +0200
+++ /var/tmp/diff_new_pack.Y199LL/_new  2024-05-13 17:59:08.288209369 +0200
@@ -17,13 +17,14 @@
 
 
 %define skip_python2 1
+%{?sle15allpythons}
 Name:           python-osc-tiny
-Version:        0.8.2
+Version:        0.9.0
 Release:        0
 Summary:        Client API for openSUSE BuildService
 License:        MIT
 Group:          Development/Languages/Python
-URL:            https://github.com/crazyscientist/osc-tiny
+URL:            https://github.com/SUSE/osc-tiny
 Source:         
https://files.pythonhosted.org/packages/source/o/osc-tiny/osc-tiny-%{version}.tar.gz
 BuildRequires:  %{python_module PyYAML}
 BuildRequires:  %{python_module lxml}

++++++ osc-tiny-0.8.2.tar.gz -> osc-tiny-0.9.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/PKG-INFO new/osc-tiny-0.9.0/PKG-INFO
--- old/osc-tiny-0.8.2/PKG-INFO 2024-03-22 11:50:35.555877000 +0100
+++ new/osc-tiny-0.9.0/PKG-INFO 2024-04-22 17:26:59.603191600 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: osc-tiny
-Version: 0.8.2
+Version: 0.9.0
 Summary: Client API for openSUSE BuildService
 Home-page: https://github.com/SUSE/osc-tiny
 Author: Andreas Hasenkopf
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osc_tiny.egg-info/PKG-INFO 
new/osc-tiny-0.9.0/osc_tiny.egg-info/PKG-INFO
--- old/osc-tiny-0.8.2/osc_tiny.egg-info/PKG-INFO       2024-03-22 
11:50:35.000000000 +0100
+++ new/osc-tiny-0.9.0/osc_tiny.egg-info/PKG-INFO       2024-04-22 
17:26:59.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: osc-tiny
-Version: 0.8.2
+Version: 0.9.0
 Summary: Client API for openSUSE BuildService
 Home-page: https://github.com/SUSE/osc-tiny
 Author: Andreas Hasenkopf
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osc_tiny.egg-info/SOURCES.txt 
new/osc-tiny-0.9.0/osc_tiny.egg-info/SOURCES.txt
--- old/osc-tiny-0.8.2/osc_tiny.egg-info/SOURCES.txt    2024-03-22 
11:50:35.000000000 +0100
+++ new/osc-tiny-0.9.0/osc_tiny.egg-info/SOURCES.txt    2024-04-22 
17:26:59.000000000 +0200
@@ -23,7 +23,10 @@
 osctiny/extensions/packages.py
 osctiny/extensions/projects.py
 osctiny/extensions/search.py
+osctiny/extensions/staging.py
 osctiny/extensions/users.py
+osctiny/models/__init__.py
+osctiny/models/staging.py
 osctiny/tests/__init__.py
 osctiny/tests/base.py
 osctiny/tests/test_attributes.py
@@ -38,6 +41,7 @@
 osctiny/tests/test_projects.py
 osctiny/tests/test_requests.py
 osctiny/tests/test_search.py
+osctiny/tests/test_staging.py
 osctiny/tests/test_utils.py
 osctiny/tests/osc/__init__.py
 osctiny/tests/osc/conf.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/__init__.py 
new/osc-tiny-0.9.0/osctiny/__init__.py
--- old/osc-tiny-0.8.2/osctiny/__init__.py      2024-03-22 11:50:30.000000000 
+0100
+++ new/osc-tiny-0.9.0/osctiny/__init__.py      2024-04-22 17:26:55.000000000 
+0200
@@ -6,4 +6,4 @@
 
 __all__ = ['Osc', 'bs_requests', 'buildresults', 'comments', 'packages',
            'projects', 'search', 'users']
-__version__ = "0.8.2"
+__version__ = "0.9.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/extensions/bs_requests.py 
new/osc-tiny-0.9.0/osctiny/extensions/bs_requests.py
--- old/osc-tiny-0.8.2/osctiny/extensions/bs_requests.py        2024-03-22 
11:50:30.000000000 +0100
+++ new/osc-tiny-0.9.0/osctiny/extensions/bs_requests.py        2024-04-22 
17:26:55.000000000 +0200
@@ -2,10 +2,13 @@
 Requests extension
 ------------------
 """
+import typing
 from urllib.parse import urljoin
 
 from lxml.etree import XMLSyntaxError
+from lxml.objectify import ObjectifiedElement
 
+from ..models import IntOrString, ParamsType
 from ..utils.base import ExtensionBase
 
 
@@ -16,7 +19,7 @@
     base_path = "/request/"
 
     @staticmethod
-    def _validate_id(request_id):
+    def _validate_id(request_id: IntOrString) -> str:
         request_id = str(request_id)
         if not request_id.isnumeric():
             raise ValueError(
@@ -24,7 +27,7 @@
             )
         return request_id
 
-    def get_list(self, **params):
+    def get_list(self, **params: ParamsType) -> ObjectifiedElement:
         """
         Get a list or request objects
 
@@ -40,7 +43,8 @@
 
         return self.osc.get_objectified_xml(response)
 
-    def get(self, request_id, withhistory=False, withfullhistory=False):
+    def get(self, request_id: IntOrString, withhistory: bool = False,
+            withfullhistory: bool = False) -> ObjectifiedElement:
         """
         Get one request object
 
@@ -65,7 +69,8 @@
 
         return self.osc.get_objectified_xml(response)
 
-    def update(self, request_id, **kwargs):
+    def update(self, request_id: IntOrString, **kwargs: ParamsType) \
+            -> typing.Union[ObjectifiedElement, str]:
         """
         Update request or execute command
 
@@ -96,7 +101,8 @@
         except XMLSyntaxError:
             return response.text
 
-    def cmd(self, request_id, cmd="diff", **kwargs):
+    def cmd(self, request_id: IntOrString, cmd: str = "diff", **kwargs: 
ParamsType) \
+            -> typing.Union[ObjectifiedElement, str]:
         """
         Get the result of the specified command
 
@@ -130,7 +136,8 @@
         request_id = self._validate_id(request_id)
         return self.update(request_id=request_id, **kwargs)
 
-    def add_comment(self, request_id, comment, parent_id=None):
+    def add_comment(self, request_id: IntOrString, comment: str,
+                    parent_id: typing.Optional[str] = None) -> bool:
         """
         Add a comment to a request
 
@@ -152,7 +159,7 @@
             parent_id=parent_id
         )
 
-    def get_comments(self, request_id):
+    def get_comments(self, request_id: IntOrString) -> ObjectifiedElement:
         """
         Get a list of comments for request
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/extensions/buildresults.py 
new/osc-tiny-0.9.0/osctiny/extensions/buildresults.py
--- old/osc-tiny-0.8.2/osctiny/extensions/buildresults.py       2024-03-22 
11:50:30.000000000 +0100
+++ new/osc-tiny-0.9.0/osctiny/extensions/buildresults.py       2024-04-22 
17:26:55.000000000 +0200
@@ -111,6 +111,28 @@
 
         return self.osc.get_objectified_xml(response)
 
+    def get_status_and_build_id(self, project, repo, arch):
+        """
+        Get build status and build ID
+
+        :param project: Project name
+        :param repo: Repository name
+        :param arch: Architecture name
+        :return: Objectified XML element
+        :rtype: lxml.objectify.ObjectifiedElement
+
+        .. versionadded:: 0.9.0
+        """
+        response = self.osc.request(
+            method="GET",
+            url=urljoin(self.osc.url, "{}/{}/{}/{}".format(
+                self.base_path, project, repo, arch
+            )),
+            params={"view": "status"}
+        )
+
+        return self.osc.get_objectified_xml(response)
+
     def get_binary_list(self, project, repo, arch, package, **params):
         """
         Get a list of built RPMs
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/extensions/projects.py 
new/osc-tiny-0.9.0/osctiny/extensions/projects.py
--- old/osc-tiny-0.8.2/osctiny/extensions/projects.py   2024-03-22 
11:50:30.000000000 +0100
+++ new/osc-tiny-0.9.0/osctiny/extensions/projects.py   2024-04-22 
17:26:55.000000000 +0200
@@ -45,7 +45,7 @@
     def get_meta(self, project, rev=None):
         """
         Get project metadata
-        
+
         .. versionchanged:: 0.8.0
             Added the ``rev`` parameter
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/extensions/staging.py 
new/osc-tiny-0.9.0/osctiny/extensions/staging.py
--- old/osc-tiny-0.8.2/osctiny/extensions/staging.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/osc-tiny-0.9.0/osctiny/extensions/staging.py    2024-04-22 
17:26:55.000000000 +0200
@@ -0,0 +1,384 @@
+"""
+Staging extension
+-----------------
+
+This extension provides access to the staging workflow of OpenBuildService.
+
+.. seealso::
+
+    
https://openbuildservice.org/help/manuals/obs-user-guide/cha.obs.stagingworkflow
+
+    
https://openbuildservice.org/help/manuals/obs-user-guide/cha.obs.best-practices.webuiusage#staging_how_to
+
+.. versionadded:: 0.9.0
+"""
+import typing
+from urllib.parse import urljoin
+
+from lxml.objectify import ObjectifiedElement, Element, SubElement
+
+from ..models.staging import E, ExcludedRequest, CheckReport
+from ..utils.base import ExtensionBase
+
+
+class Staging(ExtensionBase):
+    """
+    Osc extension for interacting with staging workflows
+    """
+    base_path_staging = "/staging"
+    base_path_status = "/status_reports"
+
+    def get_backlog(self, project: str) -> ObjectifiedElement:
+        """
+        List the requests in the staging backlog
+
+        :param project: Project name
+        :return: Objectified XML element
+        :rtype: lxml.objectify.ObjectifiedElement
+        """
+        response = self.osc.request(
+            method="GET",
+            url=urljoin(self.osc.url, 
"{}/{}/backlog".format(self.base_path_staging, project))
+        )
+
+        return self.osc.get_objectified_xml(response)
+
+    def get_excluded_requests(self, project: str) -> ObjectifiedElement:
+        """
+        List the requests excluded from a staging workflow
+
+        :param project: Project name
+        :return: Objectified XML element
+        :rtype: lxml.objectify.ObjectifiedElement
+        """
+        response = self.osc.request(
+            method="GET",
+            url=urljoin(self.osc.url, 
"{}/{}/excluded_requests".format(self.base_path_staging,
+                                                                       
project))
+        )
+
+        return self.osc.get_objectified_xml(response)
+
+    def set_excluded_requests(self, project: str, *requests: ExcludedRequest) 
-> bool:
+        """
+        Exclude requests from the staging workflow.
+
+        :param project: Project name
+        :param requests: Requests to exclude with optional reason/description
+        :return: ``True``, if successful.
+        :raises HTTPError: if comment was not saved correctly. The raised 
exception contains the
+                           full response object and API response.
+        """
+        response = self.osc.request(
+            method="POST",
+            url=urljoin(self.osc.url, 
"{}/{}/excluded_requests".format(self.base_path_staging,
+                                                                       
project)),
+            data=E.excluded_requests(*(request.asxml() for request in 
requests))
+        )
+        parsed = self.osc.get_objectified_xml(response)
+        if response.status_code == 200 and parsed.get("code") == "ok":
+            return True
+
+        return False
+
+    def delete_excluded_requests(self, project: str, *requests: 
ExcludedRequest) -> bool:
+        """
+        Remove requests from list of excluded requests
+
+        :param project: Project name
+        :param requests: Requests to exclude with optional reason/description
+        :return: ``True``, if successful.
+        :raises HTTPError: if comment was not saved correctly. The raised 
exception contains the
+                           full response object and API response.
+        """
+        response = self.osc.request(
+            method="DELETE",
+            url=urljoin(self.osc.url, 
"{}/{}/excluded_requests".format(self.base_path_staging,
+                                                                       
project)),
+            data=E.excluded_requests(*(request.asxml() for request in 
requests))
+        )
+        parsed = self.osc.get_objectified_xml(response)
+        if response.status_code == 200 and parsed.get("code") == "ok":
+            return True
+
+        return False
+
+    def get_staging_projects(self, project: str) -> ObjectifiedElement:
+        """
+        List all the staging projects of a staging workflow.
+
+        :param project: Project name
+        :return: Objectified XML element
+        :rtype: lxml.objectify.ObjectifiedElement
+        """
+        response = self.osc.request(
+            method="GET",
+            url=urljoin(self.osc.url,
+                        
"{}/{}/staging_projects".format(self.base_path_staging, project))
+        )
+
+        return self.osc.get_objectified_xml(response)
+
+    # pylint: disable=too-many-arguments
+    def get_status(self, project: str, staging_project: str, requests: bool = 
False,
+                   status: bool = False, history: bool = False) -> 
ObjectifiedElement:
+        """
+        Get the overall state of a staging project
+
+        :param project: Project name
+        :param staging_project: Staging project name
+        :param requests: Include statistics about staged, untracked and 
obsolete requests as well as
+                         missing reviews
+        :param status: Include the overall state
+        :param history: Include the history of the staging project
+        :return: Objectified XML element
+        :rtype: lxml.objectify.ObjectifiedElement
+        """
+        response = self.osc.request(
+            method="GET",
+            url=urljoin(self.osc.url,
+                        
"{}/{}/staging_projects/{}".format(self.base_path_staging, project,
+                                                           staging_project)),
+            params={"requests": requests, "status": status, "history": history}
+        )
+
+        return self.osc.get_objectified_xml(response)
+
+    def accept(self, project: str, staging_project: str) -> bool:
+        """
+        This accepts all staged requests and sets the project state back to 
'empty'
+
+        :param project: Project name
+        :param staging_project: Staging project name
+        :return: ``True``, if successful.
+        :raises HTTPError: if comment was not saved correctly. The raised 
exception contains the
+                           full response object and API response.
+        """
+        response = self.osc.request(
+            method="POST",
+            url=urljoin(self.osc.url, 
"{}/{}/staging_projects/{}/accept".format(
+                self.base_path_staging, project, staging_project))
+        )
+
+        parsed = self.osc.get_objectified_xml(response)
+        if response.status_code == 200 and parsed.get("code") == "ok":
+            return True
+
+        return False
+
+    def get_staged_requests(self, project: str, staging_project: str) 
->ObjectifiedElement:
+        """
+        List all the staged requests of a staging project
+
+        :param project: Project name
+        :param staging_project: Staging project name
+        :return: Objectified XML element
+        :rtype: lxml.objectify.ObjectifiedElement
+        """
+        response = self.osc.request(
+            method="GET",
+            url=urljoin(self.osc.url,
+                        "{}/{}/staging_projects/{}/staged_requests".format(
+                            self.base_path_staging, project, staging_project)),
+        )
+
+        return self.osc.get_objectified_xml(response)
+
+    def add_staged_requests(self, project: str, staging_project: str, 
*request_ids: int) -> bool:
+        """
+        Add requests to the staging project.
+
+        :param project: Project name
+        :param staging_project: Staging project name
+        :param request_ids: Request IDs
+        :return: ``True``, if successful.
+        :raises HTTPError: if comment was not saved correctly. The raised 
exception contains the
+                           full response object and API response.
+        """
+        requests = Element("requests")
+        for request_id in request_ids:
+            SubElement(requests, "request", id=str(request_id))
+        response = self.osc.request(
+            method="POST",
+            url=urljoin(self.osc.url, 
"{}/{}/staging_projects/{}/staged_requests".format(
+                self.base_path_staging, project, staging_project)),
+            data=requests
+        )
+        parsed = self.osc.get_objectified_xml(response)
+        if response.status_code == 200 and parsed.get("code") == "ok":
+            return True
+
+        return False
+
+    def delete_staged_requests(self, project: str, staging_project: str, 
*request_ids: int) -> bool:
+        """
+        Delete requests from the staging project
+
+        :param project: Project name
+        :param staging_project: Staging project name
+        :param request_ids: Request IDs
+        :return: ``True``, if successful.
+        :raises HTTPError: if comment was not saved correctly. The raised 
exception contains the
+                           full response object and API response.
+        """
+        requests = Element("requests")
+        for request_id in request_ids:
+            SubElement(requests, "request", id=str(request_id))
+        response = self.osc.request(
+            method="DELETE",
+            url=urljoin(self.osc.url, 
"{}/{}/staging_projects/{}/staged_requests".format(
+                self.base_path_staging, project, staging_project)),
+            data=requests
+        )
+        parsed = self.osc.get_objectified_xml(response)
+        if response.status_code == 200 and parsed.get("code") == "ok":
+            return True
+
+        return False
+
+    def get_required_checks(self, project: str, repo: typing.Optional[str] = 
None,
+                            arch: typing.Optional[str] = None) -> 
ObjectifiedElement:
+        """
+        Get list of required checks
+
+        If `repo`` and ``arch`` are specified, required checks from the built 
repository are
+        returned. If only ``repo`` is specified, required checks from the 
repository are
+        returned. Otherwise, required checks from the project are returned
+
+        :param project: (Staging) project name
+        :param repo: Repository name (optional)
+        :param arch: Architecture name (optional)
+        :return: Objectified XML element
+        :rtype: lxml.objectify.ObjectifiedElement
+        """
+        if repo and arch:
+            url_path = "{}/built_repositories/{}/{}/{}/required_checks".format(
+                self.base_path_status, project, repo, arch
+            )
+        elif repo:
+            url_path = "{}/repositories/{}/{}/required_checks".format(
+                self.base_path_status, project, repo
+            )
+        else:
+            url_path = 
"{}/projects/{}/required_checks".format(self.base_path_status, project)
+
+        response = self.osc.request(
+            method="GET",
+            url=urljoin(self.osc.url, url_path),
+        )
+
+        return self.osc.get_objectified_xml(response)
+
+    def set_required_checks(self, project: str, checks: typing.List[str],
+                            repo: typing.Optional[str] = None,
+                            arch: typing.Optional[str] = None) -> bool:
+        """
+        Submit a new or modified required checks list
+
+        If ``repo`` and ``arch`` are specified, required checks of built 
repository are
+        updated. If only ``repo`` is specified, required checks of the 
repository are updated.
+        Otherwise, required checks of the project are updated.
+
+        :param project: (Staging) project name
+        :param checks: List of check names
+        :param repo: Repository name (optional)
+        :param arch: Architecture name (optional)
+        :return: ``True``, if successful.
+        :raises HTTPError: if comment was not saved correctly. The raised 
exception contains the
+                           full response object and API response.
+        """
+        kwargs = {"project": project}
+        if repo and arch:
+            url_path = "{}/built_repositories/{}/{}/{}/required_checks".format(
+                self.base_path_status, project, repo, arch
+            )
+            kwargs.update({"repository": repo, "architecture": arch})
+        elif repo:
+            url_path = "{}/repositories/{}/{}/required_checks".format(
+                self.base_path_status, project, repo
+            )
+            kwargs["repository"] = repo
+        else:
+            url_path = 
"{}/projects/{}/required_checks".format(self.base_path_status, project)
+
+        response = self.osc.request(
+            method="POST",
+            url=urljoin(self.osc.url, url_path),
+            data=E.required_checks(*(E.name(check) for check in checks))
+        )
+        parsed = self.osc.get_objectified_xml(response)
+        if response.status_code == 200 and parsed.get("code") == "ok":
+            return True
+
+        return False
+
+    def get_status_report(self, project: str, repo: str, build_id: str,
+                          arch: typing.Optional[str] = None) -> 
ObjectifiedElement:
+        """
+        Get list of checks
+
+        If ``arch`` is specified, status report for built project is 
retrieved. Otherwise, status
+        report for published project is retrieved.
+
+        :param project: (Staging) project name
+        :param repo: Repository name
+        :param build_id: Build ID (Can be obtained via
+                         
:py:meth:`osctiny.extensions.buildresults.Build.get_status_and_build_id`)
+        :param arch: Architecture name
+        :return: Objectified XML element
+        :rtype: lxml.objectify.ObjectifiedElement
+        """
+        if arch:
+            url_path = "{}/built/{}/{}/{}/reports/{}".format(
+                self.base_path_status, project, repo, arch, build_id
+            )
+        else:
+            url_path = "{}/published/{}/{}/reports/{}".format(
+                self.base_path_status, project, repo, build_id
+            )
+
+        response = self.osc.request(
+            method="GET",
+            url=urljoin(self.osc.url, url_path)
+        )
+
+        return self.osc.get_objectified_xml(response)
+
+    def set_status_report(self, project: str, repo: str, build_id: str, 
report: CheckReport,
+                          arch: typing.Optional[str] = None) -> bool:
+        """
+        Submit a check to a status report
+
+        If ``arch`` is specified, the status report for built project is set. 
Otherwise, the status
+        report for published project is set.
+
+        :param project: (Staging) project name
+        :param repo: Repository name
+        :param build_id: Build ID (Can be obtained via
+                         
:py:meth:`osctiny.extensions.buildresults.Build.get_status_and_build_id`)
+        :param report: The status upate
+        :param arch: Architecture name
+        :return: ``True``, if successful.
+        :raises HTTPError: if comment was not saved correctly. The raised 
exception contains the
+                           full response object and API response.
+        """
+        if arch:
+            url_path = "{}/built/{}/{}/{}/reports/{}".format(
+                self.base_path_status, project, repo, arch, build_id
+            )
+        else:
+            url_path = "{}/published/{}/{}/reports/{}".format(
+                self.base_path_status, project, repo, build_id
+            )
+
+        response = self.osc.request(
+            method="POST",
+            url=urljoin(self.osc.url, url_path),
+            data=report.asxml()
+        )
+
+        parsed = self.osc.get_objectified_xml(response)
+        if response.status_code == 200 and parsed.get("code") == "ok":
+            return True
+
+        return False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/models/__init__.py 
new/osc-tiny-0.9.0/osctiny/models/__init__.py
--- old/osc-tiny-0.8.2/osctiny/models/__init__.py       1970-01-01 
01:00:00.000000000 +0100
+++ new/osc-tiny-0.9.0/osctiny/models/__init__.py       2024-04-22 
17:26:55.000000000 +0200
@@ -0,0 +1,12 @@
+"""
+Common model/type definitions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+"""
+from io import BufferedReader, BytesIO, StringIO
+import typing
+
+from lxml.objectify import ObjectifiedElement
+
+
+ParamsType = typing.Union[bytes, str, StringIO, BytesIO, BufferedReader, dict, 
ObjectifiedElement]
+IntOrString = typing.Union[int, str]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/models/staging.py 
new/osc-tiny-0.9.0/osctiny/models/staging.py
--- old/osc-tiny-0.8.2/osctiny/models/staging.py        1970-01-01 
01:00:00.000000000 +0100
+++ new/osc-tiny-0.9.0/osctiny/models/staging.py        2024-04-22 
17:26:55.000000000 +0200
@@ -0,0 +1,69 @@
+"""
+Models for Staging
+^^^^^^^^^^^^^^^^^^
+"""
+# pylint: disable=missing-class-docstring,missing-function-docstring
+import enum
+import typing
+
+from lxml.objectify import ObjectifiedElement, ElementMaker
+
+
+E = ElementMaker(annotate=False)
+
+
+class ExcludedRequest(typing.NamedTuple):
+    id: int
+    description: typing.Optional[str] = None
+
+    def asdict(self) -> typing.Dict[str, str]:
+        d = {"id": str(self.id)}
+        if self.description:
+            d["description"] = self.description
+
+        return d
+
+    def asxml(self) -> ObjectifiedElement:
+        return E.request(**self.asdict())
+
+
+class CheckState(enum.Enum):
+    PENDING = "pending"
+    ERROR = "error"
+    FAILURE = "failure"
+    SUCCESS = "success"
+
+
+class CheckReport(typing.NamedTuple):
+    name: str
+    required: bool
+    state: CheckState
+    short_description: typing.Optional[str] = None
+    url: typing.Optional[str] = None
+
+    @property
+    def required_str(self) -> str:
+        return "true" if self.required else "false"
+
+    def _optional_fields(self) -> typing.Generator[typing.Tuple[str, str], 
None, None]:
+        for key in ('url', 'short_description'):
+            value = getattr(self, key, None)
+            if value:
+                yield key, str(value)
+
+    def asdict(self) -> typing.Dict[str, str]:
+        d = {"name": self.name, "required": self.required_str,
+             "state": self.state.value}
+
+        for key, value in self._optional_fields():
+            d[key] = value
+
+        return d
+
+    def asxml(self) -> ObjectifiedElement:
+        sub_elems = [E.state(self.state.value)]
+        for key, value in self._optional_fields():
+            _E = getattr(E, key)
+            sub_elems.append(_E(value))
+
+        return E.check(*sub_elems, name=self.name, required=self.required_str)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/osc.py 
new/osc-tiny-0.9.0/osctiny/osc.py
--- old/osc-tiny-0.8.2/osctiny/osc.py   2024-03-22 11:50:30.000000000 +0100
+++ new/osc-tiny-0.9.0/osctiny/osc.py   2024-04-22 17:26:55.000000000 +0200
@@ -20,7 +20,9 @@
 from urllib.parse import quote, parse_qs, urlparse
 import warnings
 
-from requests import Session, Request
+from lxml.etree import tostring
+from lxml.objectify import ObjectifiedElement
+from requests import Session, Request, Response
 from requests.auth import HTTPBasicAuth
 from requests.cookies import RequestsCookieJar, cookiejar_from_dict
 from requests.exceptions import ConnectionError as _ConnectionError
@@ -35,7 +37,9 @@
 from .extensions.projects import Project
 from .extensions.bs_requests import Request as BsRequest
 from .extensions.search import Search
+from .extensions.staging import Staging
 from .extensions.users import Group, Person
+from .models import ParamsType
 from .utils.auth import HttpSignatureAuth
 from .utils.backports import cached_property
 from .utils.conf import BOOLEAN_PARAMS, get_credentials, get_cookie_jar
@@ -87,6 +91,8 @@
           - :py:attr:`origins`
         * - :py:class:`osctiny.extensions.attributes.Attribute`
           - :py:attr:`attributes`
+        * - :py:class:`osctiny.extensions.staging.Staging`
+          - :py:attr:`staging`
 
     :param url: API URL of a BuildService instance
     :param username: Username
@@ -120,7 +126,10 @@
 
     .. versionchanged:: 0.8.0
         * Removed the ``cache`` parameter
-        * Added the ``attributes`` extensions
+        * Added the ``attributes`` extension
+
+    .. versionchanged:: 0.9.0
+        * Added the ``staging`` extension
 
     .. _SSL Cert Verification:
         http://docs.python-requests.org/en/master/user/advanced/
@@ -163,6 +172,7 @@
         self.projects = Project(osc_obj=self)
         self.requests = BsRequest(osc_obj=self)
         self.search = Search(osc_obj=self)
+        self.staging = Staging(osc_obj=self)
         self.users = Person(osc_obj=self)
 
     def __del__(self):
@@ -222,12 +232,15 @@
         Explicit parser instance
 
         .. versionchanged:: 0.8.0
-            Content moved to :py:fun:`osctiny.utils.xml.get_xml_parser`
+            Content moved to :py:func:`osctiny.utils.xml.get_xml_parser`
         """
         return get_xml_parser()
 
-    def request(self, url, method="GET", stream=False, data=None, params=None,
-                raise_for_status=True, timeout=None):
+    def request(self, url: str, method: str = "GET", stream: bool = False,
+                data: typing.Optional[ParamsType] = None,
+                params: typing.Optional[ParamsType] = None,
+                raise_for_status: bool = True, timeout: typing.Optional[int] = 
None) \
+            -> typing.Optional[Response]:
         """
         Perform HTTP(S) request
 
@@ -351,9 +364,8 @@
 
         return ()
 
-    def handle_params(self, url: str, method: str,
-                      params: typing.Union[bytes, str, StringIO, BytesIO, 
BufferedReader, dict]) \
-            -> bytes:
+    def handle_params(self, url: str, method: str, params: ParamsType) \
+            -> bytes:  # pylint: disable=too-many-return-statements
         """
         Translate request parameters to API conform format
 
@@ -369,7 +381,6 @@
                                             /9715
 
         :param params: Request parameter
-        :type params: dict or str or io.BufferedReader
         :param url: URL to which the parameters will be sent
         :type url: str
         :param method: HTTP method to send request
@@ -380,6 +391,10 @@
         .. versionchanged:: 0.7.3
 
             Added the ``url`` and ``method`` parameters
+
+        .. versionchanged:: 0.9.0
+
+            Instances of ``ObjectifiedElement`` are accepted for argument 
``params``
         """
         if isinstance(params, bytes):
             return params
@@ -395,6 +410,9 @@
             params.seek(0)
             return params
 
+        if isinstance(params, ObjectifiedElement):
+            return tostring(params, encoding="utf-8", xml_declaration=True)
+
         if not isinstance(params, dict):
             return {}
 
@@ -422,7 +440,8 @@
             if value is not None
         ).encode()
 
-    def download(self, url, destdir, destfile=None, overwrite=False, **params):
+    def download(self, url: str, destdir: Path, destfile: typing.Optional[str] 
= None,
+                 overwrite: bool = False, **params: ParamsType) -> Path:
         """
         Shortcut for a streaming GET request
 
@@ -457,7 +476,7 @@
 
         return target
 
-    def get_objectified_xml(self, response):
+    def get_objectified_xml(self, response: Response) -> ObjectifiedElement:
         """
         Return API response as an XML object
 
@@ -471,6 +490,6 @@
 
         .. versionchanged:: 0.8.0
 
-            Content moved to :py:fun:`osctiny.utils.xml.get_objectified_xml`
+            Content moved to :py:func:`osctiny.utils.xml.get_objectified_xml`
         """
         return get_objectified_xml(response=response)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/tests/test_staging.py 
new/osc-tiny-0.9.0/osctiny/tests/test_staging.py
--- old/osc-tiny-0.8.2/osctiny/tests/test_staging.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/osc-tiny-0.9.0/osctiny/tests/test_staging.py    2024-04-22 
17:26:55.000000000 +0200
@@ -0,0 +1,248 @@
+import re
+
+import responses
+
+from osctiny.models.staging import ExcludedRequest, CheckState, CheckReport
+
+from .base import OscTest
+
+
+class StagingTest(OscTest):
+    @staticmethod
+    def _mock_generic_request():
+        responses.add(method=responses.GET,
+                      
url=re.compile("http://api.example.com/(staging|status_reports)/.*"),
+                      body="<dummy><foo/><bar>Hello World</bar></dummy>",
+                      status=200)
+
+    @responses.activate
+    def test_get_backlog(self):
+        self._mock_generic_request()
+        response = self.osc.staging.get_backlog("Dummy:Project")
+        self.assertEqual(response.tag, "dummy")
+
+    @responses.activate
+    def test_get_excluded_requests(self):
+        self._mock_generic_request()
+        response = self.osc.staging.get_excluded_requests("Dummy:Project")
+        self.assertEqual(response.tag, "dummy")
+
+    @responses.activate
+    def test_set_excluded_requests(self):
+        def callback(request):
+            element = self.osc.get_objectified_xml(response=request.body)
+            self.assertEqual(element.request[0].get("id"), '1')
+            self.assertEqual(element.request[0].get("description"), "Foo Bar")
+            self.assertEqual(element.request[1].get("id"), '2')
+            self.assertEqual(element.request[1].get("description"), "Hello 
World")
+            self.assertEqual(element.request[2].get("id"), '3')
+            return 200, {}, "<status code=\"ok\"/>"
+
+        self.mock_request(
+            method="POST",
+            
url="http://api.example.com/staging/Dummy:Project/excluded_requests";,
+            callback=callback
+        )
+
+        result = self.osc.staging.set_excluded_requests("Dummy:Project",
+                                                        ExcludedRequest(1, 
"Foo Bar"),
+                                                        ExcludedRequest(2, 
"Hello World"),
+                                                        ExcludedRequest(3))
+        self.assertTrue(result)
+
+    @responses.activate
+    def test_delete_excluded_requests(self):
+        def callback(request):
+            element = self.osc.get_objectified_xml(response=request.body)
+            self.assertEqual(element.request[0].get("id"), '1')
+            return 200, {}, "<status code=\"ok\"/>"
+
+        self.mock_request(
+            method="DELETE",
+            
url="http://api.example.com/staging/Dummy:Project/excluded_requests";,
+            callback=callback
+        )
+
+        result = self.osc.staging.delete_excluded_requests("Dummy:Project", 
ExcludedRequest(1))
+        self.assertTrue(result)
+
+    @responses.activate
+    def test_get_staging_projects(self):
+        self._mock_generic_request()
+        response = self.osc.staging.get_staging_projects("Dummy:Project")
+        self.assertEqual(response.tag, "dummy")
+
+    @responses.activate
+    def test_get_status(self):
+        self._mock_generic_request()
+        response = self.osc.staging.get_status("Dummy:Project", 
"Dummy:Project:Staging:A")
+        self.assertEqual(response.tag, "dummy")
+
+    @responses.activate
+    def test_accept(self):
+        responses.add(method="POST",
+                      
url="http://api.example.com/staging/Dummy:Project/staging_projects/Dummy:Project:Staging:A/accept";,
+                      status=200,
+                      body="<status code=\"ok\"/>")
+        self.assertTrue(self.osc.staging.accept("Dummy:Project", 
"Dummy:Project:Staging:A"))
+
+    @responses.activate
+    def test_get_staged_requests(self):
+        self._mock_generic_request()
+        response = self.osc.staging.get_staged_requests("Dummy:Project", 
"Dummy:Project:Staging:A")
+        self.assertEqual(response.tag, "dummy")
+
+    @responses.activate
+    def test_add_staged_requests(self):
+        def callback(request):
+            element = self.osc.get_objectified_xml(request.body)
+            self.assertEqual(3, len(element.request))
+            self.assertEqual(["1", "2", "3"], [elem.get("id") for elem in 
element.request])
+            return 200, {}, "<status code=\"ok\"/>"
+
+        responses.add_callback(method="POST",
+                               
url="http://api.example.com/staging/Dummy:Project/staging_projects/Dummy:Project:Staging:A/staged_requests";,
+                               callback=callback)
+        result = self.osc.staging.add_staged_requests("Dummy:Project", 
"Dummy:Project:Staging:A",
+                                                      1, 2, 3)
+        self.assertTrue(result)
+
+    @responses.activate
+    def test_delete_staged_requests(self):
+        def callback(request):
+            element = self.osc.get_objectified_xml(request.body)
+            self.assertEqual(3, len(element.request))
+            self.assertEqual(["1", "2", "3"], [elem.get("id") for elem in 
element.request])
+            return 200, {}, "<status code=\"ok\"/>"
+
+        responses.add_callback(method="DELETE",
+                               
url="http://api.example.com/staging/Dummy:Project/staging_projects/Dummy:Project:Staging:A/staged_requests";,
+                               callback=callback)
+        result = self.osc.staging.delete_staged_requests("Dummy:Project", 
"Dummy:Project:Staging:A",
+                                                         1, 2, 3)
+        self.assertTrue(result)
+
+    @responses.activate
+    def test_get_required_checks(self):
+        responses.add(
+            method="GET",
+            
url="http://api.example.com/status_reports/built_repositories/Dummy:Project/repo/x86_64/required_checks";,
+            body="<dummyrepoarch/>",
+            status=200
+        )
+        responses.add(
+            method="GET",
+            
url="http://api.example.com/status_reports/repositories/Dummy:Project/repo/required_checks";,
+            body="<dummyrepo/>",
+            status=200
+        )
+        responses.add(
+            method="GET",
+            
url="http://api.example.com/status_reports/projects/Dummy:Project/required_checks";,
+            body="<dummyproject/>",
+            status=200
+        )
+
+        with self.subTest("Repo + Arch"):
+            response = self.osc.staging.get_required_checks("Dummy:Project", 
"repo", "x86_64")
+            self.assertEqual(response.tag, "dummyrepoarch")
+
+        with self.subTest("Repo"):
+            response = self.osc.staging.get_required_checks("Dummy:Project", 
"repo")
+            self.assertEqual(response.tag, "dummyrepo")
+
+        with self.subTest("Project"):
+            response = self.osc.staging.get_required_checks("Dummy:Project")
+            self.assertEqual(response.tag, "dummyproject")
+
+    @responses.activate
+    def test_set_required_checks(self):
+        def callback(request):
+            elem = self.osc.get_objectified_xml(request.body)
+
+            if "/projects/" in request.url:
+                self.assertTrue(all("project" in child.text for child in 
elem.name))
+            if "/repositories/" in request.url:
+                self.assertTrue(all("repo" in child.text for child in 
elem.name))
+            if "/built_repositories/" in request.url:
+                self.assertTrue(all("built" in child.text for child in 
elem.name))
+
+            return 200, {}, "<status code=\"ok\"/>"
+
+        responses.add_callback(
+            method="POST",
+            
url=re.compile("http://api.example.com/status_reports/(projects|repositories|built_repositories)/.*"),
+            callback=callback
+        )
+
+        with self.subTest("Repo + Arch"):
+            result = self.osc.staging.set_required_checks(
+                "Dummy:Project", [f"built-{i}" for i in range(1, 3)], "repo", 
"x86_64"
+            )
+            self.assertTrue(result)
+
+        with self.subTest("Repo"):
+            result = self.osc.staging.set_required_checks(
+                "Dummy:Project", [f"repo-{i}" for i in range(1, 3)], "repo"
+            )
+            self.assertTrue(result)
+
+        with self.subTest("Project"):
+            result = self.osc.staging.set_required_checks(
+                "Dummy:Project", [f"project-{i}" for i in range(1, 3)]
+            )
+            self.assertTrue(result)
+
+    @responses.activate
+    def test_get_status_report(self):
+        responses.add(
+            method="GET",
+            
url="http://api.example.com/status_reports/built/Dummy:Project/repo/x86_64/reports/1";,
+            body="<dummyarch/>",
+            status=200
+        )
+        responses.add(
+            method="GET",
+            
url="http://api.example.com/status_reports/published/Dummy:Project/repo/reports/1";,
+            body="<dummyrepo/>",
+            status=200
+        )
+
+        with self.subTest("Repo + Arch"):
+            response = self.osc.staging.get_status_report("Dummy:Project", 
"repo", "1", "x86_64")
+            self.assertEqual(response.tag, "dummyarch")
+
+        with self.subTest("Repo"):
+            response = self.osc.staging.get_status_report("Dummy:Project", 
"repo", "1")
+            self.assertEqual(response.tag, "dummyrepo")
+
+    @responses.activate
+    def test_set_status_report(self):
+        report = CheckReport(
+            name="dummy-check",
+            required=False,
+            state=CheckState.FAILURE,
+            short_description="Lorem ipsum dolor sit",
+            url="http://example.com/lorem-ipsum";
+        )
+
+        def callback(request):
+            elem = self.osc.get_objectified_xml(request.body)
+            self.assertEqual(report.name, elem.get("name"))
+            self.assertEqual("false", elem.get("required"))
+            self.assertEqual(elem.state.text, report.state.value)
+            self.assertEqual(elem.short_description.text, 
report.short_description)
+            self.assertEqual(elem.url.text, report.url)
+
+            return 200, {}, "<status code=\"ok\"/>"
+
+        responses.add_callback(
+            method="POST",
+            
url=re.compile("http://api.example.com/status_reports/(published|built)"),
+            callback=callback
+        )
+
+        with self.subTest("Built"):
+            result = self.osc.staging.set_status_report("Dummy:Project", 
"repo", "id-1", report,
+                                                        "x86_64")
+            self.assertTrue(result)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/utils/base.py 
new/osc-tiny-0.9.0/osctiny/utils/base.py
--- old/osc-tiny-0.8.2/osctiny/utils/base.py    2024-03-22 11:50:30.000000000 
+0100
+++ new/osc-tiny-0.9.0/osctiny/utils/base.py    2024-04-22 17:26:55.000000000 
+0200
@@ -4,6 +4,8 @@
 """
 # pylint: disable=too-few-public-methods,
 import os
+import typing
+from pathlib import Path
 
 from lxml.etree import tounicode
 
@@ -12,7 +14,7 @@
     """
     Base class for extensions of the :py:class:`Ocs` entry point.
     """
-    def __init__(self, osc_obj):
+    def __init__(self, osc_obj: "Osc"):
         self.osc = osc_obj
 
 
@@ -25,7 +27,8 @@
     osclib_version_string = "1.0"
 
     # pylint: disable=too-many-arguments
-    def __init__(self, osc, path, project, package=None, overwrite=False):
+    def __init__(self, osc: "Osc", path: Path, project: str, package: 
typing.Optional[str] = None,
+                 overwrite: bool = False):
         self.osc = osc
         self.path = os.path.join(path, self.data_dir)
         self.project = project
@@ -42,7 +45,7 @@
         if overwrite:
             self.write_dir_contents()
 
-    def write_dir_contents(self):
+    def write_dir_contents(self) -> None:
         """
         Create files with default content in ``.osc`` sub-directory
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/utils/changelog.py 
new/osc-tiny-0.9.0/osctiny/utils/changelog.py
--- old/osc-tiny-0.8.2/osctiny/utils/changelog.py       2024-03-22 
11:50:30.000000000 +0100
+++ new/osc-tiny-0.9.0/osctiny/utils/changelog.py       2024-04-22 
17:26:55.000000000 +0200
@@ -11,6 +11,7 @@
 
 .. versionadded:: 0.1.11
 """
+import typing
 from datetime import datetime
 from io import TextIOBase
 import re
@@ -20,7 +21,10 @@
 from pytz import _UTC
 
 
-def is_aware(timestamp):
+Parsable = typing.Union[TextIOBase, str, bytes]
+
+
+def is_aware(timestamp: datetime) -> bool:
     """
     Check whether timestamp is timezone aware
 
@@ -54,12 +58,13 @@
         All lines until the beginning of the next entry; except empty lines at
         the beginning and end
     """
-    timestamp = None
-    packager = None
+    timestamp: datetime = None
+    packager: str = None
     content = ""
     default_tz = _UTC()
 
-    def __init__(self, timestamp=None, packager=None, content=""):
+    def __init__(self, timestamp: typing.Optional[datetime] = None,
+                 packager: typing.Optional[str] = None, content: str =""):
         if not isinstance(timestamp, datetime) and timestamp is not None:
             raise TypeError("`timestamp` needs to be a datetime object!")
         if timestamp and not is_aware(timestamp):
@@ -68,13 +73,13 @@
         self.packager = packager
         self.content = content
 
-    def __bool__(self):
+    def __bool__(self) -> bool:
         return bool(self.timestamp and self.packager and self.content)
 
-    def __len__(self):
+    def __len__(self) -> int:
         return 1 if self.timestamp and self.packager and self.content else 0
 
-    def now(self):
+    def now(self) -> datetime:
         """
         Return current UTC timestamp
 
@@ -83,7 +88,7 @@
         return datetime.now(tz=self.default_tz)
 
     @property
-    def formatted_timestamp(self):
+    def formatted_timestamp(self) -> str:
         """
         Return properly formatted timestamp
 
@@ -96,11 +101,11 @@
             .astimezone(self.default_tz)\
             .strftime("%a %b %d %H:%M:%S %Z %Y")
 
-    def __str__(self):
+    def __str__(self) -> str:
         return "{sep}\n{self.formatted_timestamp} - {self.packager}\n\n" \
                "{self.content}\n\n".format(sep="-" * 67, self=self)
 
-    def __unicode__(self):
+    def __unicode__(self) -> str:
         return self.__str__()
 
 
@@ -138,7 +143,8 @@
     def __init__(self):
         self.entries = []
 
-    def _parse(self, handle):
+    def _parse(self, handle: Parsable) \
+            -> typing.Generator[Entry, None, None]:
         """
         Actual method for parsing.
 
@@ -208,7 +214,7 @@
             handle.close()
 
     @classmethod
-    def parse(cls, path, generative=True):
+    def parse(cls, path: Parsable, generative: bool = True) -> "ChangeLog":
         """
         Parse a changes file
 
@@ -247,7 +253,7 @@
 
         return new
 
-    def write(self, path):
+    def write(self, path: Parsable) -> None:
         """
         Write entries to file/stream
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/osctiny/utils/xml.py 
new/osc-tiny-0.9.0/osctiny/utils/xml.py
--- old/osc-tiny-0.8.2/osctiny/utils/xml.py     2024-03-22 11:50:30.000000000 
+0100
+++ new/osc-tiny-0.9.0/osctiny/utils/xml.py     2024-04-22 17:26:55.000000000 
+0200
@@ -30,7 +30,7 @@
     return THREAD_LOCAL.parser
 
 
-def get_objectified_xml(response: typing.Union[Response, str]) -> 
ObjectifiedElement:
+def get_objectified_xml(response: typing.Union[Response, str, bytes]) -> 
ObjectifiedElement:
     """
     Return API response as an XML object
 
@@ -46,11 +46,15 @@
 
         Carved out from ``Osc`` class
 
+    .. versionchanged:: 0.9.0
+
+        Accepts also bytes
+
     :param response: An API response or XML string
     :rtype response: :py:class:`requests.Response`
     :return: :py:class:`lxml.objectify.ObjectifiedElement`
     """
-    if isinstance(response, str):
+    if isinstance(response, (str, bytes)):
         text = response
     elif isinstance(response, Response):
         text = response.text
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-tiny-0.8.2/setup.py new/osc-tiny-0.9.0/setup.py
--- old/osc-tiny-0.8.2/setup.py 2024-03-22 11:50:30.000000000 +0100
+++ new/osc-tiny-0.9.0/setup.py 2024-04-22 17:26:55.000000000 +0200
@@ -26,7 +26,7 @@
 
 setup(
     name='osc-tiny',
-    version='0.8.2',
+    version='0.9.0',
     description='Client API for openSUSE BuildService',
     long_description=long_description,
     long_description_content_type="text/markdown",

Reply via email to