Full-Stack Web Development Framework for Pythonistas
Project description
The Full-Stack Web Framework for Pythonistas.
violetear is a minimalistic yet fully capable framework for building modern web applications in pure Python. It eliminates the context switch between backend and frontend by allowing you to write your styles, your markup, and your client-side logic all in the language you love.
It features a unique 3-layer architecture:
- 🎨 Styling Layer: Generate CSS rules programmatically with a fluent, pythonic API. Includes an Atomic CSS engine that generates utility classes on the fly.
- 🧱 UI Layer: Build reusable HTML components with a fluent builder pattern. Type-safe, refactor-friendly, and composable.
- ⚡ Logic Layer: Write server-side and client-side code in the same file.
violetearhandles the compilation, bundling, RPC bridges, and state persistence seamlessly.
Use it for anything: from a simple script to generate a CSS file, to a static site generator, all the way up to a full-stack Isomorphic Web App powered by FastAPI and Pyodide.
📦 Installation
To use the core library (HTML/CSS generation only), install the base package:
pip install violetear
To build full-stack applications (with the App Engine and Server), install the server extras:
pip install "violetear[server]"
🚀 Quickstart: The Isomorphic Counter
Let's build a fully interactive "Counter" app. The state persists across reloads using Local Storage, updates instantly in the browser via the DOM API, and syncs with the server via RPC.
Zero JavaScript required.
1. Initialize the App
First, we create the application instance. This wraps FastAPI to provide a powerful server engine.
from violetear import App, StyleSheet
from violetear.markup import Document, Element
from violetear.color import Colors
from violetear.style import Style
from violetear.dom import Event
app = App(title="Violetear Counter")
2. Define Styles (CSS-in-Python)
Instead of writing CSS strings, use the fluent API to define your theme.
# Create a global stylesheet
style = StyleSheet()
style.select("body").background(Colors.AliceBlue).font(family="sans-serif") \
.flexbox(align="center", justify="center").height("320px").margin(top=20)
style.select(".counter-card").background(Colors.White).padding(40).rounded(15) \
.shadow(blur=20, color="rgba(0,0,0,0.1)").text(align="center")
style.select(".count-display").font(size=64, weight="bold").color(Colors.SlateBlue).margin(10)
style.select("button").padding("10px 20px").font(size=20, weight="bold") \
.margin(5).rounded(8).border(0).rule("cursor", "pointer").color(Colors.White)
style.select(".btn-plus").background(Colors.MediumSeaGreen)
style.select(".btn-minus").background(Colors.IndianRed)
style.select(".btn:hover").rule("opacity", "0.8")
3. Server Logic (RPC)
Define a function that runs on the server. The @app.server decorator exposes this function so your client code can call it directly.
@app.server
async def report_count(current_count: int, action: str):
"""
This runs on the SERVER.
FastAPI automatically validates that current_count is an int.
"""
print(f"[SERVER] Count is now {current_count} (Action: {action})")
return {"status": "received"}
4. Client Logic (In-Browser Python)
Define the interactivity. We use @app.startup to restore state when the page loads.
@app.startup
async def init_counter():
"""
Runs automatically when the page loads (Client-Side).
Restores the counter from Local Storage so F5 doesn't reset it.
"""
from violetear.dom import Document
from violetear.storage import store
# We can access storage like an object!
saved_count = store.count
if saved_count is not None:
Document.find("display").text = str(saved_count)
print(f"Restored count: {saved_count}")
And we use @app.client to handle user interactions and run Python code in the browser. Check out the Pythonic API for interacting with the DOM and the LocalStorage. We can also call server-side functions seamlessly, via automagic RPC (Remote Procedure Call).
@app.client
async def handle_change(event: Event):
"""
Runs in the browser on click.
"""
from violetear.dom import Document
from violetear.storage import store
# A. Get current state from DOM
display = Document.find("display")
# We can read/write text content directly
current_value = int(display.text)
# B. Determine action
action = event.target.id # "plus" or "minus"
new_value = current_value + (1 if action == "plus" else -1)
# C. Update DOM immediately (Responsive)
display.text = str(new_value)
# D. Save to Local Storage (Persistence)
# This automatically serializes the value to JSON
store.count = new_value
# E. Sync with Server (Background)
await report_count(current_count=new_value, action=action)
5. The UI (Server-Side Rendered)
Finally, create the route that renders the initial HTML. We attach the style and bind the Python function to the button's click event.
@app.route("/")
def index():
doc = Document(title="Violetear Counter")
# Auto-serve our generated CSS at this URL
doc.style(style, href="/style.css")
doc.body.add(
Element("div", classes="counter-card").extend(
Element("h2", text="Isomorphic Counter"),
# The Count
Element("div", id="display", classes="count-display", text="0"),
# Controls - Both call the same Python function
Element("button", id="minus", text="-", classes="btn-minus btn").on(
"click", handle_change
),
Element("button", id="plus", text="+", classes="btn-plus btn").on(
"click", handle_change
),
Element("p", text="Refresh the page! The count persists.").style(
Style().color(Colors.Gray).margin(top=20)
),
)
)
return doc
if __name__ == "__main__":
app.run()
Run it with python main.py and open http://localhost:8000. You have a full-stack, styled, interactive app with persistence in 70 lines of pure Python!
✨ Features
🎨 Powerful Styling Engine
- Fluent API:
style.select("div").color(Colors.Red).margin(10) - Type-Safe Colors: Built-in support for RGB, HSL, Hex, and a massive library of standard web colors (
violetear.color.Colors). - Presets:
- Atomic CSS: A complete Tailwind-compatible utility preset. Generate thousands of utility classes (
p-4,text-xl,hover:bg-red-500) purely in Python. FlexGrid&SemanticDesignincluded.
- Atomic CSS: A complete Tailwind-compatible utility preset. Generate thousands of utility classes (
🧱 Component System
- Declarative Builder: Create HTML structures without writing HTML strings.
- Reusability: Subclass
Componentto create reusable widgets (Navbars, Cards, Modals) that encapsulate their own structure and logic.
⚡ Full-Stack Application Engine
- Hybrid Architecture: Supports both Server-Side Rendering (SSR) for SEO and speed, and Client-Side Rendering (CSR) for interactivity.
- Pythonic DOM: A wrapper (
violetear.dom) that provides a clean, type-safe Python API for DOM manipulation in the browser. - Smart Storage: A Pythonic wrapper (
violetear.storage) aroundlocalStoragethat handles JSON serialization automatically and allows attribute access (store.user.name). - Asset Management: Stylesheets created in Python are served directly from memory.
- Seamless RPC: Call server functions from the browser as if they were local.
📱 Progressive Web App (PWA) Support
Violetear allows you to turn any route into an installable PWA. This enables your app to:
- Be Installed: Users can add it to their home screen (mobile/desktop).
- Work Offline: The app shell and assets are cached automatically.
- Auto-Update: Changes to your Python code are detected, ensuring users always see the latest version.
How to Enable PWA
Simply pass pwa=True (or a Manifest object) to the @app.route decorator.
Important: You must define an app version. If you don't, Violetear generates a random one on every restart, which will force users to re-download the app every time you deploy.
from violetear import App, Manifest
# 1. Set a version string (e.g., from git commit or semantic version)
app = App(title="My App", version="v1.0.2")
# 2. Enable PWA on your main route
@app.route("/", pwa=Manifest(
name="My Super App",
short_name="SuperApp",
description="An amazing Python PWA",
theme_color="#6b21a8"
))
def home():
return Document(...)
Caching Strategy
Violetear uses a hybrid strategy to ensure safety and speed:
- Navigation (HTML): Network-First. It tries to fetch the latest version from the server. If offline, it falls back to the cache.
- Assets (JS/CSS): Cache-First. Assets are versioned (e.g.,
bundle.py?v=1.0.2). This ensures instant loading while guaranteeing updates when the version changes.
Current Limitations
- Push Notifications: Not yet supported (requires VAPID key generation and a push server).
- Background Sync: Offline actions (like submitting a form while disconnected) are not automatically retried when online. You must handle connection errors manually in your client logic.
Based on the examples/broadcast.py file you provided, here is a new section for the README.md.
This section explains the Reverse RPC capability (Server-to-Client communication), detailing how to define client-side functions and trigger them from the server to broadcast updates to all connected users.
You should place this section immediately after the "🚀 Quickstart: The Isomorphic Counter" section in your README.md.
📡 Real-Time: Server Broadcasts
Violetear supports Reverse RPC, allowing the server to call functions running in the user's browser. This is perfect for real-time notifications, live feeds, or multiplayer games.
The magic happens via the .broadcast() method available on any @app.client function.
1. Define the Client Function
Create a function decorated with @app.client. This code will be compiled and run in the browser, but the server "knows" about it.
# This function runs in the User's Browser
@app.client
async def update_alert(message: str, color: str):
from violetear.dom import Document
# Update the DOM immediately
el = Document.find("status-message")
el.text = message
el.style(color=color)
2. Call it from the Server
You can call this function from anywhere in your server-side code (a background task, a cron job, or another API route) using .broadcast().
import asyncio
from contextlib import asynccontextmanager
# A background task running on the server
async def background_monitor():
while True:
await asyncio.sleep(5)
# Send data to ALL connected clients
await update_alert.broadcast(
message="Server is alive!",
color="green"
)
# Register the background task using standard FastAPI lifespan
@asynccontextmanager
async def lifespan(api):
task = asyncio.create_task(background_monitor())
yield
task.cancel()
# Hook into the app
app.api.router.lifespan_context = lifespan
3. Handle Connections
You can also hook into WebSocket lifecycle events to track users or trigger actions when they join or leave.
@app.connect
async def on_join(client_id: str):
print(f"Client {client_id} connected.")
# You could broadcast a "User Joined" message here
await update_alert.broadcast(f"User {client_id} joined!", "blue")
@app.disconnect
async def on_leave(client_id: str):
print(f"Client {client_id} left.")
🛣️ Roadmap
Here is the vision for the immediate future of Violetear:
- 📱 Progressive Web Apps (PWA): Simply pass
@app.route(..., pwa=True)to automatically generatemanifest.jsonand a Service Worker. - 📡 Reverse RPC (Broadcast and Invoke): Invoke client-side functions from the server via websockets.
- 🔥 JIT CSS: An optimization engine that scans your Python code and serves only the CSS rules actually used by your components.
- 🧭 SPA Engine: An abstraction (
violetear.spa) for building Single Page Applications. - 🔀 Client-Side Routing: Define routes that render specific components into a shell without reloading the page.
- 🗃️
@app.local: Reactive state that lives in the browser (per user). Changes update the DOM automatically. - 🌐
@app.shared: Real-time state that lives on the server (multiplayer). Changes are synced to all connected clients via WebSockets.
🤝 Contribution
violetear is open-source and we love contributions!
- Fork the repo.
- Install dependencies with
uv sync. - Run tests with
make test. - Submit a PR!
📄 License
MIT License. Copyright (c) Alejandro Piad.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file violetear-1.2.4.tar.gz.
File metadata
- Download URL: violetear-1.2.4.tar.gz
- Upload date:
- Size: 1.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57c4c91a0690772b5c46368a792b7920e53d6c20975364e839b0a48d252fab62
|
|
| MD5 |
adff4f9924875d0123c255483c4f708b
|
|
| BLAKE2b-256 |
3b61dfb905d8ede7c7465b5c87f3ab3577c89ccd96b01d423b45447bce9831ee
|
File details
Details for the file violetear-1.2.4-py3-none-any.whl.
File metadata
- Download URL: violetear-1.2.4-py3-none-any.whl
- Upload date:
- Size: 931.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d80066962453b32ad4e6e418b91c2cb6f0046555d0a34068d69dd5f67f2bff5
|
|
| MD5 |
eb5eb2b1491856bcd8d22fc6ec962561
|
|
| BLAKE2b-256 |
85b61a25d9d78c866cb53179d77967464f4d8314cd5cf92eecf5c3810b0ff1c6
|