A lot of web services provide nowadays a REST interface and clients can communicate and manipulate state on the server by exchanging messages. python-docar provides a declarative style of defining those messages as documents, and makes it possible to resue the definitions on the server as well as on the client side.
A document maps to a resource, whereas it doesn’t matter how this resource is stored. python-docar implements at the moment a backend for django models and a backend for a http endpoint.
>>> # The document declaration
>>> from docar import Document, fields
>>> from djangoproject.newspaper import ArticleModel
>>> class Article(Document):
... id = fields.NumberField()
... name = fields.StringField()
...
... class Meta:
... backend_type = 'django'
... model = ArticleModel
>>> # A server example
>>> article = Article({'id': 1})
>>> article.fetch() # Fetch this document from the backend
>>> article.to_json()
{
"id": 1,
"name": "Headline",
"link": {
"rel": "self",
"href": "http://example.org/article/1/"
}
}
>>> article.headline = "Another Headline"
>>> article.save() # Save the document to the backend model
>>> article.to_json()
{
"id": 1,
"name": "Another Headline",
"link": {
"rel": "self",
"href": "http://example.org/article/1/"
}
}
>>> # A client example taling to a remote API endpoint
>>> article = Article()
>>> article.name = "Next Headline"
>>> article.save(username='user', password='pass')
<class 'docar.http.HttpResponse'>
>>> # You can also declare a collection of documents
>>> from docar import Collection
>>> class NewsPaper(Collection):
... document = Article
>>> newspaper = NewsPaper()
>>> newspaper.add(article)
>>> newspaper.to_json()
[{
"id": 1,
"headline": "Headline"
"link": {
"rel": "self",
"href": "http://example.org/article/1/"
}
}]
All documents inherit from the docar.Document class. It acts as a representation of a resource. A resource maps to a datastructure that is stored in a backend, see the section about Backends for more information. Each attribute of the document maps to a field of the resource in the backend.
A document exposes a simple API:
Fetch the resource from the backend and bind the document to this resource.
If the document does not exist on the backend, create it. Otherwise update the existing backend with information stored in the current document.
Delete the current resource from the backend.
Render the document into a python dictionary. The process adds met information such as the link to itself to the representation.
Render the document to a json string. This basically serializes the result from to_python().
The uri() method returns the resource identifier of this resource. This method needs to be implemented by the user. It is used to render the link to itself. The return value of this method should always be the full location of the resource as a string:
class Article(Document):
id = fields.NumberField()
def uri(self):
return "http://location/articles/%s/" % self.id
The scaffold() creates a skeleton of the document. It returns a python dictionary:
>>> class Article(Document):
... id = fields.NumberField()
... name = fields.StringField()
>>> article = Article()
>>> article.scaffold()
{
"id": None,
"name": ""
}
Validate the fields of the document. It validates the correct datatypes of the fields. You can also attach validator functions to a fields that can validate additional aspects of the field.
The behaviour of the document can be controlled by setting attributes on the document’s Meta class.
class Article(Document):
id = fields.NumberField()
name = fields.StringField()
class Meta:
identifier = 'id'
There are only a few options available at the moment:
Specify the field name, that serves as an unique identifier for this document. The field is specified as a simple string. If you want to use more than one field as identifiers, write them as a list of strings:
class Meta:
identifier = ['id', 'name']
Every document needs to specify an identifer. Every resource should be uniquely selectable by the value of those fields. The default identifier is named id.
Choose the backend this document should connect to. See the section about Backends below for details. The default backend is the Django backend.
This option is only useful for documents connecting to the Django Backend. It takes a class as argument and specifies which django model use. The argument must be a class and can’t be a string:
from djangoapp.models import ArticleModel
class Article(Document):
id = fields.NumberField()
class Meta:
model = ArticleModel
A list of strings that specify which additional context variables are used by this document. See the sections about Document Context for more information.
Documents declare their attributes using fields set as class attributes. The name of a field maps straight to the name of a field of the underlying resource. See Mapping Fields for a way to use a different field name for the document and the resource.
Example
class Message(Document):
id = fields.NumberField()
name = fields.StringField()
When set to True, This field can be optional and will be ignored if not set to a value. Default is False.
Specify a default value for this field. If no value is set by the user, the default value is used when interacting with the backend.
If set to False the field gets ignored when the document gets rendered. Defaults to True.
Normally ForeignDocuments and CollectionFields render as a reference with a resource URI link and a relation attribute. When you specify the inline field option, you can force the field to render as an inline element.
Example
class Doc1(Document):
id = fields.NumberField()
name = fields.StringField()
def uri(self):
return "http://example.org/doc/%s" % self.id
class Doc2(Document):
id = fields.NumberField()
doc_inline = fields.ForeignDocument(Doc1, inline=True)
doc1 = fields.ForeignDocument(Doc1)
d = Doc2()
d.fetch()
d.render()
{
"id": 1,
"doc1": {
"id": 1,
"rel": "related",
"href": "http://example.org/doc/1/"
},
"doc_inline": {
"id": 2,
"name": "doc_inline"
}
}
Specify whether to validate the field or not. Defaults to True. If disabled, validation is skipped for this field.
You can add a list of functions that will be called when validating the field. For details on those functions see the section about Validation.
Example
def validate_name(value):
# Do something
class Article(Document):
id = fields.NumberField()
name = fields.StringField(validators=[validate_name])
You can map a field name between the document and the underlying resource by implementing a map_FIELD_field() method where FIELD is the name of the document field. The method returns a string with the actual name of the resource field.
# We define a simple django model
class ArticleModel(models.Model):
long_title = models.CharField(max_length=200)
# We define a document where we want to use name as a field name instead of
# long_title
class Article(Document):
id = fields.NumberField()
name = fields.StringField()
class Meta:
backend_type = 'django'
model = ArticleModel
map_name_field(self):
return "long_title"
In the above example the document defines a field name. For any operation with the underlying resource, it will map name to long_title.
You can map values that are fetched from a backend and set a different value on the document. Specify a fetch_FIELD_field method on the document, and it will be called whenever a representation gets fetched.
The method takes only one argument, which is the value originaly fetched from the backend.
class Article(Document):
id = fields.NumberField()
name = fields.StringField()
class Meta:
backend_type = 'http'
fetch_name_field(self, value):
if value == "some string":
return value
else:
return "UNKNOWN"
>>> art = Article({'id': 1})
>>> # this fetches a resource that looks like that:
>>> # {"id": 1, "name": "something"}
>>> art.fetch()
>>> art.name
UNKNOWN
You can as well map field values before sending the document to the backend for saving the resource. It works the same as for fetching field described above, you only specify a save_FIELD_field method on the document.
To change the rendering of the field use a render_FIELD_field method. Use it the same as fetch or save mapping method described above.
The backends are the real meat of the documents. Where the document defines what you can do, the backends implement the how of it. Documents and backends use dictionary to communicate to each other. A backend expects a dictionary representation of the document, applies it as needed, and returns the resource as a dictionary again. Each backend must implement the following methods:
The HTTP backend uses the requests library to communicate to remote backends over HTTP. It assumes currently JSON as exchange protocol. The document methods map the following way to the HTTP backend:
This backend uses the uri() method to determine its API endpoint. You can implement specific uri methods for each HTTP verb to be more precise. If a http specific uri method is not found, it will fallback to the default uri() method. The form of those methods is verb_uri.
class Article(Document):
id = fields.NumberField()
def post_uri(self):
# Use this method for POST requests
return "http://post_location"
def uri(self):
# The default uri location for all other HTTP requests
return "http://location"
The django backend stores and retrieves resources using the Django ORM.