Skip to main content

brickly-core

brickly-core lets you build the pages of a web app on the backend, in Python. You don’t write frontend code for each page: you describe what is shown at each URL, and a ready-made frontend renders it.

How it works:

  • Widgets describe the UI: a text, a form, a list, a layout. They are plain Python classes.

  • Screens say which widgets are shown at a URL, which layout wraps them, what to show if something goes wrong.

  • Routers bind screens to URLs, and middleware decides who may open them (log in, check access, redirect).

From this, brickly-core builds the routing of the whole app, which the frontend loads to know which pages exist; when a user opens a page, the frontend asks your backend for its widgets and renders them.

brickly-core has no dependencies and isn’t tied to a web framework: it holds the description of your app, and connecting it to FastAPI, Django or another framework is done by a separate adapter. Requires Python 3.13+.

Installation

pip install brickly-core

Quick start

You describe which widget is shown at which path, and brickly-core builds the routing for the frontend out of it. Everything you need is imported from the package:

from dataclasses import dataclass
from typing import Literal

from brickly_core import *


# 1. Widgets: what the frontend renders.
@dataclass(kw_only=True)
class TextWidget(Widget):
    type: Literal["text"] = "text"
    text: str


# 2. Events: functions that build the content of a screen.
@event("layout")
async def layout(**_):
    return FragmentWidget(TextWidget(text="My shop"), OutletWidget())

@event("home")
async def home(**_):
    return TextWidget(text="Welcome!")

@event("item")
async def item(item_id: int, **_):
    return TextWidget(text=f"Item #{item_id}")

@event("item.meta")
async def item_meta(item_id: int, **_):
    return {"title": f"Item #{item_id}"}


# 3. Screens bound to paths.
router = Router(paths=[
    RouterPath(template="/", screen=Screen(shell_widget=layout, widget=home)),
    RouterPath(template="item/{item_id:int}", screen=Screen(
        widget=item,
        meta=item_meta,
        fallback_widget=TextWidget(text="Item is unavailable"),
    )),
])

# 4. The routing of the app, ready to be served to the frontend.
settings = RouterSettings(routers=[router])

Opening /item/42 shows My shop (the layout of /) with Item #42 inside it.

Events

@event marks a function that the routing may call to build a screen: its widget, its layout, its meta data, or a middleware step. Only decorated functions can be used in Screen and Middleware.

@event("item")
async def item(item_id: int, **_):
    return TextWidget(text=f"Item #{item_id}")
  • Give every event a unique name ("item", "shop.cart", …). Using the same name twice is an error at import time.

  • Path parameters arrive as keyword arguments with their names from the template: item/{item_id:int} gives item_id. End the signature with **_ to accept everything else.

  • Both async def and plain def work.

  • Events can be static or class methods. Put @event above @staticmethod / @classmethod:

    class Pages:
        @event("pages.about")
        @staticmethod
        async def about(**_):
            return TextWidget(text="About")
    
        @event("pages.contacts")
        @classmethod
        async def contacts(cls, **_):
            return TextWidget(text="Contacts")
    
    RouterPath(template="about", screen=Screen(widget=Pages.about))

    Regular methods taking self can’t be events.

Screens

A Screen describes what is shown at a path. All settings are optional:

Setting

Meaning

widget

Event returning the content of the page.

shell_widget

Event returning a layout around this page and all pages below it. It must contain one OutletWidget(): the place where the nested page is rendered.

meta

Event returning data for the page, e.g. its title.

fallback_widget

A widget shown instead of the page if anything fails while preparing it (middleware, layout, content or meta).

cache

Cache the screen (False by default).

cache_ttl

How long to cache it, in seconds: 100 by default, -1 forever, 0 no cache.

Layouts nest along the path. With a layout on / and another on /dashboard, opening /dashboard/stats renders the / layout, inside it the /dashboard layout, and inside that the stats page:

router = Router(paths=[
    RouterPath(template="/", screen=Screen(shell_widget=main_layout)),
    RouterPath(template="dashboard", screen=Screen(shell_widget=dashboard_layout)),
    RouterPath(template="dashboard/stats", screen=Screen(widget=stats)),
])

