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):
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 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(TrackedModel, self).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(Mushroom, self).__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
¶ Default
kinto.core.resource.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 thekinto.core.storage
andkinto.core.permission
backends.alias of
Model
-
mapping
= <kinto.core.resource.schema.ResourceSchema object at 139983341504720 (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 schema. If the resource schema allows unknown fields, this will always returnTrue
.Parameters: field (str) – Field name Return type: bool
-
collection_get
()¶ Model
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
()¶ 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
ifIf-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
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
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 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
kinto.core.resource.UserResource.apply_changes()
orkinto.core.resource.UserResource.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): 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
-
Shareable resources allow to set permissions on records, in order to share their access or protect their modification.
alias of
ShareableModel
alias of
ShareableViewSet
List of allowed permissions names.
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.
Read permissions from request body, and in the case of
PUT
every existing ACE is removed (using empty list).
Add
permissions
attribute in response body.In the HTTP API, it was decided that
permissions
would reside outside thedata
attribute.
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: 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 inkinto.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.
-
-
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
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.
-
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
- 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
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.
- 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)¶ 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
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(MyModel, self).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(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
-
A protected collection interacts with the permission backend.
Delete permissions when collection records are deleted in bulk.
Fetch current permissions and add them to returned record.
Create record and set specified permissions.
The current principal is added to the owner (
write
permission).
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 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.
-