update all examples to work with asgi

Commit 70e7bc3 · patx · 2025-01-28T02:01:05-05:00

Changeset
70e7bc3fc1541ab3d3c732ab44fff818d92814dd
Parents
f98cfb250a629e37119ab97781c40d0b7c98e93b

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/MicroPie.py b/MicroPie.py
index 2f92931..ad02b3d 100644
--- a/MicroPie.py
+++ b/MicroPie.py
@@ -288,14 +288,15 @@ class Server:
 
     async def _send_response(
         self,
-        send: Any,
+        send,
         status_code: int,
-        body: Union[str, bytes, Any],
-        extra_headers: List[Tuple[str, str]] = None
-    ) -> None:
+        body,
+        extra_headers=None
+    ):
         if extra_headers is None:
             extra_headers = []
 
+        # Common HTTP status text
         status_map = {
             200: "200 OK",
             206: "206 Partial Content",
@@ -304,12 +305,15 @@ class Server:
             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,
@@ -318,25 +322,61 @@ class Server:
             ],
         })
 
-        response_body = b""
+        #
+        # -- 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):
-                    response_body += chunk.encode("utf-8")
-                else:
-                    response_body += chunk
+                    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:
-            if isinstance(body, str):
-                response_body = body.encode("utf-8")
-            elif isinstance(body, bytes):
-                response_body = body
-            else:
-                response_body = str(body).encode("utf-8")
+            # 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,
+            "more_body": False
         })
 
     def cleanup_sessions(self) -> None:
diff --git a/examples/websockets/templates/index.html b/examples/streaming/templates/index.html
similarity index 100%
rename from examples/websockets/templates/index.html
rename to examples/streaming/templates/index.html
diff --git a/examples/wsgi_streaming/templates/stream.html b/examples/streaming/templates/stream.html
similarity index 100%
rename from examples/wsgi_streaming/templates/stream.html
rename to examples/streaming/templates/stream.html
diff --git a/examples/wsgi_streaming/templates/watch.html b/examples/streaming/templates/watch.html
similarity index 100%
rename from examples/wsgi_streaming/templates/watch.html
rename to examples/streaming/templates/watch.html
diff --git a/examples/wsgi_streaming/text.py b/examples/streaming/text.py
similarity index 77%
rename from examples/wsgi_streaming/text.py
rename to examples/streaming/text.py
index 1021600..096aad8 100644
--- a/examples/wsgi_streaming/text.py
+++ b/examples/streaming/text.py
@@ -18,6 +18,3 @@ class Root(Server):
 
 
 app = Root()
-wsgi_app = app.wsgi_app  # Run with `gunicorn text:wsgi_app`
-if __name__ == "__main__":
-    app.run()  # Run with `python3 text.py`
diff --git a/examples/streaming/video.mp4 b/examples/streaming/video.mp4
new file mode 100644
index 0000000..9b89f62
Binary files /dev/null and b/examples/streaming/video.mp4 differ
diff --git a/examples/streaming/video1.py b/examples/streaming/video1.py
new file mode 100644
index 0000000..03f8672
--- /dev/null
+++ b/examples/streaming/video1.py
@@ -0,0 +1,83 @@
+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/wsgi_streaming/video2.py b/examples/streaming/video2.py
similarity index 87%
rename from examples/wsgi_streaming/video2.py
rename to examples/streaming/video2.py
index e0ba7e6..f1da4bc 100644
--- a/examples/wsgi_streaming/video2.py
+++ b/examples/streaming/video2.py
@@ -38,6 +38,3 @@ class VideoStreamer(Server):
 
 
 app = VideoStreamer()
