About¶
Facture is a Python framework for creating structured, portable and high-performance Web APIs. It’s built on top of Sanic and uses the blazing fast uvloop implementation of the asyncio event loop.
Compatibility¶
Python 3.6+
Installing¶
$ pip install facture
License¶
BSD 2-Clause License
Copyright (c) 2019, Robert Wikman <rbw@vault13.org> All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Application¶
At a minimum, the facture.Application
needs to be created with at least one Jetpack.
Additional parameters can be provided to further customize the instance; see the docs below for more info.
API
Note
The facture.Application
and facture.Application.run()
can be configured using the environment and files as well.
Read more about this in the Configuration section.
Example
import facture
import jet_apispec
import jet_guestbook
# Create application
app = facture.Application(
path='/api',
packages=[
('/guestbook', jet_guestbook),
('/packages', jet_apispec)
]
)
# Start server
app.run(host='192.168.0.1')
Manager¶
Usage¶
Jetpacks are used for grouping, labeling and making components ready for registration
with an Application
.
API
Important
The Jetpack’s __init__.py file must have a __version__
variable set to be successfully registered with an Application.
Example
from facture import Jetpack
from jet_guestbook import service
from .service import VisitService, VisitorService
from .model import VisitModel, VisitorModel
from .controller import Controller
__version__ = '0.1.0'
export = Jetpack(
controller=Controller,
services=[VisitService, VisitorService],
models=[VisitModel, VisitorModel],
name='guestbook',
description='Example guestbook package'
)
Controller¶
The Jetpack Controller class inherits from ControllerBase
and is registered
with the Application
upon server start.
API
Example
from facture.controller import ControllerBase
class Controller(ControllerBase):
async def on_ready(self):
self.log.debug(f'Controller ready at path: {self.pkg.path}')
async def on_request(self, request):
self.log.debug(f'Request received: {request}')
Note
Continue reading about Routing to see how handlers can be added.
Routing¶
Routing is implemented using one or more handlers decorated with a @route. Used without the @input_load decorator, the entire request object is passed to the handler.
API
Example
from sanic.response import HTTPResponse
from facture.controller import ControllerBase, route
class Controller(ControllerBase):
async def on_request(self, request):
self.log.debug(f'Request received: {request}')
@route('/<name>', 'GET'):
async def greet(self, request, name):
return HTTPResponse({'msg': f'hello {name} from {request.ip}')
Note
Continue reading about Transformation to see how request/response data can be manipulated.
Transformation¶
Request and response transformation is performed when a request reaches @input_load, and upon handler return in @output_dump. These two decorators provides a declarative way of defining what comes in and what goes out of a route handler.
API
Example of request/response transformation
from facture.controller import ControllerBase, route
from facture.schema import ParamsSchema
from .visit import svc_visit
from .visit.schemas import Visit
class Controller(ControllerBase):
async def on_request(self, request):
self.log.debug(f'Request received: {request}')
@route('/', 'GET')
@input_load(query=ParamsSchema) # Transform and validate the query string
@output_dump(Visit, many=True) # Dump many `Visit`s
async def visits_get(self, query):
# Call the service layer and dump the result as a JSON string
return await svc_visit.get_many(**query)
@route('/<visit_id>', 'PUT') # Perform an update operation
@input_load(body=Visit) # Transform and validate the JSON payload
@output_dump(Visit) # Dump one `Visit`
async def visit_update(self, remote_addr, body, visit_id):
# Call the service layer and dump the result as a JSON string
return await svc_visit.visit_update(remote_addr, visit_id, body)
Schemas¶
Schemas are used in transformation decorators to perform object serialization and generating HTTP API documentation.
Example
from facture.schema import fields, Schema
class Visit(Schema):
id = fields.Integer()
visited_on = fields.String(attribute='created_on')
message = fields.String()
name = fields.String()
class Meta:
dump_only = ['id', 'visited_on']
load_only = ['visit_id', 'visitor_id']
class VisitNew(Schema):
message = fields.String(required=True)
name = fields.String(required=True)
See also
Check out the Marshmallow API docs for more info on how to work with schemas.
Services¶
Facture provides a set of built-in service classes, or Mixins if you will - used to extend a Package’s service layer with extra features such as database and HTTP access.
Note
Create a PR or Issue if you want a Service Layer component added or updated.
BaseService¶
This Service implements the singleton pattern and is directly or indirectly used by all types of Facture services.
API
DatabaseService¶
The built-in DatabaseService
inherits from BaseService
and
provides an interface for interacting with MySQL and PostgreSQL databases using the
peewee-async manager.
Example¶
from facture.service import DatabaseService
from facture.exceptions import FactureException
from jet_guestbook.model import VisitModel
class VisitService(DatabaseService):
__model__ = VisitModel
async def get_authored(self, visit_id, remote_addr):
visit = await self.get_by_pk(visit_id)
if visit.visitor.ip_addr != remote_addr:
raise FactureException('Not allowed from your IP', 403)
return visit
async def visit_count(self, ip_addr):
return await self.count(VisitModel.visitor.ip_addr == ip_addr)
API¶
Important
The __model__ class attribute must be set for Services implementing the DatabaseService.
Models¶
Models are implemented using Peewee.Model.
Example¶
from datetime import datetime
from peewee import Model, ForeignKeyField, CharField, DateTimeField
from .visitor import VisitorModel
class VisitModel(Model):
class Meta:
table_name = 'visit'
created_on = DateTimeField(default=datetime.now)
message = CharField(null=False)
visitor = ForeignKeyField(VisitorModel)
@classmethod
def extended(cls, *fields):
return cls.select(VisitModel, VisitorModel, *fields).join(VisitorModel)
HttpClientService¶
The built-in HttpClientService
provides
an interface for interacting with HTTP servers.
Example¶
from facture.service import HttpClientService, DatabaseService
from .model import EntryModel
class EntryService(HttpClientService, DatabaseService):
__model__ = EntryModel
def __init__(self):
self.backup_url = 'https://192.168.1.10'
async def entry_add(self, entry_new):
entry = await self.create(entry_new)
self.log.info(f'sending a copy to {self.backup_url}')
await self.http_post(self.backup_url, entry_new)
return entry