The limitless Python web framework

Expanse

Routing

Easy
route definition
with a touch of
magic

Whether you need API endpoints or more standard web pages, Expanse got you covered. Use plain functions to handle your requests — or go further with controllers — and leverage the powerful dependency injection mechanism at the heart of Expanse to make even complex requirements a breeze.

Learn more

At the heart of Expanse is a powerful — yet intuitive — dependency injection mechanism that orchestrates every service that your application might need.

Type-hint any dependencies in your route functions or controllers, and they will be automatically injected for you.

In most cases you will be able to benefit from dependency injection for your own classes without doing anything.

class DocumentationController:

    def __init__(self, docs: Documentation) -> None:
        # A Documentation instance will automatically
        # be injected when instantiating the controller

    def show_page(
        self, request: Request, page: str
    ) -> Response:
        # The current request will be injected automatically


router.get(
    "/docs/{page}",
    DocumentationController.show_page,
    name="docs.page"
)

Easily render and compose views. Expanse is powered by Jinja meaning that you have natively access to all features it provides.

Expanse provides additional features and helpers to make common actions more intuitive. And if it's not enough you can easily add your own, if needed.

def list_users(view: ViewFactory):
    users = ...

    return view.make("users.list", {"users": users})


{% for user in users %}
<a href="{{ route("users.show", user_id=user.id) }}">
    User {{ user.name }}
</a>
{% endfor %}

Need to validate a form, a JSON payload or query parameters? Just specify a validation model and let Expanse handle the validation and report the errors

from pydantic import BaseModel
from expanse.http.form import Form


class ArticleModel(BaseModel):

    title: str
    content: str


def create_article(article: Form[ArticleModel]):
    if form.is_submitted() and form.is_valid():
        # Save article
        ...
from pydantic import BaseModel
from expanse.http.json import JSON


class ArticleModel(BaseModel):

    title: str
    content: str


def create_article(article: JSON[ArticleModel]):
    # `article` is a fully validated instance of ArticleModel
    ...
from datetime import datetime
from pydantic import BaseModel
from expanse.http.query import Query


class QueryModel(BaseModel):

    category: str
    before: datetime


def list_articles(q: Query[QueryModel]):
    # `q` is a fully validated instance of QueryModel
    ...
POST https://example.com/articles HTTP/1.1
Content-Type: application/json

{"content": "Content"}

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "code": "validation_error",
  "detail": [
    {
      "loc": ["title"],
      "message": "Field required",
      "type": "missing",
    }
  ]
}

Data access

Powerful database management made easy

Expanse relies on SQLAlchemy to provide a powerful, yet intuitive, database experience. Its integration is seamless: Raw queries, ORM and migrations, they are all readily available, so that you can focus on what matters.

Learn more

Automatic connection management

Connecting to and using a database should be easy. Expanse manages your database connections automatically, so you can focus on retrieving and storing data.

Multiple databases support

You can configure as many databases as you want. Need to read from an SQLite database and write to a PostgreSQL one? No problem.

def list_users(connection: Connection, view: ViewFactory):
    users = connection.execute(
        "SELECT * FROM users WHERE is_active = :active",
        {"active": True}
    )

    return view.make("users.list", {"users": users})


def list_users_from_sqlite(
    connection: Annotated[Connection, "sqlite"]
):
    ...

Automatic connection management

Connecting to and using a database should be easy. Expanse manages your database connections automatically, so you can focus on retrieving and storing data.

Multiple databases support

You can configure as many databases as you want. Need to read from an SQLite database and write to a PostgreSQL one? No problem.

def list_users(connection: Connection, view: ViewFactory):
    users = connection.execute(
        "SELECT * FROM users WHERE is_active = :active",
        {"active": True}
    )

    return view.make("users.list", {"users": users})


def list_users_from_sqlite(
    connection: Annotated[Connection, "sqlite"]
):
    ...

Expanse relies on Alembic to manage database migrations and integrate it seamlessly with the rest of your application. Expanse uses sensible defaults that requires little to no additional configuration.

And you can even let Expanse automatically detect changes to your models and generate the necessary migrations for you.

# Automatically detect changes
./beam make migration "Add users table" --auto

# Apply any pending migrations
./beam db migrate

# Rollback the last migration
./beam db rollback