Skip to content

Commit

Permalink
Add docs on streaming.
Browse files Browse the repository at this point in the history
  • Loading branch information
pelme committed Mar 21, 2024
1 parent 80c2919 commit 2f72ef4
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 12 deletions.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,6 @@ The full documentation is available at [https://htpy.dev](https://htpy.dev):
- [Common patterns](https://htpy.dev/common-patterns/)
- [Static typing](https://htpy.dev/static-typing/)
- [Usage with Django](https://htpy.dev/django/)
- [Streaming of contents](https://htpy.dev/streaming/)
- [FAQ](https://htpy.dev/faq/)
- [References](https://htpy.dev/references/)
Binary file added docs/assets/stream.webm
Binary file not shown.
112 changes: 112 additions & 0 deletions docs/streaming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Streaming of contents

Internally, htpy is built with generators. Most of the time, you would render
the full page with `str()`, but htpy can also incrementally generate pages which
can then be streamed to the browser. If your page uses a database or other
services to retrieve data, you can sending the first part of the page to the
client while the page is being generated.

!!! note

Streaming requires a bit of discipline and care to get right. You need to
ensure to avoid doing too much work up front and use lazy constructs such as
generators and callables. Most of the time, rendering the page without
streaming will be the easiest way to get going. Streaming can give you
improved user experience from faster pages/rendering.

This video shows what it looks like in the browser to generate a HTML table with [Django StreamingHttpResponse](https://docs.djangoproject.com/en/5.0/ref/request-response/#django.http.StreamingHttpResponse) ([source code](https://github.com/pelme/htpy/blob/main/examples/djangoproject/stream/views.py)):
<video width="500" controls loop >
<source src="/assets/stream.webm" type="video/webm">
</video>

This example simulates a (very) slow fetch of data and shows the power of
streaming: The browser loads CSS and gradually shows the contents. By loading
CSS files in the `<head>` tag before dynamic content, the browser can start
working on loading the CSS and styling the page while the server keeps
generating the rest of the page.

## Using generators and callables as children

Django's querysets are [lazily
evaluated](https://docs.djangoproject.com/en/5.0/topics/db/queries/#querysets-are-lazy).
They will not execute a database query before their value is actually needed.

This example shows how this property of Django querysets can be used to create a
page that streams objects:
```python
from django.http import StreamingHttpResponse
from htpy import ul, li

from myapp.models import Article

def article_list(request):
return StreamingHttpResponse(ul[
(li[article.title] for article in Article.objects.all())
])
```


## Using callables to delay evalutation

Pass a callable that does not accept any arguements as child to delay the
evaluation.

This example shows how the page starts rendering and outputs the `<h1>` tag and
then calls `calculate_magic_number`.

```python
import time
from htpy import div, h1

def calculate_magic_number() -> str:
time.sleep(1)
print(" (running the complex calculation...)")
return "42"

element = div[
h1["Welcome to my page"],
"The magic number is ",
calculate_magic_number,
]

for chunk in element:
print(chunk)
```

Output:
```
<div>
<h1>
Welcome to my page
</h1>
The magic number is
42 # <-- Appears after 3 seconds
</div>
```

You may use `lambda` to create a function without arguments to make a an expression lazy:

```py
from htpy import div, h1


def fib(n: int) -> int:
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n - 1) + fib(n - 2)


print(
div[
h1["Fibonacci!"],
"fib(20)=",
lambda: str(fib(20)),
]
)
# output: <div><h1>Fibonacci!</h1>fib(12)=6765</div>

```

6 changes: 6 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ You can pass a list, tuple or generator to generate multiple children:
<ul><li>a</li><li>b</li><li>c</li></ul>
```

!!! note

The generator will be lazily evaluated when rendering the element, not
directly when the element is constructed. See [Streaming](streaming.md) for
more information.

A `list` can be used similar to a [JSX fragment](https://react.dev/reference/react/Fragment):

```pycon title="Render a list of child elements"
Expand Down
10 changes: 2 additions & 8 deletions examples/djangoproject/stream/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,9 @@ def streaming_table_page(items: Iterable[Item]) -> Element:
body[
h1["Stream example"],
table(".table.table-striped")[
tr[
th["Table row #"],
th["Style"],
],
tr[th["Table row #"],],
(
tr(f".table-{item.style}")[
td[f"#{item.count}"],
td[item.style],
]
tr[td(style=f"background-color: {item.color}")[f"#{item.count}"],]
for item in items
),
],
Expand Down
4 changes: 2 additions & 2 deletions examples/djangoproject/stream/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
@dataclass
class Item:
count: int
style: str
color: str


def generate_items() -> Iterable[Item]:
for x in range(10):
yield Item(x, random.choice(["primary", "secondary", "success", "danger", "warning"]))
yield Item(x + 1, random.choice(["honeydew", "azure", "ivory", "mistyrose", "aliceblue"]))
time.sleep(1)
44 changes: 42 additions & 2 deletions examples/djangoproject/stream/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,47 @@
import random
import time
from collections.abc import Iterable
from dataclasses import dataclass

from django.http import HttpRequest, StreamingHttpResponse

from .components import streaming_table_page
from .items import generate_items
from htpy import Element, body, h1, head, html, link, table, td, th, title, tr


@dataclass
class Item:
count: int
color: str


def generate_items() -> Iterable[Item]:
for x in range(10):
yield Item(x + 1, random.choice(["honeydew", "azure", "ivory", "mistyrose", "aliceblue"]))
time.sleep(1)


def streaming_table_page(items: Iterable[Item]) -> Element:
return html[
head[
title["Stream example"],
link(
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css",
rel="stylesheet",
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN",
crossorigin="anonymous",
),
],
body[
h1["Stream example"],
table(".table")[
tr[th["Table row #"],],
(
tr[td(style=f"background-color: {item.color}")[f"#{item.count}"],]
for item in items
),
],
],
]


def stream(request: HttpRequest) -> StreamingHttpResponse:
Expand Down
18 changes: 18 additions & 0 deletions examples/stream_callable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import time

from htpy import div, h1


def calculate_magic_number() -> str:
time.sleep(3)
return "42"


element = div[
h1["Welcome to my page"],
"The magic number is ",
calculate_magic_number,
]

for chunk in element:
print(chunk)
20 changes: 20 additions & 0 deletions examples/stream_lambda.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from htpy import div, h1


def fib(n: int) -> int:
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n - 1) + fib(n - 2)


print(
div[
h1["Fibonacci!"],
"fib(20)=",
lambda: str(fib(20)),
]
)
# output: <div><h1>Fibonacci!</h1>fib(12)=6765</div>
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ nav:
- common-patterns.md
- static-typing.md
- django.md
- streaming.md
- faq.md
- references.md
markdown_extensions:
Expand Down

0 comments on commit 2f72ef4

Please sign in to comment.