Skip to main content

Pyrite

Release: 0.1.4.

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. Use app.run(reload=True) for automatic restart and browser refresh. Otherwise, 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 support SessionStore for bounded in-memory sessions. Action buttons submit POST requests; structured polling updates component children. Optional hot reload restarts the worker and refreshes browser pages. Token streaming and a production server are not included. 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

  1. Register or sign in at https://pypi.org and enable two-factor authentication.
  2. Open https://pypi.org/manage/account/token/ and create an API token.
  3. 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.
  4. 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.5:

.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.5.tar.gz dist/pyrite_web-0.1.5-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.5.tar.gz dist/pyrite_web-0.1.5-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.5/ 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.5
/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; SessionStore supplies bounded in-memory session storage. The existing action Origin/Fetch-Metadata checks remain active.

pyrite guide --agent --topic request
pyrite guide --agent --topic sessions

Live action response contracts

LiveForm, action Button, and default text-mode Poll actions must return {"text": "..."}. Chat actions must return {"reply": "..."}. Values must be strings; convert numbers with str(). Free-form JSON dictionaries are for custom HTTP clients that implement their own response schema. Structured polling uses Poll(mode="children") and poll_response(...) for safe component JSON, including list and table elements. Arbitrary response keys still require a custom client. A response error does not undo an action's server-side changes; avoid blindly retrying mutations.

Run pyrite doctor app.py to flag statically recognizable response mismatches for actions used by these components. Dynamic responses still need runtime tests. Use pyrite guide --agent --topic forms for a focused reference; the output includes the complete topic index, including request, sessions, polling, hover, and logs. Enable runtime logging with App(log_path="pyrite-errors.jsonl"); browse with pyrite guide --log pyrite-errors.jsonl.

For POST logout, use LiveForm(Submit("Sign out"), action="/logout") and revoke the session inside the action. Button navigates with GET. See session_example.py for expiry, rotation, and revocation.

For a per-visitor counter with separate POST increments and read-only polling, expiry, locking, and cache-control guidance, run pyrite guide --agent --topic polling.

Action buttons, sessions, and structured polling

Button("Sign out", action="/logout") sends a same-origin JSON POST with {}, using the same status and error handling as LiveForm. Its action must return {"text": "..."}. Supply exactly one of href or action; navigation buttons keep their existing behavior. No automatic retry occurs for action buttons.

Create one store outside request handlers:

from pyrite import SessionStore

sessions = SessionStore(ttl=3600)  # HTTPS; secure=False only for local HTTP

After validating credentials in a POST action, call sessions.create({"account_id": "alice"}). This rotates the current session ID. Protected handlers use sessions.get(), which returns an isolated dictionary or None. sessions.rotate() preserves data, renews expiry, and replaces the ID; it returns False for an expired or missing session. sessions.destroy() revokes the record and expires its cookie. All mutations require POST. Reads never extend expiry; modifying a returned dictionary does not save it. Use create(updated) to save changes and rotate the ID.

The thread-safe store holds JSON data server-side, behind random opaque cookie IDs. Defaults: one-hour expiry, at most 10,000 sessions, host-only HttpOnly SameSite=Lax cookies with path=/ and Secure. The default cookie name is __Host-pyrite_session, or pyrite_session with secure=False. Configure ttl, max_sessions, and cookie_name when needed. Writes remove expired records; sessions.prune() also performs cleanup. At capacity, new sessions raise RuntimeError; replacing the current session remains possible. Sessions disappear on restart and are not shared across workers. Use shared persistent storage for multi-process deployment. Store mutations are not rolled back if a later handler step fails; finish validation first and return the documented response shape. session_example.py demonstrates the complete login/logout flow.

For live lists and leaderboards, return safe component JSON:

from pyrite import App, Card, Column, Heading, Page, Poll, Text, poll_response

app = App()

@app.action("/leaders")
def leaders(data):
    rows = [("Ada", 42), ("Lin", 37)]
    return poll_response(Column(*(
        Card(Heading(name, level=2), Text(score)) for name, score in rows
    )))

@app.page("/")
def home():
    return Page(Poll(action="/leaders", mode="children", interval=10,
                     initial=Text("Loading leaderboard…")))

if __name__ == "__main__":
    app.run(reload=True)

poll_response supports Column, Row, Card, Heading, and Text. Plain list/table elements from pyrite.core.Element are also accepted. Links, forms, scripts, event attributes, arbitrary classes, and inline styles are rejected. The browser constructs elements and text nodes without innerHTML; invalid responses preserve the last display. Limits: 2,000 nodes and 32 nesting levels. Default Poll(mode="text") still expects a string text field.

Development hot reload

