About¶
Aioli was created with usability, extensibility and performance in mind, and is used for building modular, fast and highly concurrent web backend applications of any size.
It works with modern versions of Python (3.6+), is event loop driven and makes use of asyncio with uvloop.
Check out The Guestbook Repository for a comprehensive RESTful HTTP example.
Configure¶
The Application and associated Units can be configured using either environment variables,
or by a dictionary provided to the config parameter when creating the Application
.
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 |
Unit¶
A custom Unit configuration schema can be defined using the UnitConfigSchema
class,
which comes with a set of common parameters listed below.
Locations
- Dictionary key: [unit_name]
- Environment prefix: [PACKAGE_NAME]
- Run-time access:
aioli.Unit.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 Unit 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 Unit
configurations can be easily accessed from both Service and Controller instances,
using the config property.
Deploy¶
Note
Containerization deployment guidelines will be added shortly
Application¶
The Application
constructor expects one or more Unit
modules to be registered with the instance.
-
class
aioli.
Application
(units, **kwargs)[source]¶ Aioli application core
Parameters: - config – Configuration dictionary
- units – List of units
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"),
units=[
aioli_guestbook,
aioli_rdbms,
]
)
Unit¶
For code to be allowed registration with an Application–be it a local package, an extension or something else–it must adhere to the Aioli Unit format, which mainly serves the purpose of providing modularity, consistency and flexibility.
Units, in its simplest form, are tagged groups of one or more Components in the form of:
- Services: Implements application logic and exposes an API for internal consumption
- Controllers: Handles HTTP requests and typically interacts with Service APIs
-
class
aioli.
Unit
(meta=None, auto_meta=False, controllers=None, services=None, config=None)[source]¶ Associates components and meta with a unit, for registration with a Aioli Application.
Parameters: - meta – Unit metadata, cannot be used with auto_meta
- auto_meta – Attempt to automatically resolve meta for Unit, cannot be used with meta
- controllers – List of Controller classes to register with the Unit
- services – List of Services classes to register with the Unit
- config – Unit Configuration Schema
Variables: - app – Application instance
- meta – Unit meta dictionary
- log – Unit logger
- config – Unit config
- controllers – List of Controllers registered with the Unit
- services – List of Services registered with the Unit
Example – Creating a Unit with Controller and Service layers
from aioli import Unit
from .service import VisitService, VisitorService
from .controller import HttpController
from .config import ConfigSchema
export = Unit(
auto_meta=True,
controllers=[HttpController],
services=[VisitService, VisitorService],
config=ConfigSchema,
)
Extend¶
Units can be connected using integrate()
or connect()
,
and those with the sole purpose of serving others, are known as Extensions.
Example – Leverage the aioli-rdbms Unit to gain database access
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)
...
Config¶
Unit config schemas make use of the Marshmallow library and offers a clean and safe way of customizing Units.
Read more in Setup/Configure, or check out an Example.
-
class
aioli.config.
UnitConfigSchema
(*args, **kwargs)[source]¶ Unit configuration schema
Variables: - debug – Set debug level for unit, effectively overriding Application’s debug level
- path – Unit path, uses Unit name if empty
- should_import_services – Setting to False skips Service registration for this Unit
- should_import_controllers – Setting to False skips Controller registration for this Unit
Controller¶
The Controller Component takes care of routing, request handling, transformation, validation and more.
Multiple Controllers of different type may coexist in a Unit’s Controller layer.
HTTP¶
The BaseHttpController
is mainly for building RESTful HTTP APIs,
and may leverage the Aioli toolkit to perform transformation, validation, request-object plucking and more.
API
-
class
aioli.controller.
BaseHttpController
(unit, config_override=None)[source]¶ HTTP API Controller
Parameters: unit – Attach to this unit
Variables: - unit – Parent Unit
- config – Unit configuration
- log – Controller logger
-
on_shutdown
()¶ Called when the Application is shutting down gracefully
-
on_startup
()¶ Called after the Unit 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__(unit)
self.log.debug("Guestbook opening")
self.visit = VisitService(unit)
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 unit 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)
GraphQL¶
Note
GraphQL support will be added shortly.
WebSocket¶
Note
WebSocket support will be added shortly.
Service¶
The typical Service Component takes care of interacting with external systems: Databases, Remote Web APIs, Messaging systems, etc. and provides an API for internal consumption.
Check out the Connecting Services example to see how a service can integrate and interact with other services.
-
class
aioli.service.
BaseService
(unit, config_override=None)[source]¶ Base Service class
Parameters: unit – Attach to this unit
Variables: - app – Application instance
- registry – Application ImportRegistry
- unit – Parent Unit
- config – Unit configuration
- log – Unit logger
-
connect
(cls)[source]¶ Reuses existing instance of the given Service class, in the context of the Unit it was first registered with.
Parameters: cls – Service class Returns: Existing Service instance
-
integrate
(cls)[source]¶ Creates a new instance of the given Service class in the context of the current Unit.
Parameters: cls – Service class Returns: Integrated Service
-
on_shutdown
()¶ Called when the Application is shutting down gracefully
-
on_startup
()¶ Called after the Unit has been successfully attached to the Application and the Loop is available
CLI¶
The Aioli CLI provides a set of commands for managing an Aioli project.
attach¶
Attaches the given application instance.
Example – attach app at my_app:export
$ aioli --app_path my_app:export attach
info¶
Dump details about the current application.
Example – dump info about my_app:export
$ aioli info
create¶
Create a new app with the given name.
Command input
Name | Default | Description |
---|---|---|
–dst_path | Current directory | Directory in which to create the project |
–profile | minimal | One of: minimal, guesthouse, whoami |
–confirm | Not set | Answer yes to confirmations |
Example – create a new app using the guesthouse profile
$ aioli create beachhouse --profile guesthouse
start¶
Starts a development server.
Name | Default | Description |
---|---|---|
–host | 127.0.0.1 | Bind socket to this host |
–port | 5000 | Bind socket to this port |
–no_reload | Not set | Disable the reloader |
–no_debug | Not set | Disable debug mode |
–workers | 1 | Number of workers |
Example – Start the attached application on port 127.0.0.1:1234
$ aioli start --port 1234
Shell¶
The Aioli Shell is an interactive layer on top of the Aioli CLI for managing Applications and working with local or remote Units.
Invoking the shell:
$ aioli --app_path my_app:export shell
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.unit.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.
Unit config schema¶
This example uses code from the aioli_rdbms extension Unit.
Create¶
Define a custom Unit configuration schema.
File: aioli_rdbms/config.py
from aioli.config import UnitConfigSchema, fields, validate
class ConfigSchema(UnitConfigSchema):
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 Unit.
File: aioli_rdbms/__init__.py
from aioli import Unit
from .service import DatabaseService
from .config import ConfigSchema
export = Unit(
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"),
units=[
aioli_guestbook,
aioli_rdbms,
]
)