switched from http.server to wsgi built in server. this way routing logic is not reapeted twice

Commit 1336795 · patx · 2025-01-24T02:10:16-05:00

Changeset
1336795d4abb8e6fe65ed5a06041d461f8b4a8ec
Parents
f9825d66c9db6182ceb41b64ba0fcf69bb5cbaac

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/MicroPie.py b/MicroPie.py
index 25bf93f..a1aebd2 100644
--- a/MicroPie.py
+++ b/MicroPie.py
@@ -31,13 +31,12 @@ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 """
 
-import http.server
-import os
-import socketserver
+
+from wsgiref.simple_server import make_server
 import time
 import uuid
 import inspect
-from urllib.parse import parse_qs, urlparse
+from urllib.parse import parse_qs
 
 try:
     from jinja2 import Environment, FileSystemLoader
@@ -64,183 +63,24 @@ class Server:
             self.env = Environment(loader=FileSystemLoader("templates"))
 
         self.sessions = {}
-        self.request = None
         self.query_params = {}
         self.body_params = {}
         self.path_params = []
         self.session = {}
-
-        # For WSGI usage, we store environ & start_response here, if needed.
         self.environ = None
         self.start_response = None
 
     def run(self, host="127.0.0.1", port=8080):
         """
-        Start the built-in HTTP server, binding to the specified host and port.
-
-        NOTE: This built-in server does not handle partial-content streaming
-        (Range requests) by default. It's for basic usage and testing.
+        Use Python's built-in WSGI server (wsgiref) for local development,
+        reusing the WSGI app to avoid duplication.
         """
-
-        class DynamicRequestHandler(http.server.SimpleHTTPRequestHandler):
-            """
-            A dynamically generated request handler that dispatches to the
-            Server instance's methods for routing and request processing.
-            """
-
-            def do_GET(self):
-                self._handle_request("GET")
-
-            def do_POST(self):
-                self._handle_request("POST")
-
-            def _serve_static_file(self, path_parts):
-                """
-                Serve static files securely from the 'static' directory.
-
-                :param path_parts: List of path segments for the requested file.
-                """
-                # Resolve the absolute paths for security checks
-                static_dir = os.path.abspath("static")
-                safe_path = os.path.abspath(os.path.join(static_dir, *path_parts[1:]))
-
-                # Prevent directory traversal by ensuring requested path stays inside static/
-                if not safe_path.startswith(static_dir):
-                    self.send_error(403, "Forbidden")
-                    return
-
-                # Check if the file exists and serve it
-                if not os.path.isfile(safe_path):
-                    self.send_error(404, "Static file not found")
-                    return
-
-                try:
-                    # Set appropriate caching headers
-                    self.send_response(200)
-                    self.send_header("Content-Type", self.guess_type(safe_path))
-                    self.send_header("Cache-Control", "public, max-age=86400")  # 1-day cache
-                    self.end_headers()
-
-                    with open(safe_path, "rb") as file:
-                        self.wfile.write(file.read())
-
-                except IOError as e:
-                    print(f"Error handling request: {e}")
-                    self.send_error(500, f"Internal Server Error: {e}")
-
-            def _handle_request(self, method):
-                instance = self.server.instance
-                parsed_path = urlparse(self.path)
-                path_parts = parsed_path.path.strip("/").split("/")
-
-                # Serve static files if requested
-                if path_parts[0] == "static":
-                    self._serve_static_file(path_parts)
-                    return
-
-                func_name = path_parts[0] or "index"
-                func = getattr(instance, func_name, None)
-
-                if not func:
-                    self.send_error(404, "Not Found")
-                    return
-
-                instance.session = instance.get_session(self)
-                instance.request = method
-                instance.query_params = parse_qs(parsed_path.query)
-                instance.path_params = path_parts[1:]
-                instance.body_params = {}
-
-                if method == "POST":
-                    content_length = int(
-                        self.headers.get("Content-Length", 0)
-                    )
-                    body = self.rfile.read(content_length).decode("utf-8")
-                    instance.body_params = parse_qs(body)
-
-                if not instance.validate_request(method):
-                    self.send_error(400, "Invalid Request")
-                    return
-
-                try:
-                    func_args = self._get_func_args(
-                        func,
-                        instance.query_params,
-                        instance.body_params,
-                        instance.path_params,
-                        method
-                    )
-                    response = func(*func_args)
-                    self._send_response(response)
-                except Exception as e:
-                    print(f"Error handling request: {e}")
-                    self.send_error(500, f"Internal Server Error")
-
-            def _get_func_args(self, func, query_params, body_params,
-                               path_params, method):
-                """
-                Build the argument list for the function based on its
-                signature, using path, query, and body parameters as needed.
-                """
-                sig = inspect.signature(func)
-                args = []
-
-                for param in sig.parameters.values():
-                    if path_params:
-                        args.append(path_params.pop(0))
-                    elif method == "GET" and param.name in query_params:
-                        args.append(query_params[param.name][0])
-                    elif method == "POST" and param.name in body_params:
-                        args.append(body_params[param.name][0])
-                    elif param.default is not param.empty:
-                        args.append(param.default)
-                    else:
-                        raise ValueError(
-                            f"Missing required parameter: {param.name}"
-                        )
-                return args
-
-            def _send_response(self, response):
-                """
-                Send HTTP response based on the handler function return value.
-
-                This built-in server does *not* support custom headers or
-                partial content by default. For that, run under WSGI mode.
-                """
-                try:
-                    if isinstance(response, str):
-                        # String response defaults to 200 + text/html
-                        self.send_response(200)
-                        self.send_header("Content-Type", "text/html")
-                        self.end_headers()
-                        self.wfile.write(response.encode("utf-8"))
-                    elif (
-                        isinstance(response, tuple) and len(response) == 2
-                    ):
-                        # (status, body)
-                        status, body = response
-                        self.send_response(status)
-                        self.send_header("Content-Type", "text/html")
-                        self.end_headers()
-                        if isinstance(body, str):
-                            body = body.encode("utf-8")
-                        self.wfile.write(body)
-                    else:
-                        # We only handle str or (status, body) here
-                        self.send_error(500, "Invalid response format")
-                except Exception as e:
-                    print(f"Error sending response: {e}")
-                    self.send_error(500, f"Internal Server Error")
-
-        handler = DynamicRequestHandler
-
-        with socketserver.TCPServer((host, port), handler) as httpd:
-            httpd.instance = self
-            print(f"Serving on http://{host}:{port}")
+        print(f"Serving on http://{host}:{port}")
+        with make_server(host, port, self.wsgi_app) as httpd:
             try:
                 httpd.serve_forever()
             except KeyboardInterrupt:
-                print("\nShutting down...")
+                print("\nShutting down server...")
 
     def get_session(self, request_handler):
         """
