Skip to content

Commit 67fe322

Browse files
committed
ResourceManager refactoring
1 parent 686f8dd commit 67fe322

9 files changed

Lines changed: 169 additions & 71 deletions

File tree

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ Changelog
99
- ResourceSet's ``filter()`` method became more advanced. It is now possible to filter on all available
1010
resource attributes, to follow resource relationships and apply lookups to the filters (see
1111
`docs <https://python-redmine.com/introduction.html#methods>`__ for details)
12+
- ResourceManager class has been refactored:
13+
14+
* ``manager_class`` attribute on the ``Resource`` class can now be used to assign a separate
15+
``ResourceManager`` to a resource, that allows outsourcing a resource specific functionality to a
16+
separate manager class (see ``WikiPageManager`` as an example)
17+
* ``request()`` method has been removed
18+
* ``_construct_*_url()``, ``_prepare_*_request()``, ``_process_*_response()`` methods have been added
19+
for create, update and delete methods to allow a fine-grained control over these operations
20+
1221
- Ability to upload file-like objects (`Issue #186 <https://github.com/maxtepkeev/python-redmine/issues/
1322
186>`__) (thanks to `hjpotter92 <https://github.com/hjpotter92>`__)
1423
- Support for retrieving project's time entry activities (see `docs <https://python-redmine.com/resources/

redminelib/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,17 @@ def __getattr__(self, resource_name):
5353
if resource_name.startswith('_'):
5454
raise AttributeError
5555

56-
return managers.ResourceManager(self, resource_name)
56+
resource_name = ''.join(word[0].upper() + word[1:] for word in str(resource_name).split('_'))
57+
58+
try:
59+
resource_class = resources.registry[resource_name]['class']
60+
except KeyError:
61+
raise exceptions.ResourceError
62+
63+
if self.ver is not None and LooseVersion(str(self.ver)) < LooseVersion(resource_class.redmine_version):
64+
raise exceptions.ResourceVersionMismatchError
65+
66+
return resource_class.manager_class(self, resource_class)
5767

5868
@contextlib.contextmanager
5969
def session(self, **options):

redminelib/managers/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
Defines manager classes.
3+
"""
4+
5+
from .base import ResourceManager
6+
from .standard import WikiPageManager
Lines changed: 114 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,25 @@
11
"""
2-
Defines manager classes.
2+
Defines base Redmine resource manager class and it's infrastructure.
33
"""
44

5-
from distutils.version import LooseVersion
6-
7-
from . import utilities, resources, resultsets, exceptions
5+
from .. import utilities, resultsets, exceptions
86

97

108
class ResourceManager(object):
119
"""
12-
Manages Redmine resource defined by the resource_name with the help of redmine object.
10+
Manages given Redmine resource class with the help of redmine object.
1311
"""
14-
def __init__(self, redmine, resource_name, **params):
12+
def __init__(self, redmine, resource_class):
1513
"""
1614
:param redmine.Redmine redmine: (required). Redmine object.
17-
:param string resource_name: (required). Resource name.
18-
:param dict params: (optional). Parameters used for resources retrieval.
15+
:param resources.BaseResource resource_class: (required). Resource class.
1916
"""
20-
resource_name = ''.join(word[0].upper() + word[1:] for word in str(resource_name).split('_'))
21-
22-
try:
23-
resource_class = resources.registry[resource_name]['class']
24-
except KeyError:
25-
raise exceptions.ResourceError
26-
27-
if redmine.ver is not None and LooseVersion(str(redmine.ver)) < LooseVersion(resource_class.redmine_version):
28-
raise exceptions.ResourceVersionMismatchError
29-
3017
self.url = ''
18+
self.params = {}
3119
self.container = None
32-
self.params = params
3320
self.redmine = redmine
3421
self.resource_class = resource_class
3522

36-
def request(self, is_bulk, **params):
37-
"""
38-
Makes request(s) and additionally checks for resource specific stuff.
39-
40-
:param bool is_bulk: (required). Whether this is a bulk or single request.
41-
:param dict params: (optional). Parameters used for resource retrieval.
42-
"""
43-
try:
44-
if not is_bulk:
45-
return self.redmine.engine.request('get', self.url, params=params)[self.container]
46-
else:
47-
return self.redmine.engine.bulk_request('get', self.url, self.container, **self.params)
48-
except exceptions.ResourceNotFoundError as e:
49-
# This is the only place we're checking for ResourceRequirementsError
50-
# because for some POST/PUT/DELETE requests Redmine may also return 404
51-
# status code instead of 405 which can lead us to improper decisions
52-
if self.resource_class.requirements:
53-
raise exceptions.ResourceRequirementsError(self.resource_class.requirements)
54-
raise e
55-
5623
def to_resource(self, resource):
5724
"""
5825
Converts resource data to Resource object.
@@ -83,7 +50,9 @@ def new_manager(self, resource_name, **params):
8350
:param string resource_name: (required). Resource name.
8451
:param dict params: (optional). Parameters used for resources retrieval.
8552
"""
86-
return ResourceManager(self.redmine, resource_name, **params)
53+
manager = getattr(self.redmine, resource_name)
54+
manager.params = params
55+
return manager
8756

8857
def get(self, resource_id, **params):
8958
"""
@@ -109,7 +78,13 @@ def get(self, resource_id, **params):
10978