Put app.run(reload=True) under the usual if __name__ == "__main__": guard, then run your script or module. Python edits, additions, and deletions under the working directory and entry script directory restart the worker and refresh open browser pages. Hidden directories, virtual environments, and generated build folders are ignored. Syntax/import failures leave the watcher running; save a fix to retry. Ctrl+C stops both processes and releases the port. The reloader never kills an unrelated process using the port; stop that process or choose another port. /__pyrite_reload__ is reserved while reload is enabled.

Reload re-executes startup code and clears in-memory state, including sessions. Only Python files are watched. Ordinary app.run() keeps its existing manual restart behavior. Both modes are for local development.

pyrite guide --agent --topic sessions
pyrite guide --agent --topic polling
pyrite guide --agent --topic reload

Component placement and page headers

Use .layout(...) to position and size any component. It returns a new component; repeated calls merge settings, and .hover(...)/.themed(...) preserve them.

from pyrite import Card, Column, Header, Heading, Link, Nav, Page, Text

header = Header(Nav(Link("Brand", "/"), Link("About", "/about"))).layout(
    position="sticky", top=0, z_index=10,
)
badge = Card(Text("Top right")).layout(
    position="absolute", top=16, right=16, width=180,
)
content = Column(Heading("Placed with Python"), badge).layout(
    position="relative", min_height=360, padding=24,
)
page = Page(content, header=header)

Header renders <header>. The Page(header=...) slot places it before and outside <main>, independent of the centered main width. Nav retains its semantic navigation element and accessible label.

Positions: static, relative, absolute, fixed, sticky. Absolute children use the nearest positioned ancestor; give that container position="relative". Fixed content uses the viewport. Sticky content uses its offset, such as top=0. Absolute/fixed content reserves no space; account for overlap and small screens. Visual placement does not change reading or keyboard order.

Numbers are pixels. Strings accept numeric px, %, rem, em, vw, vh, svh, dvh, or ch units. Offsets and margins may be negative; offsets, width, height, and margins also accept "auto". Other lengths are nonnegative. Lengths have a maximum absolute magnitude of 100,000. padding and margin accept a scalar or tuple of one to four lengths in CSS order.

Available settings:

  • Placement: position, top, right, bottom, left, z_index.
  • Sizing: width, height, min_width, max_width, min_height, max_height.
  • Spacing: gap, padding, margin.
  • Flex/grid: display, direction, align, justify, wrap, columns, order.
  • Clipping/scrolling: overflow.

For a custom row, use Row(...).layout(gap=12, justify="space-between", align="start", wrap=False). For equal grid columns, use Column(...).layout(display="grid", columns=3, gap=16). columns accepts 1–24; z_index and order accept integers from -100,000 to 100,000. Omitted/None values leave previous settings unchanged. Choose grid columns for the target screen; this API does not add automatic breakpoints. Layout values are validated and component text remains escaped. Doctor checks known component layout calls and literal values without executing the app.

pyrite guide --agent --topic layout

Clickable hybrid bouncing ball

Run python hybrid_example.py, then open http://127.0.0.1:8000. The ball animates in the browser; clicking it calls a Python action that increments a shared, in-memory server counter. The counter resets on restart.

Simulation from pyrite.hybrid accepts on_click="/api/ball-click". Clicks inside a circle drawn by ctx.circle POST {} to that same-origin @app.action endpoint. Return {"clicks": count} with a nonnegative safe integer; the runtime updates state.clicks and announces the total below the canvas. Initialize clicks in initial if your drawing reads it. Misses send no request; failed requests show a retry message. Animation continues while the server responds.

Metadata

Release files for pyrite-web 0.1.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyrite-web 0.1.5
File Size Uploaded
pyrite_web-0.1.5.tar.gz 126.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyrite-web 0.1.5
File Interpreter ABI Platform
pyrite_web-0.1.5-py3-none-any.whl Python 3 none any Details

Total release size: 202.7 kB

Release files / pyrite_web-0.1.5.tar.gz

Download URL pyrite_web-0.1.5.tar.gz
Size 126.9 kB
Tags Source
SHA-256 checksum
How to use checksums
d5e2775033175904274da3042cf732ef0772bb2e1deb2e5313d31603fb585713
BLAKE2b-256 checksum
How to use checksums
ffa0a13f8d6c96e421c0674a890963f9d4a57b06ad2cf2af7c8ba19099e5b0f0
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.5-py3-none-any.whl

Download URL pyrite_web-0.1.5-py3-none-any.whl
Size 75.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2778e095544546ef638e6e9e7ca95629fb1a3f85028ec4ff8c7c6ce7edb436a4
BLAKE2b-256 checksum
How to use checksums
0de2b4d2f6a54aa183d0c37205385180fb8bc391b47d0964b5758764bb311c5f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.8

Release history Release notifications | RSS feed

This release

0.1.5 This release

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page