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 Collection is created that inherits morpfw.Collection

  • A Model is created that inherits morpfw.Model

  • Both Collection and Model class have a schema attribute that reference to a dataclass based schema

  • Schema must be written using dataclass, following convention from inverter project.

  • A Storage class is implemented following the storage component API, and registered against the Model class.

  • 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.