added support for streaming response type in wsgi_app for use with gunicorn, also added an example app in /examples

Commit f70cf12 · patx · 2025-01-22T11:59:54-05:00

Changeset
f70cf12a38a61314d35beb4f538ffa7def118bac
Parents
cd5462e677b8457bf8f1f42c2d5d77f13fb97a5b

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/MicroPie.py b/MicroPie.py
index 413e5b2..a0539c7 100644
--- a/MicroPie.py
+++ b/MicroPie.py
@@ -338,12 +338,10 @@ class Server:
     def wsgi_app(self, environ, start_response):
         """
         A WSGI-compatible application method that processes incoming requests,
-        manages sessions, and dispatches to the correct handler function.
-
-        :param environ: A dictionary containing CGI-style environment variables.
-        :param start_response: A callback function for starting the response.
-        :return: An iterable of bytes, which represents the response body.
+        manages sessions, and dispatches to the correct handler function,
+        now supporting streaming/generator responses.
         """
+
         path = environ['PATH_INFO'].strip("/")
         method = environ['REQUEST_METHOD']
 
@@ -391,7 +389,6 @@ class Server:
                         if "=" in cookie:
                             key, value = cookie.strip().split("=", 1)
                             cookies[key] = value
-                print("Parsed cookies:", cookies)
                 return cookies
 
             def send_response(self, code):
@@ -399,7 +396,9 @@ class Server:
                 Prepare an HTTP status to be returned in the response. For WSGI,
                 we store these as headers to be added by start_response.
                 """
-                self._headers_to_send.append(('Status', f'{code} OK'))
+                # We won't set 'Status' here because we can do it directly
+                # in start_response. But we could store the code if needed.
+                pass
 
             def send_header(self, key, value):
                 """
@@ -431,7 +430,8 @@ class Server:
             request_handler.send_header('Set-Cookie', f'session_id={session_id}; Path=/; HttpOnly')
             print(f"New session created: {session_id}")
 
-        print(f"Session data after retrieval: {session_id} -> {self.session}")
+        # Print for debug
+        print(f"Session data: {session_id} -> {self.session}")
 
         # Initialize request-related attributes
         self.request = method
@@ -442,10 +442,9 @@ class Server:
             try:
                 content_length = int(environ.get('CONTENT_LENGTH', 0) or 0)
                 body = environ['wsgi.input'].read(content_length).decode('utf-8', 'ignore')
-                # IMPORTANT: Keep as dict of lists (same as parse_qs in built-in server)
+                # parse_qs returns dict of lists
                 self.body_params = parse_qs(body)
-
-                print("POST Data Received:", self.body_params)
+                print("POST data:", self.body_params)
             except Exception as e:
                 start_response('400 Bad Request', [('Content-Type', 'text/html')])
                 return [f"400 Bad Request: {str(e)}".encode('utf-8')]
@@ -453,47 +452,81 @@ class Server:
         try:
             # Find the function to call or return 404 if not found
             handler_function = getattr(self, func_name, None)
-            if handler_function:
-                # Get function signature to determine required arguments
-                sig = inspect.signature(handler_function)
-                func_args = []
+            if not handler_function:
+                start_response('404 Not Found', [('Content-Type', 'text/html')])
+                return [b'404 Not Found']
 
-                for param in sig.parameters.values():
-                    if self.path_params:
-                        # Pull from the URL path (e.g., /delete/123)
-                        func_args.append(self.path_params.pop(0))
-                    elif param.name in self.query_params:
-                        # For GET query parameters
-                        func_args.append(self.query_params[param.name][0])
-                    elif param.name in self.body_params:
-                        # For POST body parameters (dict of lists)
-                        func_args.append(self.body_params[param.name][0])
-                    elif param.default is not param.empty:
-                        # If the param has a default, use it
-                        func_args.append(param.default)
-                    else:
-                        # Missing a required parameter
-                        start_response('400 Bad Request', [('Content-Type', 'text/html')])
-                        msg = f"400 Bad Request: Missing required parameter '{param.name}'"
-                        return [msg.encode('utf-8')]
-
-                # Call the handler with the parameters
-                response = handler_function(*func_args)
-
-                # Handle tuple (status, body) response
-                if isinstance(response, tuple) and len(response) == 2:
-                    status_code, body = response
-                    status_str = f"{status_code} OK"
-                    if status_code == 302:
-                        status_str = f"{status_code} Found"
+            # Build the function args from the signature
+            sig = inspect.signature(handler_function)
+            func_args = []
+
+            for param in sig.parameters.values():
+                if self.path_params:
+                    # Pull from /path/remaining
+                    func_args.append(self.path_params.pop(0))
+                elif param.name in self.query_params:
+                    # For GET query parameters
+                    func_args.append(self.query_params[param.name][0])
+                elif param.name in self.body_params:
+                    # For POST body parameters
+                    func_args.append(self.body_params[param.name][0])
+                elif param.default is not param.empty:
+                    func_args.append(param.default)
                 else:
