From a21f5444aa1c3cf7313b20ff638a927b5264f233 Mon Sep 17 00:00:00 2001 From: yidris Date: Mon, 23 Feb 2015 12:20:18 -0500 Subject: [PATCH 1/6] Added sqlalchemy_base extension --- royal/ext/sqlalchemy_base.py | 173 +++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 royal/ext/sqlalchemy_base.py diff --git a/royal/ext/sqlalchemy_base.py b/royal/ext/sqlalchemy_base.py new file mode 100644 index 0000000..4375c50 --- /dev/null +++ b/royal/ext/sqlalchemy_base.py @@ -0,0 +1,173 @@ +import logging + +from pyramid.location import lineage +import royal +from sqlalchemy.orm.collections import MappedCollection + +log = logging.getLogger(__name__) + + +# class LinksMixin(object): + +# @property +# def links(self): +# links = {name: self.root.request.resource_url(self, name) +# for name in self.children} +# links['self'] = self.url() +# return links + + +class Collection(royal.Collection): + sa_model = None + sa_exceptions = None + entity_cls = None + + def __init__(self, name, parent, entities=None): + super(Collection, self).__init__(name, parent) + self.entities = entities + + def __repr__(self): + return '<%s collection at %s named %r>' % (self.__class__.__name__, + id(self), + self.name) + + def load_entities(self): + self.entities = self.entity_cls.all() + + def index(self, params): + if self.entities is None: + self.load_entities() + return self + + def create(self, params): + entity = self.entity_cls(**params) + entity.save() + try: + self.sa_model.flush() + except self.sa_exceptions.DuplicatedEntity: + raise + except Exception: + log.exception('create resource=%r params=%r', self, params) + raise + item = self[entity.id] + item.entity = entity + self.sa_model.commit() + return item + + +class Item(royal.Item): + + sa_model = None + sa_exceptions = None + + # In derived Item classes, specify a model class for singular resources + # that don't belong to a collection. Otherwise, it will be determined from + # the parent resource. + entity_cls = None + + def __init__(self, name, parent, entity=None): + super(Item, self).__init__(name, parent) + self.entity = entity + if self.entity_cls is None and self.parent is not None: + self.entity_cls = self.parent.entity_cls + + def __repr__(self): + return '<%s item at %s named %r>' % (self.__class__.__name__, + id(self), + self.name) + + def on_traversing(self, key): + self.load_entity() + + def load_entity(self): + if self.entity is None: + if self.entity_cls is None: + raise royal.exceptions.NotFound(self) + + # FIXME Naively assume that entity's PK is the list of resource + # __name__ in reversed lineage so PK of /slots/123/symbols/456 is + # (123, 456). Should also be adapted to support resources + # identified by name. + pk = [item.name for item in lineage(self) + if hasattr(item, 'name') + and item.name + and not isinstance(item, Collection)] + pk.reverse() + try: + self.entity = self.entity_cls.get(pk) + except KeyError: + raise royal.exceptions.NotFound(self) + + return self.entity + + def show(self, params): + self.load_entity() + return self + + def delete(self): + self.load_entity().delete() + self.sa_model.commit() + + def update(self, params): + params_copy = params.copy() + entity = self.load_entity() + # Ignore parameters that are part of primary key. + [params_copy.pop(pk.name, '') for pk in entity.__mapper__.primary_key] + for param in params_copy: + try: + getattr(entity, param) + setattr(entity, param, params_copy[param]) + except AttributeError: + pass + try: + self.sa_model.commit() + except self.sa_exceptions.DuplicatedEntity: + raise + except Exception: + log.exception('update resource=%r params=%r', self, params) + raise + return entity + + +@royal.renderer_adapter(Collection) +def adapt_collection(collection, request): + items = [] + if collection.entities is None: + collection.load_entities() + if isinstance(collection.entities, MappedCollection): + for item_id, entity in collection.entities.items(): + item = collection[item_id] + item.entity = entity + items.append(item) + else: + for entity in collection.entities: + item = collection[entity.id] + item.entity = entity + items.append(item) + return { + u'items': items, + u'links': collection.links, + } + + +def render_hyperlink(item): + try: + return {'id': int(item.name)} + except ValueError: + return {'id': item.name} + + +def render_model(item): + entity = item.load_entity() + columns = entity.__table__.columns + return {column.name: getattr(entity, column.name) for column in columns} + + +@royal.renderer_adapter(Item) +def adapt_item(item, request): + if request.is_nested(item): + result = render_hyperlink(item) + else: + result = render_model(item) + result['links'] = item.links + return result From 37f3002231f58a4ed5b1d64061018657818f6308 Mon Sep 17 00:00:00 2001 From: yidris Date: Mon, 23 Feb 2015 12:22:17 -0500 Subject: [PATCH 2/6] changed links implementation --- royal/ext/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 royal/ext/__init__.py diff --git a/royal/ext/__init__.py b/royal/ext/__init__.py new file mode 100644 index 0000000..e69de29 From 2a4a43b7be73a607d358bccf9e06eb53d719847c Mon Sep 17 00:00:00 2001 From: yidris Date: Mon, 23 Feb 2015 13:54:49 -0500 Subject: [PATCH 3/6] Fix links commit Add includeme to sqlalchemy_base --- royal/ext/sqlalchemy_base.py | 10 ++-------- royal/resource.py | 9 ++++----- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/royal/ext/sqlalchemy_base.py b/royal/ext/sqlalchemy_base.py index 4375c50..536c2c7 100644 --- a/royal/ext/sqlalchemy_base.py +++ b/royal/ext/sqlalchemy_base.py @@ -7,14 +7,8 @@ log = logging.getLogger(__name__) -# class LinksMixin(object): - -# @property -# def links(self): -# links = {name: self.root.request.resource_url(self, name) -# for name in self.children} -# links['self'] = self.url() -# return links +def includeme(config): + config.scan(__name__) class Collection(royal.Collection): diff --git a/royal/resource.py b/royal/resource.py index b99fe6b..d8f0fa6 100644 --- a/royal/resource.py +++ b/royal/resource.py @@ -67,7 +67,6 @@ def resource_url(self, resource, request=None, **query_params): request = self.root.request return request.resource_url(resource, **kw) - def url(self, request=None, **query_params): return self.resource_url(self, request, **query_params) @@ -81,10 +80,10 @@ def name(self): @property def links(self): - _links = {name: {'href': cls(name, self).url()} - for name, cls in self.children.iteritems()} - _links['href'] = self.url() - return _links + links = {name: self.root.request.resource_url(self, name) + for name in self.children} + links['self'] = self.url() + return links def on_traversing(self, key): pass From b6a28781b3d3fb30f7c077b9b1a843249c9aeea4 Mon Sep 17 00:00:00 2001 From: yidris Date: Mon, 23 Feb 2015 17:49:48 -0500 Subject: [PATCH 4/6] Rename sqlalchemy_base to sqla --- royal/ext/{sqlalchemy_base.py => sqla.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename royal/ext/{sqlalchemy_base.py => sqla.py} (100%) diff --git a/royal/ext/sqlalchemy_base.py b/royal/ext/sqla.py similarity index 100% rename from royal/ext/sqlalchemy_base.py rename to royal/ext/sqla.py From c83c5baaf2e48ac4819b69984dd0c43d1ec897cc Mon Sep 17 00:00:00 2001 From: yidris Date: Mon, 23 Feb 2015 17:50:23 -0500 Subject: [PATCH 5/6] add missing TODO comment in sqla --- royal/ext/sqla.py | 1 + 1 file changed, 1 insertion(+) diff --git a/royal/ext/sqla.py b/royal/ext/sqla.py index 536c2c7..1058adc 100644 --- a/royal/ext/sqla.py +++ b/royal/ext/sqla.py @@ -27,6 +27,7 @@ def __repr__(self): def load_entities(self): self.entities = self.entity_cls.all() + # TODO pagination def index(self, params): if self.entities is None: From d016a34da6ecf9818839fb4ccdedd7a9be2f096a Mon Sep 17 00:00:00 2001 From: yidris Date: Mon, 23 Feb 2015 18:23:37 -0500 Subject: [PATCH 6/6] Fix tests, update change log --- CHANGES.rst | 2 ++ requirements-test.txt | 1 + royal/tests/functional/test_example.py | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b7e390c..0e73ad7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,8 @@ Changelog Development ----------- +- Add sqlalchemy extension. +- Change the way links are added to resource representation. - Fix issue #6: HTTP 500 error when using POST verb on Item resources. diff --git a/requirements-test.txt b/requirements-test.txt index 3c3b1ab..5beb6e6 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -13,3 +13,4 @@ coveralls pyramid_mongokit >= 0.2 voluptuous +sqlalchemy >= 0.9 diff --git a/royal/tests/functional/test_example.py b/royal/tests/functional/test_example.py index 2d40a03..09c8c15 100644 --- a/royal/tests/functional/test_example.py +++ b/royal/tests/functional/test_example.py @@ -29,8 +29,8 @@ def test_root(self): result = response.json self.assertIn('users', result) self.assertIn('photos', result) - self.assertEqual('http://localhost/users/', result['users']['href']) - self.assertEqual('http://localhost/photos/', result['photos']['href']) + self.assertEqual('http://localhost/users', result['users']) + self.assertEqual('http://localhost/photos', result['photos']) def test_users_index(self): self._add_users()