hypermedia


Namehypermedia JSON
Version 5.3.3 PyPI version JSON
download
home_pagehttps://github.com/thomasborgen/hypermedia
SummaryAn opinionated way to work with html in pure python with htmx support.
upload_time2024-11-08 09:04:04
maintainerNone
docs_urlNone
authorThomas Borgen
requires_python<4.0,>=3.10
licenseMIT
keywords html htmx extendable partial html html templating fastapi
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # 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__ html.

Hypermedia is made to work with `FastAPI` and `</> htmx`, but can be used by anything to create HTML.

## Features

* Build __HTML__ with python classes
* __Composable__ templates through a __slot__ system
* Seamless integration with __</> htmx__
* Fully typed and __Autocompletion__ for html/htmx attributes and styles
* Opinionated simple decorator for __FastAPI__
* Unlike other template engines like Jinja2 we have full typing since we never leave python land.

## The Basics

All html tags can be imported directly like:

```python
from hypermedia import Html, Body, Div, A
```

Tags are nested by adding children in the constructor:

```python
from hypermedia import Html, Body, Div

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

Add text to your tag:

```python
from hypermedia import Div

Div("Hello world!")
```

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


```python
from hypermedia import Bold, Div

Div("Hello ", Bold("world!")).dump()

# outputs
# '<div>Hello <b>world!</b></div>'
```

## Composability with slots

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

base = Html(
    Body(
        Menu(slot="menu"),
        Header("my header", slot="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>'
```

## Attribute names with special characters

Most `html` and `</>htmx` attributes are typed and has Aliases where needed. That means that most of the time you won't have to think about this and it should _just work_.

The attribute name output rules are:

1. Any attribute that does not have an Alias will have any underscores (`_`) changed to hyphens (`-`).
2. Any attribute that is prefixed with `$` will be outputted as is without the first `$`.

ie

```python
# Is a specified attribute(typed) with an Alias:
Div(on_afterprint="test")  # <div onafterprint='test'></div>

# Unspecified attribute without Alias:
Div(data_test="test")  # <div data-test='test'></div>

# Spread without $ prefix gets its underscores changed to hyphens:
Div(**{"funky-format_test.value": True})  # <div funky-format-test.value></div>

# Spread with $ prefix
Div(**{"$funky-format_test.value": True})  # <div funky-format_test.value></div>
Div(**{"$funky-format_test.value": "name"})  # <div funky-format_test.value='name'></div>
```

