Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-python-redmine for openSUSE:Factory checked in at 2024-04-08 17:39:54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-python-redmine (Old) and /work/SRC/openSUSE:Factory/.python-python-redmine.new.1905 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-python-redmine" Mon Apr 8 17:39:54 2024 rev:9 rq:1166107 version:2.5.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-python-redmine/python-python-redmine.changes 2024-03-04 21:25:13.306582259 +0100 +++ /work/SRC/openSUSE:Factory/.python-python-redmine.new.1905/python-python-redmine.changes 2024-04-08 17:52:09.761443408 +0200 @@ -1,0 +2,54 @@ +Sun Apr 7 19:14:48 UTC 2024 - Martin Hauke <mar...@gmx.de> + +- Update to version 2.5.0 + Deprecations: + * Requests version required >= 2.31.0 + New Features: + * Pro Edition: RedmineUP Products plugin support + * Issue copying (see docs for details) + Improvements: + * dir(resource) and list(resource) now also show properties of + an object. + * Support for issues_assigned and issues_authored relations in + User object + * Original filename will be used as a filename for all uploaded + files if a path was provided and filename wasn't set. + * Pro Edition: Added support for RedmineUP Contact avatar + add/update operations (see docs for details). + * Pro Edition: Added support for RedmineUP DealCategory create(), + update(), delete() operations (see docs for details). + * Pro Edition: RedmineUP CrmQuery resource now supports + invoices and expenses relation attributes. + * PerformanceWarning will be issued when Python-Redmine does some + unnecessary redirects before the actual request is made. + Changes: + * Backwards Incompatible: API key is now being sent in the + X-Redmine-API-Key header instead of the key GET parameter which + makes things more secure in case of a failed connection, but + it might created issues for servers that don't do custom + request header forwarding by default, so be sure to check your + web server before upgrading (Issue #328 and Issue #330). + * Backwards Incompatible: User all operation now really returns + all users, i.e. not only active, but locked, registered and + anonymous as well instead of only returning just active users + in previous versions due to the respect to Redmine's standard + behaviour. + Bugfixes: + * Tests were failing on Python 3.12 (Issue #332). + * Some closed Issues weren't converted to Resource objects + using redmine.search(). + * Pro Edition: RedmineUP Invoice resource order attribute was + returned as a dict instead of being converted to Resource + object. + * Pro Edition: RedmineUP CrmQuery resource deals and contacts + relation attributes didn't work. + * Pro Edition: RedmineUP DealStatus resource deals relation + attribute didn't work. + Documentation: + * Mentioned support for author_id in Issue's resource filter + operation. +- Drop not longer needed patches + * 328.patch + * support-python-312.patch + +------------------------------------------------------------------- Old: ---- 328.patch python-redmine-2.4.0.tar.gz support-python-312.patch New: ---- python-redmine-2.5.0.tar.gz BETA DEBUG BEGIN: Old:- Drop not longer needed patches * 328.patch * support-python-312.patch Old: * 328.patch * support-python-312.patch BETA DEBUG END: ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-python-redmine.spec ++++++ --- /var/tmp/diff_new_pack.6BO90v/_old 2024-04-08 17:52:11.449505712 +0200 +++ /var/tmp/diff_new_pack.6BO90v/_new 2024-04-08 17:52:11.473506597 +0200 @@ -17,24 +17,21 @@ Name: python-python-redmine -Version: 2.4.0 +Version: 2.5.0 Release: 0 Summary: Python library for the Redmine RESTful API License: Apache-2.0 URL: https://python-redmine.com Source: https://files.pythonhosted.org/packages/source/p/python-redmine/python-redmine-%{version}.tar.gz -Patch0: https://github.com/maxtepkeev/python-redmine/pull/328.patch -# PATCH-FIX-UPSTREAM gh#maxtepkeev/python-redmine#332 -Patch1: support-python-312.patch BuildRequires: %{python_module pip} BuildRequires: %{python_module pytest-cov} BuildRequires: %{python_module pytest} -BuildRequires: %{python_module requests >= 2.28.2} +BuildRequires: %{python_module requests >= 2.31.0} BuildRequires: %{python_module setuptools} BuildRequires: %{python_module wheel} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-requests >= 2.28.2 +Requires: python-requests >= 2.31.0 BuildArch: noarch %python_subpackages ++++++ python-redmine-2.4.0.tar.gz -> python-redmine-2.5.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/CHANGELOG.rst new/python-redmine-2.5.0/CHANGELOG.rst --- old/python-redmine-2.4.0/CHANGELOG.rst 2023-01-17 19:13:58.000000000 +0100 +++ new/python-redmine-2.5.0/CHANGELOG.rst 2024-03-31 16:46:44.000000000 +0200 @@ -1,6 +1,61 @@ Changelog --------- +2.5.0 (2024-03-31) +++++++++++++++++++ + +**Deprecations**: + +- Requests version required >= 2.31.0 + +**New Features**: + +- *Pro Edition:* RedmineUP `Products plugin <https://www.redmineup.com/pages/plugins/products>`__ support +- Issue copying (see `docs <https://python-redmine.com/resources/issue.html#copying>`__ for details) + (`Issue #203 <https://github.com/maxtepkeev/python-redmine/issues/203>`__) + +**Improvements**: + +- Migrated CI to GitHub Actions, also we now test not only on Linux, but on macOS and Windows as well +- ``dir(resource)`` and ``list(resource)`` now also show properties of an object +- Support for ``issues_assigned`` and ``issues_authored`` relations in User object + (`Issue #317 <https://github.com/maxtepkeev/python-redmine/issues/317>`__) +- Original filename will be used as a filename for all uploaded files if a path was provided and filename wasn't set +- *Pro Edition:* Added support for RedmineUP Contact avatar add/update operations + (see `docs <https://python-redmine.com/resources/contact.html#create-methods>`__ for details) +- *Pro Edition:* Added support for RedmineUP DealCategory ``create()``, ``update()``, ``delete()`` operations + (see `docs <https://python-redmine.com/resources/deal_category.html#create-methods>`__ for details) +- *Pro Edition:* RedmineUP CrmQuery resource now supports ``invoices`` and ``expenses`` relation attributes +- ``PerformanceWarning`` will be issued when Python-Redmine does some unnecessary redirects before the actual + request is made + +**Changes**: + +- *Backwards Incompatible:* API key is now being sent in the X-Redmine-API-Key header instead of the key GET + parameter which makes things more secure in case of a failed connection, but it might created issues for servers + that don't do custom request header forwarding by default, so be sure to check your web server before upgrading + (`Issue #328 <https://github.com/maxtepkeev/python-redmine/issues/328>`__ and + `Issue #330 <https://github.com/maxtepkeev/python-redmine/issues/330>`__) (thanks to `Tom Misilo <https://github.com/misilot>`__ + and `Ricardo Branco <https://github.com/ricardobranco777>`__) +- *Backwards Incompatible:* User ``all`` operation now really returns all users, i.e. not only active, but locked, + registered and anonymous as well instead of only returning just active users in previous versions due to the + respect to Redmine's standard behaviour (`Issue #327 <https://github.com/maxtepkeev/python-redmine/issues/327>`__) + +**Bugfixes**: + +- Tests were failing on Windows OS +- Tests were failing on Python 3.12 (`Issue #332 <https://github.com/maxtepkeev/python-redmine/pull/332>`__) + (thanks to `MichaŠGórny <https://github.com/mgorny>`__) +- Some closed Issues weren't converted to Resource objects using ``redmine.search()`` +- *Pro Edition:* RedmineUP Invoice resource ``order`` attribute was returned as a dict instead of being converted to + Resource object +- *Pro Edition:* RedmineUP CrmQuery resource ``deals`` and ``contacts`` relation attributes didn't work +- *Pro Edition:* RedmineUP DealStatus resource ``deals`` relation attribute didn't work + +**Documentation**: + +- Mentioned support for ``author_id`` in Issue's resource filter operation + 2.4.0 (2023-01-18) ++++++++++++++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/LICENSE new/python-redmine-2.5.0/LICENSE --- old/python-redmine-2.4.0/LICENSE 2023-01-17 19:56:47.000000000 +0100 +++ new/python-redmine-2.5.0/LICENSE 2024-03-31 15:31:59.000000000 +0200 @@ -1,4 +1,4 @@ -Copyright 2023 Maxim Tepkeev +Copyright 2024 Maxim Tepkeev Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/PKG-INFO new/python-redmine-2.5.0/PKG-INFO --- old/python-redmine-2.4.0/PKG-INFO 2023-01-17 20:07:22.000000000 +0100 +++ new/python-redmine-2.5.0/PKG-INFO 2024-03-31 17:15:21.813374500 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: python-redmine -Version: 2.4.0 +Version: 2.5.0 Summary: Library for communicating with a Redmine project management application Home-page: https://github.com/maxtepkeev/python-redmine Author: Maxim Tepkeev @@ -22,11 +22,13 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=3.7, <4 Description-Content-Type: text/x-rst +Requires-Dist: requests>=2.31.0 Python-Redmine ============== @@ -34,8 +36,8 @@ .. image:: https://badge.fury.io/py/python-redmine.svg :target: https://badge.fury.io/py/python-redmine -.. image:: https://img.shields.io/travis/com/maxtepkeev/python-redmine/master - :target: https://app.travis-ci.com/maxtepkeev/python-redmine +.. image:: https://img.shields.io/github/actions/workflow/status/maxtepkeev/python-redmine/tests.yml + :target: https://github.com/maxtepkeev/python-redmine/actions/workflows/tests.yml .. image:: https://img.shields.io/coverallsCoverage/github/maxtepkeev/python-redmine?branch=master :target: https://coveralls.io/github/maxtepkeev/python-redmine?branch=master @@ -84,7 +86,7 @@ * Supports 100% of Redmine API * Supports external Redmine plugins API -* Supports Python 3.7 - 3.11 and PyPy3 +* Supports Python 3.7 - 3.12 and PyPy3 * Supports different request engines * Extendable via custom resources and custom request engines * Extensively documented @@ -133,6 +135,61 @@ Changelog --------- +2.5.0 (2024-03-31) +++++++++++++++++++ + +**Deprecations**: + +- Requests version required >= 2.31.0 + +**New Features**: + +- *Pro Edition:* RedmineUP `Products plugin <https://www.redmineup.com/pages/plugins/products>`__ support +- Issue copying (see `docs <https://python-redmine.com/resources/issue.html#copying>`__ for details) + (`Issue #203 <https://github.com/maxtepkeev/python-redmine/issues/203>`__) + +**Improvements**: + +- Migrated CI to GitHub Actions, also we now test not only on Linux, but on macOS and Windows as well +- ``dir(resource)`` and ``list(resource)`` now also show properties of an object +- Support for ``issues_assigned`` and ``issues_authored`` relations in User object + (`Issue #317 <https://github.com/maxtepkeev/python-redmine/issues/317>`__) +- Original filename will be used as a filename for all uploaded files if a path was provided and filename wasn't set +- *Pro Edition:* Added support for RedmineUP Contact avatar add/update operations + (see `docs <https://python-redmine.com/resources/contact.html#create-methods>`__ for details) +- *Pro Edition:* Added support for RedmineUP DealCategory ``create()``, ``update()``, ``delete()`` operations + (see `docs <https://python-redmine.com/resources/deal_category.html#create-methods>`__ for details) +- *Pro Edition:* RedmineUP CrmQuery resource now supports ``invoices`` and ``expenses`` relation attributes +- ``PerformanceWarning`` will be issued when Python-Redmine does some unnecessary redirects before the actual + request is made + +**Changes**: + +- *Backwards Incompatible:* API key is now being sent in the X-Redmine-API-Key header instead of the key GET + parameter which makes things more secure in case of a failed connection, but it might created issues for servers + that don't do custom request header forwarding by default, so be sure to check your web server before upgrading + (`Issue #328 <https://github.com/maxtepkeev/python-redmine/issues/328>`__ and + `Issue #330 <https://github.com/maxtepkeev/python-redmine/issues/330>`__) (thanks to `Tom Misilo <https://github.com/misilot>`__ + and `Ricardo Branco <https://github.com/ricardobranco777>`__) +- *Backwards Incompatible:* User ``all`` operation now really returns all users, i.e. not only active, but locked, + registered and anonymous as well instead of only returning just active users in previous versions due to the + respect to Redmine's standard behaviour (`Issue #327 <https://github.com/maxtepkeev/python-redmine/issues/327>`__) + +**Bugfixes**: + +- Tests were failing on Windows OS +- Tests were failing on Python 3.12 (`Issue #332 <https://github.com/maxtepkeev/python-redmine/pull/332>`__) + (thanks to `MichaŠGórny <https://github.com/mgorny>`__) +- Some closed Issues weren't converted to Resource objects using ``redmine.search()`` +- *Pro Edition:* RedmineUP Invoice resource ``order`` attribute was returned as a dict instead of being converted to + Resource object +- *Pro Edition:* RedmineUP CrmQuery resource ``deals`` and ``contacts`` relation attributes didn't work +- *Pro Edition:* RedmineUP DealStatus resource ``deals`` relation attribute didn't work + +**Documentation**: + +- Mentioned support for ``author_id`` in Issue's resource filter operation + 2.4.0 (2023-01-18) ++++++++++++++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/README.rst new/python-redmine-2.5.0/README.rst --- old/python-redmine-2.4.0/README.rst 2023-01-13 19:48:24.000000000 +0100 +++ new/python-redmine-2.5.0/README.rst 2024-03-02 16:55:16.000000000 +0100 @@ -4,8 +4,8 @@ .. image:: https://badge.fury.io/py/python-redmine.svg :target: https://badge.fury.io/py/python-redmine -.. image:: https://img.shields.io/travis/com/maxtepkeev/python-redmine/master - :target: https://app.travis-ci.com/maxtepkeev/python-redmine +.. image:: https://img.shields.io/github/actions/workflow/status/maxtepkeev/python-redmine/tests.yml + :target: https://github.com/maxtepkeev/python-redmine/actions/workflows/tests.yml .. image:: https://img.shields.io/coverallsCoverage/github/maxtepkeev/python-redmine?branch=master :target: https://coveralls.io/github/maxtepkeev/python-redmine?branch=master @@ -54,7 +54,7 @@ * Supports 100% of Redmine API * Supports external Redmine plugins API -* Supports Python 3.7 - 3.11 and PyPy3 +* Supports Python 3.7 - 3.12 and PyPy3 * Supports different request engines * Extendable via custom resources and custom request engines * Extensively documented diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/python_redmine.egg-info/PKG-INFO new/python-redmine-2.5.0/python_redmine.egg-info/PKG-INFO --- old/python-redmine-2.4.0/python_redmine.egg-info/PKG-INFO 2023-01-17 20:07:22.000000000 +0100 +++ new/python-redmine-2.5.0/python_redmine.egg-info/PKG-INFO 2024-03-31 17:15:21.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: python-redmine -Version: 2.4.0 +Version: 2.5.0 Summary: Library for communicating with a Redmine project management application Home-page: https://github.com/maxtepkeev/python-redmine Author: Maxim Tepkeev @@ -22,11 +22,13 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=3.7, <4 Description-Content-Type: text/x-rst +Requires-Dist: requests>=2.31.0 Python-Redmine ============== @@ -34,8 +36,8 @@ .. image:: https://badge.fury.io/py/python-redmine.svg :target: https://badge.fury.io/py/python-redmine -.. image:: https://img.shields.io/travis/com/maxtepkeev/python-redmine/master - :target: https://app.travis-ci.com/maxtepkeev/python-redmine +.. image:: https://img.shields.io/github/actions/workflow/status/maxtepkeev/python-redmine/tests.yml + :target: https://github.com/maxtepkeev/python-redmine/actions/workflows/tests.yml .. image:: https://img.shields.io/coverallsCoverage/github/maxtepkeev/python-redmine?branch=master :target: https://coveralls.io/github/maxtepkeev/python-redmine?branch=master @@ -84,7 +86,7 @@ * Supports 100% of Redmine API * Supports external Redmine plugins API -* Supports Python 3.7 - 3.11 and PyPy3 +* Supports Python 3.7 - 3.12 and PyPy3 * Supports different request engines * Extendable via custom resources and custom request engines * Extensively documented @@ -133,6 +135,61 @@ Changelog --------- +2.5.0 (2024-03-31) +++++++++++++++++++ + +**Deprecations**: + +- Requests version required >= 2.31.0 + +**New Features**: + +- *Pro Edition:* RedmineUP `Products plugin <https://www.redmineup.com/pages/plugins/products>`__ support +- Issue copying (see `docs <https://python-redmine.com/resources/issue.html#copying>`__ for details) + (`Issue #203 <https://github.com/maxtepkeev/python-redmine/issues/203>`__) + +**Improvements**: + +- Migrated CI to GitHub Actions, also we now test not only on Linux, but on macOS and Windows as well +- ``dir(resource)`` and ``list(resource)`` now also show properties of an object +- Support for ``issues_assigned`` and ``issues_authored`` relations in User object + (`Issue #317 <https://github.com/maxtepkeev/python-redmine/issues/317>`__) +- Original filename will be used as a filename for all uploaded files if a path was provided and filename wasn't set +- *Pro Edition:* Added support for RedmineUP Contact avatar add/update operations + (see `docs <https://python-redmine.com/resources/contact.html#create-methods>`__ for details) +- *Pro Edition:* Added support for RedmineUP DealCategory ``create()``, ``update()``, ``delete()`` operations + (see `docs <https://python-redmine.com/resources/deal_category.html#create-methods>`__ for details) +- *Pro Edition:* RedmineUP CrmQuery resource now supports ``invoices`` and ``expenses`` relation attributes +- ``PerformanceWarning`` will be issued when Python-Redmine does some unnecessary redirects before the actual + request is made + +**Changes**: + +- *Backwards Incompatible:* API key is now being sent in the X-Redmine-API-Key header instead of the key GET + parameter which makes things more secure in case of a failed connection, but it might created issues for servers + that don't do custom request header forwarding by default, so be sure to check your web server before upgrading + (`Issue #328 <https://github.com/maxtepkeev/python-redmine/issues/328>`__ and + `Issue #330 <https://github.com/maxtepkeev/python-redmine/issues/330>`__) (thanks to `Tom Misilo <https://github.com/misilot>`__ + and `Ricardo Branco <https://github.com/ricardobranco777>`__) +- *Backwards Incompatible:* User ``all`` operation now really returns all users, i.e. not only active, but locked, + registered and anonymous as well instead of only returning just active users in previous versions due to the + respect to Redmine's standard behaviour (`Issue #327 <https://github.com/maxtepkeev/python-redmine/issues/327>`__) + +**Bugfixes**: + +- Tests were failing on Windows OS +- Tests were failing on Python 3.12 (`Issue #332 <https://github.com/maxtepkeev/python-redmine/pull/332>`__) + (thanks to `MichaŠGórny <https://github.com/mgorny>`__) +- Some closed Issues weren't converted to Resource objects using ``redmine.search()`` +- *Pro Edition:* RedmineUP Invoice resource ``order`` attribute was returned as a dict instead of being converted to + Resource object +- *Pro Edition:* RedmineUP CrmQuery resource ``deals`` and ``contacts`` relation attributes didn't work +- *Pro Edition:* RedmineUP DealStatus resource ``deals`` relation attribute didn't work + +**Documentation**: + +- Mentioned support for ``author_id`` in Issue's resource filter operation + 2.4.0 (2023-01-18) ++++++++++++++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/python_redmine.egg-info/requires.txt new/python-redmine-2.5.0/python_redmine.egg-info/requires.txt --- old/python-redmine-2.4.0/python_redmine.egg-info/requires.txt 2023-01-17 20:07:22.000000000 +0100 +++ new/python-redmine-2.5.0/python_redmine.egg-info/requires.txt 2024-03-31 17:15:21.000000000 +0200 @@ -1 +1 @@ -requests>=2.28.2 +requests>=2.31.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/redminelib/__init__.py new/python-redmine-2.5.0/redminelib/__init__.py --- old/python-redmine-2.4.0/redminelib/__init__.py 2023-01-13 19:48:24.000000000 +0100 +++ new/python-redmine-2.5.0/redminelib/__init__.py 2024-03-24 13:47:47.000000000 +0100 @@ -109,10 +109,6 @@ if self.ver is not None and self.ver < (1, 4, 0): raise exceptions.VersionMismatchError('File uploading') - url = f'{self.url}/uploads.json' - headers = {'Content-Type': 'application/octet-stream'} - params = {'filename': filename or ''} - # There are myriads of file-like object implementations here and there and some of them don't have # a "read" method, which is wrong, but that's what we have, on the other hand it looks like all of # them implement a "close" method, that's why we check for it here. Also, we don't want to close the @@ -127,8 +123,8 @@ # We need to send bytes over the socket, so in case a file-like object contains a unicode # object underneath, we need to convert it to bytes, otherwise we'll get an exception if isinstance(c, str): - warnings.warn("File-like object contains unicode, hence an additional step is performed to convert " - "its content to bytes, please consider switching to bytes to eliminate this warning", + warnings.warn('File-like object contains unicode, hence an additional step is performed to convert ' + 'its content to bytes, please consider switching to bytes to eliminate this warning', exceptions.PerformanceWarning) f = io.BytesIO(f.read().encode('utf-8')) @@ -138,9 +134,16 @@ if not os.path.isfile(f) or os.path.getsize(f) == 0: raise exceptions.NoFileError + if not filename: + filename = os.path.basename(f) + stream = open(f, 'rb') close = True + url = f'{self.url}/uploads.json' + headers = {'Content-Type': 'application/octet-stream'} + params = {'filename': filename or ''} + response = self.engine.request('post', url, params=params, data=stream, headers=headers) if close: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/redminelib/engines/base.py new/python-redmine-2.5.0/redminelib/engines/base.py --- old/python-redmine-2.4.0/redminelib/engines/base.py 2023-01-11 16:52:11.000000000 +0100 +++ new/python-redmine-2.5.0/redminelib/engines/base.py 2024-03-03 11:14:49.000000000 +0100 @@ -3,6 +3,7 @@ """ import json +import warnings from .. import exceptions @@ -24,7 +25,7 @@ self.ignore_response = options.pop('ignore_response', False) self.return_response = options.pop('return_response', True) self.return_raw_response = options.pop('return_raw_response', False) - self.requests = dict(dict(headers={}, params={}, data={}), **options.get('requests', {})) + self.requests = dict(dict(headers={}, params={}), **options.get('requests', {})) if self.ignore_response: self.requests['stream'] = True @@ -34,7 +35,7 @@ # We would like to be authenticated by API key by default if options.get('key') is not None: - self.requests['params']['key'] = options['key'] + self.requests['headers']['X-Redmine-API-Key'] = options['key'] elif options.get('username') is not None and options.get('password') is not None: self.requests['auth'] = (options['username'], options['password']) @@ -144,8 +145,17 @@ if response.history: r = response.history[0] - if r.is_redirect and r.request.url.startswith('http://') and response.request.url.startswith('https://'): - raise exceptions.HTTPProtocolError + + if 300 <= r.status_code <= 399: + url1, url2 = str(r.request.url), str(response.request.url) + + if (url1[:5] == 'http:' and url2[:6] == 'https:') or (url1[:6] == 'https:' and url2[:5] == 'http:'): + raise exceptions.HTTPProtocolError + else: + warnings.warn('Redirect detected during request-response, normally there should be no redirects, ' + 'so please check your Redmine URL for things like prepending www which redirects to ' + 'a no www domain and vice versa or using an old domain which redirects to a new one', + exceptions.PerformanceWarning) status_code = response.status_code diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/redminelib/exceptions.py new/python-redmine-2.5.0/redminelib/exceptions.py --- old/python-redmine-2.4.0/redminelib/exceptions.py 2023-01-08 19:20:13.000000000 +0100 +++ new/python-redmine-2.5.0/redminelib/exceptions.py 2024-03-02 15:34:23.000000000 +0100 @@ -282,7 +282,7 @@ Wrong HTTP protocol usage. """ def __init__(self): - super().__init__('Redmine url should start with HTTPS and not with HTTP') + super().__init__('Protocol redirect detected, Redmine URL expects HTTPS, but code uses HTTP or vice versa') class TimezoneError(BaseRedmineError): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/redminelib/managers/__init__.py new/python-redmine-2.5.0/redminelib/managers/__init__.py --- old/python-redmine-2.4.0/redminelib/managers/__init__.py 2023-01-17 19:56:47.000000000 +0100 +++ new/python-redmine-2.5.0/redminelib/managers/__init__.py 2024-03-31 15:31:59.000000000 +0200 @@ -3,4 +3,4 @@ """ from .base import ResourceManager -from .standard import ProjectManager, FileManager, WikiPageManager, UserManager, NewsManager +from .standard import ProjectManager, IssueManager, FileManager, WikiPageManager, UserManager, NewsManager diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/redminelib/managers/base.py new/python-redmine-2.5.0/redminelib/managers/base.py --- old/python-redmine-2.4.0/redminelib/managers/base.py 2023-01-15 13:44:13.000000000 +0100 +++ new/python-redmine-2.5.0/redminelib/managers/base.py 2024-03-31 15:33:21.000000000 +0200 @@ -219,7 +219,7 @@ :param dict request: Request data. """ - return {self.resource_class.container_update: self.resource_class.bulk_decode(request, self)} + return {self.container: self.resource_class.bulk_decode(request, self)} def update(self, resource_id, **fields): """ @@ -246,6 +246,8 @@ else: raise exceptions.ValidationError(f'{e} argument is required') + self.params.update(self.resource_class.query_update.formatter.used_kwargs) + self.container = self.resource_class.container_update url = self._construct_update_url(query_update) request = self._prepare_update_request(self.resource_class.query_update.formatter.unused_kwargs) response = self.redmine.engine.request(self.resource_class.http_method_update, url, data=request) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/redminelib/managers/standard.py new/python-redmine-2.5.0/redminelib/managers/standard.py --- old/python-redmine-2.4.0/redminelib/managers/standard.py 2023-01-03 14:35:51.000000000 +0100 +++ new/python-redmine-2.5.0/redminelib/managers/standard.py 2024-03-09 11:50:16.000000000 +0100 @@ -18,6 +18,25 @@ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'") +class IssueManager(ResourceManager): + def copy(self, issue_id, link_original=True, include=(), **fields): + fields['_copy'] = {'copy_from': issue_id} + + if link_original: + fields['_copy']['link_copy'] = '1' + + if include is not None: + for i in include or ('subtasks', 'attachments'): + fields['_copy'][f'copy_{i}'] = '1' + + return self.create(**fields) + + def _prepare_create_request(self, request): + request = super()._prepare_create_request(request) + request.update(request[self.container].pop('_copy', {})) + return request + + class FileManager(ResourceManager): def _process_create_response(self, request, response): if response is True: @@ -45,6 +64,18 @@ def _construct_get_url(self, path): return super()._construct_get_url(self._check_custom_url(path)) + def all(self, **params): + resourceset = super().all(**params) + + if self.redmine.ver is not None: # https://www.redmine.org/issues/32090#note-6 + if self.redmine.ver >= (5, 1, 2): + resourceset.manager.url = f'{resourceset.manager.url}*' + elif self.redmine.ver in ((5, 1, 0), (5, 1, 1)): + resourceset.manager.url = (f'{resourceset.manager.url[:-7]}f[]=status_id&' + f'op[status_id]==&v[status_id][]=1&v[status_id][]=2&v[status_id][]=3') + + return resourceset + def _prepare_create_request(self, request): request = super()._prepare_create_request(request) request['send_information'] = request[self.container].pop('send_information', False) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/redminelib/resources/base.py new/python-redmine-2.5.0/redminelib/resources/base.py --- old/python-redmine-2.4.0/redminelib/resources/base.py 2023-01-17 17:17:14.000000000 +0100 +++ new/python-redmine-2.5.0/redminelib/resources/base.py 2024-03-02 15:39:12.000000000 +0100 @@ -16,9 +16,8 @@ which name starts with Base are considered base classes and not added to the registry. """ def __new__(mcs, name, bases, attrs): - mcs.update_query_strings(attrs) - - cls = super().__new__(mcs, name, bases, attrs) + cls = super().__new__(mcs, name, bases, mcs.bulk_update_attrs(attrs)) + mcs.bulk_update_cls_attrs(cls, attrs) if name.startswith('Base'): # base classes shouldn't be added to the registry return cls @@ -59,16 +58,36 @@ return registry[name].setdefault('class', cls) @staticmethod - def update_query_strings(attrs): + def bulk_update_attrs(attrs): """ - Updates all `query_*` string attributes to use ResourceQueryFormatter by default. + Updates attrs with specific features and/or actualizes their content before a class is created. + + :param dict attrs: (required). Attributes to work with. """ - for k, v in attrs.items(): - if k.startswith('query_') and v is not None: - attrs[k] = utilities.ResourceQueryStr(v) + for attr, value in attrs.items(): + # `query_*` class attributes should use ResourceQueryFormatter by default + if attr.startswith('query_') and value is not None: + attrs[attr] = utilities.ResourceQueryStr(value) return attrs + @classmethod + def bulk_update_cls_attrs(mcs, cls, attrs): + """ + Updates attrs with specific features and/or actualizes their content after a class is created. + + :param any cls: (required). Resource class. + :param dict attrs: (required). Attributes to work with. + """ + properties = [] + + for attr, value in attrs.items(): + # `_members` class attribute should also contain all public properties + if not attr.startswith('_') and isinstance(value, property): + properties.append(attr) + + mcs.update_cls_attr(cls, '_members', properties) + @staticmethod def update_cls_attr(cls, name, value): """ @@ -77,12 +96,12 @@ :param any cls: (required). Resource class. :param string name: (required). Attribute name. - :param any value: (optional). Attribute value. + :param any value: (required). Attribute value. """ attr = getattr(cls, name, None) if isinstance(attr, list): - value = list(attr) + list(value) + value = list(set().union(attr, value)) elif isinstance(attr, dict): value = dict(attr, **value) else: @@ -211,7 +230,9 @@ """ Sets the requested attribute. """ - if attr in self._members or attr.startswith('_'): + custom_settable = [*self._single_attr_id_map, *self._multiple_attr_id_map] + + if attr.startswith('_') or attr in self._members and attr not in custom_settable: return super().__setattr__(attr, value) elif attr in self._create_readonly and self.is_new(): raise exceptions.ReadonlyAttrError @@ -396,7 +417,8 @@ if not self.is_new(): self.pre_update() self.manager.update(self.internal_id, **self._changes) - self._decoded_attrs['updated_on'] = datetime.utcnow().strftime(self.manager.redmine.datetime_format) + self._decoded_attrs['updated_on'] = datetime.now(timezone.utc).strftime( + self.manager.redmine.datetime_format) self.post_update() else: self.pre_create() @@ -481,13 +503,13 @@ """ Allows dir() to be called on a Resource object and shows Resource attributes. """ - return list(self._decoded_attrs.keys()) + return [*self._decoded_attrs, *self._members] def __iter__(self): """ Provides a way to iterate through Resource attributes and its values. """ - return iter(self._decoded_attrs.items()) + return iter(dict(self._decoded_attrs, **{m: getattr(self, m, None) for m in self._members}).items()) def __int__(self): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/redminelib/resources/standard.py new/python-redmine-2.5.0/redminelib/resources/standard.py --- old/python-redmine-2.4.0/redminelib/resources/standard.py 2023-01-16 11:12:59.000000000 +0100 +++ new/python-redmine-2.5.0/redminelib/resources/standard.py 2024-03-09 12:38:06.000000000 +0100 @@ -78,8 +78,9 @@ query_create = '/projects/{project_id}/issues.json' query_update = '/issues/{}.json' query_delete = '/issues/{}.json' - search_hints = ['issue', 'issue closed'] + search_hints = ['issue', 'issue closed', 'issue-closed'] extra_export_columns = ['description', 'last_notes'] + manager_class = managers.IssueManager _repr = [['id', 'subject'], ['title'], ['id']] _includes = ['children', 'attachments', 'relations', 'changesets', 'journals', 'watchers', 'allowed_statuses'] @@ -174,6 +175,12 @@ return super().decode(attr, value, manager) + def copy(self, link_original=True, include=(), **fields): + if 'project_id' not in fields and not self.is_new(): + fields['project_id'] = self._decoded_attrs['project']['id'] + + return self.manager.copy(self.internal_id, link_original=link_original, include=include, **fields) + class TimeEntry(BaseResource): redmine_version = (1, 1, 0) @@ -402,7 +409,7 @@ container_create = 'user' container_update = 'user' query_all_export = '/users.{format}' - query_all = '/users.json' + query_all = '/users.json?status=' query_one = '/users/{}.json' query_filter = '/users.json' query_create = '/users.json' @@ -412,7 +419,7 @@ _repr = [['id', 'firstname', 'lastname'], ['id', 'name']] _includes = ['memberships', 'groups'] - _relations = ['issues', 'time_entries'] + _relations = ['issues', 'issues_assigned', 'issues_authored', 'time_entries'] _relations_name = 'assigned_to' _unconvertible = ['status'] _create_readonly = BaseResource._create_readonly + ['api_key', 'last_login_on'] @@ -422,12 +429,18 @@ 'groups': 'Group', 'memberships': 'ProjectMembership', 'issues': 'Issue', + 'issues_assigned': 'Issue', + 'issues_authored': 'Issue', 'time_entries': 'TimeEntry', } def __getattr__(self, attr): - if attr == 'time_entries' and attr not in self._encoded_attrs: - self._relations_name = 'user' + if attr in self._relations and attr not in self._encoded_attrs: + if attr == 'issues_authored': + self._relations_name = 'author' + elif attr == 'time_entries': + self._relations_name = 'user' + value = super().__getattr__(attr) self._relations_name = 'assigned_to' return value diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/redminelib/version.py new/python-redmine-2.5.0/redminelib/version.py --- old/python-redmine-2.4.0/redminelib/version.py 2023-01-17 19:13:58.000000000 +0100 +++ new/python-redmine-2.5.0/redminelib/version.py 2024-03-31 16:46:44.000000000 +0200 @@ -1 +1 @@ -__version__ = '2.4.0' +__version__ = '2.5.0' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/setup.py new/python-redmine-2.5.0/setup.py --- old/python-redmine-2.4.0/setup.py 2023-01-17 19:24:30.000000000 +0100 +++ new/python-redmine-2.5.0/setup.py 2024-03-02 21:10:57.000000000 +0100 @@ -18,7 +18,7 @@ long_description=open('README.rst').read() + '\n\n' + open('CHANGELOG.rst').read(), keywords='redmine redmineup redminecrm redminelib easyredmine', python_requires='>=3.7, <4', - install_requires=['requests>=2.28.2'], + install_requires=['requests>=2.31.0'], zip_safe=False, classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -35,6 +35,7 @@ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/tests/__init__.py new/python-redmine-2.5.0/tests/__init__.py --- old/python-redmine-2.4.0/tests/__init__.py 2022-12-25 20:47:30.000000000 +0100 +++ new/python-redmine-2.5.0/tests/__init__.py 2024-03-02 15:34:23.000000000 +0100 @@ -4,13 +4,13 @@ class BaseRedmineTestCase(TestCase): - url = 'http://foo.bar' + url = 'https://foo.bar' patch_prefix = 'patch' patch_targets = {'requests': 'redminelib.engines.sync.requests.Session.request'} def setUp(self): self.redmine = Redmine(self.url) - self.response = mock.Mock(status_code=200, history=[]) + self.response = mock.Mock(**{'status_code': 200, 'history': [], 'request.url': self.url}) for target, path in self.patch_targets.items(): setattr(self, f'{self.patch_prefix}_{target}', mock.patch(path, return_value=self.response).start()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/tests/test_engines.py new/python-redmine-2.5.0/tests/test_engines.py --- old/python-redmine-2.4.0/tests/test_engines.py 2020-05-19 16:12:07.000000000 +0200 +++ new/python-redmine-2.5.0/tests/test_engines.py 2024-03-03 11:18:27.000000000 +0100 @@ -1,3 +1,5 @@ +import warnings + from . import mock, BaseRedmineTestCase, Redmine from redminelib import engines, exceptions @@ -6,7 +8,7 @@ class BaseEngineTestCase(BaseRedmineTestCase): def test_engine_init(self): redmine = Redmine(self.url, key='123', impersonate='jsmith', requests={'foo': 'bar'}) - self.assertEqual(redmine.engine.requests['params']['key'], '123') + self.assertEqual(redmine.engine.requests['headers']['X-Redmine-API-Key'], '123') self.assertEqual(redmine.engine.requests['headers']['X-Redmine-Switch-User'], 'jsmith') self.assertEqual(redmine.engine.requests['foo'], 'bar') redmine = Redmine(self.url, username='john', password='qwerty') @@ -91,10 +93,17 @@ self.assertRaises(exceptions.UnknownError, lambda: self.redmine.engine.request('get', self.url)) def test_http_protocol_exception(self): - self.response.history = [mock.Mock()] - self.redmine.url = 'http://foo.bar' + self.response.history = [mock.Mock(**{'status_code': 301, 'request.url': 'http://foo.bar'})] self.assertRaises(exceptions.HTTPProtocolError, lambda: self.redmine.engine.request('get', self.url)) + def test_redirect_warning(self): + self.response.history = [mock.Mock(**{'status_code': 301, 'request.url': 'https://www.foo.bar'})] + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + self.redmine.engine.request('get', self.url) + self.assertEqual(len(w), 1) + self.assertIs(w[0].category, exceptions.PerformanceWarning) + def test_engine_is_picklable(self): import pickle self.redmine.engine.requests['params']['key'] = '123' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/tests/test_managers.py new/python-redmine-2.5.0/tests/test_managers.py --- old/python-redmine-2.4.0/tests/test_managers.py 2023-01-16 16:50:28.000000000 +0100 +++ new/python-redmine-2.5.0/tests/test_managers.py 2024-03-02 15:34:23.000000000 +0100 @@ -12,7 +12,7 @@ class ResourceManagerTestCase(BaseRedmineTestCase): def test_has_custom_repr(self): - self.assertEqual(repr(self.redmine.issue), '<redminelib.managers.ResourceManager object for Issue resource>') + self.assertEqual(repr(self.redmine.query), '<redminelib.managers.ResourceManager object for Query resource>') def test_supports_additional_resources(self): self.assertIsInstance(self.redmine.foo_resource, managers.ResourceManager) @@ -125,7 +125,7 @@ with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') issue = self.redmine.issue.create(project_id=1, subject='Foo', uploads=[{'path': stream}]) - self.assertEquals(len(w), 1) + self.assertEqual(len(w), 1) self.assertIs(w[0].category, exceptions.PerformanceWarning) self.assertEqual(issue.project_id, 1) self.assertEqual(issue.subject, 'Foo') @@ -166,7 +166,7 @@ with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') self.assertEqual(self.redmine.issue.update(1, subject='Bar', uploads=[{'path': stream}]), True) - self.assertEquals(len(w), 1) + self.assertEqual(len(w), 1) self.assertIs(w[0].category, exceptions.PerformanceWarning) def test_update_resource_returns_none(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/tests/test_redmine.py new/python-redmine-2.5.0/tests/test_redmine.py --- old/python-redmine-2.4.0/tests/test_redmine.py 2023-01-05 19:00:33.000000000 +0100 +++ new/python-redmine-2.5.0/tests/test_redmine.py 2024-03-03 11:20:32.000000000 +0100 @@ -41,8 +41,8 @@ def test_session_key(self): with self.redmine.session(key='opa'): - self.assertEqual(self.redmine.engine.requests['params']['key'], 'opa') - self.assertRaises(KeyError, lambda: self.redmine.engine.requests['params']['key']) + self.assertEqual(self.redmine.engine.requests['headers']['X-Redmine-API-Key'], 'opa') + self.assertRaises(KeyError, lambda: self.redmine.engine.requests['headers']['X-Redmine-API-Key']) def test_session_username_password(self): with self.redmine.session(username='john', password='smith'): @@ -53,7 +53,8 @@ self.redmine.engine.requests['cert'] = ('bar', 'baz') requests = {'verify': False, 'timeout': 2, 'cert': ('foo', 'bar'), 'params': {'foo': 'bar'}} with self.redmine.session(key='secret', requests=requests): - self.assertEqual(self.redmine.engine.requests['params'], dict(key='secret', **requests['params'])) + self.assertEqual(self.redmine.engine.requests['headers'], {'X-Redmine-API-Key': 'secret'}) + self.assertEqual(self.redmine.engine.requests['params'], requests['params']) self.assertEqual(self.redmine.engine.requests['verify'], requests['verify']) self.assertEqual(self.redmine.engine.requests['timeout'], requests['timeout']) self.assertEqual(self.redmine.engine.requests['cert'], requests['cert']) @@ -77,23 +78,23 @@ with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') self.assertEqual(self.redmine.upload(StringIO(b'\xcf\x86oo'.decode('utf-8')))['token'], '456789') - self.assertEquals(len(w), 1) + self.assertEqual(len(w), 1) self.assertIs(w[0].category, exceptions.PerformanceWarning) @mock.patch('redminelib.open', mock.mock_open(), create=True) def test_successful_file_download(self): self.response.status_code = 200 self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.download('http://foo/bar.txt', '/some/path'), '/some/path/bar.txt') + self.assertEqual(self.redmine.download(f'{self.url}/bar.txt', '/some/path/'), '/some/path/bar.txt') def test_successful_in_memory_file_download(self): self.response.status_code = 200 self.response.iter_content = lambda: (str(num) for num in range(0, 5)) - self.assertEqual(''.join(self.redmine.download('http://foo/bar.txt').iter_content()), '01234') + self.assertEqual(''.join(self.redmine.download(f'{self.url}/bar.txt').iter_content()), '01234') def test_file_url_exception(self): self.response.status_code = 200 - self.assertRaises(exceptions.FileUrlError, lambda: self.redmine.download('http://bad_url', '/some/path')) + self.assertRaises(exceptions.FileUrlError, lambda: self.redmine.download('https://bad_url', '/some/path')) def test_file_upload_no_file_exception(self): self.assertRaises(exceptions.NoFileError, lambda: self.redmine.upload('foo',)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/tests/test_resources_standard.py new/python-redmine-2.5.0/tests/test_resources_standard.py --- old/python-redmine-2.4.0/tests/test_resources_standard.py 2023-01-17 18:35:01.000000000 +0100 +++ new/python-redmine-2.5.0/tests/test_resources_standard.py 2024-03-09 12:07:39.000000000 +0100 @@ -1,7 +1,7 @@ from . import mock, BaseRedmineTestCase from .responses import responses -from redminelib import resources, resultsets, exceptions +from redminelib import resources, managers, resultsets, exceptions class StandardResourcesTestCase(BaseRedmineTestCase): @@ -25,7 +25,7 @@ def test_export(self): self.response.json.return_value = responses['issue']['get'] self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.issue.get(1).export('txt', '/foo/bar'), '/foo/bar/1.txt') + self.assertEqual(self.redmine.issue.get(1).export('txt', '/foo/bar/'), '/foo/bar/1.txt') def test_export_not_supported_exception(self): self.response.json.return_value = responses['attachment']['get'] @@ -183,12 +183,21 @@ self.assertIn('subject', attributes) self.assertIn('relations', attributes) self.assertIn('time_entries', attributes) + self.assertIn('children', attributes) + self.assertIn('attachments', attributes) + self.assertIn('manager', attributes) + self.assertIn('url', attributes) + self.assertIn('internal_id', attributes) def test_supports_iteration(self): self.response.json.return_value = responses['project']['get'] - project = list(self.redmine.project.get(1)) + p = self.redmine.project.get(1) + project = list(p) self.assertIn(('name', 'Foo'), project) self.assertIn(('id', 1), project) + self.assertIn(('manager', p.manager), project) + self.assertIn(('url', f'{self.url}/projects/foo'), project) + self.assertIn(('internal_id', 1), project) def test_setting_custom_field_raises_exception_if_not_list_of_dicts(self): self.response.json.return_value = {'project': {'name': 'Foo', 'id': 1, 'custom_fields': [{'id': 1}]}} @@ -288,7 +297,7 @@ self.response.json.return_value = { 'project': {'name': 'Foo', 'id': 1, 'custom_fields': [{'id': 1, 'value': 'foo'}]}} project = self.redmine.project.get(1) - project.homepage = 'http://foo.bar' + project.homepage = self.url project.parent_id = 3 project.custom_fields = [{'id': 1, 'value': 'bar'}] self.assertIsInstance(project.save(), resources.Project) @@ -345,7 +354,7 @@ def test_project_export(self): self.response.json.return_value = responses['project']['all'] self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.project.all().export('txt', '/foo/bar'), '/foo/bar/projects.txt') + self.assertEqual(self.redmine.project.all().export('txt', '/foo/bar/'), '/foo/bar/projects.txt') def test_project_parent_converts_to_resource(self): self.response.json.return_value = {'project': {'name': 'Foo', 'id': 1, 'parent': {'id': 2}}} @@ -363,6 +372,18 @@ self.assertIsInstance(project.default_assignee, resources.User) self.assertEqual(project.default_assignee.id, 4) + def test_project_sets_attrs_from_single_attr_id_map(self): + self.response.json.return_value = responses['project']['get'] + project = self.redmine.project.get(1) + project.parent_id = 1 + self.assertEqual(project._decoded_attrs['parent'], {'id': 1}) + + def test_project_sets_attrs_from_multiple_attr_id_map(self): + self.response.json.return_value = responses['project']['get'] + project = self.redmine.project.get(1) + project.tracker_ids = [1, 2] + self.assertEqual(project._decoded_attrs['trackers'], [{'id': 1}, {'id': 2}]) + def test_project_supports_close_reopen_archive_unarchive(self): self.response.json.return_value = responses['project']['get'] project = self.redmine.project.get(1) @@ -428,6 +449,32 @@ self.assertIsInstance(issue.save(), resources.Issue) self.assertEqual(issue.custom_fields[0].value, 'bar') + def test_issue_copy(self): + import json + self.response.status_code = 201 + self.response.json.return_value = {'issue': {'subject': 'Foo', 'id': 1, 'project': {'id': 1}}} + self.redmine.issue.get(1).copy() + request = json.loads(self.patch_requests.call_args[1]['data']) + self.assertEqual(request['copy_from'], 1) + self.assertEqual(request['link_copy'], '1') + self.assertEqual(request['copy_subtasks'], '1') + self.assertEqual(request['copy_attachments'], '1') + + def test_issue_copy_via_manager(self): + import json + self.response.status_code = 201 + self.response.json.return_value = responses['issue']['get'] + self.redmine.issue.copy(1, project_id=1, link_original=False, include=None) + request = json.loads(self.patch_requests.call_args[1]['data']) + self.assertEqual(request['copy_from'], 1) + self.assertNotIn('link_copy', request) + self.assertNotIn('copy_subtasks', request) + self.assertNotIn('copy_attachments', request) + + def test_issue_custom_manager(self): + self.assertEqual(repr(self.redmine.issue), '<redminelib.managers.IssueManager object for Issue resource>') + self.assertIsInstance(self.redmine.issue, managers.IssueManager) + def test_issue_relations(self): self.response.json.return_value = responses['issue']['get'] issue = self.redmine.issue.get(1) @@ -561,9 +608,9 @@ def test_issue_export(self): self.response.json.return_value = responses['issue']['all'] self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar'), '/foo/bar/issues.txt') + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar/'), '/foo/bar/issues.txt') self.response.json.return_value = responses['issue']['get'] - self.assertEqual(self.redmine.issue.get(1).export('txt', '/foo/bar'), '/foo/bar/1.txt') + self.assertEqual(self.redmine.issue.get(1).export('txt', '/foo/bar/'), '/foo/bar/1.txt') def test_issue_parent_converts_to_resource(self): self.response.json.return_value = {'issue': {'subject': 'Foo', 'id': 1, 'parent': {'id': 2}}} @@ -599,6 +646,32 @@ self.assertIsInstance(issue.fixed_version, resources.Version) self.assertEqual(issue.fixed_version.id, 1) + def test_issue_sets_attrs_from_single_attr_id_map(self): + self.response.json.return_value = responses['issue']['get'] + issue = self.redmine.issue.get(1) + issue.project_id = 1 + issue.tracker_id = 1 + issue.status_id = 1 + issue.priority_id = 1 + issue.category_id = 1 + issue.fixed_version_id = 1 + issue.assigned_to_id = 1 + issue.parent_issue_id = 1 + self.assertEqual(issue._decoded_attrs['project'], {'id': 1}) + self.assertEqual(issue._decoded_attrs['tracker'], {'id': 1}) + self.assertEqual(issue._decoded_attrs['status'], {'id': 1}) + self.assertEqual(issue._decoded_attrs['priority'], {'id': 1}) + self.assertEqual(issue._decoded_attrs['category'], {'id': 1}) + self.assertEqual(issue._decoded_attrs['fixed_version'], {'id': 1}) + self.assertEqual(issue._decoded_attrs['assigned_to'], {'id': 1}) + self.assertEqual(issue._decoded_attrs['parent'], {'id': 1}) + + def test_issue_sets_attrs_from_multiple_attr_id_map(self): + self.response.json.return_value = responses['issue']['get'] + issue = self.redmine.issue.get(1) + issue.watcher_user_ids = [1, 2] + self.assertEqual(issue._decoded_attrs['watchers'], [{'id': 1}, {'id': 2}]) + def test_time_entry_version(self): self.assertEqual(self.redmine.time_entry.resource_class.redmine_version, (1, 1, 0)) @@ -675,7 +748,7 @@ def test_time_entry_export(self): self.response.json.return_value = responses['time_entry']['all'] self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.time_entry.all().export('txt', '/foo/bar'), '/foo/bar/time_entries.txt') + self.assertEqual(self.redmine.time_entry.all().export('txt', '/foo/bar/'), '/foo/bar/time_entries.txt') def test_time_entry_resource_map_converts_to_resource(self): self.response.json.return_value = responses['time_entry']['get'] @@ -693,6 +766,16 @@ self.assertIsInstance(time_entry.activity, resources.Enumeration) self.assertEqual(time_entry.activity.id, 1) + def test_time_entry_sets_attrs_from_single_attr_id_map(self): + self.response.json.return_value = responses['time_entry']['get'] + time_entry = self.redmine.time_entry.get(1) + time_entry.project_id = 1 + time_entry.issue_id = 1 + time_entry.activity_id = 1 + self.assertEqual(time_entry._decoded_attrs['project'], {'id': 1}) + self.assertEqual(time_entry._decoded_attrs['issue'], {'id': 1}) + self.assertEqual(time_entry._decoded_attrs['activity'], {'id': 1}) + def test_enumeration_version(self): self.assertEqual(self.redmine.enumeration.resource_class.redmine_version, (2, 2, 0)) @@ -766,10 +849,10 @@ @mock.patch('redminelib.open', mock.mock_open(), create=True) def test_attachment_download(self): response = responses['attachment']['get'] - response['attachment']['content_url'] = 'http://foo/bar.txt' + response['attachment']['content_url'] = f'{self.url}/bar.txt' self.response.json.return_value = response self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.attachment.get(1).download('/some/path'), '/some/path/bar.txt') + self.assertEqual(self.redmine.attachment.get(1).download('/some/path/'), '/some/path/bar.txt') def test_attachment_resource_map_converts_to_resource(self): self.response.json.return_value = responses['attachment']['get'] @@ -847,10 +930,10 @@ @mock.patch('redminelib.open', mock.mock_open(), create=True) def test_file_download(self): response = responses['attachment']['get'] - response['attachment']['content_url'] = 'http://foo/bar.txt' + response['attachment']['content_url'] = f'{self.url}/bar.txt' self.response.json.return_value = response self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.file.get(1).download('/some/path'), '/some/path/bar.txt') + self.assertEqual(self.redmine.file.get(1).download('/some/path/'), '/some/path/bar.txt') def test_file_resource_map_converts_to_resource(self): self.response.json.return_value = responses['attachment']['get'] @@ -876,7 +959,7 @@ wiki_page = self.redmine.wiki_page.get('Foo%Bar', project_id=1) self.assertEqual(self.patch_requests.call_args[0][1], f'{self.url}/projects/1/wiki/Foo%25Bar.json') self.assertEqual(wiki_page.title, 'Foo%Bar') - self.assertEqual(wiki_page.url, 'http://foo.bar/projects/1/wiki/Foo%25Bar') + self.assertEqual(wiki_page.url, f'{self.url}/projects/1/wiki/Foo%25Bar') def test_wiki_page_filter(self): self.response.json.return_value = responses['wiki_page']['filter'] @@ -965,7 +1048,7 @@ def test_wiki_page_export(self): self.response.json.return_value = responses['wiki_page']['get'] self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.wiki_page.get('Foo', project_id='Foo').export('txt', '/foo'), '/foo/Foo.txt') + self.assertEqual(self.redmine.wiki_page.get('Foo', project_id='Foo').export('txt', '/foo/'), '/foo/Foo.txt') def test_wiki_page_parent_converts_to_resource(self): self.response.json.return_value = {'wiki_page': {'title': 'Foo', 'project_id': 1, 'parent': {'title': 'Bar'}}} @@ -980,6 +1063,12 @@ self.assertIsInstance(wiki_page.author, resources.User) self.assertEqual(wiki_page.author.firstname, 'John') + def test_wiki_page_sets_attrs_from_single_attr_id_map(self): + self.response.json.return_value = responses['wiki_page']['get'] + wiki_page = self.redmine.wiki_page.get(1, project_id=1) + wiki_page.project_id = 1 + self.assertEqual(wiki_page._decoded_attrs['project'], {'id': 1}) + def test_project_membership_version(self): self.assertEqual(self.redmine.project_membership.resource_class.redmine_version, (1, 4, 0)) @@ -1046,6 +1135,20 @@ self.assertIsInstance(membership.group, resources.Group) self.assertEqual(membership.group.id, 1) + def test_project_membership_sets_attrs_from_single_attr_id_map(self): + self.response.json.return_value = responses['project_membership']['get'] + membership = self.redmine.project_membership.get(1) + membership.project_id = 1 + membership.user_id = 1 + self.assertEqual(membership._decoded_attrs['project'], {'id': 1}) + self.assertEqual(membership._decoded_attrs['user'], {'id': 1}) + + def test_project_membership_sets_attrs_from_multiple_attr_id_map(self): + self.response.json.return_value = responses['project_membership']['get'] + membership = self.redmine.project_membership.get(1) + membership.role_ids = [1, 2] + self.assertEqual(membership._decoded_attrs['roles'], [{'id': 1}, {'id': 2}]) + def test_issue_category_version(self): self.assertEqual(self.redmine.issue_category.resource_class.redmine_version, (1, 3, 0)) @@ -1102,6 +1205,14 @@ self.assertIsInstance(category.assigned_to, resources.User) self.assertEqual(category.assigned_to.firstname, 'John') + def test_issue_category_sets_attrs_from_single_attr_id_map(self): + self.response.json.return_value = responses['issue_category']['get'] + category = self.redmine.issue_category.get(1) + category.project_id = 1 + category.assigned_to_id = 1 + self.assertEqual(category._decoded_attrs['project'], {'id': 1}) + self.assertEqual(category._decoded_attrs['assigned_to'], {'id': 1}) + def test_issue_relation_version(self): self.assertEqual(self.redmine.issue_relation.resource_class.redmine_version, (1, 3, 0)) @@ -1147,6 +1258,12 @@ self.response.json.return_value = responses['issue_relation']['get'] self.assertEqual(self.redmine.issue_relation.get(1).url, f'{self.url}/relations/1') + def test_issue_relation_sets_attrs_from_single_attr_id_map(self): + self.response.json.return_value = responses['issue_relation']['get'] + relation = self.redmine.issue_relation.get(1) + relation.issue_id = 1 + self.assertEqual(relation._decoded_attrs['issue'], {'id': 1}) + def test_version_version(self): self.assertEqual(self.redmine.version.resource_class.redmine_version, (1, 3, 0)) @@ -1205,6 +1322,12 @@ self.assertIsInstance(version.project, resources.Project) self.assertEqual(version.project.identifier, 'foo') + def test_version_relation_sets_attrs_from_single_attr_id_map(self): + self.response.json.return_value = responses['version']['get'] + version = self.redmine.version.get(1) + version.project_id = 1 + self.assertEqual(version._decoded_attrs['project'], {'id': 1}) + def test_user_version(self): self.assertEqual(self.redmine.user.resource_class.redmine_version, (1, 1, 0)) @@ -1228,6 +1351,15 @@ self.assertEqual(users[1].id, 2) self.assertEqual(users[1].firstname, 'Jack') + def test_user_all_url_variations(self): + self.redmine.ver = (5, 0, 0) + self.assertEqual(self.redmine.user.all().manager.url, f'{self.url}/users.json?status=') + self.redmine.ver = (5, 1, 0) + self.assertEqual(self.redmine.user.all().manager.url, f'{self.url}/users.json?f[]=status_id&' + f'op[status_id]==&v[status_id][]=1&v[status_id][]=2&v[status_id][]=3') + self.redmine.ver = (6, 0, 0) + self.assertEqual(self.redmine.user.all().manager.url, f'{self.url}/users.json?status=*') + def test_user_filter(self): self.response.json.return_value = responses['user']['filter'] users = self.redmine.user.filter(status_id=2) @@ -1286,6 +1418,8 @@ self.response.json.return_value = responses['user']['get'] user = self.redmine.user.get(1) self.assertIsInstance(user.issues, resultsets.ResourceSet) + self.assertIsInstance(user.issues_assigned, resultsets.ResourceSet) + self.assertIsInstance(user.issues_authored, resultsets.ResourceSet) self.assertIsInstance(user.time_entries, resultsets.ResourceSet) def test_user_includes(self): @@ -1314,6 +1448,12 @@ self.response.json.return_value = responses['user']['get'] self.assertEqual(self.redmine.user.get(1).url, f'{self.url}/users/1') + @mock.patch('redminelib.open', mock.mock_open(), create=True) + def test_user_export(self): + self.response.json.return_value = responses['user']['all'] + self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) + self.assertEqual(self.redmine.user.all().export('txt', '/foo/bar/'), '/foo/bar/users.txt') + def test_group_version(self): self.assertEqual(self.redmine.group.resource_class.redmine_version, (2, 1, 0)) @@ -1383,6 +1523,12 @@ self.response.json.return_value = responses['group']['get'] self.assertEqual(self.redmine.group.get(1).url, f'{self.url}/groups/1') + def test_group_sets_attrs_from_multiple_attr_id_map(self): + self.response.json.return_value = responses['group']['get'] + group = self.redmine.group.get(1) + group.user_ids = [1, 2] + self.assertEqual(group._decoded_attrs['users'], [{'id': 1}, {'id': 2}]) + def test_role_version(self): self.assertEqual(self.redmine.role.resource_class.redmine_version, (1, 4, 0)) @@ -1464,7 +1610,7 @@ def test_news_export(self): self.response.json.return_value = responses['news']['all'] self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.news.all().export('txt', '/foo/bar'), '/foo/bar/news.txt') + self.assertEqual(self.redmine.news.all().export('txt', '/foo/bar/'), '/foo/bar/news.txt') def test_news_str(self): self.response.json.return_value = responses['news']['filter'] @@ -1495,6 +1641,12 @@ self.response.json.return_value = response_includes self.assertIsInstance(news.comments, list) + def test_news_sets_attrs_from_single_attr_id_map(self): + self.response.json.return_value = responses['news']['get'] + news = self.redmine.news.get(1) + news.project_id = 1 + self.assertEqual(news._decoded_attrs['project'], {'id': 1}) + def test_issue_status_version(self): self.assertEqual(self.redmine.issue_status.resource_class.redmine_version, (1, 3, 0)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redmine-2.4.0/tests/test_resultsets.py new/python-redmine-2.5.0/tests/test_resultsets.py --- old/python-redmine-2.4.0/tests/test_resultsets.py 2022-12-27 14:21:04.000000000 +0100 +++ new/python-redmine-2.5.0/tests/test_resultsets.py 2023-03-03 15:47:52.000000000 +0100 @@ -176,33 +176,34 @@ @mock.patch('redminelib.open', mock.mock_open(), create=True) def test_export(self): self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar'), '/foo/bar/issues.txt') + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar/'), '/foo/bar/issues.txt') @mock.patch('redminelib.open', mock.mock_open(), create=True) def test_export_with_all_columns(self): self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns='all'), '/foo/bar/issues.txt') + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar/', columns='all'), '/foo/bar/issues.txt') self.redmine.ver = (3, 3, 0) - self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns='all'), '/foo/bar/issues.txt') + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar/', columns='all'), '/foo/bar/issues.txt') @mock.patch('redminelib.open', mock.mock_open(), create=True) def test_export_with_all_gui_columns(self): self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns='all_gui'), '/foo/bar/issues.txt') + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar/', columns='all_gui'), '/foo/bar/issues.txt') self.redmine.ver = (3, 3, 0) - self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns='all_gui'), '/foo/bar/issues.txt') + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar/', columns='all_gui'), '/foo/bar/issues.txt') @mock.patch('redminelib.open', mock.mock_open(), create=True) def test_export_with_all_gui_extra_columns(self): + columns = ['all_gui'] self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns=['all_gui']), '/foo/bar/issues.txt') + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar/', columns=columns), '/foo/bar/issues.txt') self.redmine.ver = (3, 3, 0) - self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns=['all_gui']), '/foo/bar/issues.txt') + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar/', columns=columns), '/foo/bar/issues.txt') @mock.patch('redminelib.open', mock.mock_open(), create=True) def test_export_with_custom_columns(self): self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) - self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns=['status']), '/foo/bar/issues.txt') + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar/', columns=['status']), '/foo/bar/issues.txt') def test_export_not_supported_exception(self): self.assertRaises(exceptions.ExportNotSupported, lambda: self.redmine.custom_field.all().export('pdf'))