11079
self.params = self.resource_class.bulk_decode(params, self)
11180
self.container = self.resource_class.container_one
112-
return self.to_resource(self.request(False, **self.params))
81+
82+
try:
83+
return self.to_resource(self.redmine.engine.request('get', self.url, params=self.params)[self.container])
84+
except exceptions.ResourceNotFoundError as e:
85+
if self.resource_class.requirements:
86+
raise exceptions.ResourceRequirementsError(self.resource_class.requirements)
87+
raise e
11388

11489
def all(self, **params):
11590
"""
@@ -146,6 +121,22 @@ def filter(self, **filters):
146121
self.params = self.resource_class.bulk_decode(filters, self)
147122
return resultsets.ResourceSet(self)
148123

124+
def _construct_create_url(self, path):
125+
"""
126+
Constructs URL for create method.
127+
128+
:param string path: absolute URL path.
129+
"""
130+
return self.redmine.url + path
131+
132+
def _prepare_create_request(self, request):
133+
"""
134+
Makes the necessary preparations for create request data.
135+
136+
:param dict request: Request data.
137+
"""
138+
return {self.container: self.resource_class.bulk_decode(request, self)}
139+
149140
def create(self, **fields):
150141
"""
151142
Creates a new resource in Redmine and returns created Resource object on success.
@@ -161,30 +152,50 @@ def create(self, **fields):
161152
formatter = utilities.MemorizeFormatter()
162153

163154
try:
164-
url = self.redmine.url + formatter.format(self.resource_class.query_create, **fields)
165-
except KeyError as exception:
166-
raise exceptions.ValidationError('{0} field is required'.format(exception))
167-
168-
self.container = self.resource_class.container_create
169-
data = {self.resource_class.container_create: self.resource_class.bulk_decode(formatter.unused_kwargs, self)}
170-
response = self.redmine.engine.request(self.resource_class.http_method_create, url, data=data)
171-
172-
try:
173-
resource = self.to_resource(response[self.container])
174-
except TypeError:
175-
raise exceptions.ValidationError('Resource already exists') # fix for repeated PUT requests (issue #182)
155+
url = self._construct_create_url(formatter.format(self.resource_class.query_create, **fields))
156+
except KeyError as e:
157+
raise exceptions.ValidationError('{0} field is required'.format(e))
176158

177159
self.params = formatter.used_kwargs
160+
self.container = self.resource_class.container_create
161+
request = self._prepare_create_request(formatter.unused_kwargs)
162+
response = self.redmine.engine.request(self.resource_class.http_method_create, url, data=request)
163+
resource = self._process_create_response(request, response)
178164
self.url = self.redmine.url + self.resource_class.query_one.format(resource.internal_id, **fields)
179165
return resource
180166

