Type System Python API¶
To manipulate resource types, we provide a simple mechanism to interact with the collection and model.
Lets take for example, the following resource type definition:
import typing
from dataclasses import dataclass, field
import morpfw
import morpfw.sql
import sqlalchemy as sa
from morpfw.authz.pas import DefaultAuthzPolicy
from morpfw.crud import permission as crudperm
from morpfw.permission import All
class AppRoot(object):
def __init__(self, request):
self.request = request
class App(DefaultAuthzPolicy, morpfw.SQLApp):
pass
@App.path(model=AppRoot, path="/")
def get_approot(request):
return AppRoot(request)
@App.permission_rule(model=AppRoot, permission=All)
def allow_all(identity, context, permission):
""" Default permission rule, allow all """
return True
@App.json(model=AppRoot)
def index(context, request):
return {"message": "Hello World"}
@dataclass
class PageSchema(morpfw.Schema):
body: typing.Optional[str] = field(default=None, metadata={"title": "Body"})
value: typing.Optional[int] = field(default=0, metadata={"title": "Value"})
class PageCollection(morpfw.Collection):
schema = PageSchema
class PageModel(morpfw.Model):
schema = PageSchema
blob_fields = ['attachment']
# SQLALchemy model
class Page(morpfw.sql.Base):
__tablename__ = "test_page"
body = sa.Column(sa.Text())
value = sa.Collection(sa.Integer())
class PageStorage(morpfw.SQLStorage):
model = PageModel
orm_model = Page
@App.storage(model=PageModel)
def get_storage(model, request, blobstorage):
return PageStorage(request, blobstorage=blobstorage)
@App.path(model=PageCollection, path="/pages")
def get_collection(request):
storage = request.app.get_storage(PageModel, request)
return PageCollection(request, storage)
@App.path(model=PageModel, path="/pages/{identifier}")
def get_model(request, identifier):
col = get_collection(request)
return col.get(identifier)
class PageStateMachine(morpfw.StateMachine):
states = ["new", "pending", "approved"]
transitions = [
{"trigger": "approve", "source": ["new", "pending"], "dest": "approved"},
{"trigger": "submit", "source": "new", "dest": "pending"},
]
@App.statemachine(model=PageModel)
def get_pagemodel_statemachine(context):
return PageStateMachine(context)
@App.permission_rule(model=PageCollection, permission=All)
def allow_collection_all(identity, context, permission):
""" Default permission rule, allow all """
return True
@App.permission_rule(model=PageModel, permission=All)
def allow_model_all(identity, context, permission):
""" Default permission rule, allow all """
return True
@App.typeinfo(name="test.page", schema=PageSchema)
def get_typeinfo(request):
return {
"title": "Test Page",
"description": "",
"schema": PageSchema,
"collection": PageCollection,
"collection_factory": get_collection,
"model": PageModel,
"model_factory": get_model,
}
Collection API¶
Getting collection object¶
All resource types are registered in a central registry using typeinfo
directive.
This allows you to query for collection by name, and use it in your program.
col = request.get_collection('test.page')
Creating records¶
Records can be created through create
method on collections.
page1 = col.create({'body': 'Hello world', 'value': 123})
create
method by default expect a dictionary with JSON serialized values,
which mean, date and time would need to be passed to the method as
Avro style date or time integers. (unix timestamp in miliseconds for
datetime
, number of days from epoch for date
)
If you already deserialized the values beforehand, and your date and time
are date
or datetime
objects. You will need to pass deserialize=False
to the method.
page1 = col.create({'body': 'Hello world', 'value': 123}, deserialize=False)
By default, create
method is set to insecure mode, which mean, it will allow
state
to be set during record creation (which ideally, you should not do this,
because this should be handled by statemachine), and also will allow setting
values for fields which are marked with initializable=False
. If you
want to force security check, add secure=True
to the parameter.
Getting individual record¶
If you know the UUID of a specific record, you can get the record using:
page1 = col.get(record_uuid) # record_uuid is a 32 char uuid string
Searching for records¶
To search for records, you can use the search
method. Filtering of search
results is using rulez
JSON boolean statements, which you can refer
to rulez documentation
for details.
import rulez
pages = col.search(rulez.field('value') == 123) # returns a list of Page model
Aggregation query¶
It is also possible to aggregate through the collection API. Aggregation
is done through a group
query which uses the following structure:
{
"<output_field>" : {
"function": "<aggregation_function>",
"field": "<field_name>"
},
"<output_field2>" : {
"function": "<aggregation_function2>",
"field": "<field_name2>"
},
}
For example:
group = {
'hour': {
'function': 'hourly',
'field': 'created'
},
'count': {
'function': 'count',
'field': 'uuid'
}
}
results = col.aggregate(group=group)
Only basic aggregation is supported through this API, primarily for the purpose for presenting data for analytics. For more complex aggregation, it is suggested that you develop that without using this aggregate API.
SQLStorage aggregate functions¶
Aggregate functions are storage specific, and currently, only following
aggregate functions are supported for sqlstorage
:
Dimensions
year
month
day
date
hourly
Metrics
count
sum
avg
min
max
ElasticsearchStorage aggregate functions¶
Aggregate functions are storage specific, and currently, only following
aggregate functions are supported for elasticsearchstorage
:
Dimensions
year
month
day
date
interval_1m
interval_15m
interval_30m
interval_1h
Metrics
count
sum
avg
Model API¶
Reading data¶
Model is subscriptable and data can be accessed similar to a dictionary.
body = page1['body']
Updating data¶
Updating data on a record can be done using update
method, which
have similar API as Collection’s create
method.
page1.update({'body': 'new body text'})
Deleting record¶
To delete a record, you can call the delete
method.
page1.delete()
BLOB management¶
As BLOBs are not stored in the main data storage, but rather in a separate
blobstorage
, manipulating BLOBs are done usine a separate API.
Saving BLOB¶
To save a BLOB into a model, the API would be:
import os
import mimetypes
file_path = '/path/to/file'
# in a view, you likely can get these information from
# the request itself
stat = os.stat(file_path)
filename = os.path.basename(file_path)
mt = mimetypes.guess_type(filename)
with open('file','b') as f:
page1.put_blob('attachment', f,
filename=filename,
mimetype=mt[0], size=stat.st_size)
If you are in a view, and file is uploaded as multipart/form-data
, you
can get mimetype
and file
object using following example:
# assuming file is uploaded as ``upload`` field
@App.json(model=Page, name='upload-attachment', request_method='POST')
def view(context, request):
upload = request.POST.get('upload')
filename = os.path.basename(upload.filename)
mimetype = upload.type
fileobj = upload.file
context.put_blob('attachment', fileobj, filename=filename, mimetype=mimetype)
return {"status": "ok"}
Reading BLOB¶
Saved BLOBs can be read using:
blob = page1.get_blob('attachment')
with blob.open() as f:
data = f.read()
You can also return a BLOB as a streaming response in a view
@App.view(model=Page, name='get-attachment')
def get_blob(context, request):
blob = context.get_blob('attachment')
return request.get_response(blob)
Deleting BLOBs¶
To delete BLOBs, you can use:
page1.delete_blob('attachment')
Accessing state machine¶
If your model have a state machine registered with it, you can get
the state machine object using statemachine
method.
# get state machine
sm = page1.statemachine()
# trigger ``approve`` transition
sm.approve()
To learn more about state machine object, you can refer to PyTransitions documentation as the state machine is built using it.