Resources¶
Resources are at the heart of the TBone framework. They provide the foundation for the application’s communication with its consumers and facilitate its API. Resources are designed to implement a RESTful abstraction layer over HTTP and Websockets protocols and assist in the creation of your application’s design and infrastructure.
Overview¶
Resources are class-based. A single resource class implements all the methods required to communicate with your API over HTTP or Websocket, using HTTP-like verbs such as GET
and POST
. In addition it implements resource events which translate to application events sent over websockets to the consumer.
A Resource
subclass must implement all the methods it expects to respond to.
The following table lists the HTTP verbs and their respective member methods:
HTTP Verb | Resource subclass method to implement |
---|---|
GET | list() |
GET <pk> | detail() |
POST | create() |
POST <pk> | create_detail() |
PUT | update_list() |
PUT <pk> | update() |
PATCH | modify_list() |
PATCH <pk> | modify() |
DELETE | delete_list() |
DELETE <pk> | delete() |
Sanic and AioHttp¶
TBone includes two mixin classes to adapt your resources to the underlying web-server of your application.
Those are SanicResource
and AioHttpResource
.
Every resource class in your application must include one of those mixin classes, respective to your application’s HTTP and Websockets infrastructure. These mixin classes implement the specifics pertaining to their respective libraries and leave the developer with the work on implementing the application’s domain functionality.
If your application is based on Sanic
your resources will be defined like so:
class MyResource(SanicResource, Resource):
...
If your application is based on AioHttp
your resources will be defined like so:
class MyResource(AioHttpResource, Resource):
...
Note
Adapting a resource class is done with mixins rather than with single inheritance. The reason is so developers can bind the correct resource adapter to a Resource
derived class or classes that are derived from other base resources such as MongoResource
.
It obviously makes no sense to have resources mixed with both SanicResource
and AioHttpResource
in the same project.
Resource Options¶
Every resource has a ResourceOptions
class associated with it, that provides the default options related to the resource.
Such options can be overriden using the Meta
class within the resource class itself, like so:
from tbone.resources import Resource
class MyResource(Resource):
class Meta:
allowed_detail = ['get', 'post'] # In this example, only GET and POST methods are allowed
Resource options are essential to resources who wish to override built-in functionality such as:
- Serialization
- Authentication
- Allowed methods
For a full list of resource options see the API Reference
Formatters¶
Formatters are classes which help to convert Python dict
objects to text (or binary), and back, using a certain transport protocol.
In TBone terminology, formatting turns an native Python object into another representation, such as JSON or XML. Parsing is turning JSON or XML into native Python object.
Formatters are used by resource objects to convert data into a format which can be wired over the net. When using the HTTP protocol, generally APIs expose data in a text-based format.
By default, TBone formats and parses objects to and from a JSON representation. However, developers can override this behavior by writing additional Formatter
classes to suit their needs.
Authentication¶
TBone provides an authentication mechanism which is wired into the resource’s flow. All requests made on a resource are routed through a central dispatch
method. Before the request is executed an authentication mechanism is activated to determine if the request is allowed to be processed. Therefore, every resource has an Authentication
object associated with it. This is done using the Meta
class of the resource, like so:
class BookResource(Resource):
class Meta:
authentication = Authentication()
By default, all resources are associated with a NoAuthentication
class, which does not check for any authentication whatsoever. Developers need to subclass NoAuthentication
to add their own authentication mechanism. Authentication classes implement a single method is_authenticated
which has the request object passed. Normally, developers would use the request headers to check for authentication and return True
or False
based on the content of the request.
HATEOAS¶
HATEOAS (Hypermedia as the Engine of Application State) is part of the REST specification.
TBone supports basic HATEOAS directives and allows for extending this support in resource subclasses.
By default, all TBone resources include a _links
key in their serialized form, which contains a unique href
to the resource itself, like so:
{
"first_name': 'Ron",
"last_name': 'Burgundy",
"_links" : {
"self" : {
"href" : "/api/person/1/"
}
}
}
Disabling HATEOAS support is done per resource, by setting the hypermedia
flag in the ResourceOptions
class to False
, like so:
class NoHypermediaPersonResource(Resource):
class Meta:
hypermedia = False
...
Adding additional links to the resource is done by overriding add_hypermedia
on the resource subclass.
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:
- Resources which expose nested resources classes
- 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¶
The MongoResource
class provides out-of-the-box CRUD functionality over your MongoDB collections with as little as three lines of code, like so:
from tbone.resources.mongo import MongoResource
class BookResource(AioHttpResource, MongoResource):
class Meta:
object_class = Book
Important
TBone is not aware of how you manage your application’s global infrastructure. Therefore Resources and Models are not aware of your database’s handle. Because of that, TBone makes the assumption that your global app
object is attached to every request
object, which both Sanic
and AioHttp
do by default. it also assumes that the database handler is assigned to the global app
object, which you must handle yourself, like so:
app.db = connect(...)
See TBone examples for more details
CRUD¶
The MongoResource
class provides out-of-the-box CRUD operations on your data models. As mentioned in the Persistency section, models are mapped to MongoDB collections.
This allows for HTTP verbs are to be mapped directly to a MongoDB collection’s core functionality.
The following table lists the way HTTP verbs are mapped to MongoDB collections
HTTP Verb | MongoDB Collection method |
---|---|
GET | find() find_one() |
POST | insert() |
PUT | save() |
PATCH | find_and_modify() |
DELETE | delete() |
Filtering¶
The MongoResource
provides a mapping mechanism between url parameters and MongoDB query parameters.
Therefore, the url:
/api/v1/movies/?genre=drama
Will be mapped to:
coll.find(query={"genre": "drama"})
Passing additional parameters to the url will add additional parameters to the query.
In addition, it is possible to also add the query operator to the urls parameters.
Operators are added to the url parameters using a double underscore __
like so:
/api/v1/movies/?rating__gt=4
Which will be mapped to:
coll.find(query={{"rating": {"$gt": 4}})
Sorting¶
Sorting works very similar to filtering, by passing url parameters which are mapped to the sort parameter like so:
/api/v1/member/?order_by=age
Which will be mapped to:
coll.find(sort={'age': 1}) # pymongo.ASCENDING
Placing the - sign befor ethe sorted field’s name will sort the collection in decending order like so:
/api/v1/member/?order_by=-age
Which will be mapped to:
coll.find(sort={'age': -1}) # pymongo.DESCENDING
Full Text Search¶
The MongoResource
class provides an easy hook between url parameters and a full-text-search query.
However, full text search is not available on a collection by default. In order to utilize MongoDB’s FTS functionality the proper indices must be configured within the collection. Please consult with the MongoDB documentation on using text indices as well as TBone’s documentation on defining indices as part of a Model
.
FTS (full text search) is provided out-of-the-box on all MongoResource
classes, provided the relevant indices are in place.
FTS can be used using query parameters like so:
/api/books/?q=history
This will execute a FTS query on all fields that were indexed with the text index. FTS takes presedence over standard filters, which means that if the url parameters include both FTS and filters, FTS will be executed.
The default operator for accessing FTS is q
. However, this can overriden in the Meta
class by overriding the option fts_operator
like so:
class BookResource(SanicResource, MongoResource):
class Meta:
object_class = Book
fts_operator = 'fts'
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¶
Routers are optional components which help to bind resources to the application’s url router. Whether you’re using Sanic
or AioHttp
every application must have its url routes defined.
The fact that AioHttp uses a centralized system of defining routes, similar to Django
, while Sanic uses a de-centralized system of defining routes, in the form of decorators, bears no difference.
Resources are registered with routers. A router may have one or more resources registered with it. An application can have one or more routers defined.
Note
For small applications a single router for all your resources may be good enough. Larger applications may want to use multiple routers in order to seperate the application’s components, similar to the way a Django project may contain multiple apps. It is up to the developers to decide how many routes are needed in their projects.
A router may have an optional path
variable which the router prepends to all resources.
Resources are registered with a router like so:
class AccountResource(AioHttpResource, Resource):
...
class PublicUserResource(AioHttpResource, Resource):
...
router = Router(name='api/user') # api/user is the url prefix of all resources under this router
router.register(AccountResource, 'account') # the full url would be api/user/account/
router.register(PublicUserResource, 'public_user') # the full url would be api/user/public_user/
Once the router is created, the urls need to be added to the application’s urls.
With AioHttp
it looks like this:
app = web.Application()
.
.
.
for route in router.urls():
app.router.add_route(
method=route.methods,
path=route.path,
handler=route.handler,
name=route.name
)
With Sanic
it looks like this:
app = Sanic()
.
.
.
for route in router.urls():
app.add_route(
methods=route.methods,
uri=route.path,
handler=route.handler
)