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/ext/__init__.py b/royal/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/royal/ext/sqla.py b/royal/ext/sqla.py new file mode 100644 index 0000000..eb2a65b --- /dev/null +++ b/royal/ext/sqla.py @@ -0,0 +1,163 @@ +import logging + +from pyramid.location import lineage +import royal +from sqlalchemy.orm.collections import MappedCollection + +log = logging.getLogger(__name__) + + +def includeme(config): + config.scan(__name__) + + +class Collection(royal.Collection): + + sa_model = 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() + # TODO pagination + + 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 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 + + # 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 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 diff --git a/royal/renderer.py b/royal/renderer.py index 1b0d6c8..30c3121 100644 --- a/royal/renderer.py +++ b/royal/renderer.py @@ -100,7 +100,7 @@ def callback(context, name, ob): @renderer_adapter('datetime.date') @renderer_adapter('datetime.datetime') def adapt_datetime(o, request): - return o.isoformat() + return o.strftime('%Y-%m-%dT%H:%M:%SZ') # ISO8601 with Zulu marker @renderer_adapter('decimal.Decimal') 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 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()