removed websocket low level built in support in favor of 3rd party integration, updated readme

Commit e350504 · patx · 2025-01-28T04:26:34-05:00

Changeset
e35050474fc64a39daca8569bc310d60ad5b2703
Parents
70e7bc3fc1541ab3d3c732ab44fff818d92814dd

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/MicroPie.py b/MicroPie.py
index ad02b3d..e422ebc 100644
--- a/MicroPie.py
+++ b/MicroPie.py
@@ -66,29 +66,7 @@ class Server:
     async def asgi_app(self, scope: Dict[str, Any], receive: Any, send: Any) -> None:
         """ASGI application entrypoint for both HTTP and WebSockets."""
 
-        if scope["type"] == "websocket":
-            # Example approach for route-based WebSocket handler lookup:
-            path = scope["path"].lstrip("/")
-            self.scope = scope
-            path_parts = path.split("/") if path else []
-            func_name = path_parts[0] if path_parts else "default"
-            self.path_params = path_parts[1:] if len(path_parts) > 1 else []
-
-            # Use a naming convention such as websocket_<func_name>.
-            ws_handler_name = f"websocket_{func_name}"
-            handler_function = getattr(self, ws_handler_name, None)
-
-            if not handler_function:
-                await self._websocket_default(scope, receive, send)
-                return
-
-            try:
-                await handler_function(scope, receive, send)
-            except Exception:
-                await send({"type": "websocket.close", "code": 1011})
-            return
-
-        elif scope["type"] == "http":
+        if scope["type"] == "http":
             self.scope = scope
             method = scope["method"]
             path = scope["path"].lstrip("/")
@@ -208,11 +186,8 @@ class Server:
                 body=response_body,
                 extra_headers=extra_headers
             )
-
-    async def _websocket_default(self, scope: Dict[str, Any], receive: Any, send: Any):
-        """Default WebSocket handler if no match is found."""
-        await send({"type": "websocket.accept"})
-        await send({"type": "websocket.close", "code": 1000})
+        else:
+            pass
 
     def _parse_cookies(self, cookie_header: str) -> Dict[str, str]:
         cookies: Dict[str, str] = {}
@@ -322,9 +297,6 @@ class Server:
             ],
         })
 
-        #
-        # -- Begin CHUNKED/STREAMING logic --
-        #
         # 1) Check if body is an async generator (has __aiter__)
         if hasattr(body, "__aiter__"):
             async for chunk in body:
@@ -362,9 +334,6 @@ class Server:
             })
             return
 
-        #
-        # -- Fallback for normal (non-generator) body --
-        #
         if isinstance(body, str):
             response_body = body.encode("utf-8")
         elif isinstance(body, bytes):
@@ -418,25 +387,3 @@ class Server:
             content = f.read()
         return 200, content, [("Content-Type", content_type)]
 
-    def validate_request(self, method: str) -> bool:
-        try:
-            if method == "GET":
-                for key, value in self.query_params.items():
-                    if (
-                        not isinstance(key, str)
-                        or not all(isinstance(v, str) for v in value)
-                    ):
-                        return False
-
-            if method == "POST":
-                for key, value in self.body_params.items():
-                    if (
-                        not isinstance(key, str)
-                        or not all(isinstance(v, str) for v in value)
-                    ):
-                        return False
-
-            return True
-        except:
-            return False
-
diff --git a/README.md b/README.md
index d2e6023..f32aab6 100644
--- a/README.md
+++ b/README.md
@@ -220,61 +220,21 @@ http://127.0.0.1:8080/static/style.css
 ### **7. Streaming Responses**
 MicroPie provides support for streaming responses, allowing you to send data to the client in chunks instead of all at once. This is particularly useful for scenarios where data is generated or processed over time, such as live feeds, large file downloads, or incremental data generation.
 
-With the following saved as `app.py`:
-```python
-import time
-
-class Root(Server):
-
-    def index(self):
-        def generator():
-            for i in range(1, 6):
-                yield f"Chunk {i}\n"
-                time.sleep(1)  # Simulate slow processing or data generation
-        return generator()
-
-app = Root()
-```
+Check out the `streaming` folder in the `examples` to see MicroPie's streaming responses in action.
 
 
 ### **8. WebSockets**
