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
+
+- 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'></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></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",
]
)