About

Aioli is a Framework for building RESTful HTTP and WebSocket APIs. Its easy-to-use component system, which was built with emphasis on portability and composability, offers a sensible separation of application logic, data access and request/response layers.

Furthermore, it makes use of asyncio, is lightweight, and provides high performance and concurrency–especially for IO-bound workloads.

Note that Aioli only works with modern versions of Python (3.6+) and is Event loop driven, i.e. code must be asynchronous.

Not in the mood for reading docs? Check out The Guestbook Repository for a comprehensive RESTful HTTP example.

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
pretty_json AIOLI_CORE_PRETTY_JSON False
allow_origins AIOLI_CORE_ALLOW_ORIGINS [“*”]
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 an Application configuration example.

Access

Both Application and Package configurations can be easily accessed from both 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("aioli.cfg"),
    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 packages
Variables:
  • log – Aioli Application logger
  • registry – ImportRegistry instance
  • config – Application config

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("aioli.cfg"),
    packages=[
        aioli_guestbook,
        aioli_rdbms,
    ]
)

Package

The Package class is used for grouping and labeling a set of Controllers and Services. These components typically contain code that makes sense to modularize in the Application at hand.

Check out the Extensions docs to learn how Packages can be connected.

class aioli.Package(meta=None, auto_meta=False, controllers=None, services=None, config=None)[source]

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

Parameters:
  • meta – Package metadata, cannot be used with auto_meta
  • auto_meta – Attempt to automatically resolve meta for Package, cannot be used with meta
  • 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
  • meta – Package meta dictionary
  • log – Package logger
  • stash – Package Stash
  • 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(
    auto_meta=True,
    controllers=[HttpController],
    services=[VisitService, VisitorService],
    config=ConfigSchema,
)

Config

Package config schemas make 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 component takes care of routing, request and response handling, transformation and validation. Multiple Controllers of different type may coexist 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
on_request(*args)[source]

Called on request arrival for this Controller

on_shutdown()

Called when the Application is shutting down gracefully

on_startup()

Called after the Package has been successfully attached to the Application and the Loop is available

Example – Controller without route handlers

from aioli.controller import BaseHttpController

from .service import VisitService


class HttpController(BaseHttpController):
    def __init__(self):
        super(HttpController, self).__init__(pkg)

        self.log.debug("Guestbook opening")
        self.visit = VisitService(pkg)

    async def on_startup(self):
        self.log.debug(f"Guestbook opened")

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

Route

Route handlers are standard Python methods decorated with @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):
        # Pass along the query params as-is.
        # Then..
        # Return whatever get_many() returned.
        return await self.visit.get_many(**query)

Transform

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 ensuring API endpoints returns according to their schemas.

Takes

The @takes decorator is used to instruct Aioli how to deserialize and validate parts of a request, and injects the validated data as arguments into 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 using
        # ParamsSchema and pass along to get_many().
        # Then..
        # Return whatever get_many() returned.
        return await self.visit.get_many(**query)
Returns

The @returns decorator takes care of serializing data returned by its route handler, into JSON.

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 using
        # ParamsSchema and pass along to VisitService.get_many()
        # Then..
        # Transform and dump the object returned by get_many()
        # using 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]

Base Service class

Parameters:

pkg – Attach to this package

Variables:
  • app – Application instance
  • registry – Application ImportRegistry
  • pkg – Parent Package
  • config – Package configuration
  • log – Package logger
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
on_shutdown()

Called when the Application is shutting down gracefully

on_startup()

Called after the Package has been successfully attached to the Application and the Loop is available

Info

An Extension is comprised of one or more Services deriving from BaseService and typically creates an abstraction layer for accessing a remote system. Furthermore, this type of Package usually implements the Factory pattern.

Check out the aioli-rdbms extension for an example.

Import

To make use of an Extension, its Package along with dependencies needs to be registered with the Application.

Once registered, the Extension’s Service(s) can be incorporated into other Packages using integrate() or connect().

Example

Register the local users Package and its dependency; aioli_rdbms.

import aioli_rdbms

import toml

from aioli import Application

import .users

app = Application(
    config=toml.load("aioli.cfg"),
    packages=[users, 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):
    db = None

    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 code snippets 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 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(
    auto_meta=True,
    controllers=[],
    services=[DatabaseService],
    config=ConfigSchema,
)

Configure

Create the configuration using the format of choice.

File: aioli.cfg

[aioli_core]
dev_port = 5555
path = "/api"
pretty_json = false
allow_origins = ["*"]
debug = true

[aioli_guestbook]
path = "/guestbook"
# Maximum number of visits per IP
visits_max = 14

[aioli_rdbms]
type = "(mysql|postgres)"
username = "user"
password = "pass"
host = "127.0.0.1"
port = 3306
database = "aioli"

Register

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

File: my_application/main.py

import aioli_guestbook
import aioli_rdbms

import toml

from aioli import Application

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