Paths

RouterPath(template=..., screen=...) binds a screen to a path.

  • Segments are separated by /; leading and trailing slashes don’t matter, and "/" is the root of the router.

  • A parameter is written {name:type}: item/{item_id:int}, shop/{slug:str}. Its value is passed to the events of that screen and of the screens below it.

Routers

A Router groups paths under a common prefix. Split a big app into several routers, e.g. one per module, and pass them all to RouterSettings:

shop = Router(prefix="/shop", paths=[
    RouterPath(template="/", screen=Screen(widget=shop_page)),
    RouterPath(template="item/{item_id:int}", screen=Screen(widget=item)),
])
blog = Router(prefix="/blog", paths=[
    RouterPath(template="/", screen=Screen(widget=blog_page)),
])

settings = RouterSettings(routers=[shop, blog])     # /shop, /shop/item/42, /blog

Paths can also be added later with router.add(RouterPath(...)). Build RouterSettings once every router is complete.

Routers with the same prefix are merged

Several routers may use the same prefix: their paths end up in one section, and their middleware is shared by all the paths under that prefix. This lets different modules add pages to one section without knowing about each other:

# users module
admin_users = Router(prefix="/admin", middleware=Middleware(events=[require_admin]), paths=[
    RouterPath(template="users", screen=Screen(widget=users)),
])

# billing module: no middleware of its own
admin_billing = Router(prefix="/admin", paths=[
    RouterPath(template="billing", screen=Screen(widget=billing)),
])

settings = RouterSettings(routers=[admin_users, admin_billing])

Both /admin/users and /admin/billing are protected by require_admin. If both routers had middleware, the lists are joined in the order of routers, and a middleware listed in both runs only once.

Overriding a route

Registering the same path twice is an error. To replace a route on purpose, for example a page that comes from a shared module, register it again with override=True:

custom = Router(paths=[
    RouterPath(template="/", screen=Screen(widget=my_home), override=True),
])

settings = RouterSettings(routers=[router, custom])   # "/" now shows my_home

The overriding router must come after the original one in routers.

Middleware

Middleware are events that run before a screen is shown: load the current user, check access, redirect. Each one returns what should happen next:

Return

Effect

None

Continue.

Allow(context={...})

Continue, and pass data on to the next middleware.

Redirect(to="/login")

Stop and go to another page.

Deny(status=403)

Stop and show a refusal on the same page (its own widget=... or the screen’s fallback_widget).

@event("auth.load_user")
async def load_user(ctx, **_):
    user = await get_current_user()
    if user is None:
        return Redirect(to="/login")
    return Allow(context={"user": user})

@event("auth.require_admin")
async def require_admin(ctx, **_):
    if not ctx.data["user"].is_admin:          # data passed on by load_user
        return Deny(status=403)

Attach middleware to the whole app or to a router:

admin = Router(prefix="/admin", middleware=Middleware(events=[require_admin]), paths=[...])

settings = RouterSettings(
    routers=[router, admin],
    middleware=Middleware(events=[load_user]),      # runs before every screen
)
  • RouterSettings(middleware=...) runs before every screen of the app, first.

  • Router(middleware=...) runs before every screen under the router’s prefix, including pages of other routers with the same prefix (see above), and nested prefixes such as /admin/reports.

  • Middleware runs from the outside in: app first, then /admin, then /admin/reports. A middleware listed more than once runs only once.

  • Redirect and Deny stop the chain: the middleware after them don’t run.

Metadata

Release files for brickly-core 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for brickly-core 1.0.0
File Interpreter ABI Platform Actions
brickly_core-1.0.0-py3-none-any.whl Python 3 none any Details for brickly_core-1.0.0-py3-none-any.whl

Release files / brickly_core-1.0.0-py3-none-any.whl

Download URL brickly_core-1.0.0-py3-none-any.whl
Size 20.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
caddec0c0c370f76110a528fc3644b64891d49d99e749af8f029358d60aaa850
BLAKE2b-256 checksum
How to use checksums
59a2b5da5e04fcfe97147a132a3070220045d7520d1b6388fe566134afd3e08c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for brickly-core 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

1 release file No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page