IntroductionΒΆ
Let’s say you just installed Audrey and used its starter scaffold to create
a new project (as described in Creating a new project). You’d have two
example object types (Person
and Post
) to play with.
(As you read this section, if you find yourself wondering things like
how the types and their schemas are defined, you may want to jump ahead to the Resource Modelling section.)
Let’s take a look around in a pshell session:
$ pshell development.ini#main
Python 2.7.3 (default, Nov 13 2012, 15:00:33)
[GCC 4.4.5] on linux2
Type "help" for more information.
Environment:
app The WSGI application.
registry Active Pyramid registry.
request Active request object.
root Root of the default resource tree.
root_factory Default root factory used to create `root`.
>>> root
<myproject.resources.Root object at 0x9d35a6c>
>>> root.get_collection_names()
['people', 'posts']
OK. So we have a Root object with two collections named “people” and “posts”. Let’s check out one of those:
>>> people = root['people']
>>> people
<myproject.resources.People object at 0xa26c04c>
>>> people.get_children()
[]
Look’s like there aren’t any people yet. So let’s create one:
>>> from myproject import resources
>>> person = resources.Person(request)
>>> print person
{'_created': None,
'_etag': None,
'_id': None,
'_modified': None,
'firstname': None,
'lastname': None,
'photo': None}
Kinda boring. But let’s see what would happen if we tried to save it (by
adding it to the people
collection):
>>> people.add_child(person)
... traceback omitted ...
Invalid: {'firstname': u'Required', 'lastname': u'Required'}
That’s a colander.Invalid
exception letting us know that schema
validation failed. Let’s set the required attributes and try again:
>>> person.firstname = 'Audrey'
>>> person.lastname = 'Horne'
>>> people.add_child(person)
>>> print person
{'_created': datetime.datetime(2012, 12, 24, 1, 52, 45, 281718, tzinfo=<UTC>),
'_etag': '52779a9953bd01defd439bd29874c3d4',
'_id': ObjectId('50d7b56dbf90af0e96bc8433'),
'_modified': datetime.datetime(2012, 12, 24, 1, 52, 45, 281718, tzinfo=<UTC>),
'firstname': 'Audrey',
'lastname': 'Horne',
'photo': None}
The object has been persisted in MongoDB and now has an ObjectId, creation and modification timestamps and an Etag. (It was also indexed in ElasticSearch.) Let’s check the children of the People
collection again:
>>> people.get_children()
[<myproject.resources.Person object at 0xa26cbac>]
As sort of an aside, we can traverse to the new Person object by the string version of its ID like this:
>>> root['people']['50d7b56dbf90af0e96bc8433']
<myproject.resources.Person object at 0xa1f4d6c>
>>> person.__name__
'50d7b56dbf90af0e96bc8433'
>>> person.__parent__
<myproject.resources.People object at 0xa26c04c>
Note
Using the ID as the __name__ is the behavior of the base Audrey Object
and Collection
types. There exist subclasses NamedObject
and NamingCollection
that allow for explicit control over naming. Whether you use one or the other depends on your use case. For this introduction, I opted to keep it minimal and use the base classes.
Let’s add a couple more Person objects to make things a little more interesting. We can pass kwargs to the object constructor to initialize attributes:
>>> people.add_child(resources.Person(request, firstname='Laura', lastname='Palmer'))
>>> people.add_child(resources.Person(request, firstname='Dale', lastname='Cooper'))
>>> [child.get_title() for child in people.get_children()]
[u'Dale Cooper', u'Audrey Horne', u'Laura Palmer']
The order of the children is arbitrary. Let’s explicitly sort them:
>>> [child.get_title() for child in people.get_children(sort=[('_created',1)])]
[u'Audrey Horne', u'Dale Cooper', u'Laura Palmer']
Did you notice the photo
attribute earlier? Let’s set a photo for Dale.
First let’s retrieve his object:
>>> obj = people.get_child({'firstname':'Dale'})
>>> print obj
{'_created': datetime.datetime(2012, 12, 24, 2, 10, 14, 856000, tzinfo=<UTC>),
'_etag': u'a8ee673c5490be625bd720375add252f',
'_id': ObjectId('50d7b986bf90af0e96bc8434'),
'_modified': datetime.datetime(2012, 12, 24, 2, 10, 14, 856000, tzinfo=<UTC>),
'firstname': u'Dale',
'lastname': u'Cooper',
'photo': None}
Now we’ll open a file, add it to Audrey’s GridFS, update the Person and then save it:
>>> with open("dale-cooper.jpg") as f:
... obj.photo = root.create_gridfs_file(f, "dale-cooper.jpg", "image/jpeg")
>>> obj.save()
>>> print obj
{'_created': datetime.datetime(2012, 12, 24, 2, 10, 14, 856000, tzinfo=<UTC>),
'_etag': '080b9d79d888e5d6714acc8cfb07d6ae',
'_id': ObjectId('50d7b986bf90af0e96bc8434'),
'_modified': datetime.datetime(2013, 1, 3, 1, 7, 31, 134749, tzinfo=<UTC>),
'firstname': u'Dale',
'lastname': u'Cooper',
'photo': <audrey.resources.file.File object at 0xaa2190c>}
photo
is an instance of audrey.resources.file.File
. This is simply a wrapper around the ObjectId of a GridFS file. To access the GridFS file (which can be read like a normal Python file and also has a few extra attributes like name
and content_type
), call get_gridfs_file()
:
>>> gf = obj.photo.get_gridfs_file(request)
>>> gf.name
u'dale-cooper.jpg'
>>> gf.length
66953
>>> gf.content_type
u'image/jpeg'
We’ve covered creating and updating objects. Now let’s delete one:
>>> obj = people.get_child({'firstname': 'Laura'})
>>> people.delete_child(obj)
>>> [child.get_title() for child in people.get_children()]
[u'Dale Cooper', u'Audrey Horne']
Note
Collection
also has methods delete_child_by_id()
and delete_child_by_name()
. This introduction doesn’t try to demonstrate every method and parameter. Refer to the API section for more.
Now let’s switch our focus to the web api. (If you’re running locally, you can explore the api with HAL-Browser by visiting http://127.0.0.1:6543/hal-browser/ in your web browser.) For our current purposes, I’ll use curl and Python’s super-handy json.tool:
$ curl http://127.0.0.1:6543/ | python -mjson.tool
{
"_links": {
"audrey:upload": {
"href": "http://127.0.0.1:6543/@@upload"
},
"curie": {
"href": "http://127.0.0.1:6543/relations/{rel}",
"name": "audrey",
"templated": true
},
"item": [
{
"href": "http://127.0.0.1:6543/people/{?sort}",
"name": "people",
"templated": true
},
{
"href": "http://127.0.0.1:6543/posts/{?sort}",
"name": "posts",
"templated": true
}
],
"search": {
"href": "http://127.0.0.1:6543/@@search?q={q}{&sort}{&collection*}",
"templated": true
},
"self": {
"href": "http://127.0.0.1:6543/"
}
}
}
Note
These are just the default views that Audrey provides. You can override and reconfigure to suit your needs, or ignore them entirely and create your own views from scratch.
Note
This view-related documentation needs to be updated to reflect the current state of the views. Everything described here will still work, but the responses may differ slightly. For example, listing views have new “embed” and “fields” options which didn’t appear in the templated urls described here. Try out http://127.0.0.1:6543/people?embed=1 or http://127.0.0.1:6543/people?embed=1&fields=firstname,lastname
This is a HAL+JSON document representing the root. Since the root has no state of its own, the document just has a number of links keyed by link relation (“rel”) names. Besides “self” which is obligatory for HAL, Audrey tries to stick to relations from the IANA list.
Here we see “item” used to list the children of root (the “people” and “posts” collections). These urls are templated, in this case indicating that you may use an optional “sort” parameter. In a moment, we’ll follow one of these links.
There’s also a link to a “search” endpoint (again with a URL template) and another to the “upload” endpoint. Since there was no IANA rel that seemed suitable for the upload endpoint (which as you may have guessed is a factory for uploading files into the system), Audrey uses a namespaced URI. Applying the “curie” template, “audrey:upload” expands to “http://127.0.0.1:6543/relations/upload”; visiting that url returns some HTML documentation of the endpoint including the expected request and response details.
Now let’s GET the “people” collection using the “sort” parameter to sort by creation time:
$ curl http://127.0.0.1:6543/people?sort=_created | python -mjson.tool
{
"_factory": {
"method": "POST",
"schemas": [
"person"
]
},
"_links": {
"audrey:schema": [
{
"href": "http://127.0.0.1:6543/people/@@schema/person",
"name": "person"
}
],
"collection": {
"href": "http://127.0.0.1:6543/"
},
"curie": {
"href": "http://127.0.0.1:6543/relations/{rel}",
"name": "audrey",
"templated": true
},
"item": [
{
"href": "http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/",
"name": "50d7b56dbf90af0e96bc8433",
"title": "Audrey Horne"
},
{
"href": "http://127.0.0.1:6543/people/50d7b986bf90af0e96bc8434/",
"name": "50d7b986bf90af0e96bc8434",
"title": "Dale Cooper"
}
],
"self": {
"href": "http://127.0.0.1:6543/people/?sort=_created"
}
},
"_summary": {
"batch": 1,
"per_batch": 20,
"sort": "_created",
"total_batches": 1,
"total_items": 2
}
}
The Collection view has some similarities with the Root view.
Again we see the obligatory “self” link and a list of “item” links (this time
the items are the two Person
instances we created earlier).
The “collection” rel is used to indicate a link to the container of the current
resource, which in this case is the root. Finally there’s a custom namespaced
“schema” rel. As the documentation at http://127.0.0.1:6543/relations/schema explains, the “schema” rel is a list of links to JSON Schema documents; there’s one such link for each object type that can be created in the current Collection.
We also see two custom properties: “_factory” and “_summary”.
The first identifies the HTTP method to be used to create new resources inside the collection. Here it’s POST since People is a base Collection and assigns names automatically. If it was a NamingCollection, the method would be PUT indicating that clients should specify new resource names by doing a PUT to a new url (such as “/people/harry-truman”).
The “_summary” property contains some metadata about the current item listing. Here we see that there are 2 items total. Since the batch size is 20, there’s only one batch. If there were more than 20 people, the “item” link array would only include a batch of up to 20 and there may be links with the rel “next” and/or “prev” with the urls for the next and previous batches (as appropriate).
Now let’s follow the first “item” link:
$ curl http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/ | python -mjson.tool
{
"_created": "2012-12-24T01:52:45.281000+00:00",
"_etag": "52779a9953bd01defd439bd29874c3d4",
"_id": {
"ObjectId": "50d7b56dbf90af0e96bc8433"
},
"_links": {
"audrey:file": [],
"audrey:reference": [],
"collection": {
"href": "http://127.0.0.1:6543/people/"
},
"curie": {
"href": "http://127.0.0.1:6543/relations/{rel}",
"name": "audrey",
"templated": true
},
"describedby": {
"href": "http://127.0.0.1:6543/people/@@schema/person"
},
"self": {
"href": "http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/"
}
},
"_modified": "2012-12-24T01:52:45.281000+00:00",
"_object_type": "person",
"_title": "Audrey Horne",
"firstname": "Audrey",
"lastname": "Horne",
"photo": null
}
Finally, something with some state data; here we see the schema properties “firstname”, “lastname” and “photo”, as well as various metadata properties which I’ve used the convention of starting with an underscore. Now let’s look at the ubiquitous links.
There’s “self” of course. The “collection” link refers to the current object’s container. The “describedby” link refers to a JSON Schema for the object. Finally there are two custom rels “file” and “reference”.
The “file” rel is used to indicate a list of links to (you guessed it) files referenced by this resource object. In this case, if “photo” wasn’t null there would be a link to the photo file. (Keep reading and we’ll upload a photo file and update this person to refer to it.)
The “reference” rel is used to indicate a list of links to other object resources referenced by this one. The Person
type doesn’t have any reference attributes in its schema, so this will always be an empty list for this class.
Now let’s demonstrate POSTing a new Person
:
$ curl -i -XPOST http://127.0.0.1:6543/people/ -d '{
"_object_type": "person",
"firstname": "Shelly",
"lastname": "Johnson"
}'
HTTP/1.1 201 Created
Content-Length: 2
Content-Type: application/json; charset=UTF-8
Date: Mon, 24 Dec 2012 18:25:35 GMT
Location: http://127.0.0.1:6543/people/50d89e1fbf90af0d7169df5d/
Server: waitress
{}
Cool... Audrey responds with the 201 Created
success status and “Location” header with the URL of the new resource.
You might wonder what would happen if we tried to POST an invalid request. First let’s try POSTing an empty JSON document:
$ curl -i -XPOST http://127.0.0.1:6543/people/ -d '{}'
HTTP/1.1 400 Bad Request
Content-Length: 45
Content-Type: application/json; charset=UTF-8
Date: Mon, 24 Dec 2012 18:27:34 GMT
Server: waitress
{"error": "Request is missing _object_type."}
Uh oh... we got 400 Bad Request
and an error message in the body with the reason.
So now let’s POST a document that just contains an “_object_type”:
curl -i -XPOST http://127.0.0.1:6543/people/ -d '{"_object_type": "person"}'
HTTP/1.1 400 Bad Request
Content-Length: 92
Content-Type: application/json; charset=UTF-8
Date: Mon, 24 Dec 2012 18:27:57 GMT
Server: waitress
{"errors": {"lastname": "Required", "firstname": "Required"}, "error": "Validation failed."}
Another 400 error and another “error” message. Since this one’s a validation error, the JSON document in the response also includes an “errors” key with the field-specific errors (courtesy of colander).
Now let’s upload a photo:
$ curl -F file=@audrey.jpg http://127.0.0.1:6543/@@upload
{"file": [{"FileId": "50d8a64bbf90af0d7169df5e"}]}
The server creates a GridFS file in MongoDB for each file from the request and responds with a JSON document containing a list of the file ObjectIds for each parameter name from the request.
Let’s update Audrey Horne’s record with the new photo file:
$ curl -i -XPUT http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/ -d '{
"_object_type": "person",
"firstname": "Audrey",
"lastname": "Horne",
"photo": {"FileId": "50d8a64bbf90af0d7169df5e"}
}'
HTTP/1.1 412 Precondition Failed
Content-Length: 75
Content-Type: application/json; charset=UTF-8
Date: Mon, 24 Dec 2012 20:04:37 GMT
Server: waitress
{"error": "Requests must supply If-Unmodified-Since and If-Match headers."}
What’s going on here? The views implement optimistic concurrency control in an effort to avoid silent data loss. PUT requests to update an existing resource and DELETE requests to remove an existing resource must include “If-Unmodified-Since” and “If-Match” headers whose values must match the “Last-Modified” and “Etag” headers from the response to a GET of that same resource. Let’s examine the response headers to get those two values:
$ curl -i http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/
HTTP/1.1 200 OK
Content-Length: 660
Content-Type: application/hal+json; charset=UTF-8
Date: Mon, 24 Dec 2012 20:13:42 GMT
Etag: "52779a9953bd01defd439bd29874c3d4"
Last-Modified: Mon, 24 Dec 2012 01:52:45 GMT
Server: waitress
{"_links": {"audrey:file": [], "self": {"href": "http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/"}, "collection": {"href": "http://127.0.0.1:6543/people/"}, "curie": {"href": "http://127.0.0.1:6543/relations/{rel}", "name": "audrey", "templated": true}, "audrey:reference": [], "describedby": {"href": "http://127.0.0.1:6543/people/@@schema/person"}}, "photo": null, "firstname": "Audrey", "lastname": "Horne", "_modified": "2012-12-24T01:52:45.281000+00:00", "_created": "2012-12-24T01:52:45.281000+00:00", "_title": "Audrey Horne", "_id": {"ObjectId": "50d7b56dbf90af0e96bc8433"}, "_etag": "52779a9953bd01defd439bd29874c3d4", "_object_type": "person"}
Now let’s try that PUT again with the two headers for OCC:
$ curl -i -H 'If-Unmodified-Since:Mon, 24 Dec 2012 01:52:45 GMT' \
-H 'If-Match:"52779a9953bd01defd439bd29874c3d4"' \
-XPUT http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/ -d '{
"_object_type": "person",
"firstname": "Audrey",
"lastname": "Horne",
"photo": {"FileId": "50d8a64bbf90af0d7169df5e"}
}'
HTTP/1.1 204 No Content
Content-Length: 0
Location: http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/
Content-Type: application/json; charset=UTF-8
Date: Mon, 24 Dec 2012 20:19:23 GMT
Server: waitress
Success! Let’s confirm the change by doing another GET:
$ curl http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/ | python -mjson.tool
{
"_created": "2012-12-24T01:52:45.281000+00:00",
"_etag": "3c418f678d1cb636fca4cadc599bf725",
"_id": {
"ObjectId": "50d7b56dbf90af0e96bc8433"
},
"_links": {
"audrey:file": [
{
"href": "http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/@@download/50d8a64bbf90af0d7169df5e",
"name": "50d8a64bbf90af0d7169df5e",
"type": "image/jpeg"
}
],
"audrey:reference": [],
"collection": {
"href": "http://127.0.0.1:6543/people/"
},
"curie": {
"href": "http://127.0.0.1:6543/relations/{rel}",
"name": "audrey",
"templated": true
},
"describedby": {
"href": "http://127.0.0.1:6543/people/@@schema/person"
},
"self": {
"href": "http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/"
}
},
"_modified": "2012-12-24T20:19:23.660000+00:00",
"_object_type": "person",
"_title": "Audrey Horne",
"firstname": "Audrey",
"lastname": "Horne",
"photo": {
"FileId": "50d8a64bbf90af0d7169df5e"
}
}
The “photo” is no longer null and the list of “file” links now contains one item with type=”image/jpeg” and name=”50d8a64bbf90af0d7169df5e”. A client could match up that name with the value of the photo FileId.
Try viewing the photo by hitting http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/@@download/50d8a64bbf90af0d7169df5e
You could also traverse to the photo
attribute like so:
http://127.0.0.1:6543/people/50d7b56dbf90af0e96bc8433/photo
As our final stop before ending this introduction, let’s try out the most basic usage of the search api. We’ll do a search for “dale”:
$ curl http://127.0.0.1:6543/@@search?q=dale | python -mjson.tool
{
"_links": {
"item": [
{
"href": "http://127.0.0.1:6543/people/50d7b986bf90af0e96bc8434/",
"name": "people:50d7b986bf90af0e96bc8434",
"title": "Dale Cooper"
}
],
"self": {
"href": "http://127.0.0.1:6543/@@search?q=dale"
}
},
"_summary": {
"batch": 1,
"collections": [],
"per_batch": 20,
"q": "dale",
"sort": null,
"total_batches": 1,
"total_items": 1
}
}
The search found Dale’s Person
object. As you might guess, if there were lots of results they would be batched with “next” and “prev” links.
Well that wraps up this introduction. It didn’t cover everything, but hopefully it provided a sufficient taste.