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


@resource.register()
class Bookmark(resource.UserResource):
    schema = BookmarkSchema

    def process_record(self, new, old=None):
        new = super().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):
    schema = 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(resource.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 like readonly fields.

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):
        record = super().create_record(record, parent_id)
        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().__init__(request)
        self.model.id_generator = MsecId()

Python API

kinto.core.resource.register(depth=1, **kwargs)

Ressource class decorator.

Register the decorated class in the cornice registry. Pass all its keyword arguments to the register_resource function.

Resource

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

Base resource class providing every endpoint.

default_viewset

alias of ViewSet

default_model

alias of Model

schema

alias of ResourceSchema

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 schema. If the resource schema allows unknown fields, this will always return True.

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 id conflicts against an existing one, 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().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().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, requested_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, requested_changes):
    # Ignore value change if inferior
    if record['position'] > changes.get('position', -1):
        changes.pop('position', None)
    return super().apply_changes(record, requested_changes)
Raises:HTTPBadRequest if result does not comply with resource schema.
Returns:the new record with changes applied.
Return type:tuple
class kinto.core.resource.ShareableResource(*args, **kwargs)

Shareable resources allow to set permissions on records, in order to share their access or protect their modification.

default_model

alias of ShareableModel

default_viewset

alias of ShareableViewSet

permissions = ('read', 'write')

List of allowed permissions names.

get_parent_id(request)

Unlike kinto.core.resource.UserResource, records are not isolated by user.

See https://github.com/mozilla-services/cliquet/issues/549

Returns:A constant empty value.
process_record(new, old=None)

Read permissions from request body, and in the case of PUT every existing ACE is removed (using empty list).

postprocess(result, action=<ACTIONS.READ: 'read'>, old=None)

Add permissions attribute in response body.

In the HTTP API, it was decided that permissions would reside outside the data attribute.

Schema

class kinto.core.resource.schema.TimeStamp(*args, **kwargs)

This schema is deprecated, you shoud use kinto.core.schema.TimeStamp instead.

class kinto.core.resource.schema.URL(*args, **kwargs)

This schema is deprecated, you shoud use kinto.core.schema.URL instead.

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:
        readonly_fields = ('reference',)
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 = True

Define if unknown fields should be preserved or not.

The resource is schema-less by default. In other words, any field name will be accepted on records. Set this to False in order to limit the accepted fields to the ones defined in the schema.

classmethod 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.HeaderSchema(*arg, **kw)

Base schema used for validating and deserializing request headers.

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

Header schema used with PATCH requests.

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

Schema used for validating and deserializing querystrings. It will include and try to guess the type of unknown fields (field filters) on deserialization.

deserialize(cstruct=<colander.null>)

Deserialize and validate the QuerySchema fields and try to deserialize and get the native value of additional filds (field filters) that may be present on the cstruct.

e.g:: ?exclude_id=a,b&deleted=true -> {‘exclude_id’: [‘a’, ‘b’], deleted: True}

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

Querystring schema used with collections.

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

Querystring schema for GET record requests.

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

Querystring schema for GET collection requests.

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

Single JSON Patch Operation.

class kinto.core.resource.schema.JsonPatchBodySchema(*args, **kw)

Body used with JSON Patch (application/json-patch+json) as in RFC 6902.

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

Base schema for kinto requests.

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

Base schema for methods that use a JSON request body.

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

JSON Patch (application/json-patch+json) request schema.

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

Kinto API custom response headers.

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

Response schema used on 4xx and 5xx errors.

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

Response schema used on 304 Not Modified responses.

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

Response schema used with sigle resource endpoints.

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

Response schema used with plural endpoints.

class kinto.core.resource.schema.ResourceReponses

Class that wraps and handles Resource responses.

get_and_bind(endpoint_type, method, **kwargs)

Wrap resource colander response schemas for an endpoint and return a dict of status codes mapping cloned and binded responses.

class kinto.core.resource.schema.ShareableResourseResponses(**kwargs)

Class that wraps and handles SharableResource responses.

Model

class kinto.core.resource.model.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 HTTP API.

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:int
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, sorting=None, pagination_rules=None, limit=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.
  • 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 deletion 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 deleted.
  • 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)

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().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
Returns:

the newly created record.

Return type:

dict

update_record(record, parent_id=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):
    record = super().update_record(record, parent_id)
    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
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().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

class kinto.core.resource.model.ShareableModel(*args, **kwargs)

A protected collection interacts with the permission backend.

delete_records(filters=None, sorting=None, pagination_rules=None, limit=None, parent_id=None)

Delete permissions when collection records are deleted in bulk.

get_record(record_id, parent_id=None)

Fetch current permissions and add them to returned record.

create_record(record, parent_id=None)

Create record and set specified permissions.

The current principal is added to the owner (write permission).

update_record(record, parent_id=None)

Update record and the specified permissions.

If no permissions is specified, the current permissions are not modified.

The current principal is added to the owner (write permission).

delete_record(record_id, parent_id=None, last_modified=None)

Delete record and its associated permissions.

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.