Skip to content

apiad/violetear

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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 client-server isomorphic engine.

from violetear import App

app = App(title="Violetear Counter")

2. Define Styles (CSS-in-Python)

Instead of writing CSS strings, use the fluent API to define your theme.

from violetear import StyleSheet
from violetear.color import Colors
from violetear.style import Style

# 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.rpc decorator exposes this function so your client code can call it directly.

@app.server.rpc
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.client.on("ready") to restore state when the page loads and everything is setup.

@app.client.on("ready")
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.callback 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).

from violetear.dom import Event

@app.client.callback
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.

from violetear.markup import Document, HTML

@app.view("/")
def index():
    doc = Document(title="Violetear Counter")

    # Auto-serve our generated CSS at this URL
    doc.style(style, href="/style.css")

    doc.body.add(
        HTML.div(classes="counter-card").extend(
            HTML.h2(text="Isomorphic Counter"),
            # The Count
            HTML.div(id="display", classes="count-display", text="0"),
            # Controls - Both call the same Python function
            HTML.button(id="minus", text="-", classes="btn-minus btn").on(
                "click", handle_change
            ),
            HTML.button(id="plus", text="+", classes="btn-plus btn").on(
                "click", handle_change
            ),
            HTML.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.

# 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 desired route
@app.view("/", pwa=Manifest(
    name="My Super App",
    short_name="SuperApp",
    description="An amazing Python PWA",
    theme_color="#6b21a8"
))
def home():
    return Document(...)

The "application" cache is defined per route (@app.view), so you can have multiple PWAs served from the same violetear application. You can setup some routes for delivering PWA-enables app while other routes serve server-side rendered documents or standard (non-PWA) dynamic documents. You can match and mix as you wish.

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, and unclear if we ever will.
  • 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. At some time we may provide a standard mechanism for queueing this type of actions.

πŸ“‘ 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.realtime. This code will be compiled and run in the browser, but the server "knows" about it and can invoke it.

# This function runs in the User's Browser
@app.client.realtime
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

Now the server code can call update_alert.invoke(...) for any specific client. You can get the appropriate client ID via @app.server.on("connect") handlers.

You can also call it for all connected clients using .broadcast(). For example, if the server shutdowns and restarts later on, you can inform your clients like this:

import asyncio

# Register the background task using standard FastAPI lifespan
@app.server.on("startup")
async def start_monitor():
  # A background task running on the server
  await update_alert.broadcast(
      message="Server is alive!",
      color="green"
  )

3. Handle Connections

You can hook into WebSocket lifecycle events to track users or trigger actions when they join or leave.

@app.server.on("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.server.on("disconnect")
async def on_leave(client_id: str):
    print(f"Client {client_id} left.")

Similarly, you can hook @app.client.on("connect") and "disconnect" events to execute client-side code whenever the client websocket connects and disconnects.

πŸ›£οΈ Roadmap

The long-term vision for Violetear is to become a Python-native, full-stack, production-ready web framework. Here are some of the currently planned features:

  • πŸ“± 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 client-side routes that render specific components into a shell without reloading the page.
  • πŸ“ƒ Partial Views: Define server views that render only partial documents, which can be injected into the client-side DOM dynamically.
  • πŸ—ƒοΈ @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.

About

The Full-Stack Web Framework for Pythonistas

Topics

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •