About

Aioli is a Framework for building RESTful HTTP and WebSocket API packages, with a sensible separation between request/response handling (transformation, validation, etc), application logic and data access layers.

Furthermore, it makes use of asyncio, is lightweight and provides good performance and concurrency.

Not in mood for reading docs? Check out the The Guestbook example: a Comprehensive RESTful HTTP API package.

Install

The Aioli Framework can be installed using Python pip.

$ pip3 install aioli

Configure

The Application and associated Packages can be configured using either environment variables, or by a dictionary provided to the config parameter when creating the Application.

Note

Note!

Environment takes precedence over Application Constructor config.

Mappings

Environment and Dictionary configs uses different naming conventions, for obvious reasons, but follows the same logic.

Application

Mappings used for configuring core parts of an Aioli Application.

Locations

  • Dictionary key: “aioli_core”

  • Environment prefix: “AIOLI_CORE”

  • Run-time access: aioli.Application.config

Mappings

Dictionary

Environment

DEFAULT

dev_host

AIOLI_CORE_DEV_HOST

127.0.0.1

dev_port

AIOLI_CORE_DEV_PORT

5000

api_base

AIOLI_CORE_API_BASE

/api

debug

AIOLI_CORE_DEBUG

False

Package

A custom Package configuration schema can be defined using the PackageConfigSchema class, which comes with a set of common parameters, listed below.

Locations

  • Dictionary key: [package_name]

  • Environment prefix: [PACKAGE_NAME]

  • Run-time access: aioli.Package.config

Mappings

Dictionary

Environment

DEFAULT

debug

[PACKAGE_NAME]_DEBUG

None

controllers_enable

[PACKAGE_NAME]_CONTROLLERS_ENABLE

True

services_enable

[PACKAGE_NAME]_SERVICES_ENABLE

True

Check out the Package Config Schema docs for info on how to extend the base schema with custom parameters.

Environment

Configuring Aioli using Environment Variables can be useful in some environments.

Example

$ export AIOLI_CORE_DEV_HOST="0.0.0.0"
$ export AIOLI_CORE_DEV_PORT="5555"
$ export AIOLI_RDBMS_TYPE="mysql"
$ export AIOLI_RDBMS_HOST="127.0.0.1"
$ export AIOLI_RDBMS_DATABASE="aioli"
$ export AIOLI_RDBMS_USERNAME="aioli"
$ export AIOLI_RDBMS_PASSWORD="super_secret"
$ export AIOLI_GUESTBOOK_VISITS_MAX="10"

Constructor

The configuration can be provided as a dictionary to the config parameter when creating the Application.

Check out the Package config schema for an example.

Access

Both Application and Package configurations can be easily accessed from Service and Controller instances, using the config property.

Deploy

Development

Given an Application like:

# File: main.py

import aioli_guestbook
import aioli_rdbms

import toml

from aioli import Application

app = Application(
    config=toml.load("config.toml"),
    packages=[
        aioli_guestbook,
        aioli_rdbms,
    ]
)

…the Aioli CLI can be used to start the Application (using a built-in Uvicorn server).

$ python3 -m aioli dev-server main:app

Production

Work in progress

Application

To run the Aioli application, an Application instance must be created. Its constructor expects a list of at least one Package to be registered with the application.

class aioli.Application(packages, **kwargs)[source]

Creates an Aioli application

Parameters
  • config – Configuration dictionary

  • packages – List of package tuples [(<mount path>, <module>), …]

  • kwargs – Keyword arguments to pass along to Starlette

Variables
  • log – Aioli Application logger

  • packages – Packages registered with the Application

add_exception_handler(exception, handler)[source]

Add a new exception handler

Parameters
  • exception – Exception class

  • handler – Exception handler

Example – Guestbook Web API making use of the aioli_rdbms extension

import aioli_guestbook
import aioli_rdbms

import toml

from aioli import Application

app = Application(
    config=toml.load("config.toml"),
    packages=[
        aioli_guestbook,
        aioli_rdbms,
    ]
)

