Skip to content
This repository has been archived by the owner on Oct 14, 2021. It is now read-only.

Commit

Permalink
refactor resource class
Browse files Browse the repository at this point in the history
1. move hypermedia from format to dispatch method
2. add wrap_response method as dispatch argument to support response processing before data formatting
  • Loading branch information
amitnabarro committed Nov 18, 2017
1 parent f8048b9 commit ada6083
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 19 deletions.
115 changes: 114 additions & 1 deletion docs/source/resources.rst
Expand Up @@ -142,7 +142,52 @@ Adding additional links to the resource is done by overriding ``add_hypermedia``
Nested Resources
------------------

Nested resources ...
Nested resources is a technique to extend a resource's endpoints beyond basic CRUD. Every resource automatically exposes the HTTP verbs (GET, POST, PUT, PATCH, DELETE) with their respective methods, adhereing to REST principles.
However, it is sometimes neccesary to extend a resource's functionality by implementing additional endpoints.
These can be described by two categories:
1. Resources which expose nested resources classes
2. Resources which expose additional unrest endpoints serving specific functionality.

Lets look at some examples::

# model representing a user's blog comment. Internal
class Comment(Model):
user = StringField()
content = StringField()

# model representing a single blog post, includes a list of comments
class Blog(Model):
title = StringField()
content = StringField()
comments = ListField(ModelField(Comment))


class CommentResource(ModelResource):
class Meta:
object_class = Comment


class BlogResource(ModelResource):
class Meta:
object_class = Blog

@classmethod
def nested_routes(cls, base_url):
return [
Route(
path=base_url + '%s/comments/add/' % (cls.route_param('pk')),
handler=cls.add_comment,
methods=cls.route_methods(),
name='blog_add_comment')
]

@classmethod
async def add_comment(cls, request, **kwargs):





MongoDB Resources
Expand Down Expand Up @@ -261,6 +306,71 @@ This will result in a usage like so::
/api/books/?fts=history


Hooking up to application's router
------------------------------------
Once a resource has been implemented, it needs to be hooked up to the application's router.
With any web application such as Sanic or AioHttp, adding handlers to the application involves matching a uri to a specific handler method. The ``Resource`` class implements two methods ``to_list`` and ``to_detail`` which create list handlers and detail handlers respectively, for the application router, like so::

app.add_route('GET', '/books', BookResource.as_list())
app.add_route('GET', '/books/{id}', BookResource.as_detail())

The syntax varies a little, depending on the web server used.

Sanic Example
~~~~~~~~~~~~~~

::

from sanic import Sanic
from tbone.resources import Resource
from tbone.resources.sanic import SanicResource


class TestResource(SanicResource, Resource):
async def list(self, **kwargs):
return {
'meta': {},
'objects': [
{'text': 'hello world'}
]
}

app = Sanic()
app.add_route(methods=['GET'], uri='/', handler=TestResource.as_list())

if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)


AioHttp Example
~~~~~~~~~~~~~~~~

::

from aiohttp import web
from tbone.resources import Resource
from tbone.resources.aiohttp import AioHttpResource


class TestResource(AioHttpResource, Resource):
async def list(self, **kwargs):
return {
'meta': {},
'objects': [
{'text': 'hello world'}
]
}

app = web.Application()
app.router.add_get('/', TestResource.as_list())

if __name__ == "__main__":
web.run_app(app, host='127.0.0.1', port=8000)


The examples above demonstrate how to manually add resources to the application router. This can become tedious when the app has multiple resources which expose list and detail endpoints as well as some nested resources.
An alternative way is to use a ``Router`` , described below.

Routers
----------

Expand Down Expand Up @@ -329,3 +439,6 @@ With ``Sanic`` it looks like this::






36 changes: 18 additions & 18 deletions tbone/resources/resources.py
Expand Up @@ -235,7 +235,7 @@ def is_method_allowed(self, endpoint, method):
return True
return False

async def dispatch(self, endpoint, *args, **kwargs):
async def dispatch(self, endpoint, wrap_response=None, *args, **kwargs):
'''
This method handles the actual request to the resource.
It performs all the neccesary checks and then executes the relevant member method which is mapped to the method name.
Expand Down Expand Up @@ -271,7 +271,18 @@ async def dispatch(self, endpoint, *args, **kwargs):
view_method = getattr(self, self.http_methods[endpoint][method])
# call request method
data = await view_method(*args, **kwargs)
# add request_uri
# add hypermedia to the response
if self._meta.hypermedia is True:
if endpoint == 'list':
for item in data['objects']:
self.add_hypermedia(item)
elif endpoint == 'detail':
self.add_hypermedia(data)

# wrap response data
if callable(wrap_response):
data = wrap_response(data)
# format the response object
formatted = self.format(method, endpoint, data)
except Exception as ex:
return self.dispatch_error(ex)
Expand Down Expand Up @@ -299,7 +310,7 @@ def build_response(cls, data, status=200):
Given some data, generates an HTTP response.
If you're integrating with a new web framework, other than sanic or aiohttp, you **MUST**
override this method within your subclass.
:param data:
The body of the response to send
:type data:
Expand Down Expand Up @@ -333,6 +344,8 @@ def get_resource_uri(self):

def parse(self, method, endpoint, body):
''' calls parse on list or detail '''
if isinstance(body, dict): # request body was already parsed
return body
if endpoint == 'list':
return self.parse_list(body)

Expand Down Expand Up @@ -375,26 +388,12 @@ def format(self, method, endpoint, data):
def format_list(self, data):
if data is None:
return ''
if self._meta.hypermedia is True:
# add resource uri
for item in data['objects']:
self.add_hypermedia(item)

return self._meta.formatter.format(data)

def format_detail(self, data):
if data is None:
return ''
if self._meta.hypermedia is True:
self.add_hypermedia(data)

return self._meta.formatter.format(self.get_resource_data(data))

def get_resource_data(self, data):
resource_data = {}
for k, v in data.items():
resource_data[k] = v
return resource_data
return self._meta.formatter.format(data)

@classmethod
def connect_signal_receivers(cls):
Expand Down Expand Up @@ -441,6 +440,7 @@ class ModelResource(Resource):
'''
A specialized resource class for using data models. Requires further implementation for data persistency
'''

def __init__(self, *args, **kwargs):
super(ModelResource, self).__init__(*args, **kwargs)
# verify object class has a declared primary key
Expand Down

0 comments on commit ada6083

Please sign in to comment.