Nitro
A collection of Python web-dev utils inspired by smart solutions across the ecosystem. Use them together or separately, with your favourite framework.
Five Layers, One Philosophy
Each layer solves one problem. Use what you need, ignore the rest.
Templating
Python functions that return HTML. Powered by Datastar for interactivity.
from nitro.html.components import * # noqa: F403
from nitro.html import template as templ
from your.components import Sidebar, Navbar
# Shared page template
htmlkws = dict(lang="en")
page = page_template(htmlkw=htmlkws, lucide=True)
@templ
def template(content, title: str):
return page(
Fragment(
Sidebar(),
Main(
Navbar(),
Div(
Div(content, id="content"),
cls="p-4 md:p-6 xl:p-12",
),
),
),
title=title,
)from templates import template
@template
def index():
return Div(
H1("Hello, World!"),
P("This is a template."),
)HTML Generation
Rust-powered speed. Python simplicity. Inspired by FastHTML.
Output
What the browser sees
Jane Doe
Software Engineer
Building things with Nitro. It's just Python. That's the whole pitch.
from rusty_tags import Div, H3, P
from nitro.html.components import Avatar, Badge
def UserCard(user):
return Div(
Div(
Avatar(fallback=user.initials, size="lg"),
Div(
H3(user.name, cls="font-semibold"),
P(user.role, cls="text-sm text-muted-foreground"),
),
cls="flex items-center"
),
P(user.bio, cls="mt-4"),
Div(
*[Badge(tag) for tag in user.tags],
cls="flex gap-2 mt-4"
),
)
# That's it. No JSX. No templates. Just Python.Reactivity
Signals powered by Datastar. 14kb. Does what React does. No npm install.
from rusty_tags import Div, Button, Span
from rusty_tags.datastar import Signals
def Counter():
sigs = Signals(count=0)
return Div(
Button("-", on_click=sigs.count.sub(1)),
Span(data_text=sigs.count, cls="text-4xl font-bold mx-8"),
Button("+", on_click=sigs.count.add(1)),
data_signals=sigs,
)
# Datastar handles the reactivity.
# No virtual DOM. No hydration. No build step.Live Counter
State lives in HTML. UI updates automatically.
Click the buttons. The count updates. No useState, no Redux, no tears.
Rich Entities
Business logic in methods. Persistence is automatic. Built on SQLModel.
Order #1234
Created 2 minutes ago
| Customer | Acme Corp |
| Total | $1,234.00 |
| Items | 3 products |
from nitro.domain.entities.base_entity import Entity
from pydantic import Field
class Order(Entity, table=True):
customer: str = Field(title="Customer")
total: float = Field(default=0.0, title="Total")
status: str = Field(default="draft", title="Status")
def add_item(self, product, qty):
self.total += product.price * qty
self.save() # Just works
def place(self):
if self.total == 0:
raise ValueError("Empty order")
self.status = "placed"
self.save()
# order = Order(customer="Acme Corp")
# order.add_item(widget, 3)
# order.place()
#
# That's your business logic. In one place.Event Routing
Decoupled handlers. Built on Blinker with async support and priority ordering.
from nitro.events import subscribe, publish_sync
# Handlers are just decorated functions
@subscribe("order.placed")
async def send_confirmation(msg):
EmailService.send(
msg.data["customer_email"],
"Your order is confirmed!"
)
@subscribe("order.placed")
async def update_inventory(msg):
for item in msg.data["items"]:
Product.get(item["id"]).reduce_stock(item["qty"])
@subscribe("order.*")
async def notify_warehouse(msg):
WarehouseQueue.push(msg.data["id"])
# Publish from anywhere — subscribers fire independently
publish_sync("order.placed", data=order.model_dump())
# Decoupled. Testable. No spaghetti.Event Flow
What happens when an order is placed
order.place()
Fires 'order-placed' event
Model Views
Pass your Entity. Get forms, tables, cards. Field metadata controls everything.
Auto-Generated UI
One Entity, multiple views
class User(Entity)
Define once with Field metadata
from nitro.domain.entities.base_entity import Entity
from nitro.html.components.model_views import (
ModelForm, ModelTable, ModelCard
)
from pydantic import Field
class User(Entity, table=True):
name: str = Field(title="Name")
email: str = Field(
title="Email",
json_schema_extra={'format': 'email'}
)
role: str = Field(
default="member",
json_schema_extra={'icon': 'user'}
)
# Generate a complete form
form = ModelForm(User)
# Generate a data table with sorting
table = ModelTable(User, data=User.all())
# Generate a display card
card = ModelCard(User, instance=user)
# That's < 20 lines for a complete CRUD interface.Up and Running in 60 Seconds
Three commands. No configuration files. No boilerplate.
pip install nitro-boostnitro boostpython app.pyThat's It
Five layers. Pure Python. No magic, just patterns that work.
Use it if it helps. Fork it if you want. File issues if it breaks.