commit python-osc-tiny for openSUSE:Factory
Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-osc-tiny for openSUSE:Factory checked in at 2023-11-30 22:03:58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-osc-tiny (Old) and /work/SRC/openSUSE:Factory/.python-osc-tiny.new.25432 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "python-osc-tiny" Thu Nov 30 22:03:58 2023 rev:29 rq:1129978 version:0.8.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-osc-tiny/python-osc-tiny.changes 2023-03-06 18:56:46.137037468 +0100 +++ /work/SRC/openSUSE:Factory/.python-osc-tiny.new.25432/python-osc-tiny.changes 2023-11-30 22:05:16.573981173 +0100 @@ -1,0 +2,12 @@ +Wed Nov 29 18:03:47 UTC 2023 - Chen Huang <chhuang@suse.com> + +- Release 0.8.0 + * Added the attributes extension + * Project.get_meta: target the /_project path to really get specific revisions + * Add an optional rev parameter to Project.get_meta + * Reusable function to extract error message from responses and converted get_objectified_xml into standalone function + * Removed backport of lru_cache + * Session optimizations + * Add Build.get_log + +------------------------------------------------------------------- Old: ---- osc-tiny-0.7.12.tar.gz New: ---- osc-tiny-0.8.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-osc-tiny.spec ++++++ --- /var/tmp/diff_new_pack.dlsPBR/_old 2023-11-30 22:05:17.234005494 +0100 +++ /var/tmp/diff_new_pack.dlsPBR/_new 2023-11-30 22:05:17.234005494 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-osc-tiny # -# Copyright (c) 2022 SUSE LLC +# Copyright (c) 2023 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,7 +18,7 @@ %define skip_python2 1 Name: python-osc-tiny -Version: 0.7.12 +Version: 0.8.0 Release: 0 Summary: Client API for openSUSE BuildService License: MIT ++++++ osc-tiny-0.7.12.tar.gz -> osc-tiny-0.8.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/PKG-INFO new/osc-tiny-0.8.0/PKG-INFO --- old/osc-tiny-0.7.12/PKG-INFO 2023-03-06 07:03:02.507382900 +0100 +++ new/osc-tiny-0.8.0/PKG-INFO 2023-11-29 15:48:58.173556800 +0100 @@ -1,11 +1,12 @@ Metadata-Version: 2.1 Name: osc-tiny -Version: 0.7.12 +Version: 0.8.0 Summary: Client API for openSUSE BuildService -Home-page: http://github.com/crazyscientist/osc-tiny -Download-URL: http://github.com/crazyscientist/osc-tiny/tarball/master +Home-page: https://github.com/SUSE/osc-tiny Author: Andreas Hasenkopf Author-email: ahasenkopf@suse.com +Maintainer: SUSE Maintenance Automation Engineering team +Maintainer-email: maintenance-automation-team@suse.de License: MIT Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers @@ -18,8 +19,14 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 Description-Content-Type: text/markdown License-File: LICENSE +Requires-Dist: lxml +Requires-Dist: requests +Requires-Dist: python-dateutil +Requires-Dist: pytz +Requires-Dist: pyyaml OSC Tiny ======== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osc_tiny.egg-info/PKG-INFO new/osc-tiny-0.8.0/osc_tiny.egg-info/PKG-INFO --- old/osc-tiny-0.7.12/osc_tiny.egg-info/PKG-INFO 2023-03-06 07:03:02.000000000 +0100 +++ new/osc-tiny-0.8.0/osc_tiny.egg-info/PKG-INFO 2023-11-29 15:48:58.000000000 +0100 @@ -1,11 +1,12 @@ Metadata-Version: 2.1 Name: osc-tiny -Version: 0.7.12 +Version: 0.8.0 Summary: Client API for openSUSE BuildService -Home-page: http://github.com/crazyscientist/osc-tiny -Download-URL: http://github.com/crazyscientist/osc-tiny/tarball/master +Home-page: https://github.com/SUSE/osc-tiny Author: Andreas Hasenkopf Author-email: ahasenkopf@suse.com +Maintainer: SUSE Maintenance Automation Engineering team +Maintainer-email: maintenance-automation-team@suse.de License: MIT Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers @@ -18,8 +19,14 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 Description-Content-Type: text/markdown License-File: LICENSE +Requires-Dist: lxml +Requires-Dist: requests +Requires-Dist: python-dateutil +Requires-Dist: pytz +Requires-Dist: pyyaml OSC Tiny ======== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osc_tiny.egg-info/SOURCES.txt new/osc-tiny-0.8.0/osc_tiny.egg-info/SOURCES.txt --- old/osc-tiny-0.7.12/osc_tiny.egg-info/SOURCES.txt 2023-03-06 07:03:02.000000000 +0100 +++ new/osc-tiny-0.8.0/osc_tiny.egg-info/SOURCES.txt 2023-11-29 15:48:58.000000000 +0100 @@ -13,6 +13,7 @@ osctiny/__init__.py osctiny/osc.py osctiny/extensions/__init__.py +osctiny/extensions/attributes.py osctiny/extensions/bs_requests.py osctiny/extensions/buildresults.py osctiny/extensions/comments.py @@ -25,9 +26,9 @@ osctiny/extensions/users.py osctiny/tests/__init__.py osctiny/tests/base.py +osctiny/tests/test_attributes.py osctiny/tests/test_basic.py osctiny/tests/test_build.py -osctiny/tests/test_cache.py osctiny/tests/test_comments.py osctiny/tests/test_datadir.py osctiny/tests/test_distributions.py @@ -47,4 +48,5 @@ osctiny/utils/changelog.py osctiny/utils/conf.py osctiny/utils/errors.py -osctiny/utils/mapping.py \ No newline at end of file +osctiny/utils/mapping.py +osctiny/utils/xml.py \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/__init__.py new/osc-tiny-0.8.0/osctiny/__init__.py --- old/osc-tiny-0.7.12/osctiny/__init__.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/__init__.py 2023-11-29 15:48:50.000000000 +0100 @@ -6,4 +6,4 @@ __all__ = ['Osc', 'bs_requests', 'buildresults', 'comments', 'packages', 'projects', 'search', 'users'] -__version__ = "0.7.12" +__version__ = "0.8.0" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/extensions/attributes.py new/osc-tiny-0.8.0/osctiny/extensions/attributes.py --- old/osc-tiny-0.7.12/osctiny/extensions/attributes.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/extensions/attributes.py 2023-11-29 15:48:50.000000000 +0100 @@ -0,0 +1,73 @@ +""" +Attributes extension +-------------------- + +.. versionadded:: 0.8.0 +""" +import typing +from urllib.parse import urljoin + +from lxml.objectify import ObjectifiedElement + +from ..utils.base import ExtensionBase + + +class Attribute(ExtensionBase): + """ + Access attribute namespaces and definitions + """ + base_path = "/attribute" + + def list_namespaces(self) -> typing.List[str]: + """ + Get a list of all namespaces + + :return: List of namespace names + """ + response = self.osc.request( + url=urljoin(self.osc.url, f"{self.base_path}/"), + method="GET" + ) + content = self.osc.get_objectified_xml(response) + return [entry.get("name") for entry in content.findall("entry")] + + def get_namespace_meta(self, namespace: str) -> ObjectifiedElement: + """ + Get the meta of the namespace + + :param namespace: namespace name + :return: Objectified XML element + """ + response = self.osc.request( + url=urljoin(self.osc.url, f"{self.base_path}/{namespace}/_meta"), + method="GET" + ) + return self.osc.get_objectified_xml(response) + + def list_attributes(self, namespace: str) -> typing.List[str]: + """ + List the attributes available in namespace + + :param namespace: Namespace name + :return: List of attribute names + """ + response = self.osc.request( + url=urljoin(self.osc.url, f"{self.base_path}/{namespace}"), + method="GET" + ) + content = self.osc.get_objectified_xml(response) + return [entry.get("name") for entry in content.findall("entry")] + + def get_attribute_meta(self, namespace: str, name: str) -> ObjectifiedElement: + """ + Get meta data for attribute + + :param namespace: Namespace name + :param name: Attribute name + :return: Objectified XML element + """ + response = self.osc.request( + url=urljoin(self.osc.url, f"{self.base_path}/{namespace}/{name}/_meta"), + method="GET" + ) + return self.osc.get_objectified_xml(response) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/extensions/buildresults.py new/osc-tiny-0.8.0/osctiny/extensions/buildresults.py --- old/osc-tiny-0.7.12/osctiny/extensions/buildresults.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/extensions/buildresults.py 2023-11-29 15:48:50.000000000 +0100 @@ -68,6 +68,28 @@ return self.osc.get_objectified_xml(response) + def get_log(self, project, repo, arch, package): + """ + Get the build log of a package + + :param project: Project name + :param repo: Repository name + :param arch: Architecture name + :param package: Package name + :return: The package build log file + :rtype: str + + .. versionadded:: 0.8.0 + """ + + response = self.osc.request( + method="GET", + url=urljoin(self.osc.url, "{}/{}/{}/{}/{}/_log".format(self.base_path, + project,repo,arch,package)) + ) + + return response.text + def get_package_list(self, project, repo, arch): """ Get a list of packages for which build results exist diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/extensions/issues.py new/osc-tiny-0.8.0/osctiny/extensions/issues.py --- old/osc-tiny-0.7.12/osctiny/extensions/issues.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/extensions/issues.py 2023-11-29 15:48:50.000000000 +0100 @@ -2,11 +2,11 @@ Issues extension ---------------- """ +from functools import lru_cache import os from urllib.parse import urljoin -from ..utils.backports import lru_cache from ..utils.base import ExtensionBase diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/extensions/origin.py new/osc-tiny-0.8.0/osctiny/extensions/origin.py --- old/osc-tiny-0.7.12/osctiny/extensions/origin.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/extensions/origin.py 2023-11-29 15:48:50.000000000 +0100 @@ -22,12 +22,13 @@ """ # pylint: disable=too-many-ancestors,ungrouped-imports from collections import defaultdict +from functools import lru_cache import re from warnings import warn from yaml import load -from ..utils.backports import lru_cache, cached_property +from ..utils.backports import cached_property from ..utils.base import ExtensionBase from ..utils.mapping import LazyOscMappable diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/extensions/projects.py new/osc-tiny-0.8.0/osctiny/extensions/projects.py --- old/osc-tiny-0.7.12/osctiny/extensions/projects.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/extensions/projects.py 2023-11-29 15:48:50.000000000 +0100 @@ -42,20 +42,26 @@ return self.osc.get_objectified_xml(response) - def get_meta(self, project): + def get_meta(self, project, rev=None): """ Get project metadata + + .. versionchanged:: 0.8.0 + Added the ``rev`` parameter :param project: name of project + :param rev: optional revision ID + :type rev: int :return: Objectified XML element :rtype: lxml.objectify.ObjectifiedElement """ response = self.osc.request( url=urljoin( self.osc.url, - "{}/{}/_meta".format(self.base_path, project) + "{}/{}/_project/_meta".format(self.base_path, project) ), - method="GET" + method="GET", + params={"rev": rev} if rev else None ) return self.osc.get_objectified_xml(response) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/osc.py new/osc-tiny-0.8.0/osctiny/osc.py --- old/osc-tiny-0.7.12/osctiny/osc.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/osc.py 2023-11-29 15:48:50.000000000 +0100 @@ -7,6 +7,7 @@ from base64 import b64encode import typing import errno +from http.cookiejar import CookieJar from io import BufferedReader, BytesIO, StringIO import gc import logging @@ -19,13 +20,12 @@ from urllib.parse import quote, parse_qs, urlparse import warnings -# pylint: disable=no-name-in-module -from lxml.objectify import fromstring, makeparser from requests import Session, Request from requests.auth import HTTPBasicAuth from requests.cookies import RequestsCookieJar, cookiejar_from_dict from requests.exceptions import ConnectionError as _ConnectionError +from .extensions.attributes import Attribute from .extensions.buildresults import Build from .extensions.comments import Comment from .extensions.distributions import Distribution @@ -38,13 +38,10 @@ from .extensions.users import Group, Person from .utils.auth import HttpSignatureAuth from .utils.backports import cached_property -from .utils.conf import BOOLEAN_PARAMS, get_credentials +from .utils.conf import BOOLEAN_PARAMS, get_credentials, get_cookie_jar from .utils.errors import OscError +from .utils.xml import get_xml_parser, get_objectified_xml -try: - from cachecontrol import CacheControl -except ImportError: - CacheControl = None THREAD_LOCAL = threading.local() @@ -88,13 +85,14 @@ - :py:attr:`distributions` * - :py:class:`osctiny.extensions.origin.Origin` - :py:attr:`origins` + * - :py:class:`osctiny.extensions.attributes.Attribute` + - :py:attr:`attributes` :param url: API URL of a BuildService instance :param username: Username :param password: Password; this is either the user password (``ssh_key_file`` is ``None``) or the SSH passphrase, if ``ssh_key_file`` is defined :param verify: See `SSL Cert Verification`_ for more details - :param cache: Store API responses in a cache :param ssh_key_file: Path to SSH private key file :raises osctiny.errors.OscError: if no credentials are provided @@ -120,6 +118,10 @@ Support for 2FA authentication (i.e. added the ``ssh_key_file`` parameter and changed the meaning of the ``password`` parameter + .. versionchanged:: 0.8.0 + * Removed the ``cache`` parameter + * Added the ``attributes`` extensions + .. _SSL Cert Verification: http://docs.python-requests.org/en/master/user/advanced/ #ssl-cert-verification @@ -133,14 +135,12 @@ def __init__(self, url: typing.Optional[str] = None, username: typing.Optional[str] = None, password: typing.Optional[str] = None, verify: typing.Optional[str] = None, - cache: bool = False, ssh_key_file: typing.Optional[typing.Union[Path, str]] = None): # Basic URL and authentication settings self.url = url or self.url self.username = username or self.username self.password = password or self.password self.verify = verify - self.cache = cache self.ssh_key = ssh_key_file if self.ssh_key is not None and not isinstance(self.ssh_key, Path): self.ssh_key = Path(self.ssh_key) @@ -152,6 +152,7 @@ raise OscError from error # API endpoints + self.attributes = Attribute(osc_obj=self) self.build = Build(osc_obj=self) self.comments = Comment(osc_obj=self) self.distributions = Distribution(osc_obj=self) @@ -174,7 +175,7 @@ return f"session_{session_hash}_{os.getpid()}_{threading.get_ident()}" @property - def _session(self) -> Session: + def session(self) -> Session: """ Session object """ @@ -183,6 +184,11 @@ session = Session() session.verify = self.verify or get_default_verify_paths().capath + cookies = get_cookie_jar() + if cookies is not None: + cookies.load() + session.cookies = cookies + if self.ssh_key is not None: session.auth = HttpSignatureAuth(username=self.username, password=self.password, ssh_key_file=self.ssh_key) @@ -194,49 +200,31 @@ return session @property - def session(self) -> typing.Union[CacheControl, Session]: - """ - Session object - - Possibly wrapped in CacheControl, if installed. - """ - if not self.cache or CacheControl is None: - return self._session - - key = f"cached_{self._session_id}" - session = getattr(THREAD_LOCAL, key, None) - if not session: - session = CacheControl(self._session) - setattr(THREAD_LOCAL, key, session) - - return session - - @property def cookies(self) -> RequestsCookieJar: """ Access session cookies """ - return self._session.cookies + return self.session.cookies @cookies.setter - def cookies(self, value: RequestsCookieJar): - if not isinstance(value, (RequestsCookieJar, dict)): + def cookies(self, value: typing.Union[CookieJar, dict]): + if not isinstance(value, (CookieJar, dict)): raise TypeError(f"Expected a cookie jar or dict. Got instead: {type(value)}") - if isinstance(value, RequestsCookieJar): - self._session.cookies = value + if isinstance(value, CookieJar): + self.session.cookies = value else: - self._session.cookies = cookiejar_from_dict(value) + self.session.cookies = cookiejar_from_dict(value) @property def parser(self): """ Explicit parser instance - """ - if not hasattr(THREAD_LOCAL, "parser"): - THREAD_LOCAL.parser = makeparser(huge_tree=True) - return THREAD_LOCAL.parser + .. versionchanged:: 0.8.0 + Content moved to :py:fun:`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): @@ -247,9 +235,6 @@ a dictionary and contains a key ``comment``, this value is passed on as a POST parameter. - If ``stream`` is True, the server response does not get cached because - the returned file might be large or huge. - if ``raise_for_status`` is True, the used ``requests`` framework will raise an exception for occured errors. @@ -292,21 +277,16 @@ """ timeout = timeout or self.default_timeout - if stream: - session = self._session - else: - session = self.session - req = Request( method, url.replace("#", quote("#")).replace("?", quote("?")), data=self.handle_params(url=url, method=method, params=data), params=self.handle_params(url=url, method=method, params=params) ) - prepped_req = session.prepare_request(req) + prepped_req = self.session.prepare_request(req) prepped_req.headers['Content-Type'] = "application/octet-stream" prepped_req.headers['Accept'] = "application/xml" - settings = session.merge_environment_settings( + settings = self.session.merge_environment_settings( prepped_req.url, {}, None, None, None ) settings["stream"] = stream @@ -327,7 +307,7 @@ else parse_qs(req.params, keep_blank_values=True) ).items())) try: - response = session.send(prepped_req, **settings) + response = self.session.send(prepped_req, **settings) except _ConnectionError as error: warnings.warn("Problem connecting to server: {}".format(error)) log_method = logger.error if i < 1 else logger.warning @@ -489,25 +469,8 @@ Allow ``response`` to be a string - :param response: An API response or XML string - :rtype response: :py:class:`requests.Response` - :return: :py:class:`lxml.objectify.ObjectifiedElement` - """ - if isinstance(response, str): - text = response - else: - text = response.text + .. versionchanged:: 0.8.0 - try: - return fromstring(text, self.parser) - except ValueError: - # Just in case OBS returns a Unicode string with encoding - # declaration - if isinstance(text, str) and \ - "encoding=" in text: - return fromstring( - re.sub(r'encoding="[^"]+"', "", text) - ) - - # This might be something else - raise + Content moved to :py:fun:`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.7.12/osctiny/tests/test_attributes.py new/osc-tiny-0.8.0/osctiny/tests/test_attributes.py --- old/osc-tiny-0.7.12/osctiny/tests/test_attributes.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/tests/test_attributes.py 2023-11-29 15:48:50.000000000 +0100 @@ -0,0 +1,52 @@ +import responses + +from .base import OscTest + + +class TestAttribute(OscTest): + def setUp(self): + super().setUp() + + self.mock_request( + method=responses.GET, + url=self.osc.url + '/attribute/', + body="<directory><entry name='Foo'/><entry name='Bar'/></directory>" + ) + + self.mock_request( + method=responses.GET, + url=self.osc.url + '/attribute/Foo/_meta', + body="<namespace name='Foo'><modifiable_by user='A'/></namespace>" + ) + + self.mock_request( + method=responses.GET, + url=self.osc.url + '/attribute/Foo', + body="<directory><entry name='Hello'/><entry name='World'/></directory>" + ) + + self.mock_request( + method=responses.GET, + url=self.osc.url + '/attribute/Foo/Hello/_meta', + body="<definition name='Hello' namespace='Foo'><description>Lorem ipsum</description>" + "<count>1</count><modifiable_by role='B'/></definition>" + ) + + @responses.activate + def test_list_namespace(self): + self.assertEqual(["Foo", "Bar"], self.osc.attributes.list_namespaces()) + + @responses.activate + def test_get_namespace_meta(self): + meta = self.osc.attributes.get_namespace_meta("Foo") + self.assertEqual(meta.get("name"), "Foo") + + @responses.activate + def test_list_attributes(self): + self.assertEqual(["Hello", "World"], self.osc.attributes.list_attributes("Foo")) + + @responses.activate + def test_get_attribute_meta(self): + meta = self.osc.attributes.get_attribute_meta("Foo", "Hello") + self.assertEqual(meta.get("name"), "Hello") + self.assertEqual(meta.get("namespace"), "Foo") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/tests/test_cache.py new/osc-tiny-0.8.0/osctiny/tests/test_cache.py --- old/osc-tiny-0.7.12/osctiny/tests/test_cache.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/tests/test_cache.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,24 +0,0 @@ -from unittest import skipUnless - -from .test_search import TestSearch -from osctiny import Osc - -try: - import cachecontrol -except ImportError: - with_cache = False -else: - with_cache = True - - -@skipUnless(with_cache, "No cache module present, therefore not testing") -class TestSearch(TestSearch): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.osc = Osc( - url="http://api.example.com", - username="foobar", - password="helloworld", - cache=True - ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/tests/test_projects.py new/osc-tiny-0.8.0/osctiny/tests/test_projects.py --- old/osc-tiny-0.7.12/osctiny/tests/test_projects.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/tests/test_projects.py 2023-11-29 15:48:50.000000000 +0100 @@ -93,6 +93,10 @@ </status> """ headers['request-id'] = '728d329e-0e86-11e4-a748-0c84dc037c13' + if "rev" in request.params: + revision = request.params["rev"] + body = body.replace('<project name="Devel:ARM:Factory">', + f'<project name="Devel:ARM:Factory:r{revision}">') return status, headers, body self.mock_request( @@ -111,6 +115,10 @@ self.assertRaises( HTTPError, self.osc.projects.get_meta, "Devel:ARM:Fbctory" ) + with self.subTest("existing project with revision"): + response = self.osc.projects.get_meta("Devel:ARM:Factory", rev=2) + self.assertEqual(response.tag, "project") + self.assertEqual(response.get("name"), "Devel:ARM:Factory:r2") @responses.activate def test_set_meta(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/tests/test_utils.py new/osc-tiny-0.8.0/osctiny/tests/test_utils.py --- old/osc-tiny-0.7.12/osctiny/tests/test_utils.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/tests/test_utils.py 2023-11-29 15:48:50.000000000 +0100 @@ -10,10 +10,11 @@ import sys from tempfile import mkstemp from types import GeneratorType +import warnings from dateutil.parser import parse from pytz import _UTC, timezone -from requests import Response +from requests import Response, HTTPError import responses from ..osc import Osc, THREAD_LOCAL @@ -21,6 +22,7 @@ from ..utils.changelog import ChangeLog, Entry from ..utils.conf import get_config_path, get_credentials from ..utils.mapping import Mappable +from ..utils.errors import get_http_error_details sys.path.append(os.path.dirname(__file__)) @@ -489,3 +491,65 @@ "Basic realm=\"Use your developer account\", "}) response = self.osc.session.get("https://api.example.com/hello-world") self.do_assertions(response, True) + + +class TestError(TestCase): + url = "http://example.com" + @property + def osc(self) -> Osc: + return Osc(url=self.url, username="nemo", password="password") + + @responses.activate + def test_get_http_error_details(self): + status = 400 + summary = "Bla Bla Bla" + responses.add( + responses.GET, + "http://example.com", + body=f"""<status code="foo"><summary>{summary}</summary></status>""", + status=status + ) + + response = self.osc.session.get(self.url) + + with self.subTest("Response"): + self.assertEqual(response.status_code, status) + self.assertEqual(get_http_error_details(response), summary) + + with self.subTest("Exception"): + try: + response.raise_for_status() + except HTTPError as error: + self.assertEqual(get_http_error_details(error), summary) + else: + self.fail("No exception was raised") + + @responses.activate + def test_get_http_error_details__bad_response(self): + status = 502 + responses.add( + responses.GET, + "http://example.com", + body=f"""Bad Gateway HTML message""", + status=status + ) + + response = self.osc.session.get(self.url) + + with self.subTest("Response"): + self.assertEqual(response.status_code, status) + with warnings.catch_warnings(record=True) as emitted_warnings: + self.assertIn("Server replied with:", get_http_error_details(response)) + self.assertEqual(len(emitted_warnings), 1) + self.assertIn("Start tag expected", str(emitted_warnings[-1].message)) + + with self.subTest("Exception"): + try: + response.raise_for_status() + except HTTPError as error: + with warnings.catch_warnings(record=True) as emitted_warnings: + self.assertIn("Server replied with:", get_http_error_details(error)) + self.assertEqual(len(emitted_warnings), 1) + self.assertIn("Start tag expected", str(emitted_warnings[-1].message)) + else: + self.fail("No exception was raised") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/utils/backports.py new/osc-tiny-0.8.0/osctiny/utils/backports.py --- old/osc-tiny-0.7.12/osctiny/utils/backports.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/utils/backports.py 2023-11-29 15:48:50.000000000 +0100 @@ -3,21 +3,11 @@ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 0.3.0 -""" -try: - # pylint: disable=unused-import - from functools import lru_cache -except ImportError: - # Whoever had the grandiose idea to backport this to Python2? - # pylint: disable=unused-argument - def lru_cache(*args, **kwargs): - """Dummy wrapper""" - def wrapper(fun): - return fun - - return wrapper +.. versionchanged:: 0.8.0 + Removed function ``lru_cache`` +""" try: # pylint: disable=unused-import from functools import cached_property diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/utils/conf.py new/osc-tiny-0.8.0/osctiny/utils/conf.py --- old/osc-tiny-0.7.12/osctiny/utils/conf.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/utils/conf.py 2023-11-29 15:48:50.000000000 +0100 @@ -12,6 +12,7 @@ from base64 import b64decode from bz2 import decompress from configparser import ConfigParser, NoSectionError +from http.cookiejar import LWPCookieJar import os from pathlib import Path @@ -189,3 +190,25 @@ raise ValueError(f"`osc` config provides no password or SSH key for URL {url}") return username, password if sshkey is None else None, sshkey + + +def get_cookie_jar() -> typing.Optional[LWPCookieJar]: + """ + Get cookies from a persistent osc cookiejar + + .. versionadded:: 0.8.0 + """ + if _conf is not None: + path = _conf._identify_osccookiejar() # pylint: disable=protected-access + if os.path.isfile(path): + return LWPCookieJar(filename=path) + + path_suffix = Path("osc", "cookiejar") + paths = [Path(os.getenv("XDG_STATE_HOME", "/tmp")).joinpath(path_suffix), + Path.home().joinpath(".local", "state").joinpath(path_suffix)] + + for path in paths: + if path.is_file(): + return LWPCookieJar(filename=str(path)) # compatibility for Python < 3.8 + + return None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/utils/errors.py new/osc-tiny-0.8.0/osctiny/utils/errors.py --- old/osc-tiny-0.7.12/osctiny/utils/errors.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/utils/errors.py 2023-11-29 15:48:50.000000000 +0100 @@ -1,10 +1,42 @@ """ -Base classes for osc-tiny specific exceptions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Exception base classes and utilities +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ """ +import typing +from warnings import warn + +from requests import HTTPError, Response + +from .xml import get_objectified_xml + + +def get_http_error_details(error: typing.Union[HTTPError, Response]) -> str: + """ + Extract user-friendly error message from exception + + .. versionadded:: 0.8.0 + """ + if isinstance(error, HTTPError): + response = error.response + elif isinstance(error, Response): + response = error + else: + raise TypeError("Expected a Response of HTTPError instance!") + + try: + xml_obj = get_objectified_xml(response) + except Exception as error2: + warn(message=f"Failed to extract error message due to another error: {error2}", + category=RuntimeWarning) + else: + summary = xml_obj.find("summary") + if summary is not None: + return summary.text + + return f"Server replied with: {response.status_code} {response.reason}" class OscError(Exception): """ - Base class for expcetions to be raised by ``osctiny`` + Base class for exceptions to be raised by ``osctiny`` """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/osctiny/utils/xml.py new/osc-tiny-0.8.0/osctiny/utils/xml.py --- old/osc-tiny-0.7.12/osctiny/utils/xml.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-tiny-0.8.0/osctiny/utils/xml.py 2023-11-29 15:48:50.000000000 +0100 @@ -0,0 +1,74 @@ +""" +XML parsing +^^^^^^^^^^^ + +.. versionadded:: 0.8.0 +""" +import re +import threading +import typing + +from lxml.etree import XMLParser +from lxml.objectify import fromstring, makeparser, ObjectifiedElement +from requests import Response + + +THREAD_LOCAL = threading.local() + + +def get_xml_parser() -> XMLParser: + """ + Get a parser object + + .. versionchanged:: 0.8.0 + + Carved out from the ``Osc`` class + """ + if not hasattr(THREAD_LOCAL, "parser"): + THREAD_LOCAL.parser = makeparser(huge_tree=True) + + return THREAD_LOCAL.parser + + +def get_objectified_xml(response: typing.Union[Response, str]) -> ObjectifiedElement: + """ + Return API response as an XML object + + .. versionchanged:: 0.1.6 + + Allow parsing of "huge" XML inputs + + .. versionchanged:: 0.2.4 + + Allow ``response`` to be a string + + .. versionchanged:: 0.8.0 + + Carved out from ``Osc`` class + + :param response: An API response or XML string + :rtype response: :py:class:`requests.Response` + :return: :py:class:`lxml.objectify.ObjectifiedElement` + """ + if isinstance(response, str): + text = response + elif isinstance(response, Response): + text = response.text + else: + raise TypeError(f"Expected a string or response object. Got {type(response)} instead.") + + parser = get_xml_parser() + + try: + return fromstring(text, parser) + except ValueError: + # Just in case OBS returns a Unicode string with encoding + # declaration + if isinstance(text, str) and \ + "encoding=" in text: + return fromstring( + re.sub(r'encoding="[^"]+"', "", text) + ) + + # This might be something else + raise diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-tiny-0.7.12/setup.py new/osc-tiny-0.8.0/setup.py --- old/osc-tiny-0.7.12/setup.py 2023-03-06 07:02:53.000000000 +0100 +++ new/osc-tiny-0.8.0/setup.py 2023-11-29 15:48:50.000000000 +0100 @@ -26,14 +26,15 @@ setup( name='osc-tiny', - version='0.7.12', + version='0.8.0', description='Client API for openSUSE BuildService', long_description=long_description, long_description_content_type="text/markdown", author='Andreas Hasenkopf', author_email='ahasenkopf@suse.com', - url='http://github.com/crazyscientist/osc-tiny', - download_url='http://github.com/crazyscientist/osc-tiny/tarball/master', + maintainer='SUSE Maintenance Automation Engineering team', + maintainer_email='maintenance-automation-team@suse.de', + url='https://github.com/SUSE/osc-tiny', packages=find_packages(), license='MIT', install_requires=get_requires(), @@ -49,5 +50,6 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] )
participants (1)
-
Source-Sync