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 <[email protected]>
+
+- Release 0.9.0
+ * Staging extension (closes #151) (#154)
+ * Type annotations and models
+
+-------------------------------------------------------------------
+Mon May 13 10:16:52 UTC 2024 - Robert Frohl <[email protected]>
+
+- 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",