-wsgi_app = app.wsgi_app  # Run with `gunicorn text:wsgi_app`
-if __name__ == "__main__":
-    app.run()  # Run with `python3 video2.py`
diff --git a/examples/streaming/video3.py b/examples/streaming/video3.py
new file mode 100644
index 0000000..fe4601d
--- /dev/null
+++ b/examples/streaming/video3.py
@@ -0,0 +1,74 @@
+import os
+from MicroPie import Server
+
+VIDEO_PATH = "video.mp4"
+
+class Root(Server):
+    def index(self):
+        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>
+        '''
+
+    async def stream(self):
+        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)
+
+        # Decide on start/end
+        start, end = 0, file_size - 1
+        status_code = 200
+        extra_headers = [
+            ("Accept-Ranges", "bytes"),
+            ("Content-Type", "video/mp4"),
+        ]
+
+        if range_header:
+            # e.g. "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:
+                    start, end = 0, file_size - 1
+
+                content_length = end - start + 1
+                extra_headers += [
+                    ("Content-Range", f"bytes {start}-{end}/{file_size}"),
+                    ("Content-Length", str(content_length)),
+                ]
+                status_code = 206
+            except ValueError:
+                # Malformed range; fallback
+                pass
+        else:
+            # Full content
+            extra_headers.append(("Content-Length", str(file_size)))
+
+        # Make an async generator that yields file chunks
+        async def file_chunk_generator(start_pos, end_pos, chunk_size=1024 * 1024):
+            with open(VIDEO_PATH, "rb") as f:
+                f.seek(start_pos)
+                remaining = (end_pos + 1) - start_pos
+                while remaining > 0:
+                    data = f.read(min(chunk_size, remaining))
+                    if not data:
+                        break
+                    yield data
+                    remaining -= len(data)
+
+        return (status_code, file_chunk_generator(start, end), extra_headers)
+
+app = Root()
diff --git a/examples/websockets/__pycache__/MicroPie.cpython-310.pyc b/examples/websockets/__pycache__/MicroPie.cpython-310.pyc
new file mode 100644
index 0000000..1a68616
Binary files /dev/null and b/examples/websockets/__pycache__/MicroPie.cpython-310.pyc differ
diff --git a/examples/websockets/__pycache__/app.cpython-310.pyc b/examples/websockets/__pycache__/app.cpython-310.pyc
new file mode 100644
index 0000000..60c7de6
Binary files /dev/null and b/examples/websockets/__pycache__/app.cpython-310.pyc differ
diff --git a/examples/websockets/__pycache__/chatroom.cpython-310.pyc b/examples/websockets/__pycache__/chatroom.cpython-310.pyc
new file mode 100644
index 0000000..5429685
Binary files /dev/null and b/examples/websockets/__pycache__/chatroom.cpython-310.pyc differ
diff --git a/examples/websockets/chatroom.py b/examples/websockets/chatroom.py
new file mode 100644
index 0000000..ed00b9b
--- /dev/null
+++ b/examples/websockets/chatroom.py
@@ -0,0 +1,118 @@
+from typing import Dict, Set, Any
+from MicroPie import Server
+
+# We track who is "active" (registered a username)
+active_users: Set[str] = set()
+
+# We store a dictionary of channels -> sets of websocket send handles
+# For a single chat room, we only need one channel, e.g. "global".
+watchers: Dict[str, Set[Any]] = {}
+
+class ChatApp(Server):
+    async def index(self):
+        """
+        Serve a simple form where the user enters a username.
+        """
+        return self.render_template("index_chat.html")
+
+    async def submit(self, username: str):
+        """
+        Handle the POST from index.html where user enters their name.
+        Then redirect them to /chat/<username>.
+        """
+        username = username.strip()
+        if not username:
+            return self.redirect("/")  # Invalid username, go back
+
+        # Mark this user as active
+        active_users.add(username)
+        return self.redirect(f"/chat/{username}")
+
+    async def chat(self, username: str):
+        """
+        Show the chat page if the user is active; otherwise, redirect home.
+        """
+        if username not in active_users:
+            return self.redirect("/")
+        return self.render_template("chat.html", username=username)
+
+    #
+    # ------------- WEBSOCKET HANDLER -------------
+    #
+    async def websocket_chat(self, scope, receive, send):
+        """
+        If the path is /chat/<username>, MicroPie calls this method
+        because 'websocket_{pathParts[0]}' = 'websocket_chat'.
+        """
+        # Extract the <username> from self.path_params
+        username = self.path_params[0] if self.path_params else None
+
+        # 1) Make sure username is valid and active
+        if not username or username not in active_users:
+            return await self._reject_websocket(send)
+
+        # 2) Wait for 'websocket.connect' before accepting
+        msg = await receive()
+        if msg["type"] == "websocket.connect":
+            await send({"type": "websocket.accept"})
+        else:
+            # If we didn’t get the connect message, close
+            return await self._reject_websocket(send)
+
+        # 3) Add this connection to watchers["global"]
+        watchers.setdefault("global", set()).add(send)
+
+        # 4) Handle incoming messages until the client disconnects
+        try:
+            while True:
+                message = await receive()
+                if message["type"] == "websocket.receive":
+                    # If there's text, broadcast it
+                    if "text" in message:
+                        text_msg = message["text"]
+                        await self._broadcast("global", f"{username}: {text_msg}", is_binary=False)
+                    elif "bytes" in message:
+                        byte_data = message["bytes"]
+                        await self._broadcast("global", byte_data, is_binary=True)
+                elif message["type"] == "websocket.disconnect":
+                    break
+        finally:
+            # Remove the user's send handle from watchers["global"]
+            watchers["global"].discard(send)
+            # If watchers["global"] is empty, remove the channel
+            if not watchers["global"]:
+                del watchers["global"]
+
+    #
+    # ------------- HELPER METHODS -------------
+    #
+    async def _reject_websocket(self, send):
+        """
+        Accept then immediately close with a custom code.
+        This is a simple approach to gracefully reject a WebSocket.
+        """
+        await send({"type": "websocket.accept"})
+        await send({"type": "websocket.close", "code": 4000})
+
+    async def _broadcast(self, channel: str, data, is_binary: bool = False):
+        """
+        Broadcast 'data' to all watchers in the given channel.
+        """
+        for ws_send in list(watchers.get(channel, [])):
+            try:
+                await ws_send({
+                    "type": "websocket.send",
+                    "bytes" if is_binary else "text": data
+                })
+            except:
+                # If sending fails, remove that watcher's socket
+                watchers[channel].discard(ws_send)
+                if not watchers[channel]:
+                    del watchers[channel]
+
+# Create the ASGI app instance
+app = ChatApp()
+
+# If you want to run via uvicorn:
+#   uvicorn chat_app:app --host 127.0.0.1 --port 5000
+
diff --git a/examples/websockets/templates/chat.html b/examples/websockets/templates/chat.html
new file mode 100644
index 0000000..4850fe1
--- /dev/null
+++ b/examples/websockets/templates/chat.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8" />
+  <title>MicroPie Chat - {{ username }}</title>
+  <style>
+    body {
+      font-family: sans-serif;
+      margin: 20px;
+    }
+    #chatBox {
+      border: 1px solid #ccc;
+      width: 500px;
+      height: 300px;
+      overflow-y: auto;
+      padding: 10px;
+      margin-bottom: 10px;
+    }
+    #messageInput {
+      width: 400px;
+    }
+  </style>
+</head>
+<body>
+  <h1>Chat Room (User: {{ username }})</h1>
+
+  <div id="chatBox"></div>
+  <input id="messageInput" type="text" placeholder="Type a message..." />
+  <button id="sendBtn">Send</button>
+
+  <script>
+    const username = "{{ username }}";
+    const chatBox = document.getElementById("chatBox");
+    const messageInput = document.getElementById("messageInput");
+    const sendBtn = document.getElementById("sendBtn");
+
+    // Build the WebSocket URL, using the current host but ws:// or wss://
+    const wsProto = (location.protocol === "https:") ? "wss://" : "ws://";
+    const wsUrl = wsProto + window.location.host + "/chat/" + encodeURIComponent(username);
+
+    const socket = new WebSocket(wsUrl);
+
+    socket.onmessage = (event) => {
+      // Display the new message
+      const msgDiv = document.createElement("div");
+      msgDiv.textContent = event.data;
+      chatBox.appendChild(msgDiv);
+      chatBox.scrollTop = chatBox.scrollHeight;
+    };
+
+    sendBtn.onclick = () => {
+      sendMessage();
+    };
+    messageInput.addEventListener("keydown", (e) => {
+      if (e.key === "Enter") sendMessage();
+    });
+
+    function sendMessage() {
+      const text = messageInput.value.trim();
+      if (text) {
+        socket.send(text);
+        messageInput.value = "";
+      }
+    }
+  </script>
+</body>
+</html>
+
diff --git a/examples/websockets/templates/index_chat.html b/examples/websockets/templates/index_chat.html
new file mode 100644
index 0000000..9422898
--- /dev/null
+++ b/examples/websockets/templates/index_chat.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8" />
+  <title>MicroPie Chat - Home</title>
+</head>
+<body>
+  <h1>Welcome to MicroPie Chat!</h1>
+  <form method="post" action="/submit">
+    <label for="username">Enter a username:</label>
+    <input id="username" name="username" type="text" />
+    <button type="submit">Enter Chat</button>
+  </form>
+</body>
+</html>
+
diff --git a/examples/wsgi_streaming/templates/index.html b/examples/websockets/templates/index_stream.html
similarity index 100%
rename from examples/wsgi_streaming/templates/index.html
rename to examples/websockets/templates/index_stream.html
diff --git a/examples/websockets/app.py b/examples/websockets/webcam.py
similarity index 94%
rename from examples/websockets/app.py
rename to examples/websockets/webcam.py
index 6aab18d..077c6d2 100644
--- a/examples/websockets/app.py
+++ b/examples/websockets/webcam.py
@@ -9,7 +9,7 @@ watchers: Dict[str, Set[Any]] = {}
 class MyApp(Server):
 
     async def index(self):
-        return self.render_template("index.html")
+        return self.render_template("index_stream.html")
 
     async def submit(self, username: str, action: str):
         if username:
@@ -73,8 +73,3 @@ class MyApp(Server):
 
 # Create the ASGI app
 app = MyApp()
-
-if __name__ == "__main__":
-    import uvicorn
-    uvicorn.run(app, host="0.0.0.0", port=5000, reload=True)
-
diff --git a/examples/wsgi_streaming/chatroom.py b/examples/wsgi_streaming/chatroom.py
deleted file mode 100644
index 5c7b13b..0000000
--- a/examples/wsgi_streaming/chatroom.py
+++ /dev/null
@@ -1,89 +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())
-
-app = MyApp()
-
-
-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/wsgi_streaming/livestream.py b/examples/wsgi_streaming/livestream.py
deleted file mode 100644
index f4a4752..0000000
--- a/examples/wsgi_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/wsgi_streaming/video1.py b/examples/wsgi_streaming/video1.py
deleted file mode 100644
index 9a07db6..0000000
--- a/examples/wsgi_streaming/video1.py
+++ /dev/null
@@ -1,88 +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)
-
-
-
-
-app = VideoStreamer()
-wsgi_app = app.wsgi_app  # Run with `gunicorn video1:wsgi_app`
-if __name__ == "__main__":
-    app.run()  # Run with `python3 video1.py`
diff --git a/setup.py b/setup.py
index c861616..6169841 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ MicroPie is Fun
         def index(self):
             return 'Hello world!'
 
-    MyApp().run()
+    app = MyApp()  # Run with `uvicorn app:app`
 
 
 Links
@@ -25,7 +25,7 @@ Links
 from distutils.core import setup
 
 setup(name="MicroPie",
-    version="0.7",
+    version="0.8",
     description="A ultra micro web framework w/ Jinja2.",
     long_description=__doc__,
     author="Harrison Erd",