@@ -314,6 +154,33 @@ class Server:
             raise ImportError("Jinja2 is not installed.")
         return self.env.get_template(name).render(kwargs)
 
+    def serve_static(self, filepath):
+        """
+        Serve a static file securely from the 'static' directory.
+
+        This function ensures that only files within the 'static' directory
+        can be served, preventing access to other parts of the filesystem.
+
+        Parameters:
+        - filepath: The requested file path relative to the 'static' directory.
+
+        Example usage in route definition:
+        def static(self, filename):
+            return self.serve_static(filename)
+        """
+        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):
         """
         Validate incoming request data for both GET and POST.
diff --git a/README.md b/README.md
index 048ee7e..2d81252 100644
--- a/README.md
+++ b/README.md
@@ -221,11 +221,16 @@ In order to use the `render_template` method you must put your HTML template fil
 </html>
 ```
 
-### **7. Serving Static Files with `run()`**
-MicroPie can serve static files (such as CSS, JavaScript, and images) from a static directory.
+### **7. Serving Static Files**
+MicroPie can serve static files (such as CSS, JavaScript, and images) from a static directory using the built in `serve_static` method. To do this you must define a route you would like to serve your static files from. For example:
+```python
+class Root(Server):
+    def static(self, filename):
+        return self.serve_static(filename)
+```
 
 #### **Setup**
-- Create a directory named `static` in the same location as your MicroPie application.
+- Create a directory named `static` in the same location as your MicroPie application. For saftey the `serve_static` method will only work if `filename` is in the `static` directory.
 - Place your static files (e.g., style.css, script.js, logo.png) inside the static directory.
 
 #### **Accessing Static Files**
@@ -233,7 +238,6 @@ Static files can be accessed via the `/static/` URL path. For example, if you ha
 ```
 http://127.0.0.1:8080/static/style.css
 ```
-Note that this feature is only available for the default `run` method which uses `http.server` and does not currently work with the `wsgi_app` method. An easy work around is to use something like [GitHub Pages](https://pages.github.com/) to serve your static content and keep everything secure. You can also implement static files with other servers like gunicorn + nginx.
 
 ### **8. Streaming Responses and WebSockets**
 
@@ -241,7 +245,7 @@ Note that this feature is only available for the default `run` method which uses
 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.
 
 #### How It Works
-Streaming is supported through the `wsgi_app` method, making it compatible with WSGI servers like **Gunicorn**. When a route handler returns a generator or an iterable (excluding strings), MicroPie automatically streams the response to the client. *Note, streaming is only supported via WSGI:* The built-in `run` method, which relies on Python's `http.server`, does not support streaming responses. Using the built-in server will result in the entire response being buffered before being sent to the client. *Session handling remains functional* when using streaming responses via WSGI, ensuring persistent state across streamed requests.
+Streaming is supported through the `wsgi_app` method, making it compatible with WSGI servers like **Gunicorn** or the built in `run` method. When a route handler returns a generator or an iterable (excluding strings), MicroPie automatically streams the response to the client.
 
 With the following saved as `app.py`:
 ```python
@@ -270,7 +274,6 @@ MicroPie applications can seamlessly integrate **WebSockets** by running a separ
 - **The HTTP server (MicroPie)** handles regular web requests and serves the frontend.
 - **A separate WebSocket server** runs concurrently to handle real-time communication.
 - Clients connect to the WebSocket server via the frontend and exchange messages asynchronously.
-- Since the WebSocket server operates separately, it works regardless of whether the MicroPie HTTP server is running via the built-in `run` method (using `http.server`) or a production-grade WSGI server such as **Gunicorn**.
 - **Threading Considerations:** Since the WebSocket server runs in a separate thread, developers should handle shared resources carefully to avoid concurrency issues.
 - **Port Management:** The WebSocket server must run on a different port than the HTTP server to avoid conflicts.
 - **Client Compatibility:** Ensure that clients support WebSockets when implementing features relying on real-time communication.
@@ -282,7 +285,7 @@ MicroPie applications can seamlessly integrate **WebSockets** by running a separ
 ### Class: Server
 
 #### run(host='127.0.0.1', port=8080)
-Starts the HTTP server with the specified host and port.
+Starts the WSGI server with the specified host and port.
 
 #### get_session(request_handler)
 Retrieves or creates a session for the current request. Sessions are managed via cookies.
@@ -302,6 +305,9 @@ Validates incoming requests for both GET and POST methods based on query and bod
 #### wsgi_app(environ, start_response)
 WSGI-compliant method for parsing requests and returning responses. Ideal for production deployment using WSGI servers.
 
+#### serve_static(filename)
+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 template rendering, session usage, websockets, streaming and form handling.
 
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..7cb130a
--- /dev/null
+++ b/app.py
@@ -0,0 +1,11 @@
+from MicroPie import Server
+
+class root(Server):
+
+    def static(self, filename):
+        return self.serve_static(filename)
+
+    def index(self):
+        return 'hello world'
+
+root().run()
diff --git a/docs/new.html b/docs/new.html
new file mode 100644
index 0000000..cea0bb6
--- /dev/null
+++ b/docs/new.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="shortcut icon" href="https://cherrypy.dev/images/favicon.gif" type="image/x-icon">
+    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;600&family=Roboto:wght@400;700&display=swap" rel="stylesheet">
+    <title>MicroPie - An ultra-micro Python web framework</title>
+    <style>
+        body {
+            font-family: 'Roboto', sans-serif;
+            margin: 0;
+            padding: 0;
+            color: #333;
+            background: #f4f4f4;
+            transition: background 0.3s ease, color 0.3s ease;
+        }
+        .container {
+            max-width: 800px;
+            margin: 50px auto;
+            background: #fff;
+            padding: 40px;
+            border-radius: 15px;
+            box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
+            text-align: center;
+        }
+        h1, h2 {
+            color: #B22222;
+            font-weight: 700;
+        }
+        p {
+            font-size: 18px;
+            line-height: 1.6;
+            color: #555;
+        }
+        a {
+            color: #B22222;
+            text-decoration: none;
+            font-weight: 600;
+            transition: color 0.3s ease;
+        }
+        a:hover {
+            color: #8B0000;
+            border-bottom: 1px dotted #B22222;
+        }
+        pre {
+            background: #272822;
+            color: #f8f8f2;
+            padding: 20px;
+            border-radius: 10px;
+            text-align: left;
+            overflow-x: auto;
+            font-size: 1.1em;
+        }
+        .button {
+            display: inline-block;
+            background: #B22222;
+            color: #fff;
+            padding: 15px 30px;
+            border-radius: 30px;
+            font-size: 18px;
+            font-weight: 600;
+            box-shadow: 0 5px 15px rgba(178, 34, 34, 0.3);
+            transition: all 0.3s ease;
+        }
+        .button:hover {
+            background: white;
+            border: 2px solid #B22222;
+            color: #B22222;
+        }
+        .dark-mode {
+            background: #222;
+            color: #f4f4f4;
+        }
+        .dark-mode .container {
+            background: #333;
+            box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
+        }
+        .dark-mode a {
+            color: #ff6347;
+        }
+        .dark-mode pre {
+            background: #1e1e1e;
+        }
+        .toggle-btn {
+            position: fixed;
+            top: 20px;
+            right: 20px;
+            cursor: pointer;
+            background: #B22222;
+            color: #fff;
+            padding: 10px 20px;
+            border-radius: 20px;
+            font-weight: 600;
+            border: none;
+            outline: none;
+            transition: background 0.3s ease;
+        }
+        .toggle-btn:hover {
+            background: #8B0000;
+        }
+        @media (max-width: 768px) {
+            .container {
+                padding: 20px;
+            }
+            .button {
+                max-width: 100%;
+                text-align: center;
+            }
+        }
+    </style>
+</head>
+<body>
+    <button class="toggle-btn" onclick="toggleDarkMode()">Toggle Dark Mode</button>
+    <div class="container">
+        <div class="logo">
+            <img src="logo.png" alt="MicroPie logo" style="max-width: 100%;">
+        </div>
+        <p><strong>MicroPie is an ultra-lightweight Python web framework</strong> that gets out of your way, letting you build fast and dynamic web apps with ease.
+        Inspired by <a href="https://cherrypy.dev/">CherryPy</a> and licensed under the BSD three-clause license.</p>
+
+        <h2>MicroPie is Fun</h2>
+        <pre><code>
+from MicroPie import Server
+
+class MyApp(Server):
+    def index(self):
+        return 'Hello World!'
+
+MyApp().run()
+        </code></pre>
+
+        <h2>Easy to Install</h2>
+        <pre><code>$ pip install micropie</code></pre>
+
+        <p><br><br>
+        <a href="https://github.com/patx/micropie" class="button">View source code, examples and documentation on GitHub</a>
+        </p>
+    </div>
+    <script>
+        function toggleDarkMode() {
+            document.body.classList.toggle('dark-mode');
+        }
+    </script>
+</body>
+</html>
+
diff --git a/examples/chatroom/app.py b/examples/chatroom/app.py
index c0303a7..7c3948c 100644
--- a/examples/chatroom/app.py
+++ b/examples/chatroom/app.py
@@ -2,7 +2,6 @@ import asyncio
 import websockets
 import multiprocessing
 from MicroPie import Server
-from gunicorn.app.wsgiapp import run as gunicorn_run
 
 # Store connected WebSocket clients
 connected_clients = set()
@@ -70,19 +69,9 @@ def start_websocket_server():
     """Runs the WebSocket server with asyncio.run() in a separate thread."""
     asyncio.run(websocket_server())
 
-def start_gunicorn_server():
-    """Starts the Gunicorn server programmatically."""
-    import sys
-    sys.argv = [
-        "gunicorn",
-        "-w", "4",  # Number of workers
-        "-b", "127.0.0.1:8080",  # Bind to localhost:8080
-        "app:wsgi_app"  # Module:variable format for WSGI app
-    ]
-    gunicorn_run()
-
 # Create WSGI app for Gunicorn
 app = MyApp()
+
 wsgi_app = app.wsgi_app
 
 if __name__ == "__main__":
@@ -95,8 +84,8 @@ if __name__ == "__main__":
     print("WebSocket server running on ws://localhost:8765")
 
     # Start the Gunicorn server in a separate process
-    gunicorn_process = multiprocessing.Process(target=start_gunicorn_server)
-    gunicorn_process.start()
+    wsgi_process = multiprocessing.Process(target=app.run())
+    wsgi_process.start()
 
-    gunicorn_process.join()  # Keep the main process alive
+    wsgi_process.join()  # Keep the main process alive
 
diff --git a/examples/headers/app.py b/examples/headers/app.py
index 1d1f9d2..ccc3811 100644
--- a/examples/headers/app.py
+++ b/examples/headers/app.py
@@ -14,3 +14,6 @@ class Root(Server):
 
 app = Root()
 wsgi_app = app.wsgi_app
+
+if __name__ == "__main__":
+    app.run()
diff --git a/examples/streaming/text.py b/examples/streaming/text.py
index 3226a6d..a020863 100644
--- a/examples/streaming/text.py
+++ b/examples/streaming/text.py
@@ -15,4 +15,4 @@ class Root(Server):
         return generator()
 
 app = Root()
-wsgi_app = app.wsgi_app
+app.run()
diff --git a/examples/streaming/video1.py b/examples/streaming/video1.py
index 9ac67eb..ede6965 100644
--- a/examples/streaming/video1.py
+++ b/examples/streaming/video1.py
@@ -81,4 +81,7 @@ class VideoStreamer(Server):
 app = VideoStreamer()
 
 # The WSGI entry point Gunicorn will look for
-wsgi_app = app.wsgi_app
+wsgi_app = app.wsgi_apps
+
+if __name__ == "__main__":
+    app.run()
diff --git a/examples/streaming/video2.py b/examples/streaming/video2.py
index 351e49c..43559cc 100644
--- a/examples/streaming/video2.py
+++ b/examples/streaming/video2.py
@@ -34,3 +34,6 @@ class VideoStreamer(Server):
 
 app = VideoStreamer()
 wsgi_app = app.wsgi_app
+
+if __name__ == "__main__":
+    app.run()
diff --git a/examples/twutr/twutr.py b/examples/twutr/twutr.py
index 64f9999..83a71da 100644
--- a/examples/twutr/twutr.py
+++ b/examples/twutr/twutr.py
@@ -337,5 +337,5 @@ app = Twutr()
 wsgi_app = app.wsgi_app
 
 if __name__ == "__main__":
-    Twutr().run()
+    app.run()