167+
def _process_create_response(self, request, response):
168+
"""
169+
Processes create response and constructs resource object.
170+
171+
:param dict request: Original request data.
172+
:param any response: Response received from Redmine for this request data.
173+
"""
174+
return self.to_resource(response[self.container])
175+
176+
def _construct_update_url(self, path):
177+
"""
178+
Constructs URL for update method.
179+
180+
:param string path: absolute URL path.
181+
"""
182+
return self.redmine.url + path
183+
184+
def _prepare_update_request(self, request):
185+
"""
186+
Makes the necessary preparations for update request data.
187+
188+
:param dict request: Request data.
189+
"""
190+
return {self.resource_class.container_update: self.resource_class.bulk_decode(request, self)}
191+
181192
def update(self, resource_id, **fields):
182193
"""
183194
Updates a Resource object by resource id.
184195
185196
:param resource_id: (required). Resource id.
186197
:type resource_id: int or string
187-
:param dict fields: (optional). Fields which will be updated for the resource.
198+
:param dict fields: (optional). Fields that will be updated for the resource.
188199
"""
189200
if self.resource_class.query_update is None or self.resource_class.container_update is None:
190201
raise exceptions.ResourceBadMethodError
@@ -196,18 +207,44 @@ def update(self, resource_id, **fields):
196207

197208
try:
198209
query_update = formatter.format(self.resource_class.query_update, resource_id, **fields)
199-
except KeyError as exception:
200-
param = exception.args[0]
210+
except KeyError as e:
211+
param = e.args[0]
201212

202213
if param in self.params:
203214
fields[param] = self.params[param]
204215
query_update = formatter.format(self.resource_class.query_update, resource_id, **fields)
205216
else:
206-
raise exceptions.ValidationError('{0} argument is required'.format(exception))
217+
raise exceptions.ValidationError('{0} argument is required'.format(e))
207218

208-
url = self.redmine.url + query_update
209-
data = {self.resource_class.container_update: self.resource_class.bulk_decode(formatter.unused_kwargs, self)}
210-
return self.redmine.engine.request(self.resource_class.http_method_update, url, data=data)
219+
url = self._construct_update_url(query_update)
220+
request = self._prepare_update_request(formatter.unused_kwargs)
221+
response = self.redmine.engine.request(self.resource_class.http_method_update, url, data=request)
222+
return self._process_update_response(request, response)
223+
224+
def _process_update_response(self, request, response):
225+
"""
226+
Processes update response.
227+
228+
:param dict request: Original request data.
229+
:param any response: Response received from Redmine for this request data.
230+
"""
231+
return response
232+
233+
def _construct_delete_url(self, path):
234+
"""
235+
Constructs URL for delete method.
236+
237+
:param string path: absolute URL path.
238+
"""
239+
return self.redmine.url + path
240+
241+
def _prepare_delete_request(self, request):
242+
"""
243+
Makes the necessary preparations for delete request data.
244+
245+
:param dict request: Request data.
246+
"""
247+
return self.resource_class.bulk_decode(request, self)
211248

212249
def delete(self, resource_id, **params):
213250
"""
@@ -221,12 +258,22 @@ def delete(self, resource_id, **params):
221258
raise exceptions.ResourceBadMethodError
222259

223260
try:
224-
url = self.redmine.url + self.resource_class.query_delete.format(resource_id, **params)
225-
except KeyError as exception:
226-
raise exceptions.ValidationError('{0} argument is required'.format(exception))
261+
url = self._construct_delete_url(self.resource_class.query_delete.format(resource_id, **params))
262+
except KeyError as e:
263+
raise exceptions.ValidationError('{0} argument is required'.format(e))
227264

228-
return self.redmine.engine.request(
229-
self.resource_class.http_method_delete, url, params=self.resource_class.bulk_decode(params, self))
265+
request = self._prepare_delete_request(params)
266+
response = self.redmine.engine.request(self.resource_class.http_method_delete, url, params=request)
267+
return self._process_delete_response(request, response)
268+
269+
def _process_delete_response(self, request, response):
270+
"""
271+
Processes delete response.
272+
273+
:param dict request: Original request data.
274+
:param any response: Response received from Redmine for this request data.
275+
"""
276+
return response
230277

231278
def search(self, query, **options):
232279
"""
@@ -246,5 +293,5 @@ def __repr__(self):
246293
"""
247294
Official representation of a ResourceManager object.
248295
"""
249-
return '<{0}.{1} object for {2} resource>'.format(
250-
self.__class__.__module__, self.__class__.__name__, self.resource_class.__name__)
296+
return '<redminelib.managers.{0} object for {1} resource>'.format(
297+
self.__class__.__name__, self.resource_class.__name__)

