Tutorial¶
Welcome to the pyresto tutorial. This tutorial will guide you through
the development of a REST interface for
the Github API. The
implementation can be found in
the Pyresto source repository in the
pyresto.apis.github
module.
The Base¶
Start off by creating a base model class for the service you are using which
will hold the common values such as the API host, the common model
representation using __repr__
etc:
class GitHubModel(Model):
_url_base = 'https://api.github.com'
def __repr__(self):
if hasattr(self, '_links'):
desc = self._links['self']
elif hasattr(self, 'url'):
desc = self.url
else:
desc = self._current_path
return '<GitHub.{0} [{1}]>'.format(self.__class__.__name__, desc)
Simple Models¶
Then continue with implementing simple models which does not refer to any other
model, such as the Comment
model for GitHub:
class Comment(GitHubModel):
_path = '/repos/{repo_name}/comments/{id}'
_pk = ('repo_name', 'id')
Note that we didn’t define any attributes except for the mandatory _path
and _pk
attributes since pyresto automatically fills all attributes
provided by the server response. This inhibits any possible efforts to
implement client side verification though since the server already verifies all
the requests made to it, and results in simpler code. This also makes the
models “future-proof” and conforms to the best practices for “real” RESTful or
Hypermedia APIs, which many recently started to use as a term instead of “real
RESTful”.
Relations¶
After defining some “simple” models, you can start implementing models having relations with each other:
class Commit(GitHubModel):
_path = '/repos/{repo_name}/commits/{sha}'
_pk = ('repo_name', 'sha')
comments = Many(Comment, '{self._current_path}/comments?per_page=100')
Note that we used the attribute name comments
which will “shadow” any
attribute named “comments” sent by the server as documented in
Model
, so be wise when you are choosing your
relation names and use the ones provided by the
service documentation if
there are any.
Note that we used the Many
relation here. We provided the
model class itself, which will be the class of all the items in the collection
and, the path to fetch the collection. We used commit.url
in the path
format where commit
will be the commit instance we are bound to, or to be
clear, the commit “resource” which we are trying to get the comments of.
Since we don’t expect many comments for a given commit, we used the default
Many
implementation which will result in a
WrappedList
instance that can be considered as a
list
. This will cause a chain of requests when this attribute is first
accessed until all the comments are fetched and no “next” link can be extracted
from the Link
header. See
Model._continuator
for more info on this.
If we were expecting lots of items to be in the collection, or an unknown
number of items in the collection, we could have used lazy=True
like this:
class Repo(GitHubModel):
_path = '/repos/{full_name}'
_pk = 'full_name'
commits = Many(Commit, '{self._current_path}/commits?per_page=100', lazy=True)
comments = Many(Comment, '{self._current_path}/comments?per_page=100')
tags = Many(Tag, '{self._current_path}/tags?per_page=100')
branches = Many(Branch, '{self._current_path}/branches?per_page=100')
keys = Many(Key, '{self._current_path}/keys?per_page=100')
Using lazy=True
will result in a LazyList
type of
field on the model when accessed, which is basically a generator. So you can
iterate over it but you cannot directly access a specific element by index or
get the total length of the collection.
You can also use the Foreign
relation to refer to
other models:
class Tag(GitHubModel):
_path = '/repos/{repo_name}/tags/{name}'
_pk = ('repo_name', 'name')
commit = Foreign(Commit, embedded=True)
When used in its simplest form, just like in the code above, this relation
expects the primary key value for the model it is referencing, Commit
here,
to be provided by the server under the same name. So we expect from GitHub
API to provide the commit sha, which is the primary key for Commit
models,
under the label commit
when we fetch the data for a Tag
. When this
property is accessed, a simple Model.get
call is made
on the Commit
model, which fetches all the data associated with the it and
puts them into a newly created model instance.
Late Bindings¶
Since all relation types expect the class object itself for relations, it is not always possible to put all relation definitions inside the class definition. For those cases, you can simply late bind the relations as follows:
# Late bindings due to circular references
Commit.committer = Foreign(User, '__committer', embedded=True)
Commit.author = Foreign(User, '__author', embedded=True)
Repo.contributors = Many(User,
'{self._current_path}/contributors?per_page=100')
Repo.owner = Foreign(User, '__owner', embedded=True)
Repo.watcher_list = Many(User, '{self._current_path}/watchers?per_page=100')
User.follower_list = Many(User, '{self._current_path}/followers?per_page=100')
User.watched = Many(Repo, '{self._current_path}/watched?per_page=100')
Authentication¶
Most services require authentication even for only fetching data so providing means of authentication is essential. Define the possible authentication mechanisms for the service:
from ...auth import HTTPBasicAuth, AppQSAuth, AuthList, enable_auth
# Define authentication methods
auths = AuthList(basic=HTTPBasicAuth, app=AppQSAuth)
Make sure you use the provided authentication classes by requests.auth
if they suit your needs. If you still need a custom authentication class, make
sure you derive it from Auth
.
After defining the authentication methods, create a module-global function that will set the default authentication method and credentials for all requests for convenience:
# Enable and publish global authentication
auth = enable_auth(auths, GitHubModel, 'app')
Above, we provide the list of methods/classes we have previously defined, the base class for our service since all other models inherit from that and will use the authentication defined on that, unless overridden. And we also set our default authentication mechanism to remove the burden from the shoulders of the users of our API library.