Lila Framework v2.0.0
Lightweight full-stack Python web & desktop framework
Starlette + Pydantic v2 + SQLAlchemy 2.0 Async + Jinja2 + a native View Transitions SPA.
⚡ What is Lila?
Lila is a small, batteries-included Python web framework for people who want the speed of Starlette, the validation of Pydantic v2 and an SPA experience without a Node.js build step. It is designed for 1–2 worker deployments on a cheap VPS: SQLite WAL by default, an in-process LRU cache, optional Redis when it is actually reachable, and a file cache fallback.
- 🪶 Light on import:
import lilais lazy (~19 MB RSS). A fullAppis around 42 MB on Python 3.14, and no SQLAlchemy is loaded unless you use models. - 🚀 SPA without Node.js:
lila-client.jsuses the native View Transitions API, hover prefetching, persistent islands (data-persist="true") and Tailwind UI components (toast, snackbar, command palette, drawer, tabs, carousel, DataTable). - 🧩 Templates without boilerplate: a page is just its HTML.
render()adds the layout,<title>, description, Open Graph, JSON-LD, icon, styles and scripts;{{ datatable(...) }}and{{ carousel(...) }}build components from Jinja, anddata-lila-form/data-actionsend forms and buttons without page scripts. - 💾 Zero infrastructure by default: Async SQLite with WAL, an in-memory LRU
cache, and
CACHE_DRIVER="file"for a persistent fallback.CACHE_DRIVER="redis"is used only when the Redis package is installed and the server answers; otherwise Lila logs once and falls back to memory. - 🗄️ Real migrations:
lila migratecreates tables, adds and renames columns and creates indexes;--planshows the diff first and risky changes are only reported. - ⏱️ Background work without Redis:
@taskqueue and@every/@cronjobs on their own SQLite files, Jinja email templates, Server-Sent Events. - 🔐 Roles & permissions, API keys and rotating JWT refresh tokens built in.
- 📈 Lean in production:
lila css build(compiled Tailwind, no Node), precompressed static files, ETag/304, cache memory budget, Granian support,lila bench, protected/healthand Prometheus/metrics. - 🤖 AI helper (
core/ia.py): OpenAI, DeepSeek and Gemini with explicit PII masking/restoration, prompt cache and per-IP rate limits. - 🔐 Auth 2.0: cookie sessions without sensitive fields, Argon2id, CSRF for classic HTML forms, Google/GitHub/Discord OAuth, login lockout and email reset links (SMTP optional).
- 🛠️ Admin 2.0: CRUD, CSV export, cache purge, logs and honest telemetry.
- 🖥️ Desktop mode:
lila desktop dev/lila desktop build(PyInstaller bundlesmainand the Lila templates/assets). - 🌍 Locales:
en,es,ptandzhbuilt in, plus project overrides inapp/locales/*.json.
Runtime requirement: Python 3.10+ (Starlette 1.x and Uvicorn 0.54 require it).
📦 Installation
pip install lila-framework
# Optional:
pip install "lila-framework[redis]" # Redis cache/worker
pip install "lila-framework[images]" # WebP uploads
pip install "lila-framework[markdown]" # render_markdown
pip install "lila-framework[metrics]" # truthful admin telemetry
pip install "lila-framework[optimize]" # rjsmin/rcssmin + Pillow
pip install "lila-framework[desktop]" # pywebview + PyInstaller
🚀 Quick Start
lila init # scaffold main.py, app/, public/, resources/ and .env
lila dev # http://127.0.0.1:8000 with reload + dev SSE
lila doctor # check .env, SECRET_KEY, database, templates, public/
lila routes # list registered routes of main:app
# main.py
from app.config import DEBUG, HOST, PORT, WORKERS
from app.routes.web.index import router as web_router
from lila.core.app import App
app = App(
routes=[web_router],
debug=DEBUG,
security=True,
rate_limit=300,
gzip=True,
openapi=True,
debug_html=DEBUG, # mount /debug in debug mode
)
Routes with typed params
from lila.core.routing import Router
from lila.core.request import Request
from pydantic import BaseModel, EmailStr
router = Router(prefix="/api")
class UserCreate(BaseModel):
email: EmailStr
name: str
@router.get("/users")
async def users(request: Request, page: int = 1, q: str | None = None):
# page is a real int; invalid values return HTTP 422
return {"page": page, "q": q}
@router.get("/users/{id:int}", cache=60, cache_public=True)
async def get_user(request: Request, id: int):
return {"id": id}
@router.post("/users", model=UserCreate, csrf=True)
async def create_user(request: Request, data: UserCreate):
return {"success": True, "email": data.email}
Pages: just HTML, the layout is automatic
from lila.core.templates import render
@router.get("/dashboard", auth=True)
async def dashboard(request: Request):
user = request.state.user
return render(
request, "authenticated/dashboard", {"user": user, "orders": await Order.get_all_async()},
title="dashboard", # translation key -> "Dashboard - My App"
description="Your orders",
robots="noindex",
jsonld={"@context": "https://schema.org", "@type": "WebPage", "name": "Dashboard"},
scripts=["js/chart.js"],
)
{# resources/html/authenticated/dashboard.jinja — no extends, no blocks #}
<main class="max-w-7xl mx-auto px-4 py-8 space-y-6">
<h1 class="text-3xl font-black">{{ translate['welcome'] }}, {{ user.name }}!</h1>
{{ datatable(orders, ["id", ("total", "Total"), "status"], rows_per_page=10) }}
{{ carousel([{"image": "img/promo.webp", "title": "Sale"}], autoplay=5000) }}
</main>
Templates written for 2.0.0 with {% extends %} keep working unchanged.
Forms without JavaScript
<form action="/api/contact" method="post" data-lila-form data-snackbar data-reset>
<input name="email" type="email" required>
<textarea name="text" required minlength="10"></textarea>
<div data-message hidden></div>
<button type="submit" data-loading-text="Sending...">Send</button>
</form>
<button data-action="/api/items/3" data-method="DELETE" data-confirm="Delete?" data-refresh="#items">Delete</button>
HTML5 validation, CSRF, disabled buttons with a spinner, messages from the API
(success, message/msg, 422 errors per field), redirects and table refreshes
are handled by lila-client.js.
Everyday UI without page scripts: data-drawer (left/right/top/bottom),
data-dropdown, data-copy, data-confirm, data-toggle-class, data-remove,
data-password-toggle, data-filter, data-char-count, data-autosubmit,
data-tab-target, data-carousel, <time data-local-time> — see the
client docs.
Models and migrations
from sqlalchemy import Column, Integer, String
from lila.core.base_model import BaseModel
class Product(BaseModel):
__tablename__ = "products"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(120), nullable=False)
active = Column(Integer, nullable=False, default=1)
# Async API
await Product.insert_async(name="Chair")
await Product.get_all_async(limit=50, order_by="name")
await Product.get_by_id_async(1, select="id,name")
await Product.update_async(1, name="Desk")
await Product.delete_async(1) # soft delete when `active` exists
Run lila migrate after changing models; it adds missing columns. For destructive
changes use lila migrate --refresh --yes (asks for confirmation otherwise).
Cache
from lila.core.cache import Cache
await Cache.set_async("key", {"a": 1}, ttl=300)
data = await Cache.get_async("key")
await Cache.delete_async("key")
await Cache.clear_async(include_sessions=False)
CACHE_DRIVER accepts memory (default), redis or file. Redis is probed
lazily; if it is down Lila continues with the in-memory cache and logs once.
AI with PII masking
from lila.core.ia import IA
ai = IA(provider="gemini", rate_limit=20, rate_window=60, cache_ttl=3600)
result = await ai.ask(
prompt="Draft a support reply for this customer:",
data={"name": "Maria", "email": "maria@example.com", "ticket": "Cannot access invoice #402"},
sensitive_keys=["name", "email"],
request=request,
)
Set OPENAI_API_KEY, DEEPSEEK_API_KEY or GEMINI_API_KEY; override the model
with OPENAI_MODEL, DEEPSEEK_MODEL or GEMINI_MODEL.
Auth + Admin
lila auth # login/register/reset/profile/dashboard + OAuth
lila admin -u admin -p ... # create/update the admin user
lila migrate
Admin lives at /admin; OAuth callbacks at /auth/{provider}/callback. Set
APP_URL and the provider credentials in .env. Password reset requires SMTP
(SMTP_HOST, SMTP_USER, SMTP_PASSWORD, SMTP_FROM) — in development the
reset URL is printed to the console instead.
Desktop
pip install "lila-framework[desktop]"
lila desktop dev
lila desktop build --name MyApp --onefile --windowed
The generated executable bundles main, app, Lila templates and public/.
Lila binds the desktop server to 127.0.0.1, enforces a per-run local token,
prevents a second instance and stores its SQLite/cache/logs in the OS
application-data directory.
🛠️ CLI Reference
| Command | Description |
|---|---|
lila init |
Scaffold a project without overwriting existing files. |
lila dev |
Uvicorn reload + Jinja/static SSE auto-reload (reads .env HOST/PORT). |
lila run -w 2 |
Production server; warns when using memory cache with >1 worker. |
lila doctor |
Health check: Python, .env, SECRET_KEY, DB, templates, public/. |
lila routes |
List every route registered in main:app. |
lila auth |
Auth 2.0 scaffold (backups for overwritten files). |
lila admin |
Create/update admin user + link routes safely. |
lila model create |
Generate an ActiveRecord model. |
lila crud --model X |
CRUD API + HTML DataTable with CSRF. |
lila migrate |
Create tables and add missing columns. |
lila migrate --refresh --yes |
Drop and recreate (asks without --yes). |
lila optimize |
Minify JS/CSS (needs the optimize extra) + WebP images. |
lila secret-key |
Rotate SECRET_KEY. |
lila seo |
public/sitemap.xml + robots.txt (served at /sitemap.xml). |
lila worker |
Redis JSON task worker (pip install lila-framework[redis]). |
lila deploy |
Generate systemd + Nginx + Windows Server files in deploy/. |
lila css build |
Compiled, minified Tailwind CSS for production (no Node.js). |
lila bench URL |
Requests/s, latency percentiles and server RSS. |
lila tasks / lila schedule |
SQLite task queue and @every/@cron jobs. |
lila i18n check |
Translation keys missing per language. |
sudo "$(which lila)" deploy setup |
Interactive Linux install: .env for production, systemd, Nginx, Certbot HTTPS. |
lila backup |
Online, zero-downtime SQLite backup with retention. |
lila desktop dev/build |
Native window / PyInstaller executable. |
lila desktop binds to 127.0.0.1, enforces a generated local session token and
allows only one instance. The frozen build stores SQLite/cache/logs in the OS
application-data directory.
🧪 Tests
python -m unittest discover -s tests -v
📖 Documentation
Full guides and API reference: https://seip25.github.io/Lila
📄 License
MIT. See LICENSE.
Security policy: SECURITY.md · Changelog: CHANGELOG.md
Metadata
Release files for lila-framework 2.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| lila_framework-2.1.0.tar.gz | 901.6 kB | Without provenance | Details for lila_framework-2.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| lila_framework-2.1.0-py3-none-any.whl | Python 3 | none | any | Details for lila_framework-2.1.0-py3-none-any.whl |
Total release size: 1.8 MB
Release files / lila_framework-2.1.0.tar.gz
| Download URL | lila_framework-2.1.0.tar.gz |
|---|---|
| Size | 901.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2d8170f3b06646af8e73b1ffe2edaed22de31c9252dafe856cdf29c317023702
|
|
BLAKE2b-256 checksum How to use checksums |
da835fb14201518d6265c38746174c0b5c92511c73e724771c3c24e198ba276d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Release files / lila_framework-2.1.0-py3-none-any.whl
| Download URL | lila_framework-2.1.0-py3-none-any.whl |
|---|---|
| Size | 921.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
45f26eb122031a950621fa4afec024672a73d33c020857baa259b2aa5407e8d7
|
|
BLAKE2b-256 checksum How to use checksums |
339cc691171f6b1b21ad4ca4b0d43a9778ad6bc6be7f749298279c66f3d0e9d6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Security for lila-framework 2.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.