Metadata-Version: 2.4
Name: pyrite-web
Version: 0.1.0
Summary: Code websites in Python 3, without writing JavaScript or CSS.
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# Pyrite

Code websites in Python 3, for JavaScript and CSS haters.

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+.

## Run the example

```sh
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

```python
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` and `Chat`. It does not include server sessions,
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:

```python
from pyrite import Theme

theme = Theme(background="#111820", text="#eef3f8", heading_font="sans")
page = Page(Heading("Your style."), theme=theme)
```

## Forms without JavaScript

```python
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

```python
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

```python
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

```python
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

```python
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.

## Tests

```sh
python3 -m unittest discover -s tests -v
```
