├── .gitignore
├── README.md
├── home
├── __init__.py
├── apps.py
├── recommendations.py
├── templates
│ ├── chooser.html
│ ├── home
│ │ ├── _item.html
│ │ ├── home.html
│ │ └── home_sse.html
│ ├── shell.html
│ └── shell_htmx.html
└── views
│ ├── __init__.py
│ ├── jinja.py
│ ├── sse.py
│ └── stream.py
├── jinja
├── _item.html
├── home.html
└── shell.html
├── manage.py
├── requirements.in
├── requirements.txt
├── static
├── images
│ ├── avatar.png
│ ├── banner-bg.jpg
│ ├── category
│ │ ├── category-1.jpg
│ │ ├── category-2.jpg
│ │ ├── category-3.jpg
│ │ ├── category-4.jpg
│ │ ├── category-5.jpg
│ │ └── category-6.jpg
│ ├── complete.png
│ ├── favicon
│ │ ├── about.txt
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ └── site.webmanifest
│ ├── icons
│ │ ├── bed-2.svg
│ │ ├── bed.svg
│ │ ├── delivery-van.svg
│ │ ├── money-back.svg
│ │ ├── office.svg
│ │ ├── outdoor-cafe.svg
│ │ ├── phone.svg
│ │ ├── restaurant.svg
│ │ ├── service-hours.svg
│ │ ├── sofa.svg
│ │ └── terrace.svg
│ ├── logo.svg
│ ├── methods.png
│ ├── offer.jpg
│ └── products
│ │ ├── product1.jpg
│ │ ├── product10.jpg
│ │ ├── product11.jpg
│ │ ├── product12.jpg
│ │ ├── product2.jpg
│ │ ├── product3.jpg
│ │ ├── product4.jpg
│ │ ├── product5.jpg
│ │ ├── product6.jpg
│ │ ├── product7.jpg
│ │ ├── product8.jpg
│ │ └── product9.jpg
└── js
│ ├── htmx.1.9.4.min.js
│ └── htmx.1.9.4_dist_ext_sse.js
└── stream
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .idea/
3 | ### Python template
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | share/python-wheels/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | MANIFEST
31 |
32 | # PyInstaller
33 | # Usually these files are written by a python script from a template
34 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
35 | *.manifest
36 | *.spec
37 |
38 | # Installer logs
39 | pip-log.txt
40 | pip-delete-this-directory.txt
41 |
42 | # Unit test / coverage reports
43 | htmlcov/
44 | .tox/
45 | .nox/
46 | .coverage
47 | .coverage.*
48 | .cache
49 | nosetests.xml
50 | coverage.xml
51 | *.cover
52 | *.py,cover
53 | .hypothesis/
54 | .pytest_cache/
55 | cover/
56 |
57 | # Translations
58 | *.mo
59 | *.pot
60 |
61 | # Django stuff:
62 | *.log
63 | local_settings.py
64 | db.sqlite3
65 | db.sqlite3-journal
66 |
67 | # Flask stuff:
68 | instance/
69 | .webassets-cache
70 |
71 | # Scrapy stuff:
72 | .scrapy
73 |
74 | # Sphinx documentation
75 | docs/_build/
76 |
77 | # PyBuilder
78 | .pybuilder/
79 | target/
80 |
81 | # Jupyter Notebook
82 | .ipynb_checkpoints
83 |
84 | # IPython
85 | profile_default/
86 | ipython_config.py
87 |
88 | # pyenv
89 | # For a library or package, you might want to ignore these files since the code is
90 | # intended to run in multiple environments; otherwise, check them in:
91 | # .python-version
92 |
93 | # pipenv
94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
97 | # install all needed dependencies.
98 | #Pipfile.lock
99 |
100 | # poetry
101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
102 | # This is especially recommended for binary packages to ensure reproducibility, and is more
103 | # commonly ignored for libraries.
104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
105 | #poetry.lock
106 |
107 | # pdm
108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
109 | #pdm.lock
110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
111 | # in version control.
112 | # https://pdm.fming.dev/#use-with-ide
113 | .pdm.toml
114 |
115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
116 | __pypackages__/
117 |
118 | # Celery stuff
119 | celerybeat-schedule
120 | celerybeat.pid
121 |
122 | # SageMath parsed files
123 | *.sage.py
124 |
125 | # Environments
126 | .env
127 | .venv
128 | env/
129 | venv/
130 | ENV/
131 | env.bak/
132 | venv.bak/
133 |
134 | # Spyder project settings
135 | .spyderproject
136 | .spyproject
137 |
138 | # Rope project settings
139 | .ropeproject
140 |
141 | # mkdocs documentation
142 | /site
143 |
144 | # mypy
145 | .mypy_cache/
146 | .dmypy.json
147 | dmypy.json
148 |
149 | # Pyre type checker
150 | .pyre/
151 |
152 | # pytype static type analyzer
153 | .pytype/
154 |
155 | # Cython debug symbols
156 | cython_debug/
157 |
158 | # PyCharm
159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
161 | # and can be added to the global gitignore or merged into this file. For a more nuclear
162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163 | #.idea/
164 |
165 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Proof of concept: Streaming HTML with Python
2 |
3 | Inspired by Taylor Hunt's incredible explanation of how he turned [a slow Fortune 20 webapp into a snappy experience, even on a cheap Android phone](https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na), I made this repo to show the current capability of Django to render streaming HTML.
4 |
5 | This technique is best used to improve the perceptual performance for expensive database queries or slow API calls. The idea is that the user could start seeing the page come together while the query or calls are happening. Rendering of the page will pause once it hits the block that is waiting on the data, but being able to see the page render should make the user feel as though the website is performing faster.
6 |
7 | Referencing Taylor's blog post:
8 |
9 | > Both of [these pages](https://assets.codepen.io/183091/HTML+streaming+vs.+non.mp4) show search results in 2.5 seconds. But they sure don't _feel_ the same.
10 |
11 | This concept shows how a recommendation engine takes some time to recommend four products, based for the current user.
12 |
13 | ## Viewing the concept
14 |
15 | Open a terminal at the root of this project and type the following:
16 |
17 | ```shell
18 | python -m venv .venv --prompt stream_python
19 | # If in Windows:
20 | .venv/Scripts/activate
21 | # otherwise
22 | source .venv/bin/
23 | # Then
24 | pip install -r requirements.txt
25 | uvicorn stream.asgi:application --reload
26 | ```
27 |
28 | You can then click the link in the terminal or go to http://127.0.0.1:8000 to view the page.
29 |
30 |
31 | ## Thanks
32 |
33 | Thanks to https://github.com/fajar7xx/ecommerce-template-tailwind-1 for the HTML template.
34 |
--------------------------------------------------------------------------------
/home/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/home/__init__.py
--------------------------------------------------------------------------------
/home/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class HomeConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "home"
7 |
--------------------------------------------------------------------------------
/home/recommendations.py:
--------------------------------------------------------------------------------
1 | customized_recommendations = [
2 | dict(name='Comfy Chair', discount_price=745.00, normal_price=800.00, review_count=1550, img='product1.jpg'),
3 | dict(name='Bed King Size', discount_price=1055.00, normal_price=1599.99, review_count=720, img='product4.jpg'),
4 | dict(name='Lounge pairs', discount_price=350.00, normal_price=499.99, review_count=952, img='product2.jpg'),
5 | dict(name='Air mattress', discount_price=189.99, normal_price=250.00, review_count=153, img='product3.jpg'),
6 | ]
7 |
--------------------------------------------------------------------------------
/home/templates/chooser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/home/templates/home/_item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
17 |
18 |
19 |
20 | {{ recommendation.name }}
21 |
22 |
23 |
${{ recommendation.discount_price | floatformat }}
24 |
${{ recommendation.normal_price | floatformat }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
({{ recommendation.review_count }})
35 |
36 |
37 |
Add
39 | to cart
40 |
41 |
--------------------------------------------------------------------------------
/home/templates/home/home.html:
--------------------------------------------------------------------------------
1 | {% extends "shell.html" %}
2 |
3 | {% block main %}
4 | {% csrf_token %}
5 |
6 |
7 |
8 |
9 | best collection for
home decoration
10 |
11 |
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aperiam
12 | accusantium perspiciatis, sapiente
13 | magni eos dolorum ex quos dolores odio
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

27 |
28 |
Free Shipping
29 |
Order over $200
30 |
31 |
32 |
33 |

34 |
35 |
Money Returns
36 |
30 days money returns
37 |
38 |
39 |
40 |

41 |
42 |
24/7 Support
43 |
Customer support
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
recommended for you
53 |
54 | {% for item in recommendations %}
55 | {% include 'home/_item.html' with recommendation=item %}
56 | {% endfor %}
57 |
58 |
59 |
60 |
61 |
62 | {% endblock %}
63 |
--------------------------------------------------------------------------------
/home/templates/home/home_sse.html:
--------------------------------------------------------------------------------
1 | {% extends "shell_htmx.html" %}
2 |
3 | {% block main %}
4 |
5 |
6 |
7 |
8 |
9 | best collection for
home decoration
10 |
11 |
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aperiam
12 | accusantium perspiciatis, sapiente
13 | magni eos dolorum ex quos dolores odio
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

27 |
28 |
Free Shipping
29 |
Order over $200
30 |
31 |
32 |
33 |

34 |
35 |
Money Returns
36 |
30 days money returns
37 |
38 |
39 |
40 |

41 |
42 |
24/7 Support
43 |
Customer support
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
recommended for you
53 |
57 | {% for item in recommendations %}
58 | {% include 'home/_item.html' with recommendation=item %}
59 | {% empty %}
60 |
66 | {% endfor %}
67 |
68 |
69 |
70 |
71 | {% endblock %}
72 |
--------------------------------------------------------------------------------
/home/templates/shell.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Example Stream
9 |
10 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
46 |
129 |
130 |
131 |
132 |
133 |
134 |
183 |
184 |
185 |
186 |
235 |
236 |
237 | {% block main %} {% endblock %}
238 |
239 |
240 |
308 |
309 |
310 |
311 |
312 |
313 |
© TailCommerce - All Right Reserved
314 |
315 |

316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
--------------------------------------------------------------------------------
/home/templates/shell_htmx.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Example Stream
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
22 |
47 |
130 |
131 |
132 |
133 |
134 |
135 |
184 |
185 |
186 |
187 |
236 |
237 |
238 | {% block main %} {% endblock %}
239 |
240 |
241 |
309 |
310 |
311 |
312 |
313 |
314 |
© TailCommerce - All Right Reserved
315 |
316 |

317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
--------------------------------------------------------------------------------
/home/views/__init__.py:
--------------------------------------------------------------------------------
1 | # Create your views here.
2 | from django.shortcuts import render
3 |
4 |
5 | def chooser(request):
6 | return render(request, 'chooser.html')
7 |
--------------------------------------------------------------------------------
/home/views/jinja.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from django.http import StreamingHttpResponse
4 | from django.template.loader import get_template
5 |
6 | from home.recommendations import customized_recommendations
7 |
8 |
9 | async def stream_homepage_content():
10 | for item in customized_recommendations:
11 | # Faking an expensive database query or slow API
12 | await asyncio.sleep(.7)
13 | yield item
14 |
15 |
16 | async def index(request):
17 | template = get_template('home.html')
18 | return StreamingHttpResponse(
19 | template.template.generate_async({
20 | 'recommendations': stream_homepage_content()
21 | })
22 | )
23 |
--------------------------------------------------------------------------------
/home/views/sse.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import random
3 | import re
4 |
5 | from django.http import StreamingHttpResponse
6 | from django.shortcuts import render
7 | from django.template.loader import render_to_string
8 |
9 | from home.recommendations import customized_recommendations
10 |
11 |
12 | def index(request):
13 | return render(request, 'home/home_sse.html')
14 |
15 |
16 | async def sse_recommendation():
17 | recommendations = []
18 | for item in customized_recommendations:
19 | await asyncio.sleep(.7)
20 | content = render_to_string('home/_item.html', dict(recommendation=item))
21 | recommendations.append(
22 | re.sub('\n', '', content)
23 | )
24 | all_recommendations = ''.join(recommendations)
25 | yield f'data: {all_recommendations}\n\n'
26 |
27 |
28 | def handle_sse(request):
29 | return StreamingHttpResponse(streaming_content=sse_recommendation(), content_type='text/event-stream')
30 |
--------------------------------------------------------------------------------
/home/views/stream.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from django.http import StreamingHttpResponse
4 | from django.template.loader import render_to_string
5 |
6 | from home.recommendations import customized_recommendations
7 |
8 |
9 | async def stream_homepage_content(request):
10 | pre_shell, post_shell = render_to_string('home/home.html', request=request).split('')
11 | yield pre_shell
12 | for item in customized_recommendations:
13 | await asyncio.sleep(.7) # Faking an expensive database query or slow API
14 | yield render_to_string('home/_item.html', dict(recommendation=item))
15 | yield post_shell
16 |
17 |
18 | async def index(request):
19 | return StreamingHttpResponse(stream_homepage_content(request))
20 |
--------------------------------------------------------------------------------
/jinja/_item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
17 |
18 |
19 |
20 | {{ recommendation.name }}
21 |
22 |
23 |
${{ recommendation.discount_price }}
24 |
${{ recommendation.normal_price }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
({{ recommendation.review_count }})
35 |
36 |
37 |
Add
39 | to cart
40 |
41 |
--------------------------------------------------------------------------------
/jinja/home.html:
--------------------------------------------------------------------------------
1 | {% extends "shell.html" %}
2 |
3 | {% block main %}
4 |
5 |
6 |
7 |
8 |
9 | best collection for
home decoration
10 |
11 |
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aperiam
12 | accusantium perspiciatis, sapiente
13 | magni eos dolorum ex quos dolores odio
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

27 |
28 |
Free Shipping
29 |
Order over $200
30 |
31 |
32 |
33 |

34 |
35 |
Money Returns
36 |
30 days money returns
37 |
38 |
39 |
40 |

41 |
42 |
24/7 Support
43 |
Customer support
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
recommended for you
53 |
54 | {# {{ recommendations }}#}
55 | {% for recommendation in recommendations %}
56 | {% include '_item.html' %}
57 | {% endfor %}
58 |
59 |
60 |
61 |
62 |
63 | {% endblock %}
64 |
--------------------------------------------------------------------------------
/jinja/shell.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Example Stream
9 |
10 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
46 |
129 |
130 |
131 |
132 |
133 |
134 |
183 |
184 |
185 |
186 |
235 |
236 |
237 | {% block main %} {% endblock %}
238 |
239 |
240 |
308 |
309 |
310 |
311 |
312 |
313 |
© TailCommerce - All Right Reserved
314 |
315 |

316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "stream.settings")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == "__main__":
22 | main()
23 |
--------------------------------------------------------------------------------
/requirements.in:
--------------------------------------------------------------------------------
1 | django
2 | uvicorn
3 | jinja2
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with Python 3.11
3 | # by the following command:
4 | #
5 | # pip-compile requirements.in
6 | #
7 | asgiref==3.6.0
8 | # via django
9 | click==8.1.3
10 | # via uvicorn
11 | django==4.2.1
12 | # via -r requirements.in
13 | h11==0.14.0
14 | # via uvicorn
15 | jinja2==3.1.2
16 | # via -r requirements.in
17 | markupsafe==2.1.3
18 | # via jinja2
19 | sqlparse==0.4.4
20 | # via django
21 | uvicorn==0.22.0
22 | # via -r requirements.in
23 |
--------------------------------------------------------------------------------
/static/images/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/avatar.png
--------------------------------------------------------------------------------
/static/images/banner-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/banner-bg.jpg
--------------------------------------------------------------------------------
/static/images/category/category-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/category/category-1.jpg
--------------------------------------------------------------------------------
/static/images/category/category-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/category/category-2.jpg
--------------------------------------------------------------------------------
/static/images/category/category-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/category/category-3.jpg
--------------------------------------------------------------------------------
/static/images/category/category-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/category/category-4.jpg
--------------------------------------------------------------------------------
/static/images/category/category-5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/category/category-5.jpg
--------------------------------------------------------------------------------
/static/images/category/category-6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/category/category-6.jpg
--------------------------------------------------------------------------------
/static/images/complete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/complete.png
--------------------------------------------------------------------------------
/static/images/favicon/about.txt:
--------------------------------------------------------------------------------
1 | This favicon was generated using the following font:
2 |
3 | - Font Title: Roboto
4 | - Font Author: Copyright 2011 Google Inc. All Rights Reserved.
5 | - Font Source: http://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlvAx05IsDqlA.ttf
6 | - Font License: Apache License, version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html))
7 |
--------------------------------------------------------------------------------
/static/images/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/images/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/static/images/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/images/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/static/images/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/static/images/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/favicon/favicon.ico
--------------------------------------------------------------------------------
/static/images/favicon/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/static/images/icons/bed-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
--------------------------------------------------------------------------------
/static/images/icons/bed.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/static/images/icons/delivery-van.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/static/images/icons/money-back.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/static/images/icons/office.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/static/images/icons/outdoor-cafe.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
64 |
--------------------------------------------------------------------------------
/static/images/icons/phone.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/static/images/icons/restaurant.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/icons/service-hours.svg:
--------------------------------------------------------------------------------
1 |
28 |
--------------------------------------------------------------------------------
/static/images/icons/sofa.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
60 |
--------------------------------------------------------------------------------
/static/images/icons/terrace.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/logo.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/static/images/methods.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/methods.png
--------------------------------------------------------------------------------
/static/images/offer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/offer.jpg
--------------------------------------------------------------------------------
/static/images/products/product1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product1.jpg
--------------------------------------------------------------------------------
/static/images/products/product10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product10.jpg
--------------------------------------------------------------------------------
/static/images/products/product11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product11.jpg
--------------------------------------------------------------------------------
/static/images/products/product12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product12.jpg
--------------------------------------------------------------------------------
/static/images/products/product2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product2.jpg
--------------------------------------------------------------------------------
/static/images/products/product3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product3.jpg
--------------------------------------------------------------------------------
/static/images/products/product4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product4.jpg
--------------------------------------------------------------------------------
/static/images/products/product5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product5.jpg
--------------------------------------------------------------------------------
/static/images/products/product6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product6.jpg
--------------------------------------------------------------------------------
/static/images/products/product7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product7.jpg
--------------------------------------------------------------------------------
/static/images/products/product8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product8.jpg
--------------------------------------------------------------------------------
/static/images/products/product9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/2d2b356945d3b5a23f39b42832d2acdd2bb7b45a/static/images/products/product9.jpg
--------------------------------------------------------------------------------
/static/js/htmx.1.9.4.min.js:
--------------------------------------------------------------------------------
1 | (function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var G={onLoad:t,process:Nt,on:le,off:ue,trigger:oe,ajax:xr,find:b,findAll:f,closest:d,values:function(e,t){var r=er(e,t||"post");return r.values},remove:U,addClass:B,removeClass:n,toggleClass:V,takeClass:j,defineExtension:Cr,removeExtension:Rr,logAll:X,logNone:F,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"]},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=G.config.wsBinaryType;return t},version:"1.9.4"};var C={addTriggerHandler:bt,bodyContains:re,canAccessLocalStorage:M,findThisElement:he,filterValues:ar,hasAttribute:o,getAttributeValue:Z,getClosestAttributeValue:Y,getClosestMatch:c,getExpressionVars:gr,getHeaders:ir,getInputValues:er,getInternalData:ee,getSwapSpecification:sr,getTriggerSpecs:Ge,getTarget:de,makeFragment:l,mergeObjects:ne,makeSettleInfo:S,oobSwap:me,querySelectorExt:ie,selectAndSwap:De,settleImmediately:Wt,shouldCancel:Qe,triggerEvent:oe,triggerErrorEvent:ae,withExtensions:w};var R=["get","post","put","delete","patch"];var O=R.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function J(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function Z(e,t){return J(e,t)||J(e,"data-"+t)}function u(e){return e.parentElement}function K(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function T(e,t,r){var n=Z(t,r);var i=Z(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function Y(t,r){var n=null;c(t,function(e){return n=T(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function q(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=K().createDocumentFragment()}return i}function H(e){return e.match(/"+e+"",0);return r.querySelector("template").content}else{var n=q(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("",1);case"col":return i("",2);case"tr":return i("",2);case"td":case"th":return i("",3);case"script":return i(""+e+"
",1);default:return i(e,0)}}}function Q(e){if(e){e()}}function L(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function A(e){return L(e,"Function")}function N(e){return L(e,"Object")}function ee(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function I(e){var t=[];if(e){for(var r=0;r=0}function re(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return K().body.contains(e.getRootNode().host)}else{return K().body.contains(e)}}function P(e){return e.trim().split(/\s+/)}function ne(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function y(e){try{return JSON.parse(e)}catch(e){x(e);return null}}function M(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function D(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return hr(K().body,function(){return eval(e)})}function t(t){var e=G.on("htmx:load",function(e){t(e.detail.elt)});return e}function X(){G.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function F(){G.logger=null}function b(e,t){if(t){return e.querySelector(t)}else{return b(K(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(K(),e)}}function U(e,t){e=s(e);if(t){setTimeout(function(){U(e);e=null},t)}else{e.parentElement.removeChild(e)}}function B(e,t,r){e=s(e);if(r){setTimeout(function(){B(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function V(e,t){e=s(e);e.classList.toggle(t)}function j(e,t){e=s(e);te(e.parentElement.children,function(e){n(e,t)});B(e,t)}function d(e,t){e=s(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function r(e){var t=e.trim();if(t.startsWith("<")&&t.endsWith("/>")){return t.substring(1,t.length-2)}else{return t}}function W(e,t){if(t.indexOf("closest ")===0){return[d(e,r(t.substr(8)))]}else if(t.indexOf("find ")===0){return[b(e,r(t.substr(5)))]}else if(t.indexOf("next ")===0){return[_(e,r(t.substr(5)))]}else if(t.indexOf("previous ")===0){return[z(e,r(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else{return K().querySelectorAll(r(t))}}var _=function(e,t){var r=K().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ie(e,t){if(t){return W(e,t)[0]}else{return W(K().body,e)[0]}}function s(e){if(L(e,"String")){return b(e)}else{return e}}function $(e,t,r){if(A(t)){return{target:K().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function le(t,r,n){Tr(function(){var e=$(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=A(r);return e?r:n}function ue(t,r,n){Tr(function(){var e=$(t,r,n);e.target.removeEventListener(e.event,e.listener)});return A(r)?r:n}var fe=K().createElement("output");function ce(e,t){var r=Y(e,t);if(r){if(r==="this"){return[he(e,t)]}else{var n=W(e,r);if(n.length===0){x('The selector "'+r+'" on '+t+" returned no matches!");return[fe]}else{return n}}}}function he(e,t){return c(e,function(e){return Z(e,t)!=null})}function de(e){var t=Y(e,"hx-target");if(t){if(t==="this"){return he(e,"hx-target")}else{return ie(e,t)}}else{var r=ee(e);if(r.boosted){return K().body}else{return e}}}function ve(e){var t=G.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=K().querySelectorAll(t);if(r){te(r,function(e){var t;var r=i.cloneNode(true);t=K().createDocumentFragment();t.appendChild(r);if(!pe(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!oe(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Pe(o,e,e,t,a)}te(a.elts,function(e){oe(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);ae(K().body,"htmx:oobErrorNoTarget",{content:i})}return e}function xe(e,t,r){var n=Y(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();ge(e,i);s.tasks.push(function(){ge(e,a)})}}})}function we(e){return function(){n(e,G.config.addedClass);Nt(e);St(e);Se(e);oe(e,"htmx:load")}}function Se(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){be(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;B(i,G.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(we(i))}}}function Ee(e,t){var r=0;while(r-1){var t=e.replace(/