Skip to main content

An opinionated way to work with html in pure python with htmx support.

Project description

Hypermedia

Hypermedia is a pure python library for working with HTML. Hypermedia's killer feature is that it is composable through a slot concept. Because of that, it works great with HTMX where you need to respond with both partials and full page reloads.

Hypermedia is made to work with FastAPI and HTMX, but can be used by anything to create HTML.

The Basics

All html tags can be imported directly like:

from hypermedia import Html, Body, Div, A

Tags are nested by adding children in the constructor:

from hypermedia import Html, Body, Div

Html(Body(Div(), Div()))

Add text to your tag:

from hypermedia import Html, Body, Div

Html(text="Hello world!")

use .dump() to dump your code to html.

from hypermedia import Html, Body, Div

Html(text="Hello world!").dump()

# outputs
# '<html>hello world</html>'

Composability with slots

from hypermedia import Html, Body, Div, Menu, Header, Div, Ul, Li

base = Html(
    Body(
        Menu(slot="menu"),
        Header(slot="header", text="my header"),
        Div(slot="content"),
    ),
)

menu = Ul(Li(text="main"))
content = Div(text="Some content")

base.extend("menu", menu)
base.extend("content", content)

base.dump()

# outputs
# '<html><body><menu><ul><li>main</li></ul></menu><header>my header</header><div><div>Some content</div></div></body></html>'

HTMX

The Concept

The core concept of HTMX is that the server responds with HTML, and that we can choose with a CSS selector which part of the page will be updated with the HTML response from the server.

This means that we want to return snippets of HTML, or partials, as they are also called.

The Problem

The problem is that we need to differentiate if it's HTMX that called an endpoint for a partial, or if the user just navigated to it and needs the whole page back in the response.

The Solution

HTMX provides an HX-Request header that is always true. We can check for this header to know if it's an HTMX request or not.

We've chosen to implement that check in a @htmx decorator. The decorator expects partial and optionally full arguments in the endpoint definition. These must be resolved by FastAPI's dependency injection system.

The partial argument is a function that returns the partial HTML. The full argument is a function that needs to return the whole HTML, for example on first navigation or a refresh.

Note: The full argument needs to be wrapped in Depends so that the full function's dependencies are resolved! Hypermedia ships a full wrapper, which is basically just making the function lazily loaded. The full wrapper must be used, and the @htmx decorator will call the lazily wrapped function to get the full HTML page when needed.

Note: The following code is in FastAPI, but could have been anything. As long as you check for HX-Request and return partial/full depending on if it exists or not.

def render_base(...):
    """Return base HTML, used by all full renderers."""

def render_fruits_partial(...):
    """Return partial HTML."""

def render_fruits(...):
    """Return base HTML extended with `render_fruits_partial`."""

@router.get("/fruits", response_class=HTMLResponse)
@htmx
async def fruits(
    request: Request,
    partial: Element = Depends(render_fruits_partial),
    full: Element = Depends(full(render_fruits)),
) -> None:
    """Return the fruits page, partial or full."""
    pass

That's it. Now we have separated the rendering from the endpoint definition and handled returning partials and full pages when needed.

What is so cool about this is that it works so well with FastAPI's dependency injection.

Really making use of dependency injection

fruits = {1: "apple", 2: "orange"}

def get_fruit(fruit_id: int = Path(...)) -> str:
    """Get fruit ID from path and return the fruit."""
    return fruits[fruit_id]

def render_fruit_partial(
    fruit: str = Depends(get_fruit),
) -> Element:
    """Return partial HTML."""
    return Div(text=fruit)

def render_fruit(
    partial: Element = Depends(render_fruit_partial),
):
    return render_base().extend("content", partial)

@router.get("/fruits/{fruit_id}", response_class=HTMLResponse)
@htmx
async def fruit(
    request: Request,
    partial: Element = Depends(render_fruit_partial),
    full: Element = Depends(full(render_fruit)),
) -> None:
    """Return the fruit page, partial or full."""
    pass

Here we do basically the same as the previous example, except that we make use of FastAPI's great dependency injection system. Notice the path of our endpoint has fruit_id. This is not used in the definition. However, if we look at our partial renderer, it depends on fruit, which is a function that uses FastAPI's Path resolver. The DI then resolves (basically calls) the fruit function, passes the result into our partial function, and we can use it as a value.

This pattern with DI, Partials, and full renderers is what makes using FastAPI with HTMX worth it.

In addition to this, one thing many are concerned about with HTMX is that since we serve HTML, there will be no way for another app/consumer to get a fruit in JSON. But the solution is simple:

Because we already have a dependency that retrieves the fruit, we just need to add a new endpoint:

@router.get("/api/fruit/{fruit_id}")
async def fruit(
    request: Request,
    fruit: str = Depends(get_fruit),
) -> str:
    """Return the fruit data."""
    return fruit

Notice we added /api/ and just used DI to resolve the fruit and just returned it. Cool!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hypermedia-2.1.1.tar.gz (14.9 kB view details)

Uploaded Source

Built Distribution

hypermedia-2.1.1-py3-none-any.whl (15.6 kB view details)

Uploaded Python 3

File details

Details for the file hypermedia-2.1.1.tar.gz.

File metadata

  • Download URL: hypermedia-2.1.1.tar.gz
  • Upload date:
  • Size: 14.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.7.1 CPython/3.10.6 Darwin/23.5.0

File hashes

Hashes for hypermedia-2.1.1.tar.gz
Algorithm Hash digest
SHA256 0f7db5db69aaea0e113fc0dcb67f5218aaed104d08832bd7cc7735c5f346e0f3
MD5 1d2bdad091e7c9d464336d0fc0ed2f1f
BLAKE2b-256 aa62f1a1ac35672f9a2db9c99dbbd64dafc09ad70d5b6d271d140f250138face

See more details on using hashes here.

File details

Details for the file hypermedia-2.1.1-py3-none-any.whl.

File metadata

  • Download URL: hypermedia-2.1.1-py3-none-any.whl
  • Upload date:
  • Size: 15.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.7.1 CPython/3.10.6 Darwin/23.5.0

File hashes

Hashes for hypermedia-2.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 26c6bf976a36df5c90ff908d55c5435d5481d1d47e46211e738286361685d1b4
MD5 c85133e1aa3626304df5d57782e71f35
BLAKE2b-256 ad76020d513076393f4cb6698ea355a0e0118f717b24d0c8ca1959b2268bde9d

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page