From dd359372884bee53802ed829a84b919cfb4651d5 Mon Sep 17 00:00:00 2001 From: Scott Wainstock Date: Tue, 27 May 2014 18:13:36 -0700 Subject: [PATCH 1/2] add ability to pass limit param to collection queries --- sandman/sandman.py | 5 ++++- tests/test_sandman.py | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/sandman/sandman.py b/sandman/sandman.py index a39a3ea..e2f5598 100644 --- a/sandman/sandman.py +++ b/sandman/sandman.py @@ -211,6 +211,7 @@ def retrieve_collection(collection, query_arguments=None): if query_arguments: filters = [] order = [] + limit = None for key, value in query_arguments.items(): if key == 'page': continue @@ -218,9 +219,11 @@ def retrieve_collection(collection, query_arguments=None): filters.append(getattr(cls, key).like(str(value), escape='/')) elif key == 'sort': order.append(getattr(cls, value)) + elif key == 'limit': + limit = value elif key: filters.append(getattr(cls, key) == value) - resources = session.query(cls).filter(*filters).order_by(*order) + resources = session.query(cls).filter(*filters).order_by(*order).limit(limit) else: resources = session.query(cls).all() return resources diff --git a/tests/test_sandman.py b/tests/test_sandman.py index 2cc9cca..c7d5a8e 100644 --- a/tests/test_sandman.py +++ b/tests/test_sandman.py @@ -29,7 +29,7 @@ def setup_method(self, _): self.app = app.test_client() #pylint: disable=unused-variable from . import models - + def teardown_method(self, _): """Remove the database file copied during setup.""" os.unlink(self.DB_LOCATION) @@ -72,6 +72,11 @@ def test_get(self): response = self.get_response('/artists', 200) assert len(json.loads(response.get_data(as_text=True))[u'resources']) == 275 + def test_get_with_limit(self): + """Test simple HTTP GET""" + response = self.get_response('/artists', 200, params={'limit': 10}) + assert len(json.loads(response.get_data(as_text=True))[u'resources']) == 10 + def test_get_with_filter(self): """Test simple HTTP GET""" response = self.get_response('/artists', 200, params={'Name': 'AC/DC'}) From 3497d3cdd5dc5b048c8432b25072f0e6e86e7ad6 Mon Sep 17 00:00:00 2001 From: Scott Wainstock Date: Tue, 27 May 2014 19:41:33 -0700 Subject: [PATCH 2/2] allow overriding of __top_level_json_name__ in models to change top level json object name --- README.md | 4 +++- sandman/model/models.py | 6 ++++++ sandman/sandman.py | 23 +++++++++++++++++------ tests/models.py | 1 + tests/test_sandman.py | 6 ++++++ 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1fcbeb0..9c3e799 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,8 @@ register((Artist, Album, Playlist)) ``` And if you wanted to add custom logic for an endpoint? Or change the endpoint -name? Or add validation? All supported. Here's a "fancy" class definition: +name? Or change your top level json object name? Or add validation? All supported. +Here's a "fancy" class definition: ```python class Style(Model): @@ -172,6 +173,7 @@ class Style(Model): __tablename__ = 'Genre' __endpoint__ = 'styles' __methods__ = ('GET', 'DELETE') + __top_level_json_name__ = 'Genres' @staticmethod def validate_GET(resource=None): diff --git a/sandman/model/models.py b/sandman/model/models.py index 7c8cc89..1773dcd 100644 --- a/sandman/model/models.py +++ b/sandman/model/models.py @@ -29,6 +29,12 @@ class Model(object): Default: None """ + __top_level_json_name__ = None + """The top level json text to output for this class + + Default: 'resources' + """ + __methods__ = ('GET', 'POST', 'PATCH', 'DELETE', 'PUT') """override :attr:`__methods__` if you wish to change the HTTP methods this :class:`sandman.model.Model` supports. diff --git a/sandman/sandman.py b/sandman/sandman.py index e2f5598..ed66552 100644 --- a/sandman/sandman.py +++ b/sandman/sandman.py @@ -117,20 +117,31 @@ def _single_resource_html_response(resource): return make_response(render_template('resource.html', resource=resource, tablename=tablename)) -def _collection_json_response(resources, start, stop, depth=0): +def _collection_json_response(cls, resources, start, stop, depth=0): """Return the JSON representation of the collection *resources*. :param list resources: list of :class:`sandman.model.Model`s to render :rtype: :class:`flask.Response` """ + + top_level_json_name = None + if cls.__top_level_json_name__ is not None: + top_level_json_name = cls.__top_level_json_name__ + else: + top_level_json_name = 'resources' + result_list = [] for resource in resources: result_list.append(resource.as_dict(depth)) + + payload = {} if start is not None: - return jsonify(resources=result_list[start:stop]) + payload[top_level_json_name] = result_list[start:stop] else: - return jsonify(resources=result_list) + payload[top_level_json_name] = result_list + + return jsonify(payload) def _collection_html_response(resources, start=0, stop=20): """Return the HTML representation of the collection *resources*. @@ -262,7 +273,7 @@ def resource_created_response(resource): resource.resource_uri()) return response -def collection_response(resources, start=None, stop=None): +def collection_response(cls, resources, start=None, stop=None): """Return a response for the *resources* of the appropriate content type. :param resources: resources to be returned in request @@ -271,7 +282,7 @@ def collection_response(resources, start=None, stop=None): """ if _get_acceptable_response_type() == JSON: - return _collection_json_response(resources, start, stop) + return _collection_json_response(cls, resources, start, stop) else: return _collection_html_response(resources, start, stop) @@ -487,7 +498,7 @@ def get_collection(collection): page = int(request.args['page']) results_per_page = app.config.get('RESULTS_PER_PAGE', 20) start, stop = page * results_per_page, (page +1) * results_per_page - return collection_response(resources, start, stop) + return collection_response(cls, resources, start, stop) @app.route('/', methods=['GET']) @etag diff --git a/tests/models.py b/tests/models.py index f85ed0e..4914087 100644 --- a/tests/models.py +++ b/tests/models.py @@ -51,6 +51,7 @@ class Album(Model): __tablename__ = 'Album' __methods__ = ('POST', 'PATCH', 'DELETE', 'PUT', 'GET') + __top_level_json_name__ = 'Albums' def __str__(self): """Return string representation of *self*.""" diff --git a/tests/test_sandman.py b/tests/test_sandman.py index c7d5a8e..014849e 100644 --- a/tests/test_sandman.py +++ b/tests/test_sandman.py @@ -289,6 +289,12 @@ def test_put_fail_validation(self): 'UnitPrice': 0.99,})) assert response.status_code == 403 + def test_responds_with_top_level_json_name_if_present(self): + """Test top level json element is the one defined on the Model + rather than the string 'resources'""" + response = self.get_response('/albums', 200) + assert len(json.loads(response.get_data(as_text=True))[u'Albums']) == 347 + class TestSandmanValidation(TestSandmanBase): """Sandman tests related to request validation"""