Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-rt for openSUSE:Factory 
checked in at 2021-04-19 21:06:50
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-rt (Old)
 and      /work/SRC/openSUSE:Factory/.python-rt.new.12324 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-rt"

Mon Apr 19 21:06:50 2021 rev:9 rq:886730 version:2.1.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-rt/python-rt.changes      2021-03-05 
13:50:36.587913086 +0100
+++ /work/SRC/openSUSE:Factory/.python-rt.new.12324/python-rt.changes   
2021-04-19 21:07:28.576162222 +0200
@@ -1,0 +2,7 @@
+Thu Apr  8 11:38:15 UTC 2021 - Sebastian Wagner <[email protected]>
+
+- Update to version 2.1.1:
+ - Fix support for custom field values containing newlines in API responses 
(#10, #11)
+   (the previous change in v1.0.11 fixed API requests) (#64)
+
+-------------------------------------------------------------------

Old:
----
  rt-2.1.0.tar.gz

New:
----
  rt-2.1.1.tar.gz

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

Other differences:
------------------
++++++ python-rt.spec ++++++
--- /var/tmp/diff_new_pack.sgRrAV/_old  2021-04-19 21:07:29.016162882 +0200
+++ /var/tmp/diff_new_pack.sgRrAV/_new  2021-04-19 21:07:29.020162888 +0200
@@ -19,7 +19,7 @@
 # Tests require internet connection
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-rt
-Version:        2.1.0
+Version:        2.1.1
 Release:        0
 Summary:        Python interface to Request Tracker API
 License:        GPL-3.0-only

++++++ rt-2.1.0.tar.gz -> rt-2.1.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rt-2.1.0/CHANGES new/rt-2.1.1/CHANGES
--- old/rt-2.1.0/CHANGES        2021-02-25 18:54:40.000000000 +0100
+++ new/rt-2.1.1/CHANGES        2021-04-08 13:12:05.000000000 +0200
@@ -1,6 +1,11 @@
-v2.1.0, 2020-02-25
-- Add the possibility to provide cookies as dict to authenticate
-- Add 'Referer' header for CSRF check
+v2.1.1, 2021-03-23
+- Fix support for custom field values containing newlines in API responses 
(#10, #11)
+  (the previous change in v1.0.11 fixed API requests) (#64)
+
+v2.1.0, 2021-02-25
+- Add the possibility to provide cookies as dict to authenticate (#60)
+- Add 'Referer' header for CSRF check when cookies are used for authentication 
(#60)
+- Add IS and IS NOT operators to search (#57)
 
 v2.0.1, 2020-08-07
 - Fix UnicodeDecodeError in logging code for non-text attachments (#50, #51)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rt-2.1.0/PKG-INFO new/rt-2.1.1/PKG-INFO
--- old/rt-2.1.0/PKG-INFO       2021-02-25 18:54:50.863699400 +0100
+++ new/rt-2.1.1/PKG-INFO       2021-04-08 13:12:15.110848200 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: rt
-Version: 2.1.0
+Version: 2.1.1
 Summary: Python interface to Request Tracker API
 Home-page: https://github.com/CZ-NIC/python-rt
 Author: Jiri Machalek
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rt-2.1.0/rt/rt.py new/rt-2.1.1/rt/rt.py
--- old/rt-2.1.0/rt/rt.py       2021-02-25 18:54:40.000000000 +0100
+++ new/rt-2.1.1/rt/rt.py       2021-04-08 13:12:05.000000000 +0200
@@ -96,6 +96,7 @@
         'queue_pattern': re.compile(r'^# Queue (\w*) (?:updated|created)\.$'),
         'ticket_created_pattern': re.compile(r'^# Ticket ([0-9]+) created\.$'),
         'does_not_exist_pattern': re.compile(r'^# (?:Queue|User|Ticket) \w* 
does not exist\.$'),
+        'status_pattern': re.compile(r'^\S+ (\d{3}) '),
         'does_not_exist_pattern_bytes': re.compile(br'^# (?:Queue|User|Ticket) 
\w* does not exist\.$'),
         'not_related_pattern': re.compile(r'^# Transaction \d+ is not related 
to Ticket \d+'),
         'invalid_attachment_pattern_bytes': re.compile(br'^# Invalid 
attachment id: \d+$'),
@@ -268,6 +269,110 @@
             msg = "".join(msg)
         return list(map(lambda x: x.strip(), msg.split(",")))
 
+    @classmethod
+    def __parse_response_dict(cls,
+                              msg: typing.Iterable[str],
+                              expect_keys: typing.Iterable[str]=(),
+    ) -> typing.Dict[str, str]:
+        """Parse an RT API response body into a Python dictionary
+
+        This method knows the general format for key-value RT API responses,
+        and parses them into a Python dictionary as plain string values.
+
+        :keyword msg: A multiline string, or an iterable of string lines, with
+          the RT API response body.
+        :keyword expect_keys: An iterable of strings. If any of these strings
+          do not appear in the response, raise an error.
+        :raises UnexpectedMessageFormat: The body did not follow the expected 
format,
+          or an expected key was missing
+        :returns: Dictionary mapping field names to value strings
+        :rtype: Dictionary with string keys and string values
+        """
+        if isinstance(msg, str):
+            msg = msg.split('\n')
+        fields = {}  # type: typing.Dict[str, typing.List[str]]
+        key = '<no key>'
+        for line in msg:
+            if (not line
+                or line.startswith('#')
+                or (not fields and 
cls.RE_PATTERNS['status_pattern'].match(line))
+            ):
+                key = '<no key>'
+            elif line[0].isspace():
+                try:
+                    fields[key].append(line.lstrip())
+                except KeyError:
+                    raise UnexpectedMessageFormat(
+                        "Response has a continuation line with no field to 
continue",
+                    ) from None
+            else:
+                if line.startswith('CF.{'):
+                    sep = '}: '
+                else:
+                    sep = ': '
+                key, sep, value = line.partition(sep)
+                if sep:
+                    key += sep[:-2]
+                    fields[key] = [value]
+                elif line.endswith(':'):
+                    key = line[:-1]
+                    fields[key] = []
+                else:
+                    raise UnexpectedMessageFormat(
+                        "Response has a line without a field name: 
{!r}".format(line),
+                    )
+        for key in expect_keys:
+            if key not in fields:
+                raise UnexpectedMessageFormat(
+                    "Missing line starting with `{}:`.".format(key),
+                )
+        return {key: '\n'.join(lines) for key, lines in fields.items() if 
lines}
+
+    @classmethod
+    def __parse_response_numlist(cls, msg: typing.Iterable[str],
+    ) -> typing.List[typing.Tuple[int, str]]:
+        """Parse an RT API response body into a numbered list
+
+        The RT API for transactions and attachments returns a numbered list of
+        items. This method returns 2-tuples to represent them, where the first
+        item is an integer id and the second items is a string description.
+
+        :keyword msg: A multiline string, or an iterable of string lines, with
+          the RT API response body.
+        :raises UnexpectedMessageFormat: The body did not follow the expected 
format
+        :returns: List of 2-tuples with ids and descriptions
+        :rtype: List of 2-tuples (int, str)
+        """
+        return sorted(
+            (int(key), value)
+            for key, value in cls.__parse_response_dict(msg).items()
+        )
+
+    @classmethod
+    def __parse_response_ticket(cls, msg: typing.Iterable[str]) -> 
typing.Dict[str, typing.Sequence[str]]:
+        """Parse an RT API ticket response into a Python dictionary
+
+        :keyword msg: A multiline string, or an iterable of string lines, with
+          the RT API response body.
+        :raises UnexpectedMessageFormat: The body did not follow the expected 
format
+        :returns: Dictionary mapping field names to value strings, or lists of
+          strings for the People fields Requestors, Cc, and AdminCc
+        :rtype: Dictionary with string keys and values that are strings or 
lists
+          of strings
+        """
+        pairs = cls.__parse_response_dict(msg, ['Requestors'])
+        if not pairs.get('id', '').startswith('ticket/'):
+            raise UnexpectedMessageFormat('Response from RT didn\'t contain a 
valid ticket_id')
+        _, _, numerical_id = pairs['id'].partition('/')
+        ticket = typing.cast(typing.Dict[str, typing.Sequence[str]], pairs)
+        ticket['numerical_id'] = numerical_id
+        for key in ['Requestors', 'Cc', 'AdminCc']:
+            try:
+                ticket[key] = cls.__normalize_list(ticket[key])
+            except KeyError:
+                pass
+        return ticket
+
     def login(self, login: typing.Optional[str] = None, password: 
typing.Optional[str] = None) -> bool:
         """ Login with default or supplied credetials.
 
@@ -450,51 +555,15 @@
                 return []
 
         if Format == 'l':
-            msgs = map(lambda x: x.split('\n'), msg.split('\n--\n'))
-            items = []
-            for msg in msgs:
-                pairs = {}
-                req_matching = [i for i, m in enumerate(msg) if 
self.RE_PATTERNS['requestors_pattern'].match(m)]
-                req_id = req_matching[0] if req_matching else None
-                if not req_id:
-                    raise UnexpectedMessageFormat('Missing line starting with 
`Requestors:`.')
-                for i in range(req_id):
-                    if ': ' in msg[i]:
-                        header, content = self.split_header(msg[i])
-                        pairs[header.strip()] = content.strip()
-                requestors = [msg[req_id][12:]]
-                req_id += 1
-                while (req_id < len(msg)) and (msg[req_id][:12] == ' ' * 12):
-                    requestors.append(msg[req_id][12:])
-                    req_id += 1
-                pairs['Requestors'] = self.__normalize_list(requestors)
-                for i in range(req_id, len(msg)):
-                    if ': ' in msg[i]:
-                        header, content = self.split_header(msg[i])
-                        pairs[header.strip()] = content.strip()
-                if pairs:
-                    items.append(pairs)
-
-                if 'Cc' in pairs:
-                    pairs['Cc'] = self.__normalize_list(pairs['Cc'])
-                if 'AdminCc' in pairs:
-                    pairs['AdminCc'] = self.__normalize_list(pairs['AdminCc'])
-
-                if 'id' not in pairs and not pairs['id'].startswith('ticket/'):
-                    raise UnexpectedMessageFormat('Response from RT didn\'t 
contain a valid ticket_id')
-
-                pairs['numerical_id'] = pairs['id'].split('ticket/')[1]
-
-            return items
+            return [
+                self.__parse_response_ticket(ticket_msg)
+                for ticket_msg in msg.split('\n--\n')
+            ]
         if Format == 's':
-            items = []
-            msgs = lines[2:]
-            for msg in msgs:
-                if msg == '':  # Ignore blank line at the end
-                    continue
-                ticket_id, subject = self.split_header(msg)
-                items.append({'id': 'ticket/' + ticket_id, 'numerical_id': 
ticket_id, 'Subject': subject})
-            return items
+            return [
+                {'id': 'ticket/' + key, 'numerical_id': key, 'Subject': value}
+                for key, value in self.__parse_response_dict(msg).items()
+            ]
         if Format == 'i':
             items = []
             msgs = lines[2:]
@@ -542,40 +611,15 @@
         msg = self.__request('ticket/{}/show'.format(str(ticket_id), ))
         status_code = self.__get_status_code(msg)
         if status_code is not None and status_code == 200:
-            pairs = {}
             msg = msg.split('\n')
-            if (len(msg) > 2) and 
self.RE_PATTERNS['does_not_exist_pattern'].match(msg[2]):
+            try:
+                not_found = 
self.RE_PATTERNS['does_not_exist_pattern'].match(msg[2])
+            except IndexError:
+                not_found = None
+            if not_found:
                 return None
-            req_matching = [i for i, m in enumerate(msg) if 
self.RE_PATTERNS['requestors_pattern'].match(m)]
-            req_id = req_matching[0] if req_matching else None
-            if not req_id:
-                raise UnexpectedMessageFormat('Missing line starting with 
`Requestors:`.')
-            for i in range(req_id):
-                if ': ' in msg[i]:
-                    header, content = self.split_header(msg[i])
-                    pairs[header.strip()] = content.strip()
-            requestors = [msg[req_id][12:]]
-            req_id += 1
-            while (req_id < len(msg)) and (msg[req_id][:12] == ' ' * 12):
-                requestors.append(msg[req_id][12:])
-                req_id += 1
-            pairs['Requestors'] = self.__normalize_list(requestors)
-            for i in range(req_id, len(msg)):
-                if ': ' in msg[i]:
-                    header, content = self.split_header(msg[i])
-                    pairs[header.strip()] = content.strip()
-
-            if 'Cc' in pairs:
-                pairs['Cc'] = self.__normalize_list(pairs['Cc'])
-            if 'AdminCc' in pairs:
-                pairs['AdminCc'] = self.__normalize_list(pairs['AdminCc'])
-
-            if 'id' not in pairs and not pairs['id'].startswith('ticket/'):
-                raise UnexpectedMessageFormat('Response from RT didn\'t 
contain a valid ticket_id')
-
-            pairs['numerical_id'] = pairs['id'].split('ticket/')[1]
-
-            return pairs
+            else:
+                return self.__parse_response_ticket(msg)
 
         raise UnexpectedMessageFormat('Received status code is {} instead of 
200.'.format(status_code))
 
@@ -719,43 +763,14 @@
                 self.RE_PATTERNS['does_not_exist_pattern'].match(lines[2]) or 
self.RE_PATTERNS['not_related_pattern'].match(
                 lines[2])):
             return None
-        msgs = msgs.split('\n--\n')
-        items = []
-        for msg in msgs:
-            pairs = {}  # type: dict
-            msg = msg.split('\n')
-            cont_matching = [i for i, m in enumerate(msg) if 
self.RE_PATTERNS['content_pattern'].match(m)]
-            cont_id = cont_matching[0] if cont_matching else None
-            if not cont_id:
-                raise UnexpectedMessageFormat('Unexpected history entry. \
-                                               Missing line starting with 
`Content:`.')
-            atta_matching = [i for i, m in enumerate(msg) if 
self.RE_PATTERNS['attachments_pattern'].match(m)]
-            atta_id = atta_matching[0] if atta_matching else None
-            if not atta_id:
-                raise UnexpectedMessageFormat('Unexpected attachment part of 
history entry. \
-                                               Missing line starting with 
`Attachements:`.')
-            for i in range(cont_id):
-                if ': ' in msg[i]:
-                    header, content = self.split_header(msg[i])
-                    pairs[header.strip()] = content.strip()
-            content = msg[cont_id][9:]
-            cont_id += 1
-            while (cont_id < len(msg)) and (msg[cont_id][:9] == ' ' * 9):
-                content += '\n' + msg[cont_id][9:]
-                cont_id += 1
-            pairs['Content'] = content
-            for i in range(cont_id, atta_id):
-                if ': ' in msg[i]:
-                    header, content = self.split_header(msg[i])
-                    pairs[header.strip()] = content.strip()
-            attachments = []
-            for i in range(atta_id + 1, len(msg)):
-                if ': ' in msg[i]:
-                    header, content = self.split_header(msg[i])
-                    attachments.append((int(header),
-                                        content.strip()))
-            pairs['Attachments'] = attachments
-            items.append(pairs)
+        items = typing.cast(
+            typing.List[typing.Dict[str, typing.Union[str, 
typing.List[typing.Tuple[int, str]]]]],
+            [self.__parse_response_dict(msg, ['Content', 'Attachments'])
+             for msg in msgs.split('\n--\n')],
+        )
+        for body in items:
+            attachments = typing.cast(str, body.get('Attachments', ''))
+            body['Attachments'] = self.__parse_response_numlist(attachments)
         return items
 
     def get_short_history(self, ticket_id: typing.Union[str, int]) -> 
typing.Optional[typing.List[typing.Tuple[int, str]]]:
@@ -767,32 +782,12 @@
                   Returns None if ticket does not exist.
         """
         msg = self.__request('ticket/{}/history'.format(str(ticket_id), ))
-        items = []
         lines = msg.split('\n')
-        multiline_buffer = ""
-        in_multiline = False
         if self.__get_status_code(lines[0]) == 200:
             if (len(lines) > 2) and 
self.RE_PATTERNS['does_not_exist_pattern'].match(lines[2]):
                 return None
-            if len(lines) >= 4:
-                for line in lines[4:]:
-                    if line == "":
-                        if not in_multiline:
-                            # start of multiline block
-                            in_multiline = True
-                        else:
-                            # end of multiline block
-                            line = multiline_buffer
-                            multiline_buffer = ""
-                            in_multiline = False
-                    else:
-                        if in_multiline:
-                            multiline_buffer += line
-                            line = ""
-                    if ': ' in line:
-                        hist_id, desc = line.split(': ', 1)
-                        items.append((int(hist_id), desc))
-        return items
+            return self.__parse_response_numlist(lines)
+        return []
 
     def __correspond(self, ticket_id: typing.Union[str, int], text: str = '', 
action: str = 'correspond', cc: str = '', bcc: str = '',
                      content_type: str = 'text/plain', files: 
typing.Optional[typing.List[typing.Tuple[str, typing.IO, 
typing.Optional[str]]]] = None):
@@ -1077,17 +1072,12 @@
         :raises UnexpectedMessageFormat: In case that returned status code is 
not 200
         """
         msg = self.__request('user/{}'.format(str(user_id), ))
-        status_code = self.__get_status_code(msg)
+        lines = msg.split('\n')
+        status_code = self.__get_status_code(lines[0])
         if status_code is not None and status_code == 200:
-            pairs = {}
-            lines = msg.split('\n')
             if (len(lines) > 2) and 
self.RE_PATTERNS['does_not_exist_pattern'].match(lines[2]):
                 return None
-            for line in lines[2:]:
-                if ': ' in line:
-                    header, content = line.split(': ', 1)
-                    pairs[header.strip()] = content.strip()
-            return pairs
+            return self.__parse_response_dict(lines)
 
         raise UnexpectedMessageFormat('Received status code is {} instead of 
200.'.format(status_code))
 
@@ -1189,17 +1179,12 @@
         :raises UnexpectedMessageFormat: In case that returned status code is 
not 200
         """
         msg = self.__request('queue/{}'.format(str(queue_id)))
-        status_code = self.__get_status_code(msg)
+        lines = msg.split('\n')
+        status_code = self.__get_status_code(lines[0])
         if status_code is not None and status_code == 200:
-            pairs = {}
-            lines = msg.split('\n')
             if (len(lines) > 2) and 
self.RE_PATTERNS['does_not_exist_pattern'].match(lines[2]):
                 return None
-            for line in lines[2:]:
-                if ': ' in line:
-                    header, content = line.split(': ', 1)
-                    pairs[header.strip()] = content.strip()
-            return pairs
+            return self.__parse_response_dict(lines)
 
         raise UnexpectedMessageFormat('Received status code is {} instead of 
200.'.format(status_code))
 
@@ -1272,29 +1257,14 @@
         :raises UnexpectedMessageFormat: In case that returned status code is 
not 200
         """
         msg = self.__request('ticket/{}/links/show'.format(str(ticket_id), ))
-
-        status_code = self.__get_status_code(msg)
+        lines = msg.split('\n')
+        status_code = self.__get_status_code(lines[0])
         if status_code is not None and status_code == 200:
-            pairs = {}
-            msg = msg.split('\n')
-            if (len(msg) > 2) and 
self.RE_PATTERNS['does_not_exist_pattern'].match(msg[2]):
+            if (len(msg) > 2) and 
self.RE_PATTERNS['does_not_exist_pattern'].match(lines[2]):
                 return None
-            i = 2
-            while i < len(msg):
-                if ': ' in msg[i]:
-                    key, link = self.split_header(msg[i])
-                    links = [link.strip()]
-                    j = i + 1
-                    pad = len(key) + 2
-                    # loop over next lines for the same key
-                    while (j < len(msg)) and msg[j].startswith(' ' * pad):
-                        links[-1] = links[-1][:-1]  # remove trailing comma 
from previous item
-                        links.append(msg[j][pad:].strip())
-                        j += 1
-                    pairs[key] = links
-                    i = j - 1
-                i += 1
-            return pairs
+            pairs = self.__parse_response_dict(lines)
+            return {key: [link.rstrip(',') for link in links.split()]
+                    for key, links in pairs.items()}
 
         raise UnexpectedMessageFormat('Received status code is {} instead of 
200.'.format(status_code))
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rt-2.1.0/rt.egg-info/PKG-INFO 
new/rt-2.1.1/rt.egg-info/PKG-INFO
--- old/rt-2.1.0/rt.egg-info/PKG-INFO   2021-02-25 18:54:50.000000000 +0100
+++ new/rt-2.1.1/rt.egg-info/PKG-INFO   2021-04-08 13:12:14.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: rt
-Version: 2.1.0
+Version: 2.1.1
 Summary: Python interface to Request Tracker API
 Home-page: https://github.com/CZ-NIC/python-rt
 Author: Jiri Machalek
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rt-2.1.0/setup.py new/rt-2.1.1/setup.py
--- old/rt-2.1.0/setup.py       2021-02-25 18:54:40.000000000 +0100
+++ new/rt-2.1.1/setup.py       2021-04-08 13:12:05.000000000 +0200
@@ -6,7 +6,7 @@
 README = open(os.path.join(here, 'README.rst')).read()
 
 setup(name='rt',
-      version='2.1.0',
+      version='2.1.1',
       description='Python interface to Request Tracker API',
       long_description=README,
       license='GNU General Public License (GPL)',

Reply via email to