make render_template method async, update readme and examples

Commit cbe13f9 · patx · 2025-01-28T15:33:16-05:00

Changeset
cbe13f991fa148f42b6b4019b783f3f54c3f6d39
Parents
5e5d49f361e9631b02e1cd3287bcdf7336652a11

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/MicroPie.py b/MicroPie.py
index 16c5767..8b4e192 100644
--- a/MicroPie.py
+++ b/MicroPie.py
@@ -30,14 +30,14 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 """
-
-import time
-import uuid
+import asyncio
 import inspect
-import os
 import mimetypes
-from urllib.parse import parse_qs
+import os
+import time
 from typing import Optional, Dict, Any, Union, Tuple, List
+from urllib.parse import parse_qs
+import uuid
 
 try:
     from jinja2 import Environment, FileSystemLoader
@@ -366,8 +366,15 @@ class Server:
             ),
         )
 
-    def render_template(self, name: str, **kwargs: Any) -> str:
+    async def render_template(self, name: str, **kwargs: Any) -> str:
+        """
+        Async-compatible template rendering using Jinja2.
+        """
         if not JINJA_INSTALLED:
             raise ImportError("Jinja2 is not installed.")
-        return self.env.get_template(name).render(kwargs)
+
+        def render_sync():
+            return self.env.get_template(name).render(kwargs)
+
+        return await asyncio.get_event_loop().run_in_executor(None, render_sync)
 
diff --git a/README.md b/README.md
index 6824446..aec048b 100644
--- a/README.md
+++ b/README.md
@@ -59,46 +59,43 @@ Access your app at [http://127.0.0.1:8000](http://127.0.0.1:8000).
 
 ## **Core Features**
 
-### **1. Flexible Routing**
+### **1. Flexible HTTP Routing**
 MicroPie automatically maps URLs to methods within your `Server` class. Routes can be defined as either synchronous or asynchronous functions, offering good flexibility.
 
-#### **Basic Routing**
+For GET requests, pass data through query strings or URL path segments, automatically mapped to method arguments.
 ```python
 class MyApp(Server):
-    def hello(self):
-        return "Hello, world!"
-
-    async def async_hello(self):
-        return "Hello from an async route!"
+    def async greet(self, name="Guest"):
+        return f"Hello, {name}!"
 ```
 **Access:**
-- Sync route: [http://127.0.0.1:8000/hello](http://127.0.0.1:8000/hello)
-- Async route: [http://127.0.0.1:8000/async_hello](http://127.0.0.1:8000/async_hello)
+- [http://127.0.0.1:8000/greet?name=Alice](http://127.0.0.1:8000/greet?name=Alice) returns `Hello, Alice!`
+- [http://127.0.0.1:8000/greet/Alice](http://127.0.0.1:8000/greet/Alice) returns `Hello, Alice!`
 
-### **2. Query and Path Parameters**
-Pass data through query strings or URL path segments, automatically mapped to method arguments.
+MicroPie also supports handling form data submitted via HTTP POST requests. Form data is automatically mapped to method arguments. It is able to handle default values and raw POST data:
 ```python
 class MyApp(Server):
-    def async greet(self, name="Guest"):
-        return f"Hello, {name}!"
+    def submit_default_values(self, username="Anonymous"):
+        return f"Form submitted by: {username}"
+
+    def submit_catch_all(self):
+        username = self.body_params.get('username', ['Anonymous'])[0]
+        return f"Submitted by: {username}"
 ```
 
-**Access:**
-- [http://127.0.0.1:8000/greet?name=Alice](http://127.0.0.1:8000/greet?name=Alice) returns `Hello, Alice!`
-- [http://127.0.0.1:8000/greet/Alice](http://127.0.0.1:8000/greet/Alice) returns `Hello, Alice!`
 
 ### **3. Real-Time Communication with Socket.IO**
 Because of its designed simplicity, MicroPie does not handle WebSockets out of the box. While the underlying ASGI interface can theoretically handle WebSocket connections, MicroPie’s routing and request-handling logic is designed primarily for HTTP. While MicroPie does not natively support WebSockets, you can easily integrate dedicated Websockets libraries like **Socket.IO** alongside Uvicorn to handle real-time, bidirectional communication. Check out [examples/socketio](https://github.com/patx/micropie/tree/main/examples/socketio) to see this in action.
 
 
 ### **4. Jinja2 Template Rendering**
-Dynamic HTML generation is supported via Jinja2.
+Dynamic HTML generation is supported via Jinja2. This happens asynchronously using Pythons `asyncio` library, so make sure to use the `async` and `await` with this method.
 
 #### **`app.py`**
 ```python
 class MyApp(Server):
-    def index(self):
-        return self.render_template("index.html", title="Welcome", message="Hello from MicroPie!")
+    async def index(self):
+        return await self.render_template("index.html", title="Welcome", message="Hello from MicroPie!")
 ```
 
 #### **`templates/index.html`**
diff --git a/examples/chatroom/app.py b/examples/chatroom/app.py
deleted file mode 100644
index 7c3948c..0000000
--- a/examples/chatroom/app.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import asyncio
-import websockets
-import multiprocessing
-from MicroPie import Server
-
-# Store connected WebSocket clients
-connected_clients = set()
-
-class MyApp(Server):
-    def index(self):
-        return """
-        <html>
-        <head>
-            <script>
-                var ws = new WebSocket("ws://localhost:8765");
-                ws.onopen = function() {
-                    console.log("Connected to WebSocket server");
-                };
-                ws.onmessage = function(event) {
-                    document.getElementById("output").innerHTML += event.data + "<br>";
-                };
-                function sendMessage() {
-                    var message = document.getElementById("message").value;
-                    ws.send(message);
-                    document.getElementById("message").value = "";  // Clear input after sending
-                }
-                window.onbeforeunload = function() {
-                    ws.close();
-                };
-            </script>
-        </head>
-        <body>
-            <h1>WebSocket Chat</h1>
-            <input type="text" id="message" placeholder="Type a message">
-            <button onclick="sendMessage()">Send</button>
-            <div id="output"></div>
-        </body>
-        </html>
-        """
-
-async def websocket_handler(websocket):
-    """Handles incoming WebSocket connections and broadcasts messages to all clients."""
-    global connected_clients
-    connected_clients.add(websocket)
-    print("Client connected")
-
-    try:
-        async for message in websocket:
-            print(f"Received: {message}")
-            response = f"User: {message}"
-            await broadcast(response)  # Send to all connected clients
-    except websockets.exceptions.ConnectionClosed:
-        print("Client disconnected")
-    finally:
-        connected_clients.remove(websocket)
-
-async def broadcast(message):
-    """Send a message to all connected WebSocket clients."""
-    if connected_clients:
-        await asyncio.wait([client.send(message) for client in connected_clients])
-
-async def websocket_server():
-    """Start the WebSocket server within the asyncio event loop."""
-    async with websockets.serve(websocket_handler, "localhost", 8765):
-        print("WebSocket server started on ws://localhost:8765")
-        await asyncio.Future()  # Keep the server running indefinitely
-
-def start_websocket_server():
-    """Runs the WebSocket server with asyncio.run() in a separate thread."""
-    asyncio.run(websocket_server())
-
-# Create WSGI app for Gunicorn
-app = MyApp()
-
-wsgi_app = app.wsgi_app
-
-if __name__ == "__main__":
-    import threading
-
-    # Start the WebSocket server in a separate thread
-    ws_thread = threading.Thread(target=start_websocket_server, daemon=True)
-    ws_thread.start()
-
-    print("WebSocket server running on ws://localhost:8765")
-
-    # Start the Gunicorn server in a separate process
-    wsgi_process = multiprocessing.Process(target=app.run())
-    wsgi_process.start()
-
-    wsgi_process.join()  # Keep the main process alive
-
diff --git a/examples/pastebin/app.py b/examples/pastebin/app.py
index 97ca3af..866ac4d 100644
--- a/examples/pastebin/app.py
+++ b/examples/pastebin/app.py
@@ -21,14 +21,14 @@ class Root(Server):
             db.set(pid, escape(paste_content))
             db.save()
             return self.redirect(f'/paste/{pid}')
-        return self.render_template('index.html')
+        return await self.render_template('index.html')
 
     async def paste(self, paste_id, delete=None):
         if delete == 'delete':
             db.remove(paste_id)
             db.save()
             return self.redirect('/')
-        return self.render_template(
+        return await self.render_template(
             'paste.html',
             paste_id=paste_id,
             paste_content=db.get(paste_id)
diff --git a/examples/socketio/webcam.py b/examples/socketio/webcam.py
index a094793..8cb7078 100644
--- a/examples/socketio/webcam.py
+++ b/examples/socketio/webcam.py
@@ -10,7 +10,7 @@ active_users = set()
 # MicroPie Server with integrated Socket.IO
 class MyApp(Server):
     async def index(self):
-        return self.render_template("index_stream.html")
+        return await self.render_template("index_stream.html")
 
     async def submit(self, username: str, action: str):
         if username:
@@ -20,10 +20,10 @@ class MyApp(Server):
         return self.redirect("/")
 
     async def stream(self, username: str):
-        return self.render_template("stream.html", username=username) if username in active_users else self.redirect("/")
+        return await self.render_template("stream.html", username=username) if username in active_users else self.redirect("/")
 
     async def watch(self, username: str):
-        return self.render_template("watch.html", username=username) if username in active_users else self.redirect("/")
+        return await self.render_template("watch.html", username=username) if username in active_users else self.redirect("/")
 
 # Socket.IO event handlers
 @sio.event
diff --git a/examples/streaming/livestream.py b/examples/streaming/livestream.py
deleted file mode 100644
index f4a4752..0000000
--- a/examples/streaming/livestream.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import socketio
-import eventlet
-from MicroPie import Server
-
-sio = socketio.Server(cors_allowed_origins="*")
-app = Server()
-active_users = set()
-
-class MyApp(Server):
-    def index(self):
-        return self.render_template('index.html')
-
-    def submit(self, username, action):
-        if username:
-            active_users.add(username)
-            if action == 'Start Streaming':
-                return self.redirect(f'/stream/{username}')
-            elif action == 'Watch Stream':
-                return self.redirect(f'/watch/{username}')
-        return self.redirect('/')
-
-    def stream(self, username):
-        if username not in active_users:
-            return self.redirect('/')
-        return self.render_template('stream.html', username=username)
-
-    def watch(self, username):
-        if username not in active_users:
-            return self.redirect('/')
-        return self.render_template('watch.html', username=username)
-
[email protected]
-def connect(sid, environ):
-    print(f"Client {sid} connected")
-
[email protected]
-def disconnect(sid):
-    print(f"Client {sid} disconnected")
-
[email protected]
-def stream(sid, data):
-    username = data.get('username')
-    frame = data.get('frame')
-    sio.emit('broadcast', {'username': username, 'frame': frame}, skip_sid=sid)
-
-def run_server():
-    eventlet.wsgi.server(eventlet.listen(('0.0.0.0', 5000)), socketio.WSGIApp(sio, MyApp().wsgi_app))
-
-if __name__ == '__main__':
-    run_server()
-
diff --git a/examples/streaming/video1.py b/examples/streaming/video1.py
deleted file mode 100644
index efd0b72..0000000
--- a/examples/streaming/video1.py
+++ /dev/null
@@ -1,87 +0,0 @@
-import os
-from MicroPie import Server
-
-VIDEO_PATH = "video.mp4"
-
-class VideoStreamer(Server):
-    def index(self):
-        """Serve a simple HTML page with a video player."""
-        return '''
-            <html>
-            <body>
-            <center>
-                <video width="640" height="360" controls>
-                    <source src="/stream" type="video/mp4">
-                    Your browser does not support the video tag. Use Chrome for best results.
-                </video>
-            </center>
-            </body>
-            </html>
-        '''
-
-    def stream(self):
-        """
-        Stream the video file with support for range requests (seeking).
-        This will only work in WSGI mode, because the built-in server in
-        MicroPie doesn't handle custom headers for partial-content.
-        """
-        environ = self.environ  # Provided by our updated MicroPie
-        range_header = environ.get('HTTP_RANGE')
-        file_size = os.path.getsize(VIDEO_PATH)
-
-        def generator(start=0, end=None):
-            chunk_size = 1024 * 1024  # 1MB chunks
-            with open(VIDEO_PATH, 'rb') as video:
-                video.seek(start)
-                remaining = end - start if end else file_size - start
-                while remaining > 0:
-                    data = video.read(min(chunk_size, remaining))
-                    if not data:
-                        break
-                    yield data
-                    remaining -= len(data)
-
-        if range_header:
-            try:
-                # Example "Range" header: "bytes=1234-"
-                # or "bytes=1234-5678"
-                range_value = range_header.replace('bytes=', '')
-                start_str, end_str = range_value.split('-')
-                start = int(start_str)
-                end = int(end_str) if end_str else file_size - 1
-
-                # Ensure range is within file bounds
-                if start >= file_size or end >= file_size:
-                    start, end = 0, file_size - 1
-
-                content_length = end - start + 1
-
-                # Return a 3-element tuple with status, body, and custom headers
-                extra_headers = [
-                    ("Content-Range", f"bytes {start}-{end}/{file_size}"),
-                    ("Accept-Ranges", "bytes"),
-                    ("Content-Length", str(content_length)),
-                    ("Content-Type", "video/mp4"),
-                ]
-                return (206, generator(start, end + 1), extra_headers)
-
-            except ValueError:
-                # If the Range header was invalid, just fall back to full video
-                pass
-
-        # Default: return the full video
-        extra_headers = [
-            ("Content-Length", str(file_size)),
-            ("Content-Type", "video/mp4"),
-            ("Accept-Ranges", "bytes"),
-        ]
-        return (200, generator(0, file_size), extra_headers)
-
-# Create your app instance
-app = VideoStreamer()
-
-# The WSGI entry point Gunicorn will look for
-wsgi_app = app.wsgi_app
-
-if __name__ == "__main__":
-    app.run()
diff --git a/examples/streaming/video2.py b/examples/streaming/video2.py
deleted file mode 100644
index 43559cc..0000000
--- a/examples/streaming/video2.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import os
-from MicroPie import Server
-
-VIDEO_PATH = "video.mp4"
-
-class VideoStreamer(Server):
-    def index(self):
-        """Serve the HTML page with a video player."""
-        return '''
-        <html>
-        <body>
-          <center>
-            <video width="640" height="360" controls>
-                <source src="/stream" type="video/mp4">
-                Your browser does not support the video tag.
-            </video>
-          </center>
-        </body>
-        </html>
-        '''
-
-    def stream(self):
-        """Stream the video file in chunks."""
-        def generator():
-            chunk_size = 1024 * 1024  # 1MB chunks
-            try:
-                with open(VIDEO_PATH, 'rb') as video:
-                    while chunk := video.read(chunk_size):
-                        yield chunk
-            except FileNotFoundError:
-                yield b"Video file not found."
-
-        return generator()
-
-app = VideoStreamer()
-wsgi_app = app.wsgi_app
-
-if __name__ == "__main__":
-    app.run()
diff --git a/examples/todolist/app.py b/examples/todolist/app.py
deleted file mode 100644
index fbd8ccc..0000000
--- a/examples/todolist/app.py
+++ /dev/null
@@ -1,147 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-A ToDo List Example Application using the MicroPie framework and pickleDB.
-"""
-
-import os
-from uuid import uuid4
-from MicroPie import Server  # Import our MicroPie framework
-from pickledb import PickleDB
-
-
-# ----------------------------------------------------------------------------
-# Database Setup, for production use a better database like sqlite or kenobi
-# ----------------------------------------------------------------------------
-
-db = PickleDB("todo.db")
-
-def add_item(content, tags):
-    """Add an item to the database. Each document has content, tags, and an id."""
-    item_id = str(uuid4())
-    db.set(item_id, {"content": content, "tags": tags.split(), "id": item_id})
-    db.save()
-
-def matching_tags(tag):
-    """Return all documents with a tag matching the specified arg in reverse order."""
-    return [
-        db.get(key) for key in db.all() if tag in db.get(key).get("tags", [])
-    ][::-1]
-
-def get_all_items():
-    """Retrieve all documents in the database and return them in reverse order."""
-    return [db.get(key) for key in db.all()][::-1]
-
-def get_all_tags():
-    """Return a list of every unique tag in the database."""
-    tags = set()
-    for key in db.all():
-        tags.update(db.get(key).get("tags", []))
-    return list(tags)
-
-def delete_item(item_id):
-    """Delete an item by its id."""
-    db.remove(item_id)
-    db.save()
-
-
-# ----------------------------------------------------------------------------
-# ToDoApp Class
-# ----------------------------------------------------------------------------
-
-class ToDoApp(Server):
-    """Our ToDo application, based on the MicroPie Server."""
-
-    users = {"username": "password"}  # Simple user store
-
-    def login(self):
-        """
-        GET /login -> Display login form
-        POST /login -> Authenticate user and set session
-        """
-        if self.request == "GET":
-            return self.render_template("login.html")
-
-        if self.request == "POST":
-            username = self.body_params.get("username", [""])[0]
-            password = self.body_params.get("password", [""])[0]
-            if self.users.get(username) == password:
-                self.session.update({"logged_in": True, "username": username})
-                return self.redirect("/")
-            return self.render_template("login.html",
-                                        error="Invalid credentials")
-
-    def logout(self):
-        """GET /logout -> Clear session and redirect to login."""
-        self.session.clear()
-        return self.redirect("/login")
-
-    def index(self):
-        """
-        GET / -> Displays all items and tags in index.html.
-        Requires user to be logged in.
-        """
-        if not self.session.get("logged_in"):
-            return self.redirect("/login")
-        return self.render_template(
-            "index.html",
-            seq=get_all_items(),
-            tags=get_all_tags(),
-            username=self.session.get("username"),
-        )
-
-    def add(self):
-        """
-        POST /add -> Add a new item, then redirect to /.
-        Requires user to be logged in.
-        """
-        if not self.session.get("logged_in"):
-            return self.redirect("/login")
-
-        if self.request == "POST":
-            add_item(
-                self.body_params.get("content", [""])[0],
-                self.body_params.get("tags", [""])[0],
-            )
-        return self.redirect("/")
-
-    def delete(self, item_id, redirect_tag=None):
-        """Delete a document
-           id --> the id of the document to be deleted
-           redirect_tag --> if deleted from the tag page redirect back to same
-                            page
-        """
-        if not self.session.get("logged_in"):
-            return self.redirect("/login")
-
-        delete_item(item_id)
-        if redirect_tag:
-            return self.redirect("/tag/{}".format(redirect_tag))
-        else:
-            return self.redirect("/")
-
-    def tag(self, tag_value):
-        """
-        GET /tag/<tag> -> Show items with that tag.
-        Requires user to be logged in.
-        """
-        if not self.session.get("logged_in"):
-            return self.redirect("/login")
-        return self.render_template(
-            "tag.html",
-            tag=tag_value,
-            tag_items=matching_tags(tag_value),
-        )
-
-
-# ----------------------------------------------------------------------------
-# Main
-# ----------------------------------------------------------------------------
-
-app = ToDoApp()
-
-wsgi_app = app.wsgi_app
-
-if __name__ == "__main__":
-    app.run()
-
-
diff --git a/examples/todolist/templates/index.html b/examples/todolist/templates/index.html
deleted file mode 100644
index c6f12ce..0000000
--- a/examples/todolist/templates/index.html
+++ /dev/null
@@ -1,59 +0,0 @@
-<html>
-
-
-<head>
-
-<title>ToDo</title>
-
-<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
-<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
-<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
-
-<style type="text/css">
-body { font-family: "monospace"; width: 540px; margin: 40px auto; }
-h1 { font-size: 65px; text-align: right; }
-a.del { color: #FF0000; border-bottom: 1px dotted #FF0000; text-decoration: none; }
-a:hover.del { color: #FF0000; border-bottom: 1px solid #FF0000; }
-span.c1 { font-size: 18px; }
-span.c3 { font-size: 11px; }
-a.tag { color: #000000; border-bottom: 1px dotted #000000; text-decoration: none; }
-a:hover.tag { color: #000000; border-bottom: 1px solid #0000FF; }
-</style>
-
-</head>
-
-
-<body>
-
-<h1>ToDo List</h1>
-
-<form action="/add" method="POST">
-  <div class="form-group">
-    <label for="new entry">Add new a item to your list:</label>
-    <input type="text" class="form-control" name="content" id="content" placeholder="New ToDo Task">
-    <label for="tags">Add tags, seperated by a whitespace:</label>
-    <input type="text" class="form-control" name="tags" id="tags" placeholder="tag1 tag2 tag3 etc">
-  </div>
-  <button type="submit" class="btn btn-default">Add</button>
-</form>
-<hr>
-<span class="c1">All tags:</span> {% for each in tags %}
-[ <span class="c3"><a href="/tag/{{ each }}">{{ each }}</a></span> ]
-{% endfor %}<hr>
-{% for each in seq %}
-<span class="c1">{{ each.content }}</span>
-<span class="c3">
-<br>Tags:
-{% for x in each.tags %}
-<a class="tag" href="/tag/{{ x }}">{{ x }}</a> /
-{% endfor %}
-<a class="del" href="/delete/{{ each.id }}">Delete item</a>
-</span>
-<hr>
-{% endfor %}
-
-</body>
-
-
-</html>
-
diff --git a/examples/todolist/templates/login.html b/examples/todolist/templates/login.html
deleted file mode 100644
index fb7b25d..0000000
--- a/examples/todolist/templates/login.html
+++ /dev/null
@@ -1,57 +0,0 @@
-<!DOCTYPE html>
-<html>
-
-<head>
-    <title>Login</title>
-
-    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
-    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
-    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
-
-    <style type="text/css">
-        body {
-            font-family: "monospace";
-            width: 540px;
-            margin: 40px auto;
-        }
-
-        h1 {
-            font-size: 65px;
-            text-align: right;
-        }
-
-        .error {
-            color: red;
-        }
-
-        .form-group {
-            margin-bottom: 20px;
-        }
-
-        button {
-            display: block;
-            margin: 0 auto;
-        }
-    </style>
-</head>
-
-<body>
-    <h1>Login</h1>
-    {% if error %}
-    <p class="error">{{ error }}</p>
-    {% endif %}
-    <form method="POST" action="/login">
-        <div class="form-group">
-            <label for="username">Username:</label>
-            <input type="text" class="form-control" id="username" name="username" placeholder="Enter username" required>
-        </div>
-        <div class="form-group">
-            <label for="password">Password:</label>
-            <input type="password" class="form-control" id="password" name="password" placeholder="Enter password" required>
-        </div>
-        <button type="submit" class="btn btn-default">Login</button>
-    </form>
-</body>
-
-</html>
-
diff --git a/examples/todolist/templates/tag.html b/examples/todolist/templates/tag.html
deleted file mode 100644
index da2d7e5..0000000
--- a/examples/todolist/templates/tag.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<html>
-
-
-<head>
-
-<title>ToDo</title>
-
-<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
-<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
-<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
-
-<style type="text/css">
-body { font-family: "monospace"; width: 540px; margin: 40px auto; }
-h1 { font-size: 65px; text-align: right; }
-a.del { color: #FF0000; border-bottom: 1px dotted #FF0000; text-decoration: none; }
-a:hover.del { color: #FF0000; border-bottom: 1px solid #FF0000; }
-span.c1 { font-size: 17px; }
-span.c2 { font-size: 23px; font-weight: bold; }
-span.c3 { font-size: 9px; }
-a.back { color: #000000; border-bottom: none; text-decoration: none; }
-a:hover.back { color: #000000; border-bottom: none; }
-</style>
-
-</head>
-
-
-<body>
-
-<a class="back" href="/"><h1>ToDo List</h1></a>
-<span class="c2">Viewing entries tagged with <em>{{ tag }}</em>:</span>
-<hr>
-{% for each in tag_items %}
-<span class="c1">{{ each.content }}</span>
-<span class="c3"><a class="del" href="/delete/{{ each.id }}/{{ tag }}">Delete</a></span>
-<hr>
-{% endfor %}
-</body>
-
-
-</html>
-
diff --git a/examples/twutr/twutr.py b/examples/twutr/twutr.py
index c8ba804..62b1d76 100644
--- a/examples/twutr/twutr.py
+++ b/examples/twutr/twutr.py
@@ -148,7 +148,7 @@ class Twutr(Server):
     so MicroPie does not treat them as routes.
     """
 
-    def index(self):
+    async def index(self):
         """Shows the user's timeline (the messages of people they follow, including their own)."""
         if not self.session.get('logged_in'):
             return self.redirect('/public')
@@ -157,16 +157,16 @@ class Twutr(Server):
         all_messages = get_all_messages_for_user_and_following(user_id)
         all_messages = sort_messages_by_timestamp(all_messages, timestamp_index=2)
 
-        return self.render_template('timeline.html', messages=all_messages, session=self.session)
+        return await self.render_template('timeline.html', messages=all_messages, session=self.session)
 
-    def public(self):
+    async def public(self):
         """Displays the latest messages of all users with usernames."""
         all_messages = get_all_messages_from_all_users()
         all_messages = sort_messages_by_timestamp(all_messages, timestamp_index=2)
 
-        return self.render_template('public.html', messages=all_messages, session=self.session)
+        return await self.render_template('public.html', messages=all_messages, session=self.session)
 
-    def user(self, username):
+    async def user(self, username):
         """Displays a specific user's messages."""
         logged_in = self.session.get('logged_in')
         current_user = self.session.get('user_id')
@@ -189,7 +189,7 @@ class Twutr(Server):
             followers = user_data.get('followers', [])
             following_count = len(user_data.get('following', []))
 
-            return self.render_template(
+            return await self.render_template(
                 'user.html',
                 messages=messages,
                 username=username,
@@ -225,7 +225,7 @@ class Twutr(Server):
 
         return self.redirect(f'/user/{username}')
 
-    def list_followers(self, username):
+    async def list_followers(self, username):
         """Displays the list of followers for a given user."""
         username = escape(username)
         user_data = get_user_data(username)
@@ -233,14 +233,14 @@ class Twutr(Server):
             return "User not found", 404
 
         followers = user_data.get('followers', [])
-        return self.render_template(
+        return await self.render_template(
             'list_followers.html',
             username=username,
             followers=followers,
             session=self.session
         )
 
-    def list_following(self, username):
+    async def list_following(self, username):
         """Displays the list of users that a given user is following."""
         username = escape(username)
         user_data = get_user_data(username)
@@ -248,7 +248,7 @@ class Twutr(Server):
             return "User not found", 404
 
         following = user_data.get('following', [])
-        return self.render_template(
+        return await self.render_template(
             'list_following.html',
             username=username,
             following=following,
@@ -279,7 +279,7 @@ class Twutr(Server):
 
         return self.redirect('/')
 
-    def login(self):
+    async def login(self):
         """Logs the user in."""
         if self.session.get('logged_in'):
             return self.redirect('/')
@@ -289,19 +289,19 @@ class Twutr(Server):
             password = escape(self.body_params.get('password', [''])[0].strip())
 
             if not username or not password:
-                return self.render_template('login.html', error="Fields cannot be empty", session=self.session)
+                return await self.render_template('login.html', error="Fields cannot be empty", session=self.session)
 
             user = get_user_data(username)
             if not user or user['password'] != password:
-                return self.render_template('login.html', error="Invalid credentials", session=self.session)
+                return await self.render_template('login.html', error="Invalid credentials", session=self.session)
 
             self.session['user_id'] = username
             self.session['logged_in'] = True
             return self.redirect('/')
 
-        return self.render_template('login.html', session=self.session)
+        return await self.render_template('login.html', session=self.session)
 
-    def register(self):
+    async def register(self):
         """Registers a new user."""
         if self.session.get('logged_in'):
             return self.redirect('/')
@@ -311,9 +311,9 @@ class Twutr(Server):
             password = escape(self.body_params.get('password', [''])[0].strip())
 
             if not username or not password:
-                return self.render_template('login.html', error="Fields cannot be empty", session=self.session)
+                return await self.render_template('login.html', error="Fields cannot be empty", session=self.session)
             if db.get(username):
-                return self.render_template('register.html', session=self.session, error="Username already taken.")
+                return await self.render_template('register.html', session=self.session, error="Username already taken.")
 
             db.set(str(username), {
                 'username': username,
@@ -325,7 +325,7 @@ class Twutr(Server):
             db.save()
             return self.redirect('/login')
 
-        return self.render_template('register.html', session=self.session)
+        return await self.render_template('register.html', session=self.session)
 
     def logout(self):
         """Logs the user out."""