-MicroPie offers extremely basic built-in WebSocket support for real-time communication. WebSocket routes are defined with methods starting with `websocket_`. Create a simple WebSocket echo server:
-```python
-class MyApp(Server):
-    async def websocket_echo(self, scope, receive, send):
-        await send({"type": "websocket.accept"})
-        try:
-            while True:
-                message = await receive()
-                if message["type"] == "websocket.receive":
-                    await send({"type": "websocket.send", "text": message["text"]})
-                elif message["type"] == "websocket.disconnect":
-                    break
-        except Exception as e:
-            print(f"WebSocket error: {e}")
-            await send({"type": "websocket.close", "code": 1011})
+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. If you need WebSocket functionality, you’ll need to either:
 
-app = MyApp()
-```
-
-Save the above code as app.py, then run it with uvicorn:
-```python
-uvicorn app:app
-```
-Connect to the WebSocket server using a WebSocket client (e.g., websocat):
-```python
-websocat ws://127.0.0.1:8000/echo
-```
-Type messages in the client to see the server echo them back in real time.
+- Write or integrate your own custom ASGI WebSocket handler, or
+- Use a dedicated library such as Socket.IO or channels with your ASGI server alongside MicroPie.
 
+Check out the `socketio` folder in the `examples` on this repo to see Socket.io integration.
 
 ## **API Reference**
 
 ### Class: Server
 
-#### get_session(request_handler)
-Retrieves or creates a session for the current request. Sessions are managed via cookies.
-
 #### cleanup_sessions()
 Removes expired sessions that have surpassed the timeout period.
 
@@ -288,13 +248,13 @@ Renders a Jinja2 template with provided context variables.
 Serve static files from the `static` directory.
 
 ## **Examples**
