Your first Sondra API
Introduction
This tutorial serves as a basic introduction to Sondra, its data model, and how you can create an API. The classic example web API is that of a to-do list application. Our tutorial will build it up in stages, starting with the basic structure of an application, followed by data modeling, and then we’ll add exposed methods to see how those work.
Finally, you’ll work through accessing the API via Javascript (ES2015) and Python.
To do this tutorial, you will need the following installed and ready to go:
- Sondra - https://github.com/JeffHeard/sondra.git
- RethinkDB - https://rethinkdb.com
- Python 3 - https://www.python.org
- Your favorite text editor or IDE. I like Atom
First Steps
First thing’s first. Let’s make sure Sondra has all its requirements. Assuming you have a virtual environment setup already, all you have to do is install requirements.
$ cd sondra
$ pip install -r requirements.txt
$ export PYTHONPATH=$PYTHONPATH:$PWD # if you want to develop in a different directory or run the examples.
Let’s start with some imports.
from sondra.collection import Collection
from sondra.document import Document
from sondra.application import Application
from sondra.suite import Suite
from sondra.schema import S
These imports serve to introduce us to the basic building blocks of Sondra: Application, Collection, and Document. We will subclass each to create our application. The Document subclass defines the schema of a record in RethinkDB and all the methods that operate directly on a record instance.
The Collection subclass defines the way a document relates to the database. It defines things such as primary key and indexes and methods that operate on the collection as a whole. Methods defined on the collection class are conceptually similar to class methods in Python.
The Application subclass defines a group of collections that serve a common purpose. The application will often contain schema fragment definitions that are common across multiple related collections and methods that apply to multiple collections across the application.
The Suite subclass defines the complete API suite, including configurations and database connections. Applications are added to the Suite.
Finally S is a utility module whose functions return dictionaries that fit the format of JSON Schema and hyper-schema.
Modeling Data
So let’s start by creating a Document that models a to-do list item:
from sondra.collection import Collection
from sondra.document import Document
from sondra.application import Application
from sondra.suite import Suite
from sondra.schema import S
class Item(Document):
schema = {
"type": "object",
"required": ["title"],
"properties": {
"title": {"type": "string", "description": "The title of the item"},
"complete" {"type": "boolean", "default": False},
"created": {"type": "string", "format": "date-time"}
}
}
Now we’ve defined a simple item that has a title, a space to mark whether the to-do item is
complete, and a creation date. Note that schemas are just Python dicts. As long as your dictionary
and all its elements are compatible with the built-in json
package, your schema will work.
However, this syntax is rather verbose and prone to typo-induced bugs. Therefore the S
package
provides a bunch of utility functions that generate schema objects. These functions return plain old
dicts, so there’s functionally no difference between the two syntaxes, but the S module is safer:
class Item(Document):
schema = S.object(
required=['Title'],
properties=S.props(
("title", S.string(description="The title of the item")),
("complete", S.boolean(default=False)),
("created", S.datetime()),
))
That’s much shorter! Again, there’s no magic to the S.*
functions. They’re just shorthand and
provide a bit more checking. It’s the same schema as above. One difference is that the first
example does not enforce any order (except in Python 3.6!) on the dictionary elements. The version
in the most recent example does, because S.props
uses an OrderedDict
instead of a plain dict
Now let’s see how to get it into a collection and an application, and thus into the database. The
next three classes define the collection (RethinkDB table), application (RethinkDB database), and
the suite (the full group of applications, served on a single tree from a single domain):
class Items(Collection):
document_class = Item # this is the class that as instances per-record
indexes = ["title", "complete"] # the fields to build indexes on.
order_by = ["created"] # Sondra treats "format": "date-time" as a RethinkDB Date.
class TodoApp(Application):
collections = (Items,) # For now we'll just define the collections
class TodoSuite(Suite):
cross_origin = True # append CORS headers
debug = True # extra logging
Getting it into RethinkDB
Now that we have all the definitions set up, let’s get our data into the database:
>>> todo = TodoSuite()
>>> TodoApp(todo)
>>> todo.validate()
>>> todo.ensure_database_objects()
INFO:TodoSuite:Connection established to 'default'
INFO:TodoSuite:Suite base url is: 'http://localhost:5000/api'
INFO:TodoSuite:Docstring processor is {0}
INFO:TodoApp:Registering application todo-app
INFO:TodoSuite:Registered application TodoApp to http://localhost:5000/api/todo-app
INFO:TodoApp:Creating collection for todo-app/items
INFO:TodoSuite:Checking schemas for validity
INFO:TodoSuite:+ todo-app
INFO:TodoSuite:--- items
The call to todo.ensure_database_objects()
creates / ensures the existence of tables and indexes and even
databases for everything that’s a part of the suite. It should always be called once when your
application initializes. At the most basic level, you’re now ready to expose your API to your
clients now (we’ll handle authentication later), and you can play around with it in Python as well.
In Python, Sondra tries to be as pythonic as possible. All levels of the API are exposed as
dictionary-like objects. The keys are dash-cased versions of the classnames (this can be overridden,
but is the default).