Note: About the </> HTMX attributes. [The documentation](https://htmx.org/attributes/hx-on/) specifies that all hx attributes can be written with all dashes. Because of that Hypermedia lets users write hx attributes with underscores and Hypermedia changes them to dashes for you.

```python

Div(hx_on_click='alert("Making a request!")')
# <div hx-on-click='alert("Making a request!")'></div>
# Which is equivalent to:
# <div hx-on:click='alert("Making a request!"'></div>

Div(hx_on_htmx_before_request='alert("Making a request!")')
# <div hx-on-htmx-before-request='alert("Making a request!")'></div>

# shorthand version of above statement
Div(hx_on__before_request='alert("Making a request!")')
# <div hx-on--before-request='alert("Making a request!")'></div>
```

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

```python
from hypermedia.fastapi import htmx, full
```

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.

```python
def render_base():
    """Return base HTML, used by all full renderers."""
    return ElementList(Doctype(), Body(slot="body"))


def render_fruits_partial():
    """Return partial HTML."""
    return Div(Ul(Li("Apple"), Li("Banana"), Button("reload", hx_get="/fruits")))


def render_fruits():
    """Return base HTML extended with `render_fruits_partial`."""
    return render_base().extend("body", 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. Doing a full refresh will render the whole page. Clicking the button will make a htmx request and only return the partial.

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

## Really making use of dependency injection


```python
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(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 `get_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:

```python
@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. Nice!

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/thomasborgen/hypermedia",
    "name": "hypermedia",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.10",
    "maintainer_email": null,
    "keywords": "HTML, HTMX, Extendable, Partial HTML, HTML Templating, FastAPI",
    "author": "Thomas Borgen",
    "author_email": "thomasborgen91@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/0b/93/630834949f15715aac89522a14dc91bf314a2b18a421324dd8bed9e0d4af/hypermedia-5.3.3.tar.gz",
    "platform": null,
    "description": "# Hypermedia\n\nHypermedia 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__ html.\n\nHypermedia is made to work with `FastAPI` and `</> htmx`, but can be used by anything to create HTML.\n\n## Features\n\n* Build __HTML__ with python classes\n* __Composable__ templates through a __slot__ system\n* Seamless integration with __</> htmx__\n* Fully typed and __Autocompletion__ for html/htmx attributes and styles\n* Opinionated simple decorator for __FastAPI__\n* Unlike other template engines like Jinja2 we have full typing since we never leave python land.\n\n## The Basics\n\nAll html tags can be imported directly like:\n\n```python\nfrom hypermedia import Html, Body, Div, A\n```\n\nTags are nested by adding children in the constructor:\n\n```python\nfrom hypermedia import Html, Body, Div\n\nHtml(Body(Div(), Div()))\n```\n\nAdd text to your tag:\n\n```python\nfrom hypermedia import Div\n\nDiv(\"Hello world!\")\n```\n\nuse `.dump()` to dump your code to html.\n\n\n```python\nfrom hypermedia import Bold, Div\n\nDiv(\"Hello \", Bold(\"world!\")).dump()\n\n# outputs\n# '<div>Hello <b>world!</b></div>'\n```\n\n## Composability with slots\n\n```python\nfrom hypermedia import Html, Body, Div, Menu, Header, Div, Ul, Li\n\nbase = Html(\n    Body(\n        Menu(slot=\"menu\"),\n        Header(\"my header\", slot=\"header\"),\n        Div(slot=\"content\"),\n    ),\n)\n\nmenu = Ul(Li(text=\"main\"))\ncontent = Div(text=\"Some content\")\n\nbase.extend(\"menu\", menu)\nbase.extend(\"content\", content)\n\nbase.dump()\n\n# outputs\n# '<html><body><menu><ul><li>main</li></ul></menu><header>my header</header><div><div>Some content</div></div></body></html>'\n```\n\n## Attribute names with special characters\n\nMost `html` and `</>htmx` attributes are typed and has Aliases where needed. That means that most of the time you won't have to think about this and it should _just work_.\n\nThe attribute name output rules are:\n\n1. Any attribute that does not have an Alias will have any underscores (`_`) changed to hyphens (`-`).\n2. Any attribute that is prefixed with `$` will be outputted as is without the first `$`.\n\nie\n\n```python\n# Is a specified attribute(typed) with an Alias:\nDiv(on_afterprint=\"test\")  # <div onafterprint='test'></div>\n\n# Unspecified attribute without Alias:\nDiv(data_test=\"test\")  # <div data-test='test'></div>\n\n# Spread without $ prefix gets its underscores changed to hyphens:\nDiv(**{\"funky-format_test.value\": True})  # <div funky-format-test.value></div>\n\n# Spread with $ prefix\nDiv(**{\"$funky-format_test.value\": True})  # <div funky-format_test.value></div>\nDiv(**{\"$funky-format_test.value\": \"name\"})  # <div funky-format_test.value='name'></div>\n```\n\nNote: About the </> HTMX attributes. [The documentation](https://htmx.org/attributes/hx-on/) specifies that all hx attributes can be written with all dashes. Because of that Hypermedia lets users write hx attributes with underscores and Hypermedia changes them to dashes for you.\n\n```python\n\nDiv(hx_on_click='alert(\"Making a request!\")')\n# <div hx-on-click='alert(\"Making a request!\")'></div>\n# Which is equivalent to:\n# <div hx-on:click='alert(\"Making a request!\"'></div>\n\nDiv(hx_on_htmx_before_request='alert(\"Making a request!\")')\n# <div hx-on-htmx-before-request='alert(\"Making a request!\")'></div>\n\n# shorthand version of above statement\nDiv(hx_on__before_request='alert(\"Making a request!\")')\n# <div hx-on--before-request='alert(\"Making a request!\")'></div>\n```\n\n# HTMX\n\n## The Concept\n\nThe 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.\n\nThis means that we want to return snippets of HTML, or `partials`, as they are also called.\n\n## The Problem\n\nThe 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.\n\n## The Solution\n\nHTMX 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.\n\nWe'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.\n\n```python\nfrom hypermedia.fastapi import htmx, full\n```\n\nThe `partial` argument is a function that returns the partial HTML.\nThe `full` argument is a function that needs to return the whole HTML, for example on first navigation or a refresh.\n\n> 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.\n\n> 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.\n\n```python\ndef render_base():\n    \"\"\"Return base HTML, used by all full renderers.\"\"\"\n    return ElementList(Doctype(), Body(slot=\"body\"))\n\n\ndef render_fruits_partial():\n    \"\"\"Return partial HTML.\"\"\"\n    return Div(Ul(Li(\"Apple\"), Li(\"Banana\"), Button(\"reload\", hx_get=\"/fruits\")))\n\n\ndef render_fruits():\n    \"\"\"Return base HTML extended with `render_fruits_partial`.\"\"\"\n    return render_base().extend(\"body\", render_fruits_partial())\n\n\n@router.get(\"/fruits\", response_class=HTMLResponse)\n@htmx\nasync def fruits(\n    request: Request,\n    partial: Element = Depends(render_fruits_partial),\n    full: Element = Depends(full(render_fruits)),\n) -> None:\n    \"\"\"Return the fruits page, partial or full.\"\"\"\n    pass\n```\n\nThat's it. Now we have separated the rendering from the endpoint definition and handled returning partials and full pages when needed. Doing a full refresh will render the whole page. Clicking the button will make a htmx request and only return the partial.\n\nWhat is so cool about this is that it works so well with FastAPI's dependency injection.\n\n## Really making use of dependency injection\n\n\n```python\nfruits = {1: \"apple\", 2: \"orange\"}\n\ndef get_fruit(fruit_id: int = Path(...)) -> str:\n    \"\"\"Get fruit ID from path and return the fruit.\"\"\"\n    return fruits[fruit_id]\n\ndef render_fruit_partial(\n    fruit: str = Depends(get_fruit),\n) -> Element:\n    \"\"\"Return partial HTML.\"\"\"\n    return Div(fruit)\n\ndef render_fruit(\n    partial: Element = Depends(render_fruit_partial),\n):\n    return render_base().extend(\"content\", partial)\n\n@router.get(\"/fruits/{fruit_id}\", response_class=HTMLResponse)\n@htmx\nasync def fruit(\n    request: Request,\n    partial: Element = Depends(render_fruit_partial),\n    full: Element = Depends(full(render_fruit)),\n) -> None:\n    \"\"\"Return the fruit page, partial or full.\"\"\"\n    pass\n```\n\nHere 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 `get_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!\n\n__This pattern with DI, Partials, and full renderers is what makes using FastAPI with HTMX worth it.__\n\nIn 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:\n\nBecause we already have a dependency that retrieves the fruit, we just need to add a new endpoint:\n\n```python\n@router.get(\"/api/fruit/{fruit_id}\")\nasync def fruit(\n    request: Request,\n    fruit: str = Depends(get_fruit),\n) -> str:\n    \"\"\"Return the fruit data.\"\"\"\n    return fruit\n```\n\nNotice we added `/api/` and just used DI to resolve the fruit and just returned it. Nice!\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "An opinionated way to work with html in pure python with htmx support.",
    "version": "5.3.3",
    "project_urls": {
        "Homepage": "https://github.com/thomasborgen/hypermedia",
        "Repository": "https://github.com/thomasborgen/hypermedia"
    },
    "split_keywords": [
        "html",
        " htmx",
        " extendable",
        " partial html",
        " html templating",
        " fastapi"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "808e374d2107ebdd7eea3a466b5f62f040ab3d893ae65cd542ee8463a62e1be5",
                "md5": "bfc1ccdbbbedfa868e1d09016f3f779f",
                "sha256": "d86ddde559713eba3802b21306ed76664c4831a59b41e5cb1ede37a2c102f053"
            },
            "downloads": -1,
            "filename": "hypermedia-5.3.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "bfc1ccdbbbedfa868e1d09016f3f779f",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.10",
            "size": 29924,
            "upload_time": "2024-11-08T09:04:03",
            "upload_time_iso_8601": "2024-11-08T09:04:03.641802Z",
            "url": "https://files.pythonhosted.org/packages/80/8e/374d2107ebdd7eea3a466b5f62f040ab3d893ae65cd542ee8463a62e1be5/hypermedia-5.3.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "0b93630834949f15715aac89522a14dc91bf314a2b18a421324dd8bed9e0d4af",
                "md5": "373f66d4a91adadcaba7e18308450783",
                "sha256": "ce2a8705b4176f2b840150a9fcbeef1bf4b5e9f8e97d67a72c6f5dc76ce8a3d1"
            },
            "downloads": -1,
            "filename": "hypermedia-5.3.3.tar.gz",
            "has_sig": false,
            "md5_digest": "373f66d4a91adadcaba7e18308450783",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.10",
            "size": 27540,
            "upload_time": "2024-11-08T09:04:04",
            "upload_time_iso_8601": "2024-11-08T09:04:04.911068Z",
            "url": "https://files.pythonhosted.org/packages/0b/93/630834949f15715aac89522a14dc91bf314a2b18a421324dd8bed9e0d4af/hypermedia-5.3.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-11-08 09:04:04",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "thomasborgen",
    "github_project": "hypermedia",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "hypermedia"
}
        
Elapsed time: 0.42468s