Metadata-Version: 2.4
Name: airium
Version: 0.3.2
Summary: Easy and quick html builder with natural syntax correspondence (python->html). No templates needed. Serves pure pythonic library with no dependencies.
Home-page: https://gitlab.com/kamichal/airium
Author: Michał Kaczmarczyk
Author-email: michal.s.kaczmarczyk@gmail.com
Maintainer: Michał Kaczmarczyk
Maintainer-email: michal.s.kaczmarczyk@gmail.com
License: MIT
Keywords: natural html generator compiler template-less
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: System Administrators
Classifier: Intended Audience :: Telecommunications Industry
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python
Classifier: Topic :: Database :: Front-Ends
Classifier: Topic :: Documentation
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: Text Processing :: Markup :: HTML
Classifier: Topic :: Utilities
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: black~=24.8; extra == "dev"
Requires-Dist: check-manifest; extra == "dev"
Requires-Dist: mypy~=1.10; extra == "dev"
Requires-Dist: pytest-cov~=5.0; extra == "dev"
Requires-Dist: pytest-mock~=3.6; extra == "dev"
Requires-Dist: pytest~=8.3; extra == "dev"
Requires-Dist: ruff~=0.6; extra == "dev"
Requires-Dist: types-beautifulsoup4~=4.12; extra == "dev"
Requires-Dist: types-requests~=2.32; extra == "dev"
Provides-Extra: parse
Requires-Dist: requests<3,>=2.12.0; extra == "parse"
Requires-Dist: beautifulsoup4<5.0,>=4.10.0; extra == "parse"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: maintainer
Dynamic: maintainer-email
Dynamic: provides-extra
Dynamic: requires-python
Dynamic: summary

## Airium

Airium is the most natural, pure-Python HTML generator with no runtime
dependencies. It lets you describe an HTML document with ordinary Python
code, without introducing a template language or a separate template file.
The result is an HTML string or UTF-8 encoded bytes that can be returned from
a web endpoint, written to a file, or passed to another system.

Airium is designed around these principles:

- **Natural Python syntax:** use `with` blocks for nesting, function arguments
  for attributes, and normal Python control flow for conditions and loops.
- **A 1:1 representation of HTML structure:** Python indentation and context
  managers mirror the resulting DOM tree, so the code is easy to read and
  refactor.
- **No templates required:** keep the view logic and its markup together in
  one Python module instead of switching between Python and a template
  language.
- **Precise IDE assistance:** the package includes generated typing stubs for
  standard HTML elements and their attributes, while retaining a dynamic
  escape hatch for custom HTML.
- **Bidirectional translation:** convert existing HTML back into starter
  Python code with the reverse translator.
- **Readable output by default:** generate pretty-printed HTML, or choose
  minified output when the document size matters.

Airium intentionally focuses on generating HTML, not validating it. It does
not check whether a tag is valid HTML, whether an attribute belongs to a tag,
or whether the final tree satisfies browser standards. This keeps the API
small and flexible, and means that mistakes are passed through to the output
instead of being silently rewritten. Validate the generated document
separately when validation is important.

