Lato documentation

Lato is a microframework for building modular applications in Python. It helps you to build applications that are easy to maintain and extend. It is based on such concepts as modularity, dependency injection, and loose coupling.

If you are trying to build a modular monolith in Python, Lato is the right choice for you. It is framework agnostic, you can use it with any framework of your choice, i.e. Flask, FastAPI, etc.

Lato is Open Source and licensed under the MIT License.

Core Features

  • Modularity: Organize your application into smaller, independent modules for better maintainability.

  • Flexibility: Loosely couple your application components, making them easier to refactor and extend.

  • Testability: Easily test your application components in isolation.

  • Minimalistic: Intuitive and lean API for rapid development without the bloat.

  • Concurrency support: built-in support for coroutines declared with async / await syntax.

Contents

Installation

Lato is available on PyPI and can be installed with pip:

pip install lato

Getting Started

The Application serves as a top-level building block in Lato. Let’s create a simple greeting application. When you instantiate the application you need to name it, and in addition you can pass as many keyword arguments as you need - they will be used as dependencies. Later on you will see how the dependencies are being used.

Let’s create the application with one dependency - greeting_phrase:

from lato import Application

app = Application(name="Hello World", greeting_phrase="Hello")

Resolving parameters

Now, let’s see the core feature of Lato - automatic parameter resolution. To demonstrate it, let’s declare a greet_person function and invoke it via app.call:

def greet_person(person: str, greeting_phrase: str):
    return f"{greeting_phrase} {person}"

result = app.call(greet_person, "Bob")

assert result == "Hello Bob"

The greet_person function requires two arguments: person and greeting phrase. When invoking the function via Application.call(), we provided only one argument - the value of person. The other argument - greeting phrase is automatically provided (injected) by the app, as you provided it as a dependency. In general, Lato application is capable of injecting missing function arguments, if the function parameter matches the dependency (either by parameter name, or by type).

Declaring a handler

To understand what a handler is, think of it as of named function. This name is an alias, which is a string. When invoking a handler, you can pass an alias instead of the function itself. This idea promotes loose coupling.

You can create the handler and call it using its alias like so:

@app.handler("greet")
def greet_person(person: str, greeting_phrase: str):
    return f"{greeting_phrase} {person}"

result = app.call("greet", "Bob")
assert result == "Hello Bob"

You could use this behavior in a simple command line tool, like so:

if __name__ == "__main__":
    command = sys.argv[1]
    name = sys.argv[2]
    print(app.call(command, name))

Using a command handler

Lato follows the Command Pattern in its design. It is a behavioral design pattern which is used to implement lose coupling in a request-response model. The idea is to turn a request (command) into a standalone object. This object contains all parameters that are required to handle a request. Such separation allows for queuing of requests, logging of the parameters for better observability, and invocation of the request.

Instead of using an alias, you can create a command handler, and then invoke the handler by calling App.execute():

from lato import Command

class Greet(Command):
    title: str
    name: str

@app.handler(Greet)
def greet_person(command: Greet, greeting_phrase: str):
    return f"{greeting_phrase} {command.title} {command.name}"

result = app.execute(Greet(title="Mr", name="Bob"))
assert result == "Hello Mr Bob"

The Application.execute() passes the command to a designated handler. It should be noted that the first parameter of a command handler is a command, and the remaining parameters are resolved from application dependencies.

Integration with a web framework

Lato is web framework agnostic. Let’s see how it fits into our framework of choice - FastAPI:

from fastapi import FastAPI
from lato import Application, Command

app = Application("Greetings", greeting_phrase="Hello")

class Greet(Command):
    title: str
    name: str

@app.handler(Greet)
def greet_person(command: Greet, greeting_phrase: str):
    return f"{greeting_phrase} {task.title} {task.name}"


api = FastAPI()
@api.get("/greet")
def foo(request, title: str, name: str):
    command = Greet(title, name)
    message = app.execute(command)
    return {'message': message}