Quickstart
Bootstrapping a new project
MorpFW requires Python 3.7 or newer to run. Python 3.6 is also supported but
you will need to install dataclasses backport into your environment.
The recommended way to install morpfw is to use buildout,
skeleton that is generated using mfw-template. Please head to
mfw-template documentation for tutorial.
Bootstrapping without mfw-template
If you prefer to use virtualenv, or other methods, you can follow these
steps.
First, lets get morpfw installed
$ pip install morpfw
If you are using buildout, version locks files are available at
mfw_workspace repository: https://github.com/morpframework/mfw_workspace/tree/master/versions
Lets create an app.py. In this example, we are creating a SQLApp application,
which meant to use SQLAlchemy as its primary data source, and provides SQLAlchemy
transaction & session management.
import morpfw
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"}
morpfw boot up application using a settings.yml file, so lets create one:
application:
title: My First App
class: app:App
Make sure you change your working directory to where app.py is, and you can
then start the application using
$ PYTHONPATH=. morpfw -s settings.yml start
Creating a simple resource type / CRUD model
morpfw adds a type engine with RESTful CRUD on top of morepath. To utilize
it, your models will need to follow a particular convention:
A
Collectionis created that inheritsmorpfw.CollectionA
Modelis created that inheritsmorpfw.ModelBoth
CollectionandModelclass have aschemaattribute that reference to adataclassbased schemaSchema must be written using
dataclass, following convention frominverterproject.A
Storageclass is implemented following the storage component API, and registered against theModelclass.A named typeinfo component is registered with details of the resource type.
Following is an example boilerplate declaration of a resource type called page,
which will hook up the necessary RESTful API CRUD views for a simple data model
with title and body text.
import typing
from dataclasses import dataclass, field
import morpfw
import morpfw.sql
import sqlalchemy as sa
@dataclass
class PageSchema(morpfw.Schema):
title: typing.Optional[str] = field(default=None, metadata={"title": "Title"})
body: typing.Optional[str] = field(default=None, metadata={"title": "Body"})
class PageCollection(morpfw.Collection):
schema = PageSchema
class PageModel(morpfw.Model):
schema = PageSchema
# SQLALchemy model
class Page(morpfw.sql.Base):
__tablename__ = "test_page"
title = sa.Column(sa.String(length=1024))
body = sa.Column(sa.Text())
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)
@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,
}
Configuring Database Connection
At the moment, morpfw.SQLStorage requires PostgreSQL to work correctly (due to
coupling to some PostgreSQL specific dialect feature). To configure the database
connection URI for SQLStorage, in settings.yml, add in configuration option:
configuration:
morpfw.storage.sqlstorage.dburi: 'postgresql://postgres:postgres@localhost:5432/app_db'
If you want to use beaker for session and caching, you can also add:
configuration:
...
morpfw.beaker.session.type: ext:database
morpfw.beaker.session.url: 'postgresql://postgres:postgres@localhost:5432/app_cache'
morpfw.beaker.cache.type: ext:database
morpfw.beaker.cache.url: 'postgresql://postgres:postgres@localhost:5432/app_cache'
...
Initializing Database Tables
morpfw provide integration with Alembic for
generating SQLAlchemy based migrations.
To initialize alembic directory, you can run:
$ morpfw migration init migrations
To hook up your application SQLAlchemy models for alembic scan, you will
need to edit env.py and add following imports, and configure target_metadata
to include SQLStorage metadata:
from morpfw.crud.storage.sqlstorage import Base
import app
...
# configure target_metadata
target_metadata = Base.metadata
As morpfw uses some additional sqlalchemy libraries, script.py.mako need
to also be edited to add additional imports:
import sqlalchemy_utils.types
import sqlalchemy_jsonfield.jsonfield
Then, configure alembic.ini (generated together during migration init) to
point to your database:
[alembic]
...
sqlalchemy.url: 'postgresql://postgres:postgres@localhost:5432/app_db'
...
Now you can use morpfw migration to generate a migration script based on defined
SQLAlchemy models.
$ PYTHONPATH=. morpfw migration revision --autogenerate -m "initialize"
You can then apply the migration using:
$ PYTHONPATH=. morpfw migration upgrade head
Finally you can start you application:
$ PYTHONPATH=. morpfw -s settings.yml start
CRUD REST API
If nothing goes wrong, you should get a CRUD REST API registered at
http://localhost:5000/pages/.
>>> import requests
>>> resp = requests.get('http://localhost:5000/pages')
>>> resp.json()
{...}
Lets create a page
>>> resp = requests.post('http://localhost:5000/pages/', json={
... 'body': 'hello world'
... })
>>> objid = resp.json()['data']['uuid']
>>> resp = requests.get('http://localhost:5000/pages/%s' % objid)
>>> resp.json()
{...}
Lets update the body text
>>> resp = requests.patch(
... 'http://localhost:5000/pages/%s?user.id=foo' % objid, json={
... 'body': 'foo bar baz'
... })
>>> resp = requests.get('http://localhost:5000/pages/%s' % objid)
>>> resp.json()
{...}
Lets do a search
>>> resp = requests.get('http://localhost:5000/pages/+search')
>>> resp.json()
{...}
Lets delete the object
>>> resp = requests.delete('http://localhost:5000/pages/%s' % objid)
>>> resp.status_code
200
Python CRUD API
Python CRUD API is handled by Collection and Model objects. The
typeinfo registry allows name based getter to Collection` from
the ``request object.
page_collection = request.get_collection('test.page')
page = page_collection.get(page_uuid)
For more details, please refer to the type system documentation.