Skip to main content

Full-Stack Web Development Framework for Pythonistas

Project description

The Full-Stack Web Framework for Pythonistas.

PyPI - Version PyPi - Python Version PyPi - Downloads (Monthly) Github - Commits

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:

  1. 🎨 Styling Layer: Generate CSS rules programmatically with a fluent, pythonic API. Includes an Atomic CSS engine that generates utility classes on the fly.
  2. 🧱 UI Layer: Build reusable HTML components with a fluent builder pattern. Type-safe, refactor-friendly, and composable.
  3. ⚡ Logic Layer: Write server-side and client-side code in the same file. violetear handles 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 & SemanticDesign included.

🧱 Component System

  • Declarative Builder: Create HTML structures without writing HTML strings.
  • Reusability: Subclass Component to 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) around localStorage that 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:

  1. Be Installed: Users can add it to their home screen (mobile/desktop).
  2. Work Offline: The app shell and assets are cached automatically.
  3. 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 generate manifest.json and 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!

  1. Fork the repo.
  2. Install dependencies with uv sync.
  3. Run tests with make test.
  4. Submit a PR!

📄 License

MIT License. Copyright (c) Alejandro Piad.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

violetear-1.2.4.tar.gz (1.5 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

violetear-1.2.4-py3-none-any.whl (931.9 kB view details)

Uploaded Python 3

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

Hashes for violetear-1.2.4.tar.gz
Algorithm Hash digest
SHA256 57c4c91a0690772b5c46368a792b7920e53d6c20975364e839b0a48d252fab62
MD5 adff4f9924875d0123c255483c4f708b
BLAKE2b-256 3b61dfb905d8ede7c7465b5c87f3ab3577c89ccd96b01d423b45447bce9831ee

See more details on using hashes here.

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

Hashes for violetear-1.2.4-py3-none-any.whl
Algorithm Hash digest
SHA256 1d80066962453b32ad4e6e418b91c2cb6f0046555d0a34068d69dd5f67f2bff5
MD5 eb5eb2b1491856bcd8d22fc6ec962561
BLAKE2b-256 85b61a25d9d78c866cb53179d77967464f4d8314cd5cf92eecf5c3810b0ff1c6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page