redminelib/managers/standard.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""
2+
Defines standard Redmine resources managers.
3+
"""
4+
5+
from . import ResourceManager
6+
from .. import exceptions
7+
8+
9+
class WikiPageManager(ResourceManager):
10+
def _process_create_response(self, request, response):
11+
if response is True:
12+
raise exceptions.ValidationError('Resource already exists') # issue #182
13+
14+
return super(WikiPageManager, self)._process_create_response(request, response)

redminelib/resources/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from datetime import date, datetime
88

9-
from .. import utilities, exceptions
9+
from .. import managers, utilities, exceptions
1010

1111

1212
registry = {}
@@ -100,6 +100,7 @@ class BaseResource(utilities.with_metaclass(Registrar)):
100100
http_method_create = 'post'
101101
http_method_update = 'put'
102102
http_method_delete = 'delete'
103+
manager_class = managers.ResourceManager
103104

104105
_repr = [['id', 'name']]
105106
_includes = []

redminelib/resources/standard.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from distutils.version import LooseVersion
88

99
from . import BaseResource
10-
from .. import exceptions
10+
from .. import managers, exceptions
1111

1212

1313
class Project(BaseResource):
@@ -246,6 +246,7 @@ class WikiPage(BaseResource):
246246
query_delete = '/projects/{project_id}/wiki/{0}.json'
247247
search_hints = ['wiki-page']
248248
http_method_create = 'put'
249+
manager_class = managers.WikiPageManager
249250

250251
_repr = [['title']]
251252
_includes = ['attachments']

redminelib/resultsets.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,15 @@ def __iter__(self):
105105
if self._resources is None:
106106
self.manager.params.setdefault('limit', self.limit)
107107
self.manager.params.setdefault('offset', self.offset)
108-
self._resources, self._total_count = self.manager.request(True)
108+
109+
try:
110+
self._resources, self._total_count = self.manager.redmine.engine.bulk_request(
111+
'get', self.manager.url, self.manager.container, **self.manager.params)
112+
except exceptions.ResourceNotFoundError as e:
113+
if self.manager.resource_class.requirements:
114+
raise exceptions.ResourceRequirementsError(self.manager.resource_class.requirements)
115+
raise e
116+
109117
resources = self._resources
110118
# Otherwise ResourceSet object should handle slicing by itself
111119
elif self._is_sliced:

tests/test_managers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,13 @@ def test_create_validation_exception_via_put(self):
229229
def test_reraises_not_found_exception(self):
230230
self.response.status_code = 404
231231
self.assertRaises(exceptions.ResourceNotFoundError, lambda: self.redmine.project.get('non-existent-project'))
232+
self.assertRaises(exceptions.ResourceNotFoundError, lambda: list(self.redmine.project.all()))
232233

233234
def test_resource_requirements_exception(self):
234235
FooResource.requirements = ('foo plugin', ('bar plugin', '1.2.3'),)
235236
self.response.status_code = 404
236237
self.assertRaises(exceptions.ResourceRequirementsError, lambda: self.redmine.foo_resource.get(1))
238+
self.assertRaises(exceptions.ResourceRequirementsError, lambda: list(self.redmine.foo_resource.all()))
237239

238240
def test_search(self):
239241
self.response.json.return_value = {'total_count': 1, 'offset': 0, 'limit': 0, 'results': [

0 commit comments

Comments
 (0)