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 inheritsmorpfw.Collection
A
Model
is created that inheritsmorpfw.Model
Both
Collection
andModel
class have aschema
attribute that reference to adataclass
based schemaSchema must be written using
dataclass
, following convention frominverter
project.A
Storage
class is implemented following the storage component API, and registered against theModel
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.