Pyrite
Release: 0.1.3.post3.
Code websites in Python 3, for JavaScript and CSS haters.
Want the short version? Start here: button colors, styling, first page.
Pyrite is a small, dependency-free starting point: compose Python components, return a page, and serve HTML with built-in responsive styling. Requires Python 3.10+.
Install from PyPI
Create a folder for your website, then use a virtual environment. This also avoids
Arch Linux's externally-managed-environment error.
python3 -m venv .venv
.venv/bin/python -m pip install pyrite-web
The package name on PyPI is pyrite-web; the Python import is pyrite.
On Windows, replace .venv/bin/python with .venv\Scripts\python.exe.
Your first website
Save this as app.py in your project folder:
from pyrite import App, Button, Column, Heading, Page, Text
app = App()
@app.page("/")
def home():
return Page(Column(
Heading("My first Pyrite website."),
Text("Written in Python. No JavaScript or CSS files."),
Button("About this site", "/about"),
), title="My website")
@app.page("/about")
def about():
return Page(Column(
Heading("About"),
Text("Another URL. Another Python function."),
Button("Back home", "/"),
), title="About")
if __name__ == "__main__":
app.run()
Run it, then open http://127.0.0.1:8000:
.venv/bin/python app.py
Stop with Ctrl+C. After editing Python files, restart the server and refresh your
browser. Use app.run(port=8001) if port 8000 is already occupied.
For your own shared style, create styles.py next to app.py:
from pyrite import Gradient, Theme
theme = Theme(
background="#111820",
text="#eef3f8",
heading_font="sans",
radius=16,
button_radius=999,
gradient=Gradient(
colors=("#111820", "#214a42"),
moving=True,
duration=14,
),
)
Import theme with from styles import theme, then pass theme=theme to each
Page(...). The root styles.py in this repository is an example, not a file
installed into your project by pip.
The sections below cover components, forms, HTTP calls, animation, and chat.
The demo URLs such as /try and /chat belong to this repository's example.py;
installing the library does not automatically add these routes to your app.
Run the example
python3 example.py
Open http://127.0.0.1:8000. The example is Pyrite's first website: a homepage
with a code/output comparison, quickstart, expandable FAQ, and a component
guide at /guide. Try /playground for a Python-powered calculator.
Its application source uses only Python components.
No installation is required when running from this repository.
Build a page
from pyrite import App, Column, Heading, Page, Text
app = App()
@app.page("/")
def home():
return Page(
Column(Heading("Hello, Pyrite."), Text("Just Python.")),
title="My website",
)
if __name__ == "__main__":
app.run()
Available components: Heading, Text, Column, Row, Card, Link, and
Button, Section, Nav, Code, and Details. Nest components directly.
Section(..., id="start") creates an anchor target. Code(source, label="Python")
displays escaped source. Details(question, ...) provides a native expandable answer.
Heading accepts level=1 through 6.
Link(label, href) and Button(label, href) navigate to another URL.
Text and HTML attributes are escaped automatically.
Register exact paths with @app.page("/about"). Handlers take no arguments and
return a Page. Query strings do not affect route matching. Page accepts
title and language; app.run accepts host and port.
Current scope
This version serves server-rendered pages with native GET forms and JSON POST
callbacks through LiveForm, Chat, and Poll. Request access and cookies
allow application-managed sessions. It does not include a built-in session store,
token streaming, live reload, or a production server. The built-in
HTTP server is for local development. Styling is supplied by Pyrite; applications
do not need CSS files or JavaScript tooling.
Styling in Python
Edit the root styles.py to change the example website's shared theme. Restart
the server after editing. Theme supports six-digit hex colors, sans, serif,
or mono fonts, page width, corner radius, and spacing. Apply a theme to any page:
from pyrite import Theme
theme = Theme(background="#111820", text="#eef3f8", heading_font="sans")
page = Page(Heading("Your style."), theme=theme)
Forms without JavaScript
from pyrite import Form, Input, Submit, query
@app.page("/hello")
def hello():
return Page(Column(
Form(Input("name", "Your name", value=query("name")), Submit("Say hello")),
Text(f"Hello, {query('name', 'world')}!"),
))
Slider(name, label, value=5, minimum=1, maximum=10) and
Select(name, label, {"value": "Label"}, value="value") also work in forms.
Submission reloads the page; query reads request-local URL parameters. GET forms
are for read-only calculations and searches; their values appear in the URL.
Try Pyrite in Browser
Visit /try to edit Python and render a real Page without installing Python.
Assign your result to page; do not call app.run(). The editor loads Pyodide
from jsDelivr and runs code in a disposable Web Worker, with a 10-second execution
limit and an isolated preview. First download can take up to two minutes.
Code is not executed on the application server. Browser networking follows CORS
rules; the standard-library HTTP client below is for server-side Python.
Images and motion
from pyrite import Image, Motion
from styles import hero_image
banner = Motion(Image(hero_image, "Mountain landscape"), effect="rise")
Choose rise, fade, or float. In styles.py, set motion=False to disable
component animations, or change motion_duration (100–5000 milliseconds) for
entrances. Reduced-motion browser preferences disable animations automatically.
Theme(background_image="https://example.com/background.jpg") attaches a page
background. Image accepts relative or HTTP(S) URLs and requires alt text.
External images load directly from their host; local file serving is not included.
Requests and changing API data
from pyrite import requests
data = requests.get(
"https://api.mymemory.translated.net/get",
params={"q": "Good morning", "langpair": "en|uk"},
).json()
# For an API you control, with the authentication it requires:
# requests.post("https://your-api.example/items", json={"name": "New"})
# requests.put("https://your-api.example/items/1", json={"name": "Changed"})
# requests.delete("https://your-api.example/items/1")
The dependency-free client supports get, post, put, and delete, optional
headers, JSON bodies, query parameters, and a timeout (8 seconds by default).
Responses expose status_code, text, and json(). Catch requests.RequestError
for HTTP/network/decoding failures. Responses are limited to 1 MiB. Supply trusted
URLs in application code; do not forward visitor-provided URLs to this client.
GET page handlers should only read data. POST actions can make remote writes;
add application authentication and authorization before exposing private data or
operations. Pyrite's local development server does not provide authentication.
/translate demonstrates input, a real free MyMemory request, and updated page
content. No API key required; availability, translation quality, and quotas depend
on MyMemory. Submitted text is sent to that service.
Corners and gradients
from pyrite import Gradient, Theme
theme = Theme(
radius=20, # Panels, code blocks, images, details
button_radius=999, # Pill buttons; 0 gives square corners
input_radius=12, # Inputs and dropdowns
gradient=Gradient(
colors=("#101923", "#214a42", "#17283d"),
angle=120,
moving=True,
duration=14, # Seconds per cycle
),
)
button_radius and input_radius inherit radius when omitted. Gradients accept
2–8 hex colors; set moving=False for a static gradient or gradient=None for
the usual background. A fixed background layer animates independently of page
length. motion=False or the browser's reduced-motion preference stops its movement.
The gradient layer sits over a background image when both are configured.
In-page requests and chat
from pyrite import App, Chat, Page
app = App()
@app.action("/api/reply")
def reply(data):
message = data["message"]
history = data.get("history", [])
# Call a model here with pyrite.requests; keep API keys on the server.
return {"reply": "You said: " + message}
@app.page("/")
def home():
return Page(Chat(action="/api/reply"))
Visit /chat for a working local rule-based chatbot and a live translation form.
Chat sends {message, history} and displays reply without a page reload.
History stays in this page's browser memory (last 20 messages), is sent with each
request, and clears on reload. It is untrusted input; validate it in callbacks.
This is completed-response chat, not token streaming. The demo uses no AI service.
For other interfaces, use LiveForm(Input(...), Submit(...), action="/api/action")
and return {"text": "Updated result"} from the action. All field values arrive
as strings. Pyrite supplies the JavaScript, pending state, errors, 30-second
client timeout, and safe text rendering. The callback can use requests to reach
external APIs without exposing credentials to the browser. Aborting a request
does not cancel Python work already running.
Actions accept JSON objects up to 16 KiB and reject cross-origin browser requests. These checks do not authenticate users. Use application validation and authorization for sensitive actions. Live components require JavaScript; ordinary pages and GET forms continue working without it. Browser playground previews remain isolated and cannot call server actions.
Per-component color labels
Define reusable labels in your project's styles.py:
from pyrite import Color, Theme
theme = Theme(colors={
"danger": Color("#b91c1c", text="#ffffff", hover="#991b1b"),
"success": Color("#166534", text="#ffffff", hover="#14532d"),
})
Use Button("Continue", "/next", theme=theme["success"]) or
Submit("Save", theme=theme["success"]). This changes only that component.
Text, Heading, Column, Row, and Card also accept a color through theme=.
Other built-in elements support .themed(theme["danger"]).
Page(..., theme=theme) still sets the whole page's theme.
theme=theme:label is not valid Python; use theme=theme["label"].
Labels contain validated Color objects, not arbitrary CSS. Missing labels raise
an error explaining where to define them.
Pyrite doctor
From your project folder:
.venv/bin/pyrite doctor
# Or specify a directory:
.venv/bin/python -m pyrite doctor /path/to/your/project
Doctor reports known issues with file, line, explanation, and suggested fix:
| Severity | Meaning | Exit code |
|---|---|---|
| Severe | Syntax, invalid settings/colors, or known component misuse | 2 |
| Medium | Missing page theme or low text contrast | 1 |
| Mild | Usability advice, such as overly fast gradients | 0 |
The highest severity determines the exit code. No issues also returns 0. Doctor reads Python source without executing apps or sending API requests. It skips virtual environments and build folders. Dynamic values and runtime behavior still need testing; passing doctor is not proof that every issue is fixed.
Tests
python3 -m unittest discover -s tests -v
Publishing a release to PyPI
These steps are for maintainers publishing Pyrite itself. Run from the repository root. Users building websites only need the installation instructions above.
Set up publishing tools
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade build twine
Keep publishing tools inside this environment. Do not install them into Arch's
managed system Python or use --break-system-packages.
Create an upload token
- Register or sign in at https://pypi.org and enable two-factor authentication.
- Open https://pypi.org/manage/account/token/ and create an API token.
- For an existing project, restrict its scope to
pyrite-web. An account-wide token is needed only when creating a project for the first time. - Copy the token immediately; PyPI shows it once. Keep it out of source files, Git, logs, screenshots, and chat.
Build and check
Choose an unused version in pyproject.toml. PyPI cannot overwrite an uploaded
release, even when only the README changes. The README becomes the package's PyPI
description when the new artifacts are uploaded.
For version 0.1.3.post3:
.venv/bin/python -m unittest discover -s tests -v
.venv/bin/python -m build
.venv/bin/python -m twine check dist/pyrite_web-0.1.3.tar.gz dist/pyrite_web-0.1.3.post3-py3-none-any.whl
Use the matching version in every filename for later releases. Upload explicit
filenames so old artifacts in dist/ are not accidentally uploaded again.
Upload and verify
.venv/bin/python -m twine upload dist/pyrite_web-0.1.3.tar.gz dist/pyrite_web-0.1.3.post3-py3-none-any.whl
When prompted, use __token__ as the username and the full token, including its
pypi- prefix, as the password. The password prompt hides your input.
Check https://pypi.org/project/pyrite-web/0.1.3.post3/ and install in a fresh environment:
python3 -m venv /tmp/pyrite-release-check
/tmp/pyrite-release-check/bin/python -m pip install pyrite-web==0.1.3.post3
/tmp/pyrite-release-check/bin/python -c 'from pyrite import App, Chat, Gradient, Theme; print("Pyrite installed")'
If Twine reports No module named twine, use .venv/bin/python instead of system
python3. A 403 usually means missing project permissions or an invalid token;
an already-existing-file error requires a new version, not another upload of the
same filename. Never paste your token into an error report.
Doctor also checks imported Pyrite component arguments, literal heading levels,
motion effects, input kinds, slider bounds, link/image URLs, live-action paths,
empty labels and page metadata, select options, and duplicate or invalid App
endpoint paths. Import aliases and import pyrite as p are supported for these
checks. Dynamic values are left for runtime validation; application code is
never executed. Route checks are local to each file and App variable.
# Machine-readable issues and severity counts
python -m pyrite doctor app.py --json
# Report severe issues only (also controls exit status)
python -m pyrite doctor . --severity severe
# Skip additional directory names; repeat as needed
python -m pyrite doctor . --exclude generated --exclude vendor
Exit status: 2 for severe issues, 1 for medium issues, 0 otherwise.
--severity defaults to mild, reporting every severity. JSON output contains
issues (severity, path, line, message, fix) and counts.
Guide, rating, and helpful errors
Run pyrite guide app.py for a cyan terminal workspace with Guide, Doctor,
and Logs tabs. Doctor findings refresh as you edit your source. Use Tab to
switch views, arrows or j/k to select, Page Up/Down to scroll explanations,
/ to search, Escape to clear search, r to refresh, and q to quit.
Click the underlined topic links to open explanations inside the guide;
Enter opens the selected finding's explanation. Mouse support depends on
your terminal. Use g to return to the guide topics. The TUI needs a terminal
of at least 65 columns by 14 rows and Python's standard-library curses module.
pyrite guide app.py
pyrite guide app.py --topic theme
pyrite guide app.py --log pyrite-errors.jsonl
pyrite guide app.py --plain # also automatic when output is redirected
pyrite rating app.py
pyrite rating app.py --json
pyrite run app.py --log startup-errors.jsonl
guide and rating only inspect application source; they never run it.
Both accept repeatable --exclude DIRECTORY arguments. The guide includes
runnable getting-started code and explanations of components, forms, routing,
themes, URLs, diagnostics, and Python errors. Logs refresh every second,
showing the latest 200 lines from at most 128 KiB, newest first. Both JSONL
runtime logs and existing plain-text logs are supported; select an entry
and open its explanation. A missing log file can be created while the guide
is running. --plain prints the selected topic and a diagnostic/log snapshot.
Rating measures static diagnostic health: start at 100 and subtract 20 for each severe finding, 5 for each medium finding, and 1 for each mild finding, stopping at zero. It shows the five highest-priority fixes; JSON includes all tips, severity counts, and penalties. No source files or a missing path produces an unavailable score. A clean score does not prove correct runtime behavior, security, performance, or visual quality. Rating uses Doctor's exit codes: 2 for severe findings, 1 for medium, otherwise 0.
Request-handler failures now print the Python exception, application source location, a suggested fix, a guide topic, and the full traceback to the server terminal. Missing page returns point to the handler definition. Page configuration and execution failures return HTTP 500; malformed query parameters return 400. Actions may raise ValueError to reject user input with 400; other failures return 500. HTTP error responses stay generic.
Enable structured runtime error logs explicitly:
from pyrite import App
app = App(log_path="pyrite-errors.jsonl")
Use pyrite run app.py to execute an application with the same explanations
for startup errors, including syntax mistakes. Its --log writes startup
failures; use App(log_path=...) for failures during HTTP requests. Running
python app.py also gets helpful request-handler errors, while Python
handles startup exceptions normally. run returns 1 on a caught startup
error and preserves explicit application exit codes.
New error records do not include request bodies, headers, query strings, or local variables. Exception messages may contain private data; keep logs outside source control. New log files use owner-only permissions on POSIX; existing files keep their current permissions. Standard HTTP access logs remain separate and can include request URLs.
Doctor also flags recognizable security risks: eval/exec, shell execution,
unsafe pickle/shelve/YAML deserialization, SQL built with string interpolation,
and disabled TLS certificate verification. Findings include file and line numbers
and advice such as using explicit action allowlists, argument lists with
shell=False, safe data formats, parameterized SQL, and trusted CA certificates.
Imported aliases are recognized for these APIs.
These are static risk warnings, not proof of an exploitable vulnerability or a security audit. Doctor does not trace input through variables or across files, and dynamic imports and some rebinding patterns can evade checks. A clean scan does not prove safety. Validate request data, enforce authentication and per-action authorization, protect sensitive actions against CSRF, keep secrets server-side, and run production services behind a hardened HTTPS server rather than exposing Pyrite's development server directly.
Hover effects
Chain .hover(...) onto any component to change its appearance when the pointer
hovers over it. Focusable components use the same effects on keyboard focus.
Button("Hover me", "/").hover(
scale=1.1,
background="#6633cc",
color="#ffffff",
font="mono",
font_size=20,
glow="#9966ff",
)
All options are optional and can be combined. scale is a size multiplier
(0.1–3); font_size is pixels (1–300). Colors use six-digit hex values.
font accepts sans, serif, or mono. Effects transition smoothly and reset
when hover/focus ends. Reduced-motion preferences disable transitions and scaling.
The original component stays unchanged; repeated calls merge settings. Works
with .themed(...) in either order and requires no JavaScript. Hover is a visual
state, not a Python callback; touch users can still activate buttons normally.
Noninteractive guide for AI agents
pyrite guide --agent
pyrite guide --agent --topic hover
pyrite guide --agent > pyrite-reference.txt
--agent prints a plain-text reference once and exits, even in a terminal.
Without --topic, it includes every guide topic and public component signatures.
It never prompts, opens the TUI, scans project files, reads logs, or executes your
app. Use pyrite doctor app.py --json separately for static diagnostics.
For a human-readable topic with project diagnostics, use
pyrite guide app.py --plain --topic hover.
Automatic updates
Poll refreshes text from a Python action without submitting a form or reloading
the page. For example, this clock updates every two seconds:
from datetime import datetime, timezone
from pyrite import App, Page, Column, Heading, Poll
app = App()
@app.action("/clock")
def clock(data):
return {"text": datetime.now(timezone.utc).strftime("%H:%M:%S UTC")}
@app.page("/")
def home():
return Page(Column(Heading("Server time"),
Poll(action="/clock", interval=2, initial="Loading clock…")))
if __name__ == "__main__":
app.run()
Poll(action=..., interval=5, initial="Loading…") accepts an interval of 1–86400
seconds. It calls the action immediately with {}, then waits the interval after
each request finishes. Return {"text": "..."}; format numbers as strings.
Responses are displayed as text, so HTML stays escaped.
Requests never overlap for one component. Errors retain the last value and show a status message while retrying; requests time out after 30 seconds. Hidden tabs pause new requests, and removed components stop polling. Each component in each visitor's browser polls separately. Use read-only actions and choose an interval that fits your server load. JavaScript and a reachable Pyrite action server are required; Pyrite supplies the browser code.
pyrite guide --agent --topic polling
pyrite guide app.py --plain --topic polling
Request access and session cookies
Page handlers still take no arguments; action handlers still take one payload
dictionary. Import request to read the current HTTP request inside either:
from pyrite import App, Page, Text, request
app = App()
@app.page("/")
def home():
current = request()
user_agent = current.headers.get("User-Agent", "unknown")
return Page(Text(f"Client: {current.client_ip}; browser: {user_agent}"))
request() exposes method, path (without the query string), case-insensitive
headers, cookies, query (each value is a tuple of strings), and client_ip.
These mappings are read-only. query("name", "default") also works inside actions.
Request context is isolated per request and cleared on completion or failure;
request() raises RuntimeError outside a request. client_ip is the direct
socket peer, which may be your reverse proxy; forwarded IP headers are untrusted.
Cookies and headers are client input, not proof of authentication.
Inside a handler, use request().set_cookie(name, value, max_age=3600) and
request().delete_cookie(name). Cookies default to host-only, path="/",
secure=True, httponly=True, and samesite="Lax". Use secure=False explicitly
for local HTTP. max_age is seconds; omit it for a browser-session cookie.
delete_cookie expires the browser cookie; use the original path and revoke
its server-side session too. Multiple cookies are sent as separate Set-Cookie
headers, and cookie changes are discarded if the handler or rendering fails.
Responses use Cache-Control: no-store.
Session login is now possible: validate credentials in a POST action, generate
an unpredictable opaque session ID, store its account and expiry server-side,
and set that ID as a cookie. Look up the ID and check expiry for every protected
page/action. Rotate IDs on login and revoke them on logout. HttpOnly keeps
JavaScript from reading the cookie; browser requests still send it automatically,
including LiveForm, Chat, and Poll. Use Input(..., kind="password") inside
a LiveForm for credentials; GET forms put their fields in the URL.
The repository includes a runnable single-account example with login, expiry, session rotation, a protected account page, and logout:
PYRITE_DEMO_PASSWORD='choose-a-local-demo-password' python3 session_example.py
Open http://127.0.0.1:8000. The example uses an in-memory store and explicitly allows local HTTP cookies. Sessions disappear on restart. A deployed application needs HTTPS, shared persistent session storage, proper account/password storage, and login rate limits. Pyrite supplies the request and cookie primitives; application code owns authentication and session storage. The existing action Origin/Fetch-Metadata checks remain active.
pyrite guide --agent --topic request
pyrite guide --agent --topic sessions
Metadata
Release files for pyrite-web 0.1.3.post3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyrite_web-0.1.3.post3.tar.gz | 80.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyrite_web-0.1.3.post3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 130.0 kB
Release files / pyrite_web-0.1.3.post3.tar.gz
| Download URL | pyrite_web-0.1.3.post3.tar.gz |
|---|---|
| Size | 80.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bb0d3e061b31a396cf934bc0564f0621c8ea78392a2181fa7ce50936202b0d15
|
|
BLAKE2b-256 checksum How to use checksums |
947f8b553b46d05f9be42b1295ffcd527580720e6886cd68d94c58430d7ec0de
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / pyrite_web-0.1.3.post3-py3-none-any.whl
| Download URL | pyrite_web-0.1.3.post3-py3-none-any.whl |
|---|---|
| Size | 49.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a1b27e4b8fd05d917cb2393d5f79b27e0f875c0cd845c45b78e21cdc0dadcc5b
|
|
BLAKE2b-256 checksum How to use checksums |
f68217c724579b39475280969092c34f7ab5cc928e4dbde469a6108c63a04a8f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|