Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-osc-tiny for openSUSE:Factory
checked in at 2022-07-03 18:26:52
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-osc-tiny (Old)
and /work/SRC/openSUSE:Factory/.python-osc-tiny.new.1548 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-osc-tiny"
Sun Jul 3 18:26:52 2022 rev:15 rq:986371 version:0.6.2
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-osc-tiny/python-osc-tiny.changes
2022-06-18 22:06:29.843682325 +0200
+++
/work/SRC/openSUSE:Factory/.python-osc-tiny.new.1548/python-osc-tiny.changes
2022-07-03 18:26:53.320735019 +0200
@@ -1,0 +2,9 @@
+Thu Jun 30 08:48:29 UTC 2022 - Andreas Hasenkopf <[email protected]>
+
+- Release 0.6.2
+ * Added `cmd` method to `Build` extension
+ * Fixes for sessions and authentication:
+ * Use thread-safe sessions and support huge trees again
+ * Support for server returning multiple `WWW-Authenticate` headers
+
+-------------------------------------------------------------------
Old:
----
osc-tiny-0.6.1.tar.gz
New:
----
osc-tiny-0.6.2.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-osc-tiny.spec ++++++
--- /var/tmp/diff_new_pack.fePfgJ/_old 2022-07-03 18:26:53.732735627 +0200
+++ /var/tmp/diff_new_pack.fePfgJ/_new 2022-07-03 18:26:53.732735627 +0200
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%define skip_python2 1
Name: python-osc-tiny
-Version: 0.6.1
+Version: 0.6.2
Release: 0
Summary: Client API for openSUSE BuildService
License: MIT
++++++ osc-tiny-0.6.1.tar.gz -> osc-tiny-0.6.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/osc-tiny-0.6.1/PKG-INFO new/osc-tiny-0.6.2/PKG-INFO
--- old/osc-tiny-0.6.1/PKG-INFO 2022-06-17 11:47:36.354644500 +0200
+++ new/osc-tiny-0.6.2/PKG-INFO 2022-06-30 10:46:34.485521000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: osc-tiny
-Version: 0.6.1
+Version: 0.6.2
Summary: Client API for openSUSE BuildService
Home-page: http://github.com/crazyscientist/osc-tiny
Download-URL: http://github.com/crazyscientist/osc-tiny/tarball/master
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/osc-tiny-0.6.1/osc_tiny.egg-info/PKG-INFO
new/osc-tiny-0.6.2/osc_tiny.egg-info/PKG-INFO
--- old/osc-tiny-0.6.1/osc_tiny.egg-info/PKG-INFO 2022-06-17
11:47:35.000000000 +0200
+++ new/osc-tiny-0.6.2/osc_tiny.egg-info/PKG-INFO 2022-06-30
10:46:33.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: osc-tiny
-Version: 0.6.1
+Version: 0.6.2
Summary: Client API for openSUSE BuildService
Home-page: http://github.com/crazyscientist/osc-tiny
Download-URL: http://github.com/crazyscientist/osc-tiny/tarball/master
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/osc-tiny-0.6.1/osctiny/__init__.py
new/osc-tiny-0.6.2/osctiny/__init__.py
--- old/osc-tiny-0.6.1/osctiny/__init__.py 2022-06-17 11:47:24.000000000
+0200
+++ new/osc-tiny-0.6.2/osctiny/__init__.py 2022-06-30 10:46:21.000000000
+0200
@@ -6,4 +6,4 @@
__all__ = ['Osc', 'bs_requests', 'buildresults', 'comments', 'packages',
'projects', 'search', 'users']
-__version__ = "0.6.1"
+__version__ = "0.6.2"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/osc-tiny-0.6.1/osctiny/extensions/buildresults.py
new/osc-tiny-0.6.2/osctiny/extensions/buildresults.py
--- old/osc-tiny-0.6.1/osctiny/extensions/buildresults.py 2022-06-17
11:47:24.000000000 +0200
+++ new/osc-tiny-0.6.2/osctiny/extensions/buildresults.py 2022-06-30
10:46:21.000000000 +0200
@@ -137,3 +137,26 @@
)
return response.text
+
+ def cmd(self, project, cmd, **params):
+ """
+ Execute ``cmd`` for ``project`` and get response
+
+ .. versionadded:: 0.6.2
+
+ :param str project: Project name
+ :param str cmd: Command to execute
+ :param params: Additional parameters
+ """
+ allowed_cmds = ["rebuild", "abortbuild", "restartbuild", "unpublish",
"sendsysrq",
+ "wipe"]
+ if cmd not in allowed_cmds:
+ raise ValueError(f"Invalid command: '{cmd}'. Use one of: {',
'.join(allowed_cmds)}")
+
+ params["cmd"] = cmd
+ response = self.osc.request(
+ url=urljoin(self.osc.url, f"{self.base_path}/{project}"),
+ method="POST",
+ params=params
+ )
+ return self.osc.get_objectified_xml(response)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/osc-tiny-0.6.1/osctiny/osc.py
new/osc-tiny-0.6.2/osctiny/osc.py
--- old/osc-tiny-0.6.1/osctiny/osc.py 2022-06-17 11:47:24.000000000 +0200
+++ new/osc-tiny-0.6.2/osctiny/osc.py 2022-06-30 10:46:21.000000000 +0200
@@ -4,6 +4,7 @@
"""
from __future__ import unicode_literals
+from base64 import b64encode
import typing
from io import BufferedReader, BytesIO, StringIO
import gc
@@ -12,11 +13,12 @@
import re
from ssl import get_default_verify_paths
import time
+import threading
from urllib.parse import quote
import warnings
# pylint: disable=no-name-in-module
-from lxml.objectify import fromstring
+from lxml.objectify import fromstring, makeparser
from requests import Session, Request
from requests.auth import HTTPBasicAuth
from requests.cookies import RequestsCookieJar, cookiejar_from_dict
@@ -41,6 +43,8 @@
except ImportError:
CacheControl = None
+THREAD_LOCAL = threading.local()
+
# pylint: disable=too-many-instance-attributes,too-many-arguments
# pylint: disable=too-many-locals
@@ -120,8 +124,6 @@
url = 'https://api.opensuse.org'
username = ''
password = ''
- session = None
- _registered = {}
default_timeout = (60, 300)
default_connection_retries = 5
default_retry_timeout = 5
@@ -159,59 +161,82 @@
self.search = Search(osc_obj=self)
self.users = Person(osc_obj=self)
- self._session, self.session = None, None
+ hash_value =
b64encode(f'{self.username}@{self.url}@{self.ssh_key}'.encode())
+ self._session_id = f"session_{hash_value}"
def __del__(self):
# Just in case ;-)
gc.collect()
@property
+ def _session(self) -> Session:
+ """
+ Session object
+ """
+ session = getattr(THREAD_LOCAL, self._session_id, None)
+ if not session:
+ session = Session()
+ session.verify = self.verify or get_default_verify_paths().capath
+
+ if self.ssh_key is not None:
+ session.auth = HttpSignatureAuth(username=self.username,
password=self.password,
+ ssh_key_file=self.ssh_key)
+ else:
+ session.auth = HTTPBasicAuth(self.username, self.password)
+
+ setattr(THREAD_LOCAL, self._session_id, session)
+
+ return session
+
+ @property
+ def session(self) -> typing.Union[CacheControl, Session]:
+ """
+ Session object
+
+ Possibly wrapped in CacheControl, if installed.
+ """
+ key = f"cached_{self._session_id}"
+ session = getattr(THREAD_LOCAL, key, None)
+ if not session:
+ if self.cache:
+ # pylint: disable=broad-except
+ try:
+ session = CacheControl(self._session)
+ except Exception as error:
+ session = self._session
+ warnings.warn("Cannot use the cache: {}".format(error),
RuntimeWarning)
+ else:
+ session = self._session
+ setattr(THREAD_LOCAL, key, session)
+
+ return session
+
+ @property
def cookies(self) -> RequestsCookieJar:
"""
Access session cookies
"""
- if self._session is None:
- self._init_session()
-
- return self.session.cookies
+ return self._session.cookies
@cookies.setter
def cookies(self, value: RequestsCookieJar):
if not isinstance(value, (RequestsCookieJar, dict)):
raise TypeError(f"Expected a cookie jar or dict. Got instead:
{type(value)}")
- if self._session is None:
- self._init_session()
-
if isinstance(value, RequestsCookieJar):
self._session.cookies = value
else:
self._session.cookies = cookiejar_from_dict(value)
- def _init_session(self):
+ @property
+ def parser(self):
"""
- Lazy session initialization
+ Explicit parser instance
"""
- self._session = Session()
- self._session.verify = self.verify or get_default_verify_paths().capath
-
- if self.ssh_key is not None:
- self._session.auth = HttpSignatureAuth(username=self.username,
password=self.password,
- ssh_key_file=self.ssh_key)
- else:
- self._session.auth = HTTPBasicAuth(self.username, self.password)
+ if not hasattr(THREAD_LOCAL, "parser"):
+ THREAD_LOCAL.parser = makeparser(huge_tree=True)
- # Cache
- if self.cache:
- # pylint: disable=broad-except
- try:
- self.session = CacheControl(self._session)
- except Exception as error:
- self.session = self._session
- warnings.warn("Cannot use the cache: {}".format(error),
- RuntimeWarning)
- else:
- self.session = self._session
+ return THREAD_LOCAL.parser
def request(self, url, method="GET", stream=False, data=None, params=None,
raise_for_status=True, timeout=None):
@@ -266,8 +291,6 @@
https://2.python-requests.org/en/master/user/advanced/#timeouts
"""
timeout = timeout or self.default_timeout
- if self._session is None:
- self._init_session()
if stream:
session = self._session
@@ -380,7 +403,7 @@
text = response.text
try:
- return fromstring(text)
+ return fromstring(text, self.parser)
except ValueError:
# Just in case OBS returns a Unicode string with encoding
# declaration
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/osc-tiny-0.6.1/osctiny/tests/test_utils.py
new/osc-tiny-0.6.2/osctiny/tests/test_utils.py
--- old/osc-tiny-0.6.1/osctiny/tests/test_utils.py 2022-06-17
11:47:24.000000000 +0200
+++ new/osc-tiny-0.6.2/osctiny/tests/test_utils.py 2022-06-30
10:46:21.000000000 +0200
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+import re
from base64 import b64encode
from bz2 import compress
from unittest import TestCase, mock
@@ -12,7 +13,10 @@
from dateutil.parser import parse
from pytz import _UTC, timezone
+from requests import Response
+import responses
+from ..osc import Osc
from ..utils.changelog import ChangeLog, Entry
from ..utils.conf import get_config_path, get_credentials
from ..utils.mapping import Mappable
@@ -388,3 +392,79 @@
finally:
os.remove(path1)
os.remove(path2)
+
+
[email protected]("osctiny.utils.auth.time", return_value=123456)
+class TestAuth(TestCase):
+ def setUp(self):
+ super().setUp()
+ mocked_path = mock.MagicMock(spec=Path)
+ mocked_path.configure_mock(**{"is_file.return_value": True})
+ self.osc = Osc("https://api.example.com", "nemo", "password",
ssh_key_file=mocked_path)
+ self.osc.session.auth.ssh_sign = lambda *args, **kwargs: "Hello World"
+
+ def setup_response(self, headers: dict):
+ responses.reset()
+ responses.add(
+ responses.GET,
+ re.compile("https?://.*"),
+ adding_headers=headers,
+ body="Bla bla",
+ status=401
+ )
+
+ def do_assertions(self, response: Response, expected_challenge: bool):
+ self.assertEqual(401, response.status_code)
+ if expected_challenge:
+ self.assertEqual(
+ {'realm': 'Use your developer account', 'headers':
['created'], 'created': 123456},
+ self.osc.session.auth._thread_local.chal
+ )
+ else:
+ self.assertEqual(0, len(self.osc.session.auth._thread_local.chal))
+
+ @responses.activate
+ def test_handle_401(self, *_):
+ with self.subTest("No WWW-Authenticate header"):
+ self.setup_response({"Foo": "Bar"})
+ response =
self.osc.session.get("https://api.example.com/hello-world")
+ self.do_assertions(response, False)
+
+ with self.subTest("WWW-Authenticate: Only Basic"):
+ self.setup_response({"www-authenticate": "Basic realm=\"Use your
developer account\""})
+ response =
self.osc.session.get("https://api.example.com/hello-world")
+ self.do_assertions(response, False)
+
+ with self.subTest("WWW-Authenticate: Only Signature"):
+ self.setup_response({"www-authenticate":
+ "Signature realm=\"Use your developer
account\","
+ "headers=\"(created)\""})
+ response =
self.osc.session.get("https://api.example.com/hello-world")
+ self.do_assertions(response, True)
+
+ responses.reset()
+ responses.add(
+ responses.GET,
+ re.compile("https?://.*"),
+ adding_headers={"www-authenticate": "Basic realm=\"Use your
developer account\", "
+ "Signature realm=\"Use your
developer account\","
+ "headers=\"(created)\""},
+ body="Bla bla",
+ status=401
+ )
+
+ with self.subTest("WWW-Authenticate: Basic & Signature"):
+ self.setup_response({"www-authenticate":
+ "Basic realm=\"Use your developer
account\", "
+ "Signature realm=\"Use your developer
account\","
+ "headers=\"(created)\""})
+ response =
self.osc.session.get("https://api.example.com/hello-world")
+ self.do_assertions(response, True)
+
+ with self.subTest("WWW-Authenticate: Signature & Basic"):
+ self.setup_response({"www-authenticate":
+ "Signature realm=\"Use your developer
account\","
+ "headers=\"(created)\", "
+ "Basic realm=\"Use your developer
account\", "})
+ response =
self.osc.session.get("https://api.example.com/hello-world")
+ self.do_assertions(response, True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/osc-tiny-0.6.1/osctiny/utils/auth.py
new/osc-tiny-0.6.2/osctiny/utils/auth.py
--- old/osc-tiny-0.6.1/osctiny/utils/auth.py 2022-06-17 11:47:24.000000000
+0200
+++ new/osc-tiny-0.6.2/osctiny/utils/auth.py 2022-06-30 10:46:21.000000000
+0200
@@ -89,6 +89,31 @@
return f'Signature
keyId="{self.username}",algorithm="ssh",signature={self.ssh_sign()},' \
f'headers="{headers}",created={self._thread_local.chal["created"]}'
+ def get_auth_header(self, r: Response) -> str:
+ """
+ Extract the relevant header for Signature authentication
+
+ :param r: Response
+ :return: Header text
+ """
+ try:
+ # pylint: disable=protected-access
+ headers = [header
+ for header in
r.raw._original_response.headers.get_all("www-authenticate")
+ if "signature" in header.lower()]
+ if headers:
+ return headers[0]
+ except AttributeError:
+ headers = r.headers.get("www-authenticate")
+ if headers:
+ parts = headers.split(",")
+ start = [p for p in parts if "signature" in p.lower()]
+ if start:
+ start_index = parts.index(start[0])
+ return ",".join(parts[start_index:start_index + 2]).strip()
+
+ return ""
+
def handle_401(self, r: Response, **kwargs) -> Response:
"""
Handle authentication in case of 401
@@ -103,9 +128,9 @@
# Rewind the file position indicator of the body to where
# it was to resend the request.
r.request.body.seek(self._thread_local.pos)
- s_auth = r.headers.get('www-authenticate', '')
+ s_auth = self.get_auth_header(r)
- if s_auth.lower().startswith("signature") and
self._thread_local.num_401_calls < 2:
+ if "signature" in s_auth.lower() and self._thread_local.num_401_calls
< 2:
self._thread_local.num_401_calls += 1
_, challenge = s_auth.split(" ", maxsplit=1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/osc-tiny-0.6.1/setup.py new/osc-tiny-0.6.2/setup.py
--- old/osc-tiny-0.6.1/setup.py 2022-06-17 11:47:24.000000000 +0200
+++ new/osc-tiny-0.6.2/setup.py 2022-06-30 10:46:21.000000000 +0200
@@ -19,7 +19,7 @@
setup(
name='osc-tiny',
- version='0.6.1',
+ version='0.6.2',
description='Client API for openSUSE BuildService',
long_description=long_description,
long_description_content_type="text/markdown",