A minimal React-like Python UI framework on FastAPI (CSR runtime + component model).
Project description
Xania — Python UI Framework for SPAs
Build fast, interactive web apps entirely in Python. No JavaScript required.
Xania compiles Python code to standalone Single-Page Applications (SPAs) that run 100% in the browser.
Features
- Write UI in Python – VDOM elements, components, state management
- Compile to JavaScript – Your Python spec →
index.html+app.js - Deploy anywhere – Static SPA with no server needed
- Production-ready – Built on FastAPI with auth, rate limiting, CSRF protection
- Zero client-side code – Framework handles all routing, state, and rendering
Quick Start
Requirements
- Python 3.12+
1. Create a new project
pip install xania
xania init my_website
cd my_website
2. Edit your site
Edit app.py to add pages and content:
from xania import SpaApp, StaticPage, Div, H1, Button
app = SpaApp(name="MySite")
app.route(
"/",
StaticPage(
title="Home",
html='<h1>Welcome!</h1><p>Built with Xania</p>'
)
)
3. Compile
python app.py
# Generates: index.html + static/app.js + static/spa_runtime.js
4. Run
xania serve .
# 🚀 Serving SPA from /path/to/my_website
# 📍 http://127.0.0.1:8000
Open your browser to http://127.0.0.1:8000
5. Deploy
Copy the generated files to any static host:
# Upload to Netlify, Vercel, GitHub Pages, S3, etc.
cp -r . /path/to/deployment/
How It Works
SPA Mode (Recommended)
- Define your app in Python – Use
SpaAppwith routes and pages - Compile – Xania generates JavaScript that runs entirely in the browser
- Deploy – Copy
index.html+static/to any static host
No server needed for the SPA itself. Client-side routing via History API.
from xania import SpaApp, StaticPage
app = SpaApp(name="MySite")
app.route("/", StaticPage(title="Home", html="..."))
app.route("/about", StaticPage(title="About", html="..."))
# Then: python app.py → generates index.html + app.js
Server Mode (Advanced)
If you need a backend, integrate with FastAPI:
from fastapi import FastAPI
from xania import mount_spa
app = FastAPI()
# Add API routes
@app.get("/api/data")
def get_data():
return {"items": [...]}
# Mount compiled SPA
mount_spa(app, ".")
Then: xania serve . or uvicorn app:app
Two Modes Explained
SPA Mode (Default)
- HTML shell + JavaScript SPA
- Client-side routing (History API)
- No server needed for UI
- Perfect for: static sites, blogs, documentation, dashboards
- Deploy to: Netlify, Vercel, GitHub Pages, S3
Server Mode (Optional)
- FastAPI backend + SPA frontend
- Add custom APIs, auth, data
- Still compiles to static files
- Perfect for: full-stack apps with complex backends
- Deploy to: Heroku, Railway, AWS, DigitalOcean
Core Concepts
VDOM Elements – Python classes representing HTML:
from xania import Div, H1, Button, Input
Div(
H1("Welcome"),
Input(type="text", placeholder="Enter name"),
Button("Click me", onclick="App.dispatch('Counter', 'click')"),
class_name="flex flex-col gap-4"
)
Components – Stateful UI units:
from xania import Component, Div, Span, Button
class Counter(Component):
def initial_state(self):
return {"count": 0}
def render(self, state):
return Div(
Span(str(state.count)),
Button("+", onclick="App.dispatch('Counter', 'inc')")
)
def on_inc(self, state, payload):
state.count += 1
Pages – Routes in your SPA:
from xania import SpaApp, StaticPage, TemplatePage, JsExpr
app = SpaApp()
# Static page
app.route("/", StaticPage(
title="Home",
html="<h1>Welcome</h1>"
))
# Template page with placeholders
app.route("/user/{id}", TemplatePage(
title="User",
template="<h1>User {name}</h1>",
placeholders={"name": "John"}
))
Project Structure
Core Modules:
renderer/– VDOM elements, components, state managementruntime/– SPA compiler (Python → JavaScript)web/– FastAPI integration, serving, authstatic/– Browser runtime (spa_runtime.js)cli.py– Command-line interface
See also: documentation/ for full tutorial SPA built with Xania
Examples
Counter with State
from xania import Component, Div, Button, Span
class Counter(Component):
def initial_state(self):
return {"count": 0}
def render(self, state):
return Div(
Span(f"Count: {state.count}"),
Button("+", onclick="App.dispatch('Counter', 'inc')"),
Button("-", onclick="App.dispatch('Counter', 'dec')"),
)
def on_inc(self, state, payload):
state.count += 1
def on_dec(self, state, payload):
state.count -= 1
Multi-Page SPA
from xania import SpaApp, StaticPage, TemplatePage
app = SpaApp(name="MyBlog")
# Home page
app.route("/", StaticPage(
title="Home",
html="<h1>Welcome to My Blog</h1>"
))
# About page
app.route("/about", StaticPage(
title="About",
html="<h1>About Me</h1><p>I build with Xania.</p>"
))
# Post page with placeholders
app.route("/post/{id}", TemplatePage(
title="Post",
template="<h1>{title}</h1><p>{content}</p>",
placeholders={"title": "My First Post", "content": "..."}
))
Deployment
Option 1: Static Hosting (Simplest)
# Compile
python app.py
# Upload everything to your host
# - Netlify (drag & drop)
# - Vercel (git integration)
# - GitHub Pages
# - S3 + CloudFront
Option 2: FastAPI Server
# serve.py
from fastapi import FastAPI
from xania import mount_spa
app = FastAPI()
@app.get("/api/hello")
def hello():
return {"message": "Hello from API!"}
mount_spa(app, ".")
# Local: uvicorn serve:app --reload
# Production: gunicorn serve:app (on Heroku, Railway, etc.)
Development
Clone and install:
git clone https://github.com/xania/framework.git
cd xania
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
Run examples:
xania serve documentation # Full tutorial
xania dev # Dev server
Contributing
PRs welcome! See GitHub
- DOM events calling
App.dispatch('Counter', 'increment')
Tags / Elements (VDOM)
Xania’s “tags” are Python functions/classes that build a VDOM tree in renderer/elements.py.
- Use built-in tag helpers:
Div(...),Span(...),Button(...),H1(...), etc. - Use
Element(tag, ...)for any HTML tag (even if there is no helper yet):
from renderer.elements import Element
node = Element("section", "Hello")
Children
- Text children: strings (they are HTML-escaped by
engine/serializer.py) - Element children: nested
Elementinstances - Lists: spread them with
*itemswhen building children
Attributes (“props”)
Attributes are passed as keyword args:
class_name→classfor_→forhttp_equiv→http-equiv- Any
_becomes-(e.g.data_component="Counter"→data-component="Counter") - Booleans:
disabled=Truerendersdisableddisabled=Falseomits it
Example:
from renderer.elements import Button
Button(
"Click",
class_name="px-3 py-2 rounded",
data_component="Counter",
disabled=False,
)
Registering components
In app.py:
from renderer.registry import ComponentRegistry
from example.counter import Counter
ComponentRegistry.register("Counter", Counter(id="counter"))
The string "Counter" must match what the client dispatches:
onclick="App.dispatch('Counter','increment')"
Web API
GET /
Returns the HTML shell (mount points + <script src="/static/runtime.js">).
POST /event
Request (web/schemas.py):
{
"component": "Counter",
"action": "increment",
"payload": {}
}
Response:
{
"updates": [
{ "id": "counter", "html": "<div>...</div>" }
]
}
The client applies updates by doing document.getElementById(id).innerHTML = html.
Publish readiness (honest checklist)
This codebase is a solid architectural baseline, but it is not ready to publish as a production framework yet. Here’s what’s missing / risky:
- Session/user isolation:
ComponentRegistrystores singletons in-process.- In production you need per-user (cookie/session) component instances or a state store.
- Concurrency & scaling:
- Multiple workers/processes will not share component state.
- Need a storage layer (Redis/DB) or stateless model (client-owned state + patches).
- Security:
onclick="App.dispatch(...)"is safe here because HTML is generated server-side, but any user-provided text/attrs must be carefully validated.- Consider CSRF protection for
/event. - Demo auth exists for stress-testing, but production needs strong secrets + HTTPS-only cookies + real user storage.
- Performance:
- Current updates replace
innerHTMLof the whole component. - Next step: diffing + patching (you already have a base architecture for it).
- Current updates replace
- Testing:
- No unit tests or integration tests yet.
- Packaging polish:
- Ensure
pyproject.tomlmetadata is correct (name/urls/authors). - Add a changelog and decide versioning strategy.
- Ensure
Roadmap (recommended next steps)
- Add per-session registry/state store (cookie -> component state)
- Add diff/patch engine (update minimal DOM, not full
innerHTML) - Add router + multipage CSR navigation
- Add test suite + CI
- Add docs site and examples gallery
Publish checklist (minimal)
- Package identity: confirm
pyproject.tomlname/version/description are correct. - License: ensure
LICENSEexists and matchespyproject.toml(MIThere). - Versioning: adopt SemVer (
0.xwhile APIs are changing quickly). - README: keep Quick start + minimal example runnable.
- Security: decide on CSRF/auth strategy for
/eventbefore public deploys. - State model: decide per-user state persistence (sessions/Redis) before scaling.
- CI: add at least
python -m py_compile+ basic tests on every push.
Demo auth (stress testing only)
This repo includes a minimal cookie-session auth demo used by the SPA stress page:
- Login endpoint:
POST /api/auth/login(demo user:admin/admin) - Logout endpoint:
POST /api/auth/logout - Current user:
GET /api/auth/me - Protected stress endpoints:
GET /api/private/big-json,POST /api/private/write-echo
Environment variables:
XANIA_ENV=dev|prod(defaultdev)XANIA_SECRET_KEY=...(required whenXANIA_ENVis not dev)XANIA_COOKIE_SECURE=true|false(defaults to true outside dev)
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 xania-0.1.4.tar.gz.
File metadata
- Download URL: xania-0.1.4.tar.gz
- Upload date:
- Size: 50.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b1769781ba3775adb33904638f4cab3076578c6f060bc44a2b13a3a7f5f0426
|
|
| MD5 |
6fe5522018cf3665ee898555bc9d001a
|
|
| BLAKE2b-256 |
70918d67ed16c1f478058d3bc2c81bc8104ba81f401c339bbd4eee36c3c50a2b
|
File details
Details for the file xania-0.1.4-py3-none-any.whl.
File metadata
- Download URL: xania-0.1.4-py3-none-any.whl
- Upload date:
- Size: 54.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
879caa36163de8661c4675b319c503cf30750e7fdb17825ef6e61147b4ce992b
|
|
| MD5 |
5fcd47f31dec161220db2f45a5d32f05
|
|
| BLAKE2b-256 |
ff31b8ed1e2c680756b7c44e1bfd4954f3e0417906f2786c351295557719ed93
|