Package

A Package is a namespaced and labelled group of components that can be imported into an Application.

There are two main components for building Web API Packages: Controller & Service

When developing an Aioli application, local Packages typically contain code that makes sense to modularize in the Application at hand.

class aioli.Package(name, description, version, controllers=None, services=None, config=None)[source]

Associates components and meta with a package, for registration with a Aioli Application.

Parameters
  • name – Package name ([a-z, A-Z, 0-9, -])

  • description – Package description

  • version – Package semver version

  • controllers – List of Controller classes to register with the Package

  • services – List of Services classes to register with the Package

  • config – Package Configuration Schema

Variables
  • app – Application instance

  • log – Package logger

  • state – Package state

  • path – Package Path

  • name – Package Name

  • version – Package Version

  • config – Package config

  • controllers – List of Controllers registered with the Package

  • services – List of Services registered with the Package

Example – Creating a Package with Controller and Service layers

from aioli import Package

from .service import VisitService, VisitorService
from .controller import HttpController
from .config import ConfigSchema


export = Package(
    name="aioli_guestbook",
    version="0.1.0",
    description="Example guestbook Package",
    controllers=[HttpController],
    services=[VisitService, VisitorService],
    config=ConfigSchema,
)

Config

Package config schemas makes use of the Marshmallow library and offers a simple, clean and safe way of customizing Packages.

Read more in Setup/Configure, or check out an Example.

class aioli.config.PackageConfigSchema(*args, **kwargs)[source]

Package configuration schema

Variables
  • debug – Set debug level for package, effectively overriding Application’s debug level

  • path – Package path, uses Package name if empty

  • should_import_services – Setting to False skips Service registration for this Package

  • should_import_controllers – Setting to False skips Controller registration for this Package

Controller

The Controller layer takes care of routing & request/response handling (transformation, validation, etc). Multiple Controllers of different type may coexisting in a Package’s Controller layer.

HTTP

Creating an HTTP Interface – be it RESTful or otherwise – is done using the BaseHttpController class.

API

class aioli.controller.BaseHttpController(pkg)[source]

HTTP API Controller

Parameters

pkg – Attach to this package

Variables
  • pkg – Parent Package

  • config – Package configuration

  • log – Controller logger

async on_request(*args)[source]

Called on request arrival for this Controller

async on_shutdown()

Called when the Application is shutting down gracefully

async on_startup()

Called after the Package has been successfully attached to the Application

Example – Controller without route handlers

from aioli.controller import BaseHttpController

from .service import VisitService


class HttpController(BaseHttpController):
    def __init__(self):
        self.visit = VisitService()
        self.log.debug("Guestbook opening")

    async def on_startup(self):
        self.log.debug(f"Guestbook opened at {self.package.path}")

    async def on_request(self, request):
        self.log.debug(f"Request received: {request}")

Routing

Route handlers are standard Python methods decorated with the @route.

API

aioli.controller.decorators.route(path, method, description=None)[source]

Prepares route registration, and performs handler injection.

Parameters
  • path – Handler path, relative to application and package paths

  • method – HTTP Method

  • description – Endpoint description

Returns

Route handler

Example – Route handler without transformation helpers

from aioli.controller import BaseHttpController, Method, route

from .service import VisitService


class Controller(BaseController):
    def __init__(self):
        self.visit = VisitService()

    @route("/", Method.GET, "List of entries")
    async def visits_get(self, request):
        # Just pass along the query params as-is.
        #
        # Serialize and return whatever get_many() returns.
        return await self.visit.get_many(**request.query_params)

Transformation

Transformation is implemented on route handlers using @takes and @returns. These decorators offer a simple yet powerful way of shaping and validating request data, while also making sure API endpoints only returns expected data.

This makes the API more secure and consistent.

Takes

The @takes decorator is used to instruct Aioli how to deserialize and validate parts of a request, and injects the resulting dictionaries as arguments to the decorated function.

API

aioli.controller.decorators.takes(props=None, **schemas)[source]

Takes a list of schemas used to validate and transform parts of a request object. The selected parts are injected into the route handler as arguments.

Parameters
  • props – List of Pluck targets

  • schemas – list of schemas (kwargs)

Returns

Route handler

Example – Route handler making use of @takes

from aioli.controller import (
    BaseHttpController, ParamsSchema, RequestProp,
    Method, route, takes
)

from .service import VisitService


class Controller(BaseController):
    def __init__(self):
        self.visit = VisitService()

    @route("/", Method.GET, "List of entries")
    @takes(query=ParamsSchema)
    async def visits_get(self, query):
        # Transform and validate query params against ParamsSchema,
        # then pass it along to get_many().
        #
        # Serialize and return whatever get_many() returns.
        return serialize(await self.visit.get_many(**query))
Returns

The @returns decorator takes care of serializing the data returned by the route handler.

API

aioli.controller.decorators.returns(schema_cls=None, status=200, many=False)[source]

Returns a transformed and serialized Response

Parameters
  • schema_cls – Marshmallow.Schema class

  • status – Return status (on success)

  • many – Whether to return a list or single object

Returns

Response

Example – Route handler making use of @takes and @returns

from aioli.controller import (
    BaseHttpController, ParamsSchema, RequestProp,
    Method, route, takes, returns
)

from .service import VisitService


class Controller(BaseController):
    def __init__(self):
        self.visit = VisitService()

    @route("/", Method.GET, "List of entries")
    @takes(query=ParamsSchema)
    @returns(Visit, many=True)
    async def visits_get(self, query):
        # Transform and validate query params against ParamsSchema,
        # then pass it along to get_many().
        #
        # Transform and dump the object returned from get_many() -
        # according to the Visit schema, as a JSON encoded response
        return await self.visit.get_many(**query)

WebSocket

Note

Work in progress

WebSocket support is not fully integrated with the Framework yet, hence not documented.

Service

The Service layer typically takes care of interacting with external applications: Databases, Remote Web APIs, Message Queues, etc.

Services can be connected and–to provide a good level of flexibility–supports both Inheritance and two types of Composition.

Check out the Connecting Services example to see how a service can integrate and interact with other services.

class aioli.service.BaseService(pkg)[source]
connect(svc)[source]

Reuses existing instance of the given Service class, in the context of the Package it was first registered with.

Parameters

svc – Service class

Returns

Existing Service instance

integrate(svc)[source]

Creates a new instance of the given Service class in the context of the current Package.

Parameters

svc – Service class

Returns

Service instance

async on_shutdown()

Called when the Application is shutting down gracefully

async on_startup()

Called after the Package has been successfully attached to the Application

About

The typical Aioli extension-type Package manages one or more Service objects, provides an API of its own, and may contain Controller code as well.

Create

Extensions make use of the BaseService class and usually implements the Factory pattern teamed by the integrate() method.

Check out the aioli-rdbms extension for an example.

Use

Extensions are registered with the Application, just like a regular Package– and usually have their Services incorporated into other Packages.

Example

Register the local users Package and its dependency, aioli_rdbms.

import aioli_rdbms

from .packages import users

app = Application(
    packages=[
        ("/users", users),
        (None, aioli_rdbms),
        ...
    ]
)

The aioli_rdbms.Service can now be attached to users.UsersService:

from aioli import BaseService
from aioli_rdbms import DatabaseService

from .database import UserModel

class UsersService(BaseService):
    async def on_startup(self):
        self.db = (
            self.integrate(DatabaseService)
            .use_model(UserModel)
        )

    async def get_one(user_id):
        return await self.db.get_one(pk=user_id)

    ...

Publish

Shortly, a Package Management CLI will be added, along with the https://pkgs.aioli.dev website for showing useful info about extension-type Packages; their trust status, install instructions, author and license data, as well as links to source code and more.

About

