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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.