Metadata-Version: 2.5
Name: gramlot-py-server
Version: 0.2.1
Summary: Python server adapters for Gramlot pages: Uvicorn, Django, Flask, FastAPI and Kajenn
Project-URL: Homepage, https://github.com/gramlot-org/gramlot-py-server
Project-URL: Repository, https://github.com/gramlot-org/gramlot-py-server
Project-URL: Issues, https://github.com/gramlot-org/gramlot-py-server/issues
Project-URL: Documentation, https://gramlot-py-server.readthedocs.io/
Author-email: Genropy Team <info@softwell.it>
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Requires-Python: >=3.11
Requires-Dist: gramlot>=0.2.1
Provides-Extra: django
Requires-Dist: asgiref<4,>=3.8.1; extra == 'django'
Requires-Dist: django<6.2,>=5.2; extra == 'django'
Provides-Extra: docs
Requires-Dist: myst-parser==5.1.0; extra == 'docs'
Requires-Dist: sphinx-rtd-theme==3.1.0; extra == 'docs'
Requires-Dist: sphinx==8.2.3; extra == 'docs'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.115; extra == 'fastapi'
Provides-Extra: flask
Requires-Dist: flask<4,>=3.1; extra == 'flask'
Provides-Extra: kajenn
Requires-Dist: kajenn>=0.1.0; extra == 'kajenn'
Provides-Extra: test
Requires-Dist: genro-tytx[msgpack]; extra == 'test'
Requires-Dist: httpx>=0.27; extra == 'test'
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
Requires-Dist: pytest-cov>=4.0; extra == 'test'
Requires-Dist: pytest>=7.0; extra == 'test'
Provides-Extra: uvicorn
Requires-Dist: uvicorn>=0.30; extra == 'uvicorn'
Description-Content-Type: text/markdown

# gramlot-py-server

