Resource¶
Cliquet provides a basic component to build resource oriented APIs. In most cases, the main customization consists in defining the schema of the records for this resource.
Full example¶
import colander
from cliquet import resource
from cliquet import schema
from cliquet import utils
class BookmarkSchema(resource.ResourceSchema):
url = schema.URL()
title = colander.SchemaNode(colander.String())
favorite = colander.SchemaNode(colander.Boolean(), missing=False)
device = colander.SchemaNode(colander.String(), missing='')
class Options:
readonly_fields = ('device',)
unique_fields = ('url',)
@resource.register()
class Bookmark(resource.BaseResource):
mapping = BookmarkSchema()
def process_record(self, new, old=None):
if new['device'] != old['device']:
new['device'] = self.request.headers.get('User-Agent')
return new
See the ReadingList and Kinto projects source code for real use cases.
Resource Schema¶
Override the base schema to add extra fields using the Colander API.
class Movie(ResourceSchema):
director = colander.SchemaNode(colander.String())
year = colander.SchemaNode(colander.Int(),
validator=colander.Range(min=1850))
genre = colander.SchemaNode(colander.String(),
validator=colander.OneOf(['Sci-Fi', 'Comedy']))
-
class
cliquet.schema.
ResourceSchema
(*arg, **kw)¶ Base resource schema, with Cliquet specific built-in options.
-
class
Options
¶ Resource schema options.
This is meant to be overriden for changing values:
class Product(ResourceSchema): reference = colander.SchemaNode(colander.String()) class Options: unique_fields = ('reference',)
-
unique_fields
= ()¶ Fields that must have unique values for the user collection. During records creation and modification, a conflict error will be raised if unicity is about to be violated.
-
readonly_fields
= ()¶ Fields that cannot be updated. Values for fields will have to be provided either during record creation, through default values using
missing
attribute or implementing a custom logic incliquet.resource.BaseResource.process_record()
.
-
preserve_unknown
= False¶ Define if unknown fields should be preserved or not.
For example, in order to define a schema-less resource, in other words a resource that will accept any form of record, the following schema definition is enough:
class SchemaLess(ResourceSchema): class Options: preserve_unknown = True
-
-
ResourceSchema.
is_readonly
(field)¶ Return True if specified field name is read-only.
Parameters: field (str) – the field name in the schema Returns: True
if the specified field is read-only,False
otherwise.Return type: bool
-
class
-
class
cliquet.schema.
PermissionsSchema
(*args, **kwargs)¶ A permission mapping defines ACEs.
It has permission names as keys and principals as values.
{ "write": ["fxa:af3e077eb9f5444a949ad65aa86e82ff"], "groups:create": ["fxa:70a9335eecfe440fa445ba752a750f3d"] }
-
class
cliquet.schema.
TimeStamp
(*arg, **kw)¶ Basic integer schema field that can be set to current server timestamp in milliseconds if no value is provided.
class Book(ResourceSchema): added_on = TimeStamp() read_on = TimeStamp(auto_now=False, missing=-1)
-
schema_type
¶ alias of
Integer
-
title
= 'Epoch timestamp'¶ Default field title.
-
auto_now
= True¶ Set to current server timestamp (milliseconds) if not provided.
-
missing
= None¶ Default field value if not provided in record.
-
Resource class¶
In order to customize the resource URLs or behaviour on record processing, the resource class can be extended:
-
class
cliquet.resource.
BaseResource
(request, context=None)¶ Base resource class providing every endpoint.
-
default_viewset
¶ Default viewset class to use when the resource is registered.
alias of
ViewSet
-
mapping
= <cliquet.schema.ResourceSchema object at 140383825371984 (named )>¶ Schema to validate records.
-
get_parent_id
(request)¶ Return the parent_id of the resource with regards to the current request.
Parameters: request – The request used to create the resource. Return type: str
-
is_known_field
(field)¶ Return
True
if field is defined in the resource mapping.Parameters: field (str) – Field name Return type: bool
-
collection_get
()¶ Collection
GET
endpoint: retrieve multiple records.Raises: HTTPNotModified
ifIf-None-Match
header is provided and collection not modified in the interim.Raises: HTTPPreconditionFailed
ifIf-Match
header is provided and collection modified in the iterim.Raises: HTTPBadRequest
if filters or sorting are invalid.
-
collection_post
()¶ Collection
POST
endpoint: create a record.If the new record conflicts against a unique field constraint, the posted record is ignored, and the existing record is returned, with a
200
status.Raises: HTTPPreconditionFailed
ifIf-Match
header is provided and collection modified in the iterim.See also
Add custom behaviour by overriding
cliquet.resource.BaseResource.process_record()
-
collection_delete
()¶ Collection
DELETE
endpoint: delete multiple records.Raises: HTTPPreconditionFailed
ifIf-Match
header is provided and collection modified in the iterim.Raises: HTTPBadRequest
if filters are invalid.
-
get
()¶ Record
GET
endpoint: retrieve a record.Raises: HTTPNotFound
if the record is not found.Raises: HTTPNotModified
ifIf-None-Match
header is provided and record not modified in the interim.Raises: HTTPPreconditionFailed
ifIf-Match
header is provided and record modified in the iterim.
-
put
()¶ Record
PUT
endpoint: create or replace the provided record and return it.Raises: HTTPPreconditionFailed
ifIf-Match
header is provided and record modified in the iterim.Note
If
If-None-Match: *
request header is provided, thePUT
will succeed only if no record exists with this id.See also
Add custom behaviour by overriding
cliquet.resource.BaseResource.process_record()
.
-
patch
()¶ Record
PATCH
endpoint: modify a record and return its new version.If a request header
Response-Behavior
is set tolight
, only the fields whose value was changed are returned. If set todiff
, only the fields whose value became different than the one provided are returned.Raises: HTTPNotFound
if the record is not found.Raises: HTTPPreconditionFailed
ifIf-Match
header is provided and record modified in the iterim.See also
Add custom behaviour by overriding
cliquet.resource.BaseResource.apply_changes()
orcliquet.resource.BaseResource.process_record()
.
-
delete
()¶ Record
DELETE
endpoint: delete a record and return it.Raises: HTTPNotFound
if the record is not found.Raises: HTTPPreconditionFailed
ifIf-Match
header is provided and record modified in the iterim.
-
process_record
(new, old=None)¶ Hook for processing records before they reach storage, to introduce specific logics on fields for example.
def process_record(self, new, old=None): version = old['version'] if old else 0 new['version'] = version + 1 return new
Or add extra validation based on request:
from cliquet.errors import raise_invalid def process_record(self, new, old=None): if new['browser'] not in request.headers['User-Agent']: raise_invalid(self.request, name='browser', error='Wrong') return new
Parameters: - new (dict) – the validated record to be created or updated.
- old (dict) – the old record to be updated,
None
for creation endpoints.
Returns: the processed record.
Return type: dict
-
apply_changes
(record, changes)¶ Merge changes into record fields.
Note
This is used in the context of PATCH only.
Override this to control field changes at record level, for example:
def apply_changes(self, record, changes): # Ignore value change if inferior if record['position'] > changes.get('position', -1): changes.pop('position', None) return super(MyResource, self).apply_changes(record, changes)
Raises: HTTPBadRequest
if result does not comply with resource schema.Returns: the new record with changes applied. Return type: dict
-
Interaction with storage¶
In order to customize the interaction of a HTTP resource with its storage, a custom collection can be plugged-in:
from cliquet import resource
class TrackedCollection(resource.Collection):
def create_record(self, record, parent_id=None, unique_fields=None):
record = super(TrackedCollection, self).create_record(record,
parent_id,
unique_fields)
trackid = index.track(record)
record['trackid'] = trackid
return record
class Payment(resource.BaseResource):
def __init__(request):
super(Payment, self).__init__(request)
self.collection = TrackedCollection(
storage=self.collection.storage,
id_generator=self.collection.id_generator,
collection_id=self.collection.collection_id,
parent_id=self.collection.parent_id,
auth=self.collection.auth)
-
class
cliquet.resource.
Collection
(storage, id_generator=None, collection_id='', parent_id='', auth=None)¶ A collection stores and manipulate records in its attached storage.
It is not aware of HTTP environment nor protocol.
Records are isolated according to the provided name and parent_id.
Those notions have no particular semantic and can represent anything. For example, the collection name can be the type of objects stored, and parent_id can be the current user id or a group where the collection belongs. If left empty, the collection records are not isolated.
-
id_field
= 'id'¶ Name of id field in records
-
modified_field
= 'last_modified'¶ Name of last modified field in records
-
deleted_field
= 'deleted'¶ Name of deleted field in deleted records
-
timestamp
(parent_id=None)¶ Fetch the collection current timestamp.
Parameters: parent_id (str) – optional filter for parent id Return type: integer
-
get_records
(filters=None, sorting=None, pagination_rules=None, limit=None, include_deleted=False, parent_id=None)¶ Fetch the collection records.
Override to post-process records after feching them from storage.
Parameters: - filters (list of
cliquet.storage.Filter
) – Optionally filter the records by their attribute. Each filter in this list is a tuple of a field, a value and a comparison (see cliquet.utils.COMPARISON). All filters are combined using AND. - sorting (list of
cliquet.storage.Sort
) – Optionnally sort the records by attribute. Each sort instruction in this list refers to a field and a direction (negative means descending). All sort instructions are cumulative. - pagination_rules (list of list of
cliquet.storage.Filter
) – Optionnally paginate the list of records. This list of rules aims to reduce the set of records to the current page. A rule is a list of filters (see filters parameter), and all rules are combined using OR. - limit (int) – Optionnally limit the number of records to be retrieved.
- include_deleted (bool) – Optionnally include the deleted records that match the filters.
- parent_id (str) – optional filter for parent id
Returns: A tuple with the list of records in the current page, the total number of records in the result set.
Return type: tuple
- filters (list of
-
delete_records
(filters=None, parent_id=None)¶ Delete multiple collection records.
Override to post-process records after their deletion from storage.
Parameters: - filters (list of
cliquet.storage.Filter
) – Optionally filter the records by their attribute. Each filter in this list is a tuple of a field, a value and a comparison (see cliquet.utils.COMPARISON). All filters are combined using AND. - parent_id (str) – optional filter for parent id
Returns: The list of deleted records from storage.
- filters (list of
-
get_record
(record_id, parent_id=None)¶ Fetch current view related record, and raise 404 if missing.
Parameters: - record_id (str) – record identifier
- parent_id (str) – optional filter for parent id
Returns: the record from storage
Return type: dict
-
create_record
(record, parent_id=None, unique_fields=None)¶ Create a record in the collection.
Override to perform actions or post-process records after their creation in storage.
def create_record(self, record): record = super(MyCollection, self).create_record(record) idx = index.store(record) record['index'] = idx return record
Parameters: - record (dict) – record to store
- parent_id (str) – optional filter for parent id
- unique_fields (tuple) – list of fields that should remain unique
Returns: the newly created record.
Return type: dict
-
update_record
(record, parent_id=None, unique_fields=None)¶ Update a record in the collection.
Override to perform actions or post-process records after their modification in storage.
def update_record(self, record, parent_id=None,unique_fields=None): record = super(MyCollection, self).update_record(record, parent_id, unique_fields) subject = 'Record {} was changed'.format(record[self.id_field]) send_email(subject) return record
Parameters: - record (dict) – record to store
- parent_id (str) – optional filter for parent id
- unique_fields (tuple) – list of fields that should remain unique
Returns: the updated record.
Return type: dict
-
delete_record
(record, parent_id=None)¶ Delete a record in the collection.
Override to perform actions or post-process records after deletion from storage for example:
def delete_record(self, record): deleted = super(MyCollection, self).delete_record(record) erase_media(record) deleted['media'] = 0 return deleted
Parameters: - record (dict) – the record to delete
- record – record to store
- parent_id (str) – optional filter for parent id
Returns: the deleted record.
Return type: dict
-
Custom record ids¶
By default, records ids are UUID4 <http://en.wikipedia.org/wiki/Universally_unique_identifier>_.
A custom record id generator can be set globally in Configuration, or at the resource level:
from cliquet import resource
from cliquet import utils
from cliquet.storage import generators
class MsecId(generators.Generator):
def __call__(self):
return '%s' % utils.msec_time()
@resource.register()
class Mushroom(resource.BaseResource):
def __init__(request):
super(Mushroom, self).__init__(request)
self.collection.id_generator = MsecId()
Generators objects¶
-
class
cliquet.storage.generators.
Generator
(config=None)¶ Base generator for records ids.
Id generators are used by storage backend during record creation, and at resource level to validate record id in requests paths.
-
regexp
= '^[a-zA-Z0-9\\-]+$'¶ Default record id pattern. Can be changed to comply with custom ids.
-
match
(record_id)¶ Validate that record ids match the generator. This is used mainly when a record id is picked arbitrarily (e.g with
PUT
requests).Returns: True if the specified record id matches expected format. Return type: bool
-
-
class
cliquet.storage.generators.
UUID4
(config=None)¶ UUID4 record id generator.
UUID block are separated with
-
. (example:'472be9ec-26fe-461b-8282-9c4e4b207ab3'
)UUIDs are very safe in term of unicity. If 1 billion of UUIDs are generated every second for the next 100 years, the probability of creating just one duplicate would be about 50% (source).
-
regexp
= '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'¶ UUID4 accurate pattern.
-
Custom Usage¶
Within views¶
In views, a request
object is available and allows to use the storage
configured in the application:
from cliquet import resource
def view(request):
registry = request.registry
flowers = resource.Collection(storage=registry.storage,
name='app:flowers')
flowers.create_record({'name': 'Jonquille', 'size': 30})
flowers.create_record({'name': 'Amapola', 'size': 18})
min_size = resource.Filter('size', 20, resource.COMPARISON.MIN)
records, total = flowers.get_records(filters=[min_size])
flowers.delete_record(records[0])
Outside views¶
Outside views, an application context has to be built from scratch.
As an example, let’s build a code that will copy a remote Kinto collection into a local storage:
from cliquet import resource, DEFAULT_SETTINGS
from pyramid import Configurator
config_local = Configurator(settings=DEFAULT_SETTINGS)
config_local.add_settings({
'cliquet.storage_backend': 'cliquet.storage.postgresql'
'cliquet.storage_url': 'postgres://user:pass@db.server.lan:5432/dbname'
})
local = resource.Collection(storage=config_local.registry.storage,
parent_id='browsing',
name='history')
config_remote = Configurator(settings=DEFAULT_SETTINGS)
config_remote.add_settings({
'cliquet.storage_backend': 'kinto.storage',
'cliquet.storage_url': 'https://cloud-storage.services.mozilla.com'
})
remote = resource.Collection(storage=config_remote.registry.storage,
parent_id='browsing',
name='history',
auth='Basic bWF0Og==')
records, total = in remote.get_records():
for record in records:
local.create_record(record)