# 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"
}