[![PyPI version](https://img.shields.io/pypi/v/airium.svg)](https://pypi.python.org/pypi/airium/)
[![pipeline status](https://gitlab.com/kamichal/airium/badges/master/pipeline.svg)](https://gitlab.com/kamichal/airium/-/commits/master)
[![coverage report](https://gitlab.com/kamichal/airium/badges/master/coverage.svg)](https://gitlab.com/kamichal/airium/-/commits/master)
[![PyPI pyversion](https://img.shields.io/pypi/pyversions/AIRIUM.svg)](https://pypi.org/project/airium/)
[![PyPI license](https://img.shields.io/pypi/l/AIRIUM.svg)](https://pypi.python.org/pypi/airium/)
[![PyPI status](https://img.shields.io/pypi/status/AIRIUM.svg)](https://pypi.python.org/pypi/airium/)

Key features:

- simple and straightforward
- template-less: write HTML directly in Python
- DOM structure represented by Python indentation and context managers
- reverse translator from HTML to Python
- pretty (default) or minified HTML output

## Type checking

Airium ships a `py.typed` marker and public HTML typing helpers. Projects using
Airium can run mypy normally after installing the package:

```bash
python -m pip install mypy airium
python -m mypy your_project
```

`TagName` describes standard HTML element names and `HTMLAttributes` provides
common attribute-name completion while still allowing Airium's dynamic
`get_tag_` escape hatch for custom elements and attributes.

Standard tags with tag-specific stubs provide keyword completion in supported
IDEs. For example, `a.img(` exposes image attributes such as `src`, `alt`,
`width`, `height`, `loading`, and `srcset`. The `img` element is an HTML void
element, so it must be called directly rather than used as `with a.img():`.

The element and attribute signatures are generated by
`tools/generate_html_stubs.py`. Run it after updating the HTML vocabulary:

```bash
python tools/generate_html_stubs.py
```

The generated signatures include global HTML attributes and the standard
element-specific attributes for every supported HTML element. `aria-*`,
`data-*`, custom attributes, and custom elements remain available through
Airium's dynamic keyword and `get_tag_` escape hatches.

## How it works

An `Airium` instance is both a document builder and a callable text writer.
Accessing an attribute on it, such as `a.div` or `a.custom_element`, invokes
`Airium.__getattr__` and creates a tag factory dynamically. There is no fixed
runtime list of allowed element names: any attribute name can be used as a
tag name. The generated factory accepts positional attribute fragments and
keyword attributes.

Use a context manager when an element has children:

```python
from airium import Airium

a = Airium()
with a.section(id="intro", **{"data-page": "home"}):
    a.h1(_t="Welcome")
    a("Text can also be written directly.")
```

This produces:

```html
<section id="intro" data-page="home">
  <h1>Welcome</h1>
  Text can also be written directly.
</section>
```

Use `_t` for text content and `klass` (or `class_`) for the HTML `class`
attribute, because `class` is a Python keyword:

```python
a.input(type="email", required=True)
a.div(klass="card", _t="A card")
```

Void elements such as `img`, `input`, `br`, and `meta` are called directly
and must not be used as context managers. Python expressions work naturally
around the markup, so loops and conditions can generate repeated or optional
elements.

The dynamic API also means Airium can emit custom elements and attributes:

```python
with a.my_widget(**{"data-state": "ready", "aria-label": "Status"}):
    a.span(_t="Ready")
```
would produce:

```html
<my_widget data-state="ready" aria-label="Status">
  <span>Ready</span>
</my_widget>
```

For standard elements, the bundled `.pyi` stubs add exact tag and attribute
completion in supported IDEs and help static type checkers catch spelling and
value errors. Dynamic access remains available when the HTML vocabulary is
custom or ahead of the generated stubs.

# Generating `HTML` code in python using `airium`

#### Basic `HTML` page (hello world)

```python
from airium import Airium

a = Airium()

a('<!DOCTYPE html>')
with a.html(lang="pl"):
    with a.head():
        a.meta(charset="utf-8")
        a.title(_t="Airium example")

    with a.body():
        with a.h3(id="id23409231", klass='main_header'):
            a("Hello World.")

html = str(a)  # casting to string extracts the value
# or directly to UTF-8 encoded bytes:
html_bytes = bytes(a)  # casting to bytes is a shortcut to str(a).encode('utf-8')

print(html)
```

Prints such a string:

```html
<!DOCTYPE html>
<html lang="pl">
  <head>
    <meta charset="utf-8" />
    <title>Airium example</title>
  </head>
  <body>
    <h3 id="id23409231" class="main_header">
      Hello World.
    </h3>
  </body>
</html>
```

In order to store it as a file, just:

```python
with open('that/file/path.html', 'wb') as f:
    f.write(bytes(html))
```

#### Simple image in a div

```python
from airium import Airium

a = Airium()

with a.div():
    a.img(src='source.png', alt='alt text')
    a('the text')

html_str = str(a)
print(html_str)
```

```html

<div>
    <img src="source.png" alt="alt text"/>
    the text
</div>
```

#### Table

```python
from airium import Airium

a = Airium()

with a.table(id='table_372'):
    with a.tr(klass='header_row'):
        a.th(_t='no.')
        a.th(_t='Firstname')
        a.th(_t='Lastname')

    with a.tr():
        a.td(_t='1.')
        a.td(id='jbl', _t='Jill')
        a.td(_t='Smith')  # can use _t or text

    with a.tr():
        a.td(_t='2.')
        a.td(_t='Roland', id='rmd')
        a.td(_t='Mendel')

table_str = str(a)
print(table_str)

# To store it to a file:
with open('/tmp/airium_www.example.com.py') as f:
    f.write(table_str)
```

Now `table_str` contains such a string:

```html

<table id="table_372">
  <tr class="header_row">
    <th>no.</th>
    <th>Firstname</th>
    <th>Lastname</th>
  </tr>
  <tr>
    <td>1.</td>
    <td id="jbl">Jill</td>
    <td>Smith</td>
  </tr>
  <tr>
    <td>2.</td>
    <td id="rmd">Roland</td>
    <td>Mendel</td>
  </tr>
</table>
```

### Chaining shortcut for elements with only one child

_New in version 0.2.2_

Having a structure with large number of `with` statements:

```python
from airium import Airium

a = Airium()

with a.article():
    with a.table():
        with a.thead():
            with a.tr():
                a.th(_t='Column 1')
                a.th(_t='Column 2')
        with a.tbody():
            with a.tr():
                with a.td():
                    a.strong(_t='Value 1')
                a.td(_t='Value 2')

table_str = str(a)
print(table_str)
```

You may use a shortcut that is equivalent to:

```python
from airium import Airium

a = Airium()

with a.article().table():
    with a.thead().tr():
        a.th(_t="Column 1")
        a.th(_t="Column 2")
    with a.tbody().tr():
        a.td().strong(_t="Value 1")
        a.td(_t="Value 2")

table_str = str(a)
print(table_str)
```

```html

<article>
  <table>
    <thead>
      <tr>
        <th>Column 1</th>
        <th>Column 2</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>
          <strong>Value 1</strong>
        </td>
        <td>Value 2</td>
      </tr>
    </tbody>
  </table>
</article>
```

# Options

### Pretty or Minify

By default, Airium builds `HTML` code indented with spaces and with line
breaks using line feed (`\n`) characters. These defaults can be changed while
creating an `Airium` instance:

```python
a = Airium(
    base_indent='  ',  # str
    current_level=0,  # int
    source_minify=False,  # bool
    source_line_break_character="\n",  # str
)
```

#### minify

That's a mode when size of the code is minimized, i.e. contains as less whitespaces as it's possible.
The option can be enabled with `source_minify` argument, i.e.:

```python
a = Airium(source_minify=True)
```

In case if you need to explicitly add a line break in the source code (not the `<br/>`):

```python
a = Airium(source_minify=True)
a.h1(_t="Here's your table")
with a.table():
    with a.tr():
        a.break_source_line()
        a.th(_t="Cell 11")
        a.th(_t="Cell 12")
    with a.tr():
        a.break_source_line()
        a.th(_t="Cell 21")
        a.th(_t="Cell 22")
    a.break_source_line()
a.p(_t="Another content goes here")
```

Will result with such a code:

```html
<h1>Here's your table</h1><table><tr>
<th>Cell 11</th><th>Cell 12</th></tr><tr>
<th>Cell 21</th><th>Cell 22</th></tr>
</table><p>Another content goes here</p>
```

Note that the `break_source_line` cannot be used
in [context manager chains](#chaining-shortcut-for-elements-with-only-one-child).

#### indent style

The default indent of the generated HTML code has two spaces per each indent level.
You can change it to `\t` or 4 spaces by setting `Airium` constructor argument, e.g.:

```python
a = Airium(base_indent="\t")  # one tab symbol
a = Airium(base_indent="    ")  # 4 spaces per each indentation level
a = Airium(base_indent=" ")  # 1 space per one level
# pick one of the above statements, it can be mixed with other arguments
```

Note that this setting is ignored when `source_minify` argument is set to `True` (see above).

There is a special case when you set the base indent to empty string. It would disable indentation,
but line breaks will be still added. In order to get rid of line breaks, check the `source_minify` argument.

#### indent level

The `current_level` integer can be set to a non-negative value. Airium then
starts indentation at the given level offset.

#### line break character

By default, just a line feed (`\n`) is used for terminating lines of the generated code.
You can change it to different style, e.g. `\r\n` or `\r` by setting `source_line_break_character` to the desired value.

```python
a = Airium(source_line_break_character="\r\n")  # windows' style
```

Note that the setting has no effect when `source_minify` argument is set to `True` (see above).

# Using Airium with web frameworks

Airium works with any framework that accepts a string or bytes as an HTTP
response. The following FastAPI example is a complete HTML endpoint with
inline CSS and JavaScript. Inline assets are useful for small pages,
prototypes, and self-contained responses; larger applications can generate
`link` and `script` elements pointing to static files instead.

```python
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

from airium import Airium

app = FastAPI()


@app.get("/", response_class=HTMLResponse)
def home() -> HTMLResponse:
    a = Airium()
    a("<!DOCTYPE html>")

    with a.html(lang="en"):
        with a.head():
            a.meta(charset="utf-8")
            a.meta(
                name="viewport",
                content="width=device-width, initial-scale=1",
            )
            a.title(_t="Airium + FastAPI")
            a.style(
                _t="""
                :root { font-family: system-ui, sans-serif; }
                body { margin: 2rem auto; max-width: 42rem; padding: 0 1rem; }
                button { cursor: pointer; padding: .6rem 1rem; }
                .message { color: #176b3a; }
                """,
            )

        with a.body():
            with a.main():
                a.h1(_t="Hello from FastAPI")
                a.p(
                    id="message",
                    klass="message",
                    _t="This page was generated with pure Python.",
                )
                a.button(id="toggle-message", type="button", _t="Toggle message")

            a.script(
                _t="""
                const button = document.querySelector("#toggle-message");
                const message = document.querySelector("#message");
                button.addEventListener("click", () => {
                    message.hidden = !message.hidden;
                });
                """,
            )

    return HTMLResponse(content=bytes(a))
```

Run it with:

```bash
pip install fastapi uvicorn airium
uvicorn main:app --reload
```

The endpoint returns the generated document directly; no template loader,
template directory, or HTML file is needed.

Airium can also be used with frameworks like Flask or Django. It can replace
template engines, reducing code-file scatter and keeping related presentation
and application logic together.

Here is an example of using airium with django. It implements reusable `basic_body` and a view called `index`.

```python
# file: your_app/views.py
import contextlib
import inspect

from airium import Airium
from django.http import HttpResponse


@contextlib.contextmanager
def basic_body(a: Airium, useful_name: str = ''):
    """Works like a Django/Ninja template."""

    a('<!DOCTYPE html>')
    with a.html(lang='en'):
        with a.head():
            a.meta(charset='utf-8')
            a.meta(content='width=device-width, initial-scale=1', name='viewport')
            # do not use CSS from this URL in a production, it's just for an educational purpose
            a.link(href='https://unpkg.com/@picocss/pico@1.4.1/css/pico.css', rel='stylesheet')
            a.title(_t=f'Hello World')

        with a.body():
            with a.div():
                with a.nav(klass='container-fluid'):
                    with a.ul():
                        with a.li():
                            with a.a(klass='contrast', href='./'):
                                a.strong(_t="⌨ Foo Bar")
                    with a.ul():
                        with a.li():
                            a.a(klass='contrast', href='#', **{'data-theme-switcher': 'auto'}, _t='Auto')
                        with a.li():
                            a.a(klass='contrast', href='#', **{'data-theme-switcher': 'light'}, _t='Light')
                        with a.li():
                            a.a(klass='contrast', href='#', **{'data-theme-switcher': 'dark'}, _t='Dark')

                with a.header(klass='container'):
                    with a.hgroup():
                        a.h1(_t=f"You're on the {useful_name}")
                        a.h2(_t="It's a page made by our automatons with a power of steam engines.")

            with a.main(klass='container'):
                yield  # This is the point where main content gets inserted

            with a.footer(klass='container'):
                with a.small():
                    margin = 'margin: auto 10px;'
                    a.span(_t='© Airium HTML generator example', style=margin)

            # do not use JS from this URL in a production, it's just for an educational purpose
            a.script(src='https://picocss.com/examples/js/minimal-theme-switcher.js')


def index(request) -> HttpResponse:
    a = Airium()
    with basic_body(a, f'main page: {request.path}'):
        with a.article():
            a.h3(_t="Hello World from Django running Airium")
            with a.p().small():
                a("This bases on ")
                with a.a(href="https://picocss.com/examples/company/"):
                    a("Pico.css / Company example")

            with a.p():
                a("Instead of a HTML template, airium has been used.")
                a("The whole body is generated by a template "
                  "and the article code looks like that:")

            with a.code().pre():
                a(inspect.getsource(index))

    return HttpResponse(bytes(a))  # from django.http import HttpResponse
```

Route it in `urls.py` just like a regular view:

```python
# file: your_app/urls.py
from django.contrib import admin
from django.urls import path

import your_app

urlpatterns = [
    path('index/', your_app.views.index),
    path('admin/', admin.site.urls),
]
```

The resulting web page on my machine looks like this:

![Airium/Django templateless example](airium_django_example.png)

# Reverse translation

Airium is equipped with a transpiler `[HTML -> py]`.
It generates python code out of a given `HTML` string.

### Using reverse translator as a binary:

Ensure you have [installed](#installation) `[parse]` extras. Then call in command line:

```bash
airium http://www.example.com
```

That will fetch the document and translate it to python code.
The code calls `airium` statements that reproduce the `HTML` document given.
It may give a clue - how to define `HTML` structure for a given
web page using `airium` package.

To store the translation's result into a file:

```bash
airium http://www.example.com > /tmp/airium_example_com.py
```

You can also parse local `HTML` files:

```bash
airium /path/to/your_file.html > /tmp/airium_my_file.py
```

You may also try to parse your Django templates. I'm not sure if it works,
but there will be probably not much to fix.

### Using reverse translator as python code:

```python
from airium import from_html_to_airium

# assume we have such a page given as a string:
html_str = """\
<!DOCTYPE html>
<html lang="pl">
  <head>
    <meta charset="utf-8" />
    <title>Airium example</title>
  </head>
  <body>
    <h3 id="id23409231" class="main_header">
      Hello World.
    </h3>
  </body>
</html>
"""

# to convert the html into python, just call:

py_str = from_html_to_airium(html_str)

# airium tests ensure that the result of the conversion is equal to the string:
assert py_str == """\
#!/usr/bin/env python
# File generated by reverse AIRIUM translator (version 0.3.2).
# Any change will be overridden on next run.
# flake8: noqa E501 (line too long)

from airium import Airium

a = Airium()

a('<!DOCTYPE html>')
with a.html(lang='pl'):
    with a.head():
        a.meta(charset='utf-8')
        a.title(_t='Airium example')
    with a.body():
        a.h3(klass='main_header', id='id23409231', _t='Hello World.')
"""
```

### <a name="transpiler_limitations">Transpiler limitations</a>

> so far in version 0.2.2:

- result of translation does not keep exact amount of leading whitespaces
  within `<pre>` tags. They come over-indented in python code.

This is not however an issue when code is generated from python to `HTML`.

- although it keeps the proper tags structure, the transpiler does not
  chain all the `with` statements, so in some cases the generated
  code may be much indented.

- it's not too fast

# <a name="installation">Installation</a>

If you need a new virtual environment, call:

```bash
virtualenv venv
source venv/bin/activate
```

Having it activated - you may install airium like this:

```bash
pip install airium
```

In order to use reverse translation - two additional packages are needed, run:

```bash
pip install airium[parse]
```

Then check if the transpiler works by calling:

```bash
airium --help
```

> Enjoy!