The idea with these examples, or snippets if you will, is to show how components can be built using the Aioli Framework.

If you’re looking for a fully functional example; check out The Guestbook example – a Comprehensive RESTful HTTP API package.

Service

Read more in the Service documentation.

Connecting Services

Service making use of other services with integrate and connect.

from aioli.service import BaseService
from aioli.exceptions import AioliException, NoMatchFound

from aioli_rdbms import DatabaseService

from .visitor import VisitorService

from .. import database


class VisitService(BaseService):
    visitor: VisitorService
    db = None

    async def on_startup(self):
        self.db = self.integrate(DatabaseService).use_model(database.VisitModel)
        self.visitor = self.connect(VisitorService)

    async def get_authored(self, visit_id, remote_addr):
        visit = await self.db.get_one(pk=visit_id)
        if visit.visitor.ip_addr != remote_addr:
            raise AioliException(status=403, message="Not allowed from your IP")

        return visit

    async def delete(self, visit_id, remote_addr):
        visit = await self.get_authored(visit_id, remote_addr)
        await visit.delete()

    async def update(self, visit_id, payload, remote_addr):
        visit = await self.get_authored(visit_id, remote_addr)
        return await self.db.update(visit, payload)

    async def create(self, remote_addr, visit):
        visit_count = await self.db.count(visitor__ip_addr__iexact=remote_addr)
        visits_max = self.config["visits_max"]

        if visit_count >= visits_max:
            raise AioliException(
                status=400,
                message=f"Max {visits_max} entries per IP. Try deleting some old ones.",
            )

        async with self.db.manager.database.transaction():
            city, country = await self.visitor.ipaddr_location(remote_addr)

            visitor = dict(
                name=visit.pop("visitor_name"),
                ip_addr=remote_addr,
                location=f"{city}, {country}",
            )

            try:
                visit["visitor"] = await self.visitor.db.get_one(**visitor)
            except NoMatchFound:
                visit["visitor"] = await self.visitor.db.create(**visitor)
                self.pkg.log.info(f"New visitor: {visit['visitor'].name}")

            visit_new = await self.db.create(**visit)
            self.log.info(f"New visit: {visit_new.id}")

        return await self.db.get_one(pk=visit_new.id)

Config

Read more about the Aioli Configuration System in the Configuration documentation.

Package config schema

This example uses code from the aioli_rdbms extension Package.

Create

Define a custom Package configuration schema.

File: aioli_rdbms/config.py

from aioli.config import PackageConfigSchema, fields, validate


class ConfigSchema(PackageConfigSchema):
    type = fields.String(
        validate=validate.OneOf(["mysql", "postgres"]),
        required=True
    )
    username = fields.String(required=True)
    password = fields.String(required=True)
    host = fields.String(missing="127.0.0.1")
    port = fields.Integer(missing=3306)
    database = fields.String(missing="aioli")

Associate

Associate the configuration schema with a Package.

File: aioli_rdbms/__init__.py

from aioli import Package

from .service import DatabaseService
from .config import ConfigSchema


export = Package(
    name="aioli_rdbms",
    version="0.1.0",
    description="ORM and CRUD Service for Aioli with support for MySQL and PostgreSQL",
    controllers=[],
    services=[DatabaseService],
    config=ConfigSchema,
)

Configure

Create the configuration using the format of choice.

File: config.toml

[aioli_core]
dev_port = 5555
path = "/api"
debug = true

[aioli_rdbms]
type = "mysql"
host = "127.0.0.1"
port = 3306
database = "aioli"
username = "aioli01"
password = "super_secret"

[aioli_guestbook]
visits_max = 10

Load

Parse the file and pass it as a Dictionary to the Application constructor.

File: my_application/main.py

import toml
import aioli

import aioli_rdbms
import aioli_guestbook

app = aioli.Application(
    config=toml.loads(["/path/to/config.toml"]),
    packages=[
        (None, aioli.rdbms),
        ("/guestbook", aioli_guestbook)
    ]
)