{"id":15288708,"url":"https://github.com/thomasborgen/hypermedia","last_synced_at":"2025-04-13T05:34:39.713Z","repository":{"id":233618637,"uuid":"787565287","full_name":"thomasborgen/hypermedia","owner":"thomasborgen","description":"Composable HTML rendering in pure python with FastAPI and HTMX in mind","archived":false,"fork":false,"pushed_at":"2024-09-25T19:05:08.000Z","size":756,"stargazers_count":17,"open_issues_count":8,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-01T15:52:30.357Z","etag":null,"topics":["composable","fastapi","html","htmx","hypermedia","partials","python","templating"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thomasborgen.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-04-16T19:05:43.000Z","updated_at":"2024-09-25T19:02:21.000Z","dependencies_parsed_at":null,"dependency_job_id":"f0739c2e-3386-468b-878b-e1203b0983cc","html_url":"https://github.com/thomasborgen/hypermedia","commit_stats":null,"previous_names":["thomasborgen/pal"],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasborgen%2Fhypermedia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasborgen%2Fhypermedia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasborgen%2Fhypermedia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasborgen%2Fhypermedia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thomasborgen","download_url":"https://codeload.github.com/thomasborgen/hypermedia/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":219846842,"owners_count":16556424,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["composable","fastapi","html","htmx","hypermedia","partials","python","templating"],"created_at":"2024-09-30T15:52:26.610Z","updated_at":"2024-10-14T19:40:31.435Z","avatar_url":"https://github.com/thomasborgen.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 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 `\u003c/\u003e htmx` where you need to respond with both __partials__ and __full page__ html.\n\nHypermedia is made to work with `FastAPI` and `\u003c/\u003e 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 __\u003c/\u003e 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# '\u003cdiv\u003eHello \u003cb\u003eworld!\u003c/b\u003e\u003c/div\u003e'\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# '\u003chtml\u003e\u003cbody\u003e\u003cmenu\u003e\u003cul\u003e\u003cli\u003emain\u003c/li\u003e\u003c/ul\u003e\u003c/menu\u003e\u003cheader\u003emy header\u003c/header\u003e\u003cdiv\u003e\u003cdiv\u003eSome content\u003c/div\u003e\u003c/div\u003e\u003c/body\u003e\u003c/html\u003e'\n```\n\n## Attribute names with special characters\n\nMost `html` and `\u003c/\u003ehtmx` 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\")  # \u003cdiv onafterprint='test'\u003e\u003c/div\u003e\n\n# Unspecified attribute without Alias:\nDiv(data_test=\"test\")  # \u003cdiv data-test='test'\u003e\u003c/div\u003e\n\n# Spread without $ prefix gets its underscores changed to hyphens:\nDiv(**{\"funky-format_test.value\": True})  # \u003cdiv funky-format-test.value\u003e\u003c/div\u003e\n\n# Spread with $ prefix\nDiv(**{\"$funky-format_test.value\": True})  # \u003cdiv funky-format_test.value\u003e\u003c/div\u003e\nDiv(**{\"$funky-format_test.value\": \"name\"})  # \u003cdiv funky-format_test.value='name'\u003e\u003c/div\u003e\n```\n\nNote: About the \u003c/\u003e 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# \u003cdiv hx-on-click='alert(\"Making a request!\")'\u003e\u003c/div\u003e\n# Which is equivalent to:\n# \u003cdiv hx-on:click='alert(\"Making a request!\"'\u003e\u003c/div\u003e\n\nDiv(hx_on_htmx_before_request='alert(\"Making a request!\")')\n# \u003cdiv hx-on-htmx-before-request='alert(\"Making a request!\")'\u003e\u003c/div\u003e\n\n# shorthand version of above statement\nDiv(hx_on__before_request='alert(\"Making a request!\")')\n# \u003cdiv hx-on--before-request='alert(\"Making a request!\")'\u003e\u003c/div\u003e\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\u003e 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\u003e 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) -\u003e 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(...)) -\u003e 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) -\u003e 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) -\u003e 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) -\u003e 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","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomasborgen%2Fhypermedia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthomasborgen%2Fhypermedia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomasborgen%2Fhypermedia/lists"}