Resource

Kinto-Core 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 kinto.core import resource
from kinto.core import utils


class BookmarkSchema(resource.ResourceSchema):
    url = colander.SchemaNode(colander.String(), validator=colander.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.UserResource):
    mapping = BookmarkSchema()

    def process_record(self, new, old=None):
        new = super(Bookmark, self).process_record(new, old)
        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.

URLs

By default, a resource defines two URLs:

  • /{classname}s for the list of records
  • /{classname}s/{id} for single records

Since adding an s suffix for the plural form might not always be relevant, URLs can be specified during registration:

@resource.register(collection_path='/user/bookmarks',
                   record_path='/user/bookmarks/{{id}}')
class Bookmark(resource.UserResource):
    mapping = BookmarkSchema()

Note

The same resource can be registered with different URLs.

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']))

See the resource schema options to define schema-less resources or specify rules for unicity or readonly.

Permissions

Using the kinto.core.resource.UserResource, the resource is accessible by any authenticated request, but the records are isolated by user id.

In order to define resources whose records are not isolated, open publicly or controlled with individual fined-permissions, a kinto.core.resource.ShareableResource could be used.

But there are other strategies, please refer to dedicated section about permissions.

HTTP methods and options

In order to specify which HTTP verbs (GET, PUT, PATCH, ...) are allowed on the resource, as well as specific custom Pyramid (or cornice) view arguments, refer to the viewset section.

Events

When a record is created/deleted in a resource, an event is sent. See the dedicated section about notifications to plug events in your Pyramid/Kinto-Core application or plugin.

Model

Plug custom model

In order to customize the interaction of a HTTP resource with its storage, a custom model can be plugged-in:

from kinto.core import resource


class TrackedModel(resource.Model):
    def create_record(self, record, parent_id=None, unique_fields=None):
        record = super(TrackedModel, self).create_record(record,
                                                         parent_id,
                                                         unique_fields)
        trackid = index.track(record)
        record['trackid'] = trackid
        return record


class Payment(resource.UserResource):
    default_model = TrackedModel

Relationships

With the default model and storage backend, Kinto-Core does not support complex relations.

However, it is possible to plug a custom model class, that will take care of saving and retrieving records with relations.

Note

This part deserves more love, please come and discuss!

In Pyramid views

In Pyramid views, a request object is available and allows to use the storage configured in the application:

from kinto.core import resource

def view(request):
    registry = request.registry

    flowers = resource.Model(storage=registry.storage,
                             collection_id='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 collection into another:

from kinto.core import resource, DEFAULT_SETTINGS
from pyramid import Configurator


config = Configurator(settings=DEFAULT_SETTINGS)
config.add_settings({
    'kinto.storage_backend': 'kinto.core.storage.postgresql'
    'kinto.storage_url': 'postgres://user:pass@db.server.lan:5432/dbname'
})
kinto.core.initialize(config, '0.0.1')

local = resource.Model(storage=config.registry.storage,
                       parent_id='browsing',
                       collection_id='history')

remote = resource.Model(storage=config_remote.registry.storage,
                        parent_id='',
                        collection_id='history')

records, total = in remote.get_records():
for record in records:
    local.create_record(record)

Custom record ids

By default, records ids are UUID4.

A custom record ID generator can be set globally in Configuration, or at the resource level:

from kinto.core import resource
from kinto.core import utils
from kinto.core.storage import generators


class MsecId(generators.Generator):
    def __call__(self):
        return '%s' % utils.msec_time()


@resource.register()
class Mushroom(resource.UserResource):
    def __init__(request):
        super(Mushroom, self).__init__(request)
        self.model.id_generator = MsecId()

Python API

Resource

class kinto.core.resource.UserResource(request, context=None)

Base resource class providing every endpoint.

default_viewset

Default kinto.core.viewset.ViewSet class to use when the resource is registered.

alias of ViewSet

default_model

Default kinto.core.resource.model.Model class to use for interacting the kinto.core.storage and kinto.core.permission backends.

alias of Model

mapping = <kinto.core.resource.schema.ResourceSchema object at 139639639324560 (named )>

Schema to validate records.

timestamp

Return the current collection timestamp.

Return type:int
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()

Model GET endpoint: retrieve multiple records.

Raises:HTTPNotModified if If-None-Match header is provided and collection not modified in the interim.
Raises:HTTPPreconditionFailed if If-Match header is provided and collection modified in the iterim.
Raises:HTTPBadRequest if filters or sorting are invalid.
collection_post()

Model 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 if If-Match header is provided and collection modified in the iterim.

See also

Add custom behaviour by overriding kinto.core.resource.UserResource.process_record()

collection_delete()

Model DELETE endpoint: delete multiple records.

Raises:HTTPPreconditionFailed if If-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 if If-None-Match header is provided and record not modified in the interim.
Raises:HTTPPreconditionFailed if If-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 if If-Match header is provided and record modified in the iterim.

Note

If If-None-Match: * request header is provided, the PUT will succeed only if no record exists with this id.

See also

Add custom behaviour by overriding kinto.core.resource.UserResource.process_record().

patch()

Record PATCH endpoint: modify a record and return its new version.

If a request header Response-Behavior is set to light, only the fields whose value was changed are returned. If set to diff, only the fields whose value became different than the one provided are returned.

Raises:HTTPNotFound if the record is not found.
Raises:HTTPPreconditionFailed if If-Match header is provided and record modified in the iterim.
delete()

Record DELETE endpoint: delete a record and return it.

Raises:HTTPNotFound if the record is not found.
Raises:HTTPPreconditionFailed if If-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):
    new = super(MyResource, self).process_record(new, old)
    version = old['version'] if old else 0
    new['version'] = version + 1
    return new

Or add extra validation based on request:

from kinto.core.errors import raise_invalid

def process_record(self, new, old=None):
    new = super(MyResource, self).process_record(new, old)
    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

Schema

class kinto.core.resource.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 in kinto.core.resource.UserResource.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 kinto.core.resource.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 kinto.core.resource.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.

class kinto.core.resource.schema.URL(*arg, **kw)

String field representing a URL, with max length of 2048. This is basically a shortcut for string field with ~colander:colander.url.

class BookmarkSchema(ResourceSchema):
    url = URL()
schema_type

alias of String

Model

class kinto.core.resource.Model(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 kinto.core.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 kinto.core.utils.COMPARISON). All filters are combined using AND.
  • sorting (list of kinto.core.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 kinto.core.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

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 kinto.core.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 kinto.core.utils.COMPARISON). All filters are combined using AND.
  • parent_id (str) – optional filter for parent id
Returns:

The list of deleted records from storage.

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(MyModel, 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(MyModel, 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, last_modified=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(MyModel, 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

Generators

class kinto.core.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][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 kinto.core.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}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'

UUID4 accurate pattern.