Interfaces¶
There are two major entities in this framework: resources and links between them.
Resource API defines interfaces to be implemented in order to expose the entities.
NOTE: all methods must be implemnted. In case if some of the methods are not supposed to do anything, raise NotImplemntedError within their implementations and return False for respective authorization methods if needed.
Resource¶
Resource concept is similar to the one mentioned in Roy Fielding’s desertation.
- class resource_api.interfaces.Resource(context)¶
Represents entity that is supposed to be exposed via public interface
Methods have the following arguments:
- pk
- PK of exisiting resource
- data (dict)
- information to be stored within the resource
- params (dict)
- extra parameters to be used for collection filtering
- user (object)
- entity that corresponds to the user that performs certain operation on the resource
- UriPolicy¶
alias of PkUriPolicy
- __init__(context)¶
- context (object)
- entity that is supposed to hold DAL (data access layer) related functionality like database connections, network sockets, etc.
- can_create(user, data)¶
Returns True if user is allowed to create resource with certain data
- can_delete(user, pk)¶
Returns True if user is allowed to delete the resource
- can_discover(user, pk)¶
Returns False if user is not allowed to know about resoure’s existence
- can_get_data(user, pk, data)¶
Returns only the fields that user is allowed to fetch
- can_get_uris(user)¶
Returns True if user is allowed to list the items in the collection or get their count
- can_update(user, pk, data)¶
Returns True if user is allowed to update the resource
- create(user, pk, data)¶
Creates a new instance
- delete(user, pk)¶
Removes the resource
- exists(user, pk)¶
Returns True if the resource exists
- get_count(user, params=None)¶
Returns total amount of items that fit filtering criterias
- get_data(user, pk)¶
Returns fields of the resource
- get_uris(user, params=None)¶
Returns an iterable over primary keys
- update(user, pk, data)¶
Updates specified fields of a given instance
URI is represented by a PK (Primary Key) in Resource API.
Resource interface defines two types of methods.
First, DAL related CRUD methods: get_data, get_pks, set, delete, exists
Second, authorization related methods starting with can_
Each resource must define a UriPolicy:
- class resource_api.interfaces.AbstractUriPolicy(resource_instance)¶
Defines a way to generate URI based on data that was passed when creating the resource.
- __init__(resource_instance)¶
- resource_instance (Resource instance)
- entity that can be used to access previously created items
- deserialize(pk)¶
Transforms data sent over the wire into sth. usable inside DAL
- pk
- PK value as it comes over the wire - e.g. string in case of HTTP
- @return
- PK transformed to the data type expected to by DAL in order to fetch data
- generate_pk(data, link_data=None)¶
Generates a PK based on input data
- data (dict):
- the same data that is passed to Resource’s create method
- link_data (dict):
- the same link_data that is passed to Resource’s create method
- @return
- generated PK
- get_schema()¶
Returns meta information (dict) to be included into resource’s schema
- serialize(pk)¶
Transforms value into sth. ready to transfer over the wire
- pk
- PK value used within DAL to identify stored entries
- @return
- PK transformed into something that can be sent over the wire - e.g. string in case of HTTP
- type¶
A string that would give a hint to the client which PK policy is in use
The default pk policy is this one:
- class resource_api.interfaces.PkUriPolicy(resource_instance)¶
Uses value of a field marked as “pk=True” as resource’s URI
Note, there are certain cases when the URI is supposed to be generated within peristence (data acess) layer. E.g. via autoincrementing primary key in SQL database. In such case the URI is supposed to be returned by create method.
class Example(Resource):
class UriPolicy(AbstractUriPolicy):
def deserialize(self, pk):
try:
return int(pk)
except ValueError:
raise ValidationError("URI is not int")
def serialize(self, pk):
return pk
@property
def type(self):
return "autoincrement_pk_policy"
def generate_pk(self, data, link_data=None):
return None
def create(self, pk, data):
# assert pk is None
row_id = self._sql_database.create_row(data)
return row_id
...
Link¶
Link concept is derived from RDF triples . RDF is at the same time a part of two big W3C standardized concepts: Linked Data and Semantic Web.
NOTE: Resource API does not aim to follow any of standards defined by the concepts mentioned above. It just uses a portion of interesting ideas that those concepts describe.
- class resource_api.interfaces.Link(context)¶
Represents a relationship between two resources that needs to be exposed via public interface
Methods have the following arguments:
- pk
- PK of exisiting source resource (the one that defines link field)
- data (dict)
- extra information to be stored for this relationship
- rel_pk (digit|string)
- PK of exisiting target resource (the one to which we are linking to)
- params (dict)
- extra parameters to be used for collection filtering
- user (object)
- entity that corresponds to the user that performs certain operation on the link
- __init__(context)¶
- context (object)
- entity that is supposed to hold DAL (data access layer) related functionality like database connections, network sockets, etc.
- can_create(user, pk, rel_pk, data)¶
Returns True if user is allowed to create resource with certain data
- can_delete(user, pk, rel_pk)¶
Returns True if user is allowed to delete the resource
- can_discover(user, pk, rel_pk)¶
Returns False if user is not allowed to know about resoure’s existence
- can_get_data(user, pk, rel_pk, data)¶
Returns only the fields that user is allowed to fetch
- can_get_uris(user, pk)¶
Returns True if user is allowed to list the items in the collection or get their count
- can_update(user, pk, rel_pk, data)¶
Returns True if user is allowed to update the resource
- create(user, pk, rel_pk, data=None)¶
Creates a new link with optional extra data
- delete(user, pk, rel_pk)¶
Removes the link. If rel_pk is None - removes all links
- exists(user, pk, rel_pk)¶
Returns True if the link exists (is not nullable)
- get_count(user, pk, params=None)¶
Returns total amount of items that fit filtering criterias
- get_data(user, pk, rel_pk)¶
Returns link data
- get_uris(user, pk, params=None)¶
Returns an iterable over target primary keys
- update(user, pk, rel_pk, data)¶
Updates exisiting link with specified data
Link interface defines two types of methods similar to the ones of Resource interface.
There are conceptual differences between those methods in Link and Resource though.
First, link uses a triple (mentioned above) to address exisiting entities.
Second, data is optional for links.
It is critical to note that predicate part of a triple is not passed to any of Link methods. Since all links are supposed to be defined via nested classes in the context of resources they connect - link classes themselves serve as those predicates.
Link declaration¶
Links between resources are defined using nested classes:
class Course(Resource):
class Links:
class attendants(Link):
target = "Student"
related_name = "active_cources"
master = True
def get(self, pk, rel_pk):
...
class Student(Resource):
class Links:
class active_cources(Link):
target = "Course"
related_name = "attendants"
def get(self, pk, rel_pk):
...
target has to be a string. It can point to a resource in the same module (“Target”) or in any other one (“module.name.Target”). related_name must be defined as a string as well and it should equal to the name of a related link.
Also one of the links must be defined as a master one. Authorization is done against master link. And extra data is stored only in DAL related to master link.
Any link can be marked as changeable = False. Unchangeable links can be set only upon resource creation. Once the resource is created links cannot be modified (i.e. updated/set or deleted).
All link declarations must be done within Links inner class.
One way links¶
Note
If the link is marked as one_way Resource API will not be able to enforce relational integrity.
One way links do not need a related_name nor a master flag to be defined. One way links can be declared the following way:
class Source(Resource):
class Links:
class targets:
target = "foo.bar.Target"
one_way = True
Link cardinality¶
More on relationship cardinality - here.
MANY to ONE relationship can be defined this way:
class Target(Resource):
class Links:
class sources(Link):
cardinality = Link.cardinalities.MANY # could be ommited - it is the default one
target = "Source"
related_name = "target"
class Source(Resource):
class Links:
class target(Link):
cardinality = Link.cardinalities.ONE
target = "Target"
related_name = "sources"
master = True
...
ONE to ONE relationship can be defined this way:
class Target(Resource):
class Links:
class source(Link):
cardinality = Link.cardinalities.ONE
target = "Source"
related_name = "target"
class Source(Resource):
class Links:
class target(Link):
cardinality = Link.cardinalities.ONE
target = "Target"
related_name = "source"
master = True
...
MANY to MANY is the default one but explicitly can be defined this way:
class Target(Resource):
class Links:
class sources(Link):
cardinality = Link.cardinalities.MANY # could be ommited - it is the default one
target = "Source"
related_name = "targets"
class Source(Resource):
class Links:
class targets(Link):
cardinality = Link.cardinalities.MANY # could be ommited - it is the default one
target = "Target"
related_name = "sources"
master = True
...
NOTE: relationships with cardinality ONE can be marked as required:
class Target(Resource):
class Links:
class sources(Link):
cardinality = Link.cardinalities.MANY # could be ommited - it is the default one
target = "Source"
related_name = "target"
class Source(Resource):
class Links:
class target(Link):
cardinality = Link.cardinalities.ONE
target = "Target"
related_name = "sources"
required = True
master = True
...
Relationships with MANY cardinality cannot be marked as required.
In case of required relationships, data for them must be passed together with main resource data during creation phase.
Schema and QuerySchema¶
Resources and Links may define schema that shall be used via Resource API for input validation.
The schema is defined the following way:
class CustomResource(Resource):
class Schema:
name = schema.StringField(pk=True)
count = schema.IntegerField()
class Links:
class target(Link):
class Schema:
timestamp = schema.DateTimeField()
Schema fields are defined within a nested class with a reserved name Schema. A comprehensive reference for built-in fields can be found here.
Additionally both Resources and Links may define query schema to validate all parameters that client uses for filtering the collections.
Query schema is defined the following way:
class CustomResource(Resource):
class QuerySchema:
name = schema.StringField(pk=True)
count = schema.IntegerField()
class Links:
class target(Link):
class QuerySchema:
timestamp = schema.DateTimeField()
Query parameters are defined in a similar manner as Schema ones but inside QuerySchema nested subclass. The key functional difference between two schemas is the fact that Schema may have required fields and QuerySchema may not.
NOTE: it is not necessary for Schema and QuerySchema inner classes to inherit from Schema class. Resource API adds this inheritance automatically.