-Check out the [examples folder](https://github.com/patx/micropie/tree/main/examples) for more advanced usage, including:
+Check out the [examples folder](https://github.com/patx/micropie/tree/development/examples) for more advanced usage, including:
 - Template rendering
 - Custom HTTP request handling
 - File uploads
 - Session usage
-- Websockets
-- Streaming
+- Websockets with Socket.io
+- Async Streaming
 - Form handling.
 
 ## **Feature Comparison**
@@ -310,6 +270,7 @@ Check out the [examples folder](https://github.com/patx/micropie/tree/main/examp
 | **WSGI Support**    | No (ASGI) | Yes        | Yes       | Yes        | Yes               | No (ASGI)  |
 | **Async Support**   | Yes       | No (Quart) | No        | No         | Limited           | Yes        |
 | **Deployment**      | Simple    | Moderate   | Moderate  | Simple     | Complex           | Moderate   |
+| **Built-in Server** | No        | No         | Yes       | Yes        | Yes               | No         |
 
 
 ## **Suggestions or Feedback?**
diff --git a/examples/socketio/chatroom.py b/examples/socketio/chatroom.py
new file mode 100644
index 0000000..da59e21
--- /dev/null
+++ b/examples/socketio/chatroom.py
@@ -0,0 +1,60 @@
+import socketio
+from MicroPie import Server
+
+# Create a Socket.IO server with CORS support
+sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")  # Allow all origins
+
+# Create the MicroPie server
+class MyApp(Server):
+    def index(self):
+        return """
+        <html>
+        <head>
+            <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
+            <script>
+                var socket = io("http://localhost:8000");
+                socket.on("connect", function() {
+                    console.log("Connected to Socket.IO server");
+                });
+                socket.on("message", function(data) {
+                    document.getElementById("output").innerHTML += data + "<br>";
+                });
+                function sendMessage() {
+                    var message = document.getElementById("message").value;
+                    socket.send(message);
+                    document.getElementById("message").value = "";  // Clear input after sending
+                }
+                window.onbeforeunload = function() {
+                    socket.disconnect();
+                };
+            </script>
+        </head>
+        <body>
+            <h1>Socket.IO Chat</h1>
+            <input type="text" id="message" placeholder="Type a message">
+            <button onclick="sendMessage()">Send</button>
+            <div id="output"></div>
+        </body>
+        </html>
+        """
+
+# Socket.IO event handlers
[email protected]
+async def connect(sid, environ):
+    print(f"Client connected: {sid}")
+
[email protected]
+async def disconnect(sid):
+    print(f"Client disconnected: {sid}")
+
[email protected]
+async def message(sid, data):
+    print(f"Received message from {sid}: {data}")
+    # Broadcast the message to all connected clients
+    await sio.emit("message", f"User: {data}", room=None)
+
+
+
+# Attach Socket.IO to the ASGI app
+app = MyApp()
+asgi_app = socketio.ASGIApp(sio, app)
diff --git a/examples/socketio/templates/index_stream.html b/examples/socketio/templates/index_stream.html
new file mode 100644
index 0000000..b710858
--- /dev/null
+++ b/examples/socketio/templates/index_stream.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Webcam Streaming</title>
+</head>
+<body>
+    <h1>Webcam Streaming App</h1>
+    <form method="post" action="/submit">
+        <input type="text" name="username" placeholder="Enter username" required>
+        <button type="submit" name="action" value="Start Streaming">Start Streaming</button>
+        <button type="submit" name="action" value="Watch Stream">Watch Stream</button>
+    </form>
+</body>
+</html>
+
diff --git a/examples/socketio/templates/stream.html b/examples/socketio/templates/stream.html
new file mode 100644
index 0000000..b4f4e37
--- /dev/null
+++ b/examples/socketio/templates/stream.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+  <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
+  <title>Streaming: {{ username }}</title>
+</head>
+<body>
+  <h1>Streaming as {{ username }}</h1>
+  <video id="webcam" autoplay playsinline></video>
+
+<script>
+  const username = "{{ username }}";
+  const socket = io();
+
+  // Join the room as a streamer
+  socket.emit("join_room", { username });
+
+  socket.on("connect", () => {
+    console.log("Connected as streamer for", username);
+    startWebcam();
+  });
+
+  socket.on("disconnect", () => {
+    console.log("Disconnected");
+  });
+
+  async function startWebcam() {
+    try {
+      const stream = await navigator.mediaDevices.getUserMedia({ video: true });
+      const videoElement = document.getElementById("webcam");
+      videoElement.srcObject = stream;
+
+      const canvas = document.createElement("canvas");
+      const context = canvas.getContext("2d");
+      const track = stream.getVideoTracks()[0];
+      const settings = track.getSettings();
+      canvas.width = settings.width || 640;
+      canvas.height = settings.height || 480;
+
+      setInterval(() => {
+        context.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
+        const frameDataUrl = canvas.toDataURL("image/webp");
+
+        // Send frame to server
+        socket.emit("stream_frame", { username, frame: frameDataUrl });
+      }, 100);
+    } catch (err) {
+      console.error("Error accessing webcam:", err);
+    }
+  }
+</script>
+
+</body>
+</html>
+
diff --git a/examples/socketio/templates/watch.html b/examples/socketio/templates/watch.html
new file mode 100644
index 0000000..e4f432d
--- /dev/null
+++ b/examples/socketio/templates/watch.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+  <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
+  <title>Watching: {{ username }}</title>
+</head>
+<body>
+  <h1>Watching Stream of {{ username }}</h1>
+  <img id="videoFeed" style="width: 100%; max-width: 800px;" />
+
+
+<script>
+  const username = "{{ username }}";
+  const socket = io();
+
+  // Join the room as a watcher
+  socket.emit("join_room", { username });
+
+  socket.on("connect", () => {
+    console.log("Connected as watcher for", username);
+  });
+
+  socket.on("video_frame", (data) => {
+    if (data.username === username) {
+      document.getElementById("videoFeed").src = data.frame;
+    }
+  });
+
+  socket.on("disconnect", () => {
+    console.log("Disconnected");
+  });
+</script>
+
+</body>
+</html>
diff --git a/examples/socketio/webcam.py b/examples/socketio/webcam.py
new file mode 100644
index 0000000..a094793
--- /dev/null
+++ b/examples/socketio/webcam.py
@@ -0,0 +1,64 @@
+import socketio
+from MicroPie import Server
+
+# Create the Socket.IO server
+sio = socketio.AsyncServer(async_mode="asgi")
+
+# Track active users and their watchers/streamers
+active_users = set()
+
+# MicroPie Server with integrated Socket.IO
+class MyApp(Server):
+    async def index(self):
+        return self.render_template("index_stream.html")
+
+    async def submit(self, username: str, action: str):
+        if username:
+            active_users.add(username)
+            route = f"/stream/{username}" if action == "Start Streaming" else f"/watch/{username}"
+            return self.redirect(route)
+        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("/")
+
+    async def watch(self, username: str):
+        return self.render_template("watch.html", username=username) if username in active_users else self.redirect("/")
+
+# Socket.IO event handlers
[email protected]
+async def connect(sid, environ):
+    print(f"Client connected: {sid}")
+
[email protected]
+async def disconnect(sid):
+    print(f"Client disconnected: {sid}")
+
[email protected]("stream_frame")
+async def handle_stream_frame(sid, data):
+    """Broadcast the streamed frame to all watchers."""
+    username = data.get("username")
+    frame = data.get("frame")
+    if username in active_users:
+        # Emit the frame to watchers
+        await sio.emit("video_frame", {"username": username, "frame": frame}, room=username)
+
[email protected]("join_room")
+async def join_room(sid, data):
+    """Add a client to a room (either as a streamer or watcher)."""
+    username = data.get("username")
+    if username in active_users:
+        await sio.enter_room(sid, username)  # Await the method
+        print(f"{sid} joined room for {username}")
+
[email protected]("leave_room")
+async def leave_room(sid, data):
+    """Remove a client from a room."""
+    username = data.get("username")
+    if username in active_users:
+        sio.leave_room(sid, username)
+        print(f"{sid} left room for {username}")
+
+# Attach the Socket.IO server to the ASGI app
+app = MyApp()
+asgi_app = socketio.ASGIApp(sio, app)
diff --git a/examples/streaming/video3.py b/examples/streaming/video.py
similarity index 100%
rename from examples/streaming/video3.py
rename to examples/streaming/video.py
diff --git a/examples/streaming/video1.py b/examples/streaming/video1.py
deleted file mode 100644
index 03f8672..0000000
--- a/examples/streaming/video1.py
+++ /dev/null
@@ -1,83 +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):
-        """
-        Return a tuple that MicroPie interprets as:
-        (status_code, body, [extra_headers])
-        We'll parse the Range header from scope and produce partial or full content.
-        """
-        headers = {
-            k.decode('latin-1').lower(): v.decode('latin-1')
-            for k, v in self.scope.get('headers', [])
-        }
-
-        range_header = headers.get('range')
-        file_size = os.path.getsize(VIDEO_PATH)
-
-        def read_bytes(start=0, end=None) -> bytes:
-            """Synchronous read of requested byte range."""
-            if end is None or end > file_size:
-                end = file_size
-            length = end - start
-            with open(VIDEO_PATH, 'rb') as f:
-                f.seek(start)
-                return f.read(length)
-
-        if range_header:
-            # Typical format: "bytes=1234-" or "bytes=1234-5678"
-            try:
-                byte_range = range_header.replace("bytes=", "")
-                start_str, end_str = byte_range.split("-")
-                start = int(start_str) if start_str else 0
-                end = int(end_str) if end_str else file_size - 1
-
-                if start >= file_size or end >= file_size:
-                    # Out-of-range request, fallback to full content
-                    start, end = 0, file_size - 1
-
-                content_length = (end - start) + 1
-                content = read_bytes(start, end + 1)
-
-                extra_headers = [
-                    ("Content-Range", f"bytes {start}-{end}/{file_size}"),
-                    ("Accept-Ranges", "bytes"),
-                    ("Content-Length", str(content_length)),
-                    ("Content-Type", "video/mp4"),
-                ]
-                return (206, content, extra_headers)
-
-            except ValueError:
-                # Malformed Range header; fallback
-                pass
-
-        # No valid Range header; return full content
-        content = read_bytes(0, file_size)
-        extra_headers = [
-            ("Content-Length", str(file_size)),
-            ("Content-Type", "video/mp4"),
-            ("Accept-Ranges", "bytes"),
-        ]
-        return (200, content, extra_headers)
-
-
-app = VideoStreamer()
-
diff --git a/examples/streaming/video2.py b/examples/streaming/video2.py
deleted file mode 100644
index f1da4bc..0000000
--- a/examples/streaming/video2.py
+++ /dev/null
@@ -1,40 +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()
diff --git a/examples/todolist/app.py b/examples/todolist/app.py
deleted file mode 100644
index c32f52d..0000000
--- a/examples/todolist/app.py
+++ /dev/null
@@ -1,140 +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 Root(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.scope['method'] == "GET":
-            return self.render_template("login.html")
-
-        if self.scope['method'] == "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.scope['method'] == "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 = Root()
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/websockets/MicroPie.py b/examples/websockets/MicroPie.py
new file mode 100644
index 0000000..ad02b3d
--- /dev/null
+++ b/examples/websockets/MicroPie.py
@@ -0,0 +1,442 @@
+"""
+MicroPie: A simple Python ultra-micro web framework with ASGI
+support. https://patx.github.io/micropie
+
+Copyright Harrison Erd
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from this
+   software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+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 inspect
+import os
+import mimetypes
+from urllib.parse import parse_qs
+from typing import Optional, Dict, Any, Union, Tuple, List
+
+try:
+    from jinja2 import Environment, FileSystemLoader
+    JINJA_INSTALLED = True
+except ImportError:
+    JINJA_INSTALLED = False
+
+
+class Server:
+    SESSION_TIMEOUT: int = 8 * 3600  # 8 hours
+
+    def __init__(self) -> None:
+        if JINJA_INSTALLED:
+            self.env = Environment(loader=FileSystemLoader("templates"))
+
+        self.sessions: Dict[str, Dict[str, Any]] = {}
+        self.query_params: Dict[str, List[str]] = {}
+        self.body_params: Dict[str, List[str]] = {}
+        self.path_params: List[str] = []
+        self.session: Dict[str, Any] = {}
+        self.files: Dict[str, Any] = {}
+
+    async def __call__(self, scope, receive, send):
+        await self.asgi_app(scope, receive, send)
+
+    async def asgi_app(self, scope: Dict[str, Any], receive: Any, send: Any) -> None:
+        """ASGI application entrypoint for both HTTP and WebSockets."""
+
+        if scope["type"] == "websocket":
+            # Example approach for route-based WebSocket handler lookup:
+            path = scope["path"].lstrip("/")
+            self.scope = scope
+            path_parts = path.split("/") if path else []
+            func_name = path_parts[0] if path_parts else "default"
+            self.path_params = path_parts[1:] if len(path_parts) > 1 else []
+
+            # Use a naming convention such as websocket_<func_name>.
+            ws_handler_name = f"websocket_{func_name}"
+            handler_function = getattr(self, ws_handler_name, None)
+
+            if not handler_function:
+                await self._websocket_default(scope, receive, send)
+                return
+
+            try:
+                await handler_function(scope, receive, send)
+            except Exception:
+                await send({"type": "websocket.close", "code": 1011})
+            return
+
+        elif scope["type"] == "http":
+            self.scope = scope
+            method = scope["method"]
+            path = scope["path"].lstrip("/")
+            path_parts = path.split("/") if path else []
+            func_name = path_parts[0] if path_parts else "index"
+            self.path_params = path_parts[1:] if len(path_parts) > 1 else []
+
+            handler_function = getattr(self, func_name, None)
+            if not handler_function:
+                self.path_params = path_parts
+                handler_function = getattr(self, "index", None)
+
+            raw_query = scope.get("query_string", b"")
+            self.query_params = parse_qs(raw_query.decode("utf-8", "ignore"))
+
+            headers_dict = {
+                k.decode("latin-1").lower(): v.decode("latin-1")
+                for k, v in scope.get("headers", [])
+            }
+            cookies = self._parse_cookies(headers_dict.get("cookie", ""))
+
+            session_id = cookies.get("session_id")
+            if session_id and session_id in self.sessions:
+                self.session = self.sessions[session_id]
+                self.session["last_access"] = time.time()
+            else:
+                session_id = str(uuid.uuid4())
+                self.session = {"last_access": time.time()}
+                self.sessions[session_id] = self.session
+
+            self.body_params = {}
+            self.files = {}
+            if method in ("POST", "PUT", "PATCH"):
+                body_data = bytearray()
+                while True:
+                    msg = await receive()
+                    if msg["type"] == "http.request":
+                        body_data += msg.get("body", b"")
+                        if not msg.get("more_body"):
+                            break
+                content_type = headers_dict.get("content-type", "")
+                if "multipart/form-data" in content_type:
+                    self.parse_multipart(bytes(body_data), content_type)
+                else:
+                    body_str = body_data.decode("utf-8", "ignore")
+                    self.body_params = parse_qs(body_str)
+
+            sig = inspect.signature(handler_function)
+            func_args = []
+            for param in sig.parameters.values():
+                if self.path_params:
+                    func_args.append(self.path_params.pop(0))
+                elif param.name in self.query_params:
+                    func_args.append(self.query_params[param.name][0])
+                elif param.name in self.body_params:
+                    func_args.append(self.body_params[param.name][0])
+                elif param.name in self.files:
+                    func_args.append(self.files[param.name])
+                elif param.name in self.session:
+                    func_args.append(self.session[param.name])
+                elif param.default is not param.empty:
+                    func_args.append(param.default)
+                else:
+                    await self._send_response(
+                        send,
+                        status_code=400,
+                        body=f"400 Bad Request: Missing required parameter '{param.name}'",
+                    )
+                    return
+
+            if handler_function == getattr(self, "index", None) and not func_args and path:
+                await self._send_response(send, status_code=404, body="404 Not Found")
+                return
+
+            try:
+                if inspect.iscoroutinefunction(handler_function):
+                    result = await handler_function(*func_args)
+                else:
+                    result = handler_function(*func_args)
+            except Exception as e:
+                print(f"Error processing request: {e}")
+                await self._send_response(
+                    send, status_code=500, body="500 Internal Server Error"
+                )
+                return
+
+            status_code = 200
+            response_body = result
+            extra_headers: List[Tuple[str, str]] = []
+
+            if isinstance(result, tuple):
+                if len(result) == 2:
+                    status_code, response_body = result
+                elif len(result) == 3:
+                    status_code, response_body, extra_headers = result
+                else:
+                    await self._send_response(
+                        send, status_code=500,
+                        body="500 Internal Server Error: Invalid response tuple"
+                    )
+                    return
+
+            session_cookie_header = (
+                "Set-Cookie",
+                f"session_id={session_id}; Path=/; HttpOnly; SameSite=Strict"
+            )
+            has_session_cookie = any(
+                h[0].lower() == "set-cookie" and "session_id=" in h[1]
+                for h in extra_headers
+            )
+            if not has_session_cookie:
+                extra_headers.append(session_cookie_header)
+
+            await self._send_response(
+                send,
+                status_code=status_code,
+                body=response_body,
+                extra_headers=extra_headers
+            )
+
+    async def _websocket_default(self, scope: Dict[str, Any], receive: Any, send: Any):
+        """Default WebSocket handler if no match is found."""
+        await send({"type": "websocket.accept"})
+        await send({"type": "websocket.close", "code": 1000})
+
+    def _parse_cookies(self, cookie_header: str) -> Dict[str, str]:
+        cookies: Dict[str, str] = {}
+        if not cookie_header:
+            return cookies
+        for cookie in cookie_header.split(";"):
+            if "=" in cookie:
+                k, v = cookie.strip().split("=", 1)
+                cookies[k] = v
+        return cookies
+
+    def parse_multipart(self, body: bytes, content_type: str) -> None:
+        boundary = None
+        parts = content_type.split(";")
+        for part in parts:
+            part = part.strip()
+            if part.startswith("boundary="):
+                boundary = part.split("=", 1)[1]
+                break
+
+        if not boundary:
+            raise ValueError("Boundary not found in Content-Type header.")
+
+        boundary_bytes = boundary.encode("utf-8")
+        delimiter = b"--" + boundary_bytes
+        sections = body.split(delimiter)
+        for section in sections:
+            if not section or section in (b"--", b"--\r\n"):
+                continue
+            if section.startswith(b"\r\n"):
+                section = section[2:]
+            if section.endswith(b"\r\n"):
+                section = section[:-2]
+            if section == b"--":
+                continue
+
+            try:
+                headers, content = section.split(b"\r\n\r\n", 1)
+            except ValueError:
+                continue
+
+            headers_list = headers.decode("utf-8", "ignore").split("\r\n")
+            header_dict = {}
+            for header_line in headers_list:
+                if ":" in header_line:
+                    key, value = header_line.split(":", 1)
+                    header_dict[key.strip().lower()] = value.strip()
+
+            disposition = header_dict.get("content-disposition", "")
+            disposition_parts = disposition.split(";")
+            disposition_dict = {}
+            for disp_part in disposition_parts:
+                if "=" in disp_part:
+                    k, v = disp_part.strip().split("=", 1)
+                    disposition_dict[k] = v.strip('"')
+
+            name = disposition_dict.get("name")
+            filename = disposition_dict.get("filename")
+
+            if filename:
+                file_content_type = header_dict.get("content-type", "application/octet-stream")
+                self.files[name] = {
+                    "filename": filename,
+                    "content_type": file_content_type,
+                    "data": content
+                }
+            elif name:
+                value = content.decode("utf-8", "ignore")
+                if name in self.body_params:
+                    self.body_params[name].append(value)
+                else:
+                    self.body_params[name] = [value]
+
+    async def _send_response(
+        self,
+        send,
+        status_code: int,
+        body,
+        extra_headers=None
+    ):
+        if extra_headers is None:
+            extra_headers = []
+
+        # Common HTTP status text
+        status_map = {
+            200: "200 OK",
+            206: "206 Partial Content",
+            302: "302 Found",
+            403: "403 Forbidden",
+            404: "404 Not Found",
+            500: "500 Internal Server Error",
+        }
+        # Fallback if not in map
+        status_text = status_map.get(status_code, f"{status_code} OK")
+
+        # Ensure there's a Content-Type unless already provided
+        has_content_type = any(h[0].lower() == "content-type" for h in extra_headers)
+        if not has_content_type:
+            extra_headers.append(("Content-Type", "text/html; charset=utf-8"))
+
+        # Send the initial response start
+        await send({
+            "type": "http.response.start",
+            "status": status_code,
+            "headers": [
+                (k.encode("latin-1"), v.encode("latin-1")) for k, v in extra_headers
+            ],
+        })
+
+        #
+        # -- Begin CHUNKED/STREAMING logic --
+        #
+        # 1) Check if body is an async generator (has __aiter__)
+        if hasattr(body, "__aiter__"):
+            async for chunk in body:
+                if isinstance(chunk, str):
+                    chunk = chunk.encode("utf-8")
+                await send({
+                    "type": "http.response.body",
+                    "body": chunk,
+                    "more_body": True
+                })
+            # Send a final empty chunk to mark the end
+            await send({
+                "type": "http.response.body",
+                "body": b"",
+                "more_body": False
+            })
+            return
+
+        # 2) Check if body is a *sync* generator (has __iter__) and
+        #    is not a plain string/bytes
+        if hasattr(body, "__iter__") and not isinstance(body, (bytes, str)):
+            for chunk in body:
+                if isinstance(chunk, str):
+                    chunk = chunk.encode("utf-8")
+                await send({
+                    "type": "http.response.body",
+                    "body": chunk,
+                    "more_body": True
+                })
+            # Send a final empty chunk
+            await send({
+                "type": "http.response.body",
+                "body": b"",
+                "more_body": False
+            })
+            return
+
+        #
+        # -- Fallback for normal (non-generator) body --
+        #
+        if isinstance(body, str):
+            response_body = body.encode("utf-8")
+        elif isinstance(body, bytes):
+            response_body = body
+        else:
+            # Convert anything else to string then to bytes
+            response_body = str(body).encode("utf-8")
+
+        await send({
+            "type": "http.response.body",
+            "body": response_body,
+            "more_body": False
+        })
+
+    def cleanup_sessions(self) -> None:
+        now = time.time()
+        self.sessions = {
+            sid: data
+            for sid, data in self.sessions.items()
+            if data.get("last_access", now) + self.SESSION_TIMEOUT > now
+        }
+
+    def redirect(self, location: str) -> Tuple[int, str]:
+        return (
+            302,
+            (
+                "<html><head>"
+                f"<meta http-equiv='refresh' content='0;url={location}'>"
+                "</head></html>"
+            ),
+        )
+
+    def render_template(self, name: str, **kwargs: Any) -> str:
+        if not JINJA_INSTALLED:
+            raise ImportError("Jinja2 is not installed.")
+        return self.env.get_template(name).render(kwargs)
+
+    def serve_static(
+        self, filepath: str
+    ) -> Union[Tuple[int, str], Tuple[int, bytes, List[Tuple[str, str]]]]:
+        safe_root = os.path.abspath("static")
+        requested_file = os.path.abspath(os.path.join("static", filepath))
+        if not requested_file.startswith(safe_root):
+            return 403, "403 Forbidden"
+        if not os.path.isfile(requested_file):
+            return 404, "404 Not Found"
+        content_type, _ = mimetypes.guess_type(requested_file)
+        if not content_type:
+            content_type = "application/octet-stream"
+        with open(requested_file, "rb") as f:
+            content = f.read()
+        return 200, content, [("Content-Type", content_type)]
+
+    def validate_request(self, method: str) -> bool:
+        try:
+            if method == "GET":
+                for key, value in self.query_params.items():
+                    if (
+                        not isinstance(key, str)
+                        or not all(isinstance(v, str) for v in value)
+                    ):
+                        return False
+
+            if method == "POST":
+                for key, value in self.body_params.items():
+                    if (
+                        not isinstance(key, str)
+                        or not all(isinstance(v, str) for v in value)
+                    ):
+                        return False
+
+            return True
+        except:
+            return False
+
diff --git a/examples/websockets/__pycache__/MicroPie.cpython-310.pyc b/examples/websockets/__pycache__/MicroPie.cpython-310.pyc
deleted file mode 100644
index 1a68616..0000000
Binary files a/examples/websockets/__pycache__/MicroPie.cpython-310.pyc and /dev/null differ
diff --git a/examples/websockets/__pycache__/app.cpython-310.pyc b/examples/websockets/__pycache__/app.cpython-310.pyc
deleted file mode 100644
index 60c7de6..0000000
Binary files a/examples/websockets/__pycache__/app.cpython-310.pyc and /dev/null differ
diff --git a/examples/websockets/__pycache__/chatroom.cpython-310.pyc b/examples/websockets/__pycache__/chatroom.cpython-310.pyc
deleted file mode 100644
index 5429685..0000000
Binary files a/examples/websockets/__pycache__/chatroom.cpython-310.pyc and /dev/null differ
diff --git a/setup.py b/setup.py
index 6169841..67cd881 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@ MicroPie is Fun
 
 ::
 
-    from MicroPie import Server
+    from MicroPie import
 
     class MyApp(Server):