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 <[email protected]>
+
+- 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'))