-                    status_str, body = '200 OK', response
+                    # Missing a required parameter
+                    start_response('400 Bad Request', [('Content-Type', 'text/html')])
+                    msg = f"400 Bad Request: Missing required parameter '{param.name}'"
+                    return [msg.encode('utf-8')]
 
-                start_response(status_str, request_handler._headers_to_send + [('Content-Type', 'text/html')])
-                return [body.encode('utf-8')]
+            # Call the actual endpoint
+            response = handler_function(*func_args)
+
+            # Decide if it's (status, body) or just body
+            if isinstance(response, tuple) and len(response) == 2:
+                status_code, response_body = response
             else:
-                start_response('404 Not Found', [('Content-Type', 'text/html')])
-                return [b'404 Not Found']
+                status_code, response_body = 200, response
+
+            # Convert status_code -> "XXX <Message>"
+            if status_code == 302:
+                status_str = "302 Found"
+            elif status_code == 404:
+                status_str = "404 Not Found"
+            elif status_code == 500:
+                status_str = "500 Internal Server Error"
+            else:
+                status_str = f"{status_code} OK"
+
+            # Attach any headers the user set
+            headers = request_handler._headers_to_send
+            # If you want text/html by default, add it if user didn't
+            if not any(h[0].lower() == 'content-type' for h in headers):
+                headers.append(('Content-Type', 'text/html; charset=utf-8'))
+
+            # --- NEW STREAMING LOGIC ---
+            # Check if the response is a generator or an iterable (non-string).
+            if hasattr(response_body, '__iter__') and not isinstance(response_body, (bytes, str)):
+                # It's a streaming response. We'll return the generator
+                start_response(status_str, headers)
+
+                # Return the generator in a form that yields bytes.
+                def byte_stream(gen):
+                    for chunk in gen:
+                        if isinstance(chunk, str):
+                            yield chunk.encode('utf-8')
+                        else:
+                            yield chunk
+                return byte_stream(response_body)
+            else:
+                # Normal string/bytes response
+                if isinstance(response_body, str):
+                    response_body = response_body.encode('utf-8')
+                elif not isinstance(response_body, (bytes, bytearray)):
+                    # If it's some other object, string-ify it
+                    response_body = str(response_body).encode('utf-8')
+
+                start_response(status_str, headers)
+                return [response_body]
 
         except Exception as e:
             print(f"Error processing request: {e}")
diff --git a/examples/streaming/app.py b/examples/streaming/app.py
new file mode 100644
index 0000000..3226a6d
--- /dev/null
+++ b/examples/streaming/app.py
@@ -0,0 +1,18 @@
+import time
+from MicroPie import Server
+
+class Root(Server):
+    def index(self):
+        # Normal, immediate response (non-streaming)
+        return "Hello from index!"
+
+    def slow_stream(self):
+        # Streaming response using a generator
+        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()
+wsgi_app = app.wsgi_app
diff --git a/examples/twutr/twutr.py b/examples/twutr/twutr.py
index a6f7d0a..5188666 100644
--- a/examples/twutr/twutr.py
+++ b/examples/twutr/twutr.py
@@ -324,6 +324,8 @@ class Twutr(Server):
             self.session.clear()
         return self.redirect('/public')
 
+app = Twutr()
+wsgi_app = app.wsgi_app
 
 if __name__ == "__main__":
     Twutr().run()
diff --git a/setup.py b/setup.py
index 80559c6..4f543ca 100644
--- a/setup.py
+++ b/setup.py
@@ -38,7 +38,7 @@ Links
 from distutils.core import setup
 
 setup(name="MicroPie",
-    version="0.2",
+    version="0.3",
     description="A ultra micro web framework w/ Jinja2.",
     long_description=__doc__,
     author="Harrison Erd",