[![tests](https://github.com/gramlot-org/gramlot-py-server/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/gramlot-org/gramlot-py-server/actions/workflows/tests.yml)
[![Coverage](https://codecov.io/gh/gramlot-org/gramlot-py-server/branch/main/graph/badge.svg)](https://app.codecov.io/gh/gramlot-org/gramlot-py-server)
[![Documentation](https://readthedocs.org/projects/gramlot-py-server/badge/?version=latest)](https://gramlot-py-server.readthedocs.io/en/latest/)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue)](LICENSE)

Gramlot describes web interfaces in Python or JavaScript and keeps them bound to
application state in the browser. See
[The Gramlot family](https://gramlot.readthedocs.io/en/latest/docs/public/055-family.html)
for the core and the other repositories.

## What this repository is

`gramlot-py-server` serves Gramlot pages written in Python from a Python web
server. One package holds one adapter per framework; each comes with its extra:

| Extra | Module | Names |
| --- | --- | --- |
| `uvicorn` | `gramlot_py_server.uvicorn` | `Application`, `create_application(pages, **options)`: an ASGI application for Uvicorn or any ASGI server |
| `django` | `gramlot_py_server.django` | `Pages`: its `urls` go into a URLconf |
| `flask` | `gramlot_py_server.flask` | `Pages`, `mount_pages(app, pages, **options)` |
| `fastapi` | `gramlot_py_server.fastapi` | `Pages`, `mount_pages(app, pages, **options)`, `Application` |
| `kajenn` | `gramlot_py_server.kajenn` | `Application`: a Kajenn routed application |

Every adapter serves one folder of `Page` modules through the core `FileHost`:
it sends the bootstrap document, answers the main and remote Source requests,
serves the page companions (`.css` and `_aux.js`) and the browser runtime, with
a mount prefix and a Content Security Policy of your choice. It does not serve
JavaScript pages
([gramlot-js-server](https://github.com/gramlot-org/gramlot-js-server) does)
and it does not talk to a database.

## Quick start

Python 3.11 or later. Install the package with the extra of your framework:

```sh
python -m pip install "gramlot-py-server[uvicorn]"
```

Create a folder `pages` and write `pages/hello.py`:

```python
from gramlot import Page as BasePage


class Page(BasePage):
    title = "Hello"

    def main(self, root):
        pane = root.div(datapath="person")
        pane.html_label("Name", for_="name")
        pane.input(id="name", value="^.name", live=True)
        pane.p("^.greeting")
        pane.dataFormula(".greeting", "'Hello, ' + name", name="^.name", _init=True)
        pane.dataSetter(".name", "Ada")
```

The formula of this page is inline code, so every example below sends the
permissive Content Security Policy profile. Each one lives in
[`examples/`](examples/), beside a folder `pages`; the docstring of each file
gives the command that starts it. The page opens at
<http://127.0.0.1:8000/hello>, on port 5000 for the Flask development server.
FastAPI needs a server: install `gramlot-py-server[fastapi,uvicorn]`.

Uvicorn, `examples/uvicorn/app.py`:

```python
"""Serve the example pages with Uvicorn: ``uvicorn app:application``."""
from pathlib import Path

from gramlot_py_server.uvicorn import create_application

PAGES = Path(__file__).resolve().parents[1] / "pages"

application = create_application(
    PAGES,
    content_security_policy="script-src 'nonce-{nonce}' 'unsafe-eval'; object-src 'none'; base-uri 'none'",
)
```

Django, `examples/django/settings.py` and `examples/django/urls.py`:

```python
"""Django settings of the example: ``django-admin runserver --settings=settings --pythonpath=.``."""

SECRET_KEY = "example-only-change-me"
DEBUG = True
ALLOWED_HOSTS = ["127.0.0.1", "localhost", "testserver"]
ROOT_URLCONF = "urls"
INSTALLED_APPS: list[str] = []
MIDDLEWARE = ["django.middleware.csrf.CsrfViewMiddleware"]
```

```python
"""URLconf of the example: the Gramlot pages at the site root."""
from pathlib import Path

from django.urls import include, path

from gramlot_py_server.django import Pages

PAGES = Path(__file__).resolve().parents[1] / "pages"

pages = Pages(
    PAGES,
    content_security_policy="script-src 'nonce-{nonce}' 'unsafe-eval'; object-src 'none'; base-uri 'none'",
)

urlpatterns = [path("", include(pages.urls))]
```

Flask, `examples/flask/app.py`:

```python
"""Serve the example pages inside a Flask app: ``flask --app app run``."""
from pathlib import Path

from flask import Flask

from gramlot_py_server.flask import mount_pages

PAGES = Path(__file__).resolve().parents[1] / "pages"

app = Flask(__name__)
mount_pages(
    app,
    PAGES,
    content_security_policy="script-src 'nonce-{nonce}' 'unsafe-eval'; object-src 'none'; base-uri 'none'",
)
```

FastAPI, `examples/fastapi/app.py`:

```python
"""Serve the example pages inside a FastAPI app: ``fastapi dev app.py`` or ``uvicorn app:app``."""
from pathlib import Path

from fastapi import FastAPI

from gramlot_py_server.fastapi import mount_pages

PAGES = Path(__file__).resolve().parents[1] / "pages"

app = FastAPI()
mount_pages(
    app,
    PAGES,
    content_security_policy="script-src 'nonce-{nonce}' 'unsafe-eval'; object-src 'none'; base-uri 'none'",
)
```

Kajenn, `examples/kajenn/config.py` (the page opens at
<http://127.0.0.1:8000/pages/hello>):

```python
"""Kajenn site of the example: ``kajenn serve config.py --port 8000``, pages under ``/pages``."""
from pathlib import Path

from kajenn.config.templates import DefaultConfiguration

from gramlot_py_server.kajenn import Application

PAGES = Path(__file__).resolve().parents[1] / "pages"


class Site(DefaultConfiguration):
    def applications_section(self, cfg):
        cfg.applications().application(
            code="pages",
            mount="pages",
            app_class=Application,
            pages=PAGES,
            content_security_policy=(
                "script-src 'nonce-{nonce}' 'unsafe-eval'; object-src 'none'; base-uri 'none'"
            ),
        ).request(body="raw")
```

The page shows a field with `Ada` and the text `Hello, Ada`. Typing `Grace` in
the field changes the text to `Hello, Grace` at every keystroke. The tests
`tests/<framework>/test_<framework>_examples.py` serve these examples in CI, and
`tests/test_readme.py` checks that this README shows the same files.

## Next steps

- The guides on Read the Docs, sources in [docs/](docs/) and the concise view
  in [docs_llm/](docs_llm/):
  - common: [Introduction](https://gramlot-py-server.readthedocs.io/en/latest/005-introduction.html),
    [Writing pages for these hosts](https://gramlot-py-server.readthedocs.io/en/latest/010-writing-pages.html),
    [Troubleshooting](https://gramlot-py-server.readthedocs.io/en/latest/015-troubleshooting.html);
  - Uvicorn: [Tutorial](https://gramlot-py-server.readthedocs.io/en/latest/105-tutorial.html),
    [Configuration](https://gramlot-py-server.readthedocs.io/en/latest/110-configuration.html),
    [Deployment](https://gramlot-py-server.readthedocs.io/en/latest/115-deployment.html),
    [Reference](https://gramlot-py-server.readthedocs.io/en/latest/120-reference.html);
  - [Django](https://gramlot-py-server.readthedocs.io/en/latest/205-django.html), [Flask](https://gramlot-py-server.readthedocs.io/en/latest/305-flask.html),
    [FastAPI](https://gramlot-py-server.readthedocs.io/en/latest/405-fastapi.html), [Kajenn](https://gramlot-py-server.readthedocs.io/en/latest/505-kajenn.html).
- The core: [The Gramlot family](https://gramlot.readthedocs.io/en/latest/docs/public/055-family.html),
  [Classes, repository and server adapters](https://gramlot.readthedocs.io/en/latest/docs/public/090-classes-and-hosts.html)
  (the shared adapter contract: mount prefix, companions, CSP profiles),
  [Writing pages](https://gramlot.readthedocs.io/en/latest/docs/public/095-writing-pages.html).

## Compatibility

| | Verified |
| --- | --- |
| Gramlot core | 0.2.1 (PyPI `gramlot`) |
| Python | 3.11 and 3.12 in CI; 3.12.12 locally |
| Frameworks | Uvicorn 0.54.0, Django 6.1.1 with asgiref 3.12.1, Flask 3.1.3, FastAPI 0.142.2, Kajenn 0.1.0 |
| Browsers | not run for this package; the core runtime it serves, unchanged since 0.2.0, passed the core 0.2.0 qualification on Chromium 153, WebKit 26.6 and Firefox 155 |

## Tests and contributing

```sh
python -m pip install -e ".[uvicorn,django,flask,fastapi,kajenn,test]"
python -m pytest -q                      # every adapter
python -m pytest -q tests/flask          # one adapter, with its extra only
python scripts/check_docs.py             # paired guides and Sphinx build
```

CI runs each adapter with its own extra against the released core and, as an
informational job, every adapter against the core's `main` checkout, and builds
the documentation; coverage goes to Codecov with one flag per framework. See
[CONTRIBUTING.md](CONTRIBUTING.md) and [AGENTS.md](AGENTS.md); the internal
notes are in [docs/internal/](docs/internal/).

Apache License 2.0. Copyright 2026 Softwell S.r.l. See [LICENSE](LICENSE) and
[NOTICE](NOTICE).
