Metadata-Version: 2.5
Name: oxid
Version: 0.20.0
Summary: Fine-grained reactive UI framework for Python in the browser, on its own Python runtime compiled to WebAssembly
Project-URL: Homepage, https://academy.optersoft.com/python/frontage
Project-URL: Documentation, https://academy.optersoft.com/python/frontage
Project-URL: Website, https://frontage.optersoft.com
Author-email: "Optersoft, S.L." <david@optersoft.com>
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: browser,framework,frontend,reactive,spa,wasm,webassembly
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Environment :: WebAssembly
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Education
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: >=3.14
Provides-Extra: chat
Provides-Extra: content
Requires-Dist: markdown-it-py>=3; extra == 'content'
Requires-Dist: mdit-py-plugins>=0.4; extra == 'content'
Requires-Dist: pyyaml>=6; extra == 'content'
Provides-Extra: polars
Requires-Dist: polars>=1.0; extra == 'polars'
Description-Content-Type: text/markdown

# Oxid

[documentation](https://academy.optersoft.com/python/frontage) · [PyPI](https://pypi.org/project/oxid/)

**A fine-grained reactive UI framework for Python in the browser.** Signals, memos and
effects; templates that clone once and bind only their holes; a keyed `For`; a nested
router; running on its own Python runtime compiled to WebAssembly, with the framework delivered as
precompiled bytecode. No JavaScript, no Node, no bundler: you write Python and the browser
runs it.

> **Status: 0.17, alpha.** The API is young and will move. What is in it, by release: the
> reactive core, templates (`h` and `html(t"…")`), control flow, a nested router and widgets
> (0.2–0.3); **prerendering with hydration** (0.4); transitions and async memos (0.5–0.7);
> `oxid serve` (0.8); **oxid's own Python runtime, written in Rust** (0.10), with the
> reactive graph, the DOM operations and the template path native inside it; **islands and
> static pages** (0.11); **content collections** (0.12); **`oxid site`** and locales (0.13);
> **`oxid-server`**, the HTTP server on the same runtime (0.14–0.17: routing and policies
> matched in Rust, sessions, uploads, auth, WebSockets, OpenAPI, and the page rendered per
> request with islands); declarative **charts** on SVG or a canvas (0.15). Since 0.16 the
> wheel for your machine carries the compiler and the server binary, so `pip install oxid`
> is the whole install. The browser suite runs every example in Chromium before a release.

```python
from oxid import Signal, component, html, mount


@component
def counter(initial=0):
    count = Signal(initial)

    def inc(ev):
        count.update(lambda n: n + 1)

    def dec(ev):
        count.update(lambda n: n - 1)

    return html(t"""
        <div class="counter">
            <button on:click={dec}>-</button>
            <span>Value: {count}</span>
            <button on:click={inc}>+</button>
        </div>
    """)


mount(lambda: counter(initial=0), "#app")
```

Signals hold state; anything callable in a template is a hole that updates in place when what
it read changes; a component body runs once. Name the functions you put in holes: a `lambda`
inside a template's braces is hard to read and `oxid check` will tell you so.

**Try it** at [frontage.optersoft.com/playground](https://frontage.optersoft.com/playground/),
which runs your code in the browser and keeps it in the link. **Learn it** at
[academy.optersoft.com/python/frontage](https://academy.optersoft.com/python/frontage), twenty-five
chapters, thirteen of them with an app published on GitLab Pages. **Install it** with pip,
and `oxid build` writes a directory that runs anywhere:

```sh
uvx oxid build myapp        # index.html, your .py, and _oxid/ beside them
```

A model writing Oxid code for you can read
[frontage.optersoft.com/llms.txt](https://frontage.optersoft.com/llms.txt): what the framework is,
the rules it most often gets wrong (the runtime is a subset of Python, templates are t-strings,
holes are named functions), and every chapter of the course.

| The counter, cold cache | 0.10.0 | 0.9.1 (MicroPython) | 0.8.3 (PyScript) |
|---|---|---|---|
| requests | 17 | 6 | 29 |
| transferred | 0.78 MB | 0.46 MB | 0.91 MB |
| compressed | 0.32 MB | 0.19 MB | 0.33 MB |
| to first paint | 23 ms | 59 ms | 88 ms |

Most of that is the runtime, 261 KB of gzip; the rest is one file per module of bytecode,
each named by its content so a host can cache it forever, and none of it is parsed in the
browser. It is a bigger download than MicroPython's and a much faster page: the runtime
starts in a few milliseconds and builds a thousand rows in 24.8 ms against 71.1
(`project/plan/2026-09-08-runtime.md` §9). Medians of five, this laptop's Chromium, measured
at 0.10.0; `project/design/architecture.md` §12 has the method.

The same package is a small command line on your machine, stdlib only:

```sh
uvx oxid serve              # a dev server that swaps a changed module into the live page
uvx oxid check app.py       # the rules the browser enforces and CPython does not
uvx oxid tailwind           # Tailwind CSS: the standalone CLI, fetched once, no Node
uvx oxid prerender . --out build --crawl   # every route the pages link to, as finished HTML
```

(`uvx` runs the `oxid` script straight from PyPI; `pip install oxid` puts the same
`oxid` command on PATH, and `python -m oxid` is the same thing.)

`prerender` runs the app on your machine, waits for its resources and async memos, and writes each route as
finished HTML with the values embedded. In the browser `mount` hydrates: it adopts the HTML
already on screen instead of building it, skips the fetches the page already holds, and
replays the clicks made before Python was ready. Static hosting only, no server: Leptos's
async rendering mode as a build step.

A page with nothing to run should download nothing to run it. `mount(view, "#app",
when="never")` says so: `prerender` writes the HTML and no boot tag, so a content page is its
own bytes and stops there. What is interactive on it is an **island** —

```python
from oxid import h, island, mount


def page():
    return h.main(
        h.article(...),  # static: rendered once, at build
        island(theme_toggle, when="idle"),  # alive when the browser is free
        island("charts:sparkline", when="visible"),  # its own chunk, fetched when seen
    )


mount(page, "#app", when="never")
```

— and the first trigger to fire boots the runtime once, shared by every island on the page.
`examples/islands` is the whole of it in forty lines.

What such a page says comes from a **collection**: a directory of Markdown whose front matter
is checked by the same `oxid.schema` record that checks a form, rendered once, on your
machine, and shipped as HTML.

```python
from oxid.content import collection
from oxid.schema import iso_date, record, text

Post = record(("title", text(min=1)), ("date", iso_date()), ("summary", text(), None))
posts = collection("posts", Post)  # content/posts/*.md, newest first

for post in posts.entries():
    post.slug, post.data["title"], post.view()
```

A file whose front matter does not match fails the build, naming the file and the field, and
a `::: island widgets:reactions when="visible"` container in a post is an island where it
stands. `pip install "oxid[content]"`; `examples/blog` is a blog in one page.

A whole site is a directory, and the directory is the site map:

```
site/
  pages/index.py          → /
  pages/about.py          → /about/
  pages/blog/index.py     → /blog/
  pages/blog/[slug].py    → /blog/<slug>/, one per static_paths()
  pages/sitemap.xml.py    → /sitemap.xml, a module with a get()
  layouts/site.py         a component taking children; no new concept
  content/posts/*.md      the collection above
  public/                 copied as it is
```

`oxid site` renders every page on your machine and writes it where its path says. A page
with nothing interactive on it carries **no script at all**; the runtime is written once,
beside the pages, only if some page has an island. `oxid serve --prerender` is the same
build with a file watcher in front of it. `examples/site` is seven pages, six of which fetch
nothing.

A parameter can be a directory, so `pages/[lang]/blog/[slug].py` is a site in as many
languages as `LOCALES` in `site.py` names — the first of them at `/`, the rest under `/es/`,
`/ca/`. `oxid.i18n` writes the `hreflang` alternates and the language switcher, and the
switcher is **plain links**: the build made every page, so `site.translate(path, "es")` is an
answer rather than a guess, and a reader changing language waits for a document instead of a
runtime. `examples/locales` is twelve pages in three languages that fetch nothing at all.

A page that needs a server has one in the same package. `oxid-server` is Rust (axum)
around the same runtime, and a route is a Python function:

```python
from oxid_server import App, Policy, SessionAuth

app = App(static="www")


@app.get("/notes/{note_id}", policy=Policy(auth=SessionAuth("user"), limit="30/min"))
async def note(note_id: int, user):
    return {"id": note_id, "by": user}
```

```sh
oxid-server app.py          # routes, /openapi.json and /docs, the files in www/
oxid new server notes       # an app, its two kinds of test, and a Dockerfile
```

Matching, limits, body sizes, sign-in and CSRF are checked in Rust before Python runs; the
same `oxid.schema` record that checks a form in the page validates its post; `page(view)`
renders a view per request, with an island where something is interactive. `--runtime
cpython` runs the same file with the handlers in CPython, for an app that needs pandas.

Tailwind with no build at all: the playground loads Tailwind's browser build, so utility
classes work as you type. The [Style](https://academy.optersoft.com/python/frontage/style),
[Ship](https://academy.optersoft.com/python/frontage/ship) and
[Prerender](https://academy.optersoft.com/python/frontage/prerender) chapters cover all of it.

## Why

Python in the browser exists — CPython and MicroPython both compile to WebAssembly — and the
frameworks that use it either carry a server into the browser (Streamlit's stlite, Shiny's
Shinylive: tens of seconds to start) or rebuild and diff the page on every change. Oxid
is browser-first: no session, no transport, no DSL, and a state change touches only the DOM
nodes that read it. Its runtime is its own, because the framework's hot paths are inside it:
`project/plan/2026-09-08-runtime.md` in the source distribution is why, with the measurements
that decided it. The whole reasoning, with the frameworks it learned from, is in
`project/design/architecture.md`; the behaviours it must have, one line each, are in
`project/design/spec.md`.

## Develop

The repo uses [uv](https://docs.astral.sh/uv/) and [mkrun](https://github.com/optersoft/make) (`mk`).

```sh
mk sync                 # .venv with every dependency group
mk check                # lint, types, unit tests: the gate
mk runtime.build        # build the runtime from rust/ into oxid/_runtime/ (cargo, wasm-opt)
mk serve                # examples and playground at http://127.0.0.1:8000/, package read live, reload on save
mk test --browser       # every example in Chromium, on the runtime in WebAssembly
mk build examples/todo  # a static directory that boots from WebAssembly
cargo test --profile native   # in rust/: the runtime's own tests, against CPython
mk site.deploy          # publish frontage.optersoft.com by hand (Cloudflare Pages): landing page, gallery, playground, wheels
```

Without `mk`: `uv sync --all-groups`, `uv run pytest`, `uv run ruff check`, `uv run ty check`.

## Contributing

Contributions are accepted under the Apache License 2.0, the same terms the project is
published under: sending one means you agree it may be distributed under that license,
including its patent grant (section 3). There is no separate contributor agreement. The
rewrite is clean-room: code is written from `project/design/spec.md`, not from any reference
framework's source, and a contribution says so.

## License

Apache License 2.0, copyright Optersoft, S.L. See [LICENSE](LICENSE) and [NOTICE](NOTICE).

**Oxid** and the Oxid logo are trademarks of Optersoft, S.L. The license grants no
rights to the name or the logo (Apache License section 6). You may say that your work uses or
is built with Oxid; a fork or a derivative must ship under another name.

Oxid's reactive model follows [Solid](https://www.solidjs.com/) and
[Leptos](https://leptos.dev/).
