patx/micropie
improve async-ness, jinja2 async full, aiofiles introduced as codependency with multipart
Commit 9959bf6 · patx · 2025-02-04T02:31:44-05:00
Comments
No comments yet.
Diff
diff --git a/MicroPie.py b/MicroPie.py
index a1e4087..c618989 100644
--- a/MicroPie.py
+++ b/MicroPie.py
@@ -43,7 +43,7 @@ from typing import Any, Awaitable, BinaryIO, Callable, Dict, List, Optional, Tup
from urllib.parse import parse_qs
try:
- from jinja2 import Environment, FileSystemLoader
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
JINJA_INSTALLED = True
except ImportError:
JINJA_INSTALLED = False
@@ -51,6 +51,7 @@ except ImportError:
try:
from multipart import PushMultipartParser, MultipartSegment
MULTIPART_INSTALLED = True
+ import aiofiles
except ImportError:
MULTIPART_INSTALLED = False
@@ -87,7 +88,10 @@ class App:
If Jinja2 is installed, set up the template environment.
"""
if JINJA_INSTALLED:
- self.env: Optional[Environment] = Environment(loader=FileSystemLoader("templates"))
+ self.env: Optional[Environment] = Environment(
+ loader=FileSystemLoader("templates"),
+ autoescape=select_autoescape(["html", "xml"]),
+ enable_async=True)
else:
self.env = None
self.sessions: Dict[str, Any] = {}
@@ -288,7 +292,7 @@ class App:
boundary: The boundary bytes extracted from the Content-Type header.
"""
if not MULTIPART_INSTALLED:
- raise ImportError("Multipart form data not supported. Install `multipart` via pip.")
+ raise ImportError("Multipart form data not supported. Install multipart aiofiles via pip.")
with PushMultipartParser(boundary) as parser:
current_field_name: Optional[str] = None
current_filename: Optional[str] = None
@@ -296,7 +300,7 @@ class App:
current_file: Optional[BinaryIO] = None
form_value: str = ""
upload_directory: str = "uploads"
- os.makedirs(upload_directory, exist_ok=True)
+ await asyncio.to_thread(os.makedirs, upload_directory, exist_ok=True)
while not parser.closed:
chunk: bytes = await reader.read(65536)
for result in parser.parse(chunk):
@@ -311,18 +315,18 @@ class App:
if current_filename:
safe_filename: str = f"{uuid.uuid4()}_{current_filename}"
file_path: str = os.path.join(upload_directory, safe_filename)
- current_file = open(file_path, "wb")
+ current_file = await aiofiles.open(file_path, "wb")
else:
if current_field_name not in self.request.body_params:
self.request.body_params[current_field_name] = []
elif result:
if current_file:
- current_file.write(result)
+ await current_file.write(result)
else:
form_value += result.decode("utf-8", "ignore")
else:
if current_file:
- current_file.close()
+ await current_file.close()
current_file = None
if current_field_name:
self.request.files[current_field_name] = {
@@ -458,11 +462,9 @@ class App:
The rendered template as a string.
"""
if not JINJA_INSTALLED:
- raise ImportError("`_render_template` not available. Install `jinja2` via pip.")
+ raise ImportError("_render_template not available. Install `jinja2` via pip.")
- def render_sync() -> str:
- assert self.env is not None
- return self.env.get_template(name).render(kwargs)
-
- return await asyncio.get_event_loop().run_in_executor(None, render_sync)
+ assert self.env is not None
+ template = self.env.get_template(name)
+ return await template.render_async(**kwargs)
diff --git a/README.md b/README.md
index 6236678..86a5b0d 100644
--- a/README.md
+++ b/README.md
@@ -19,16 +19,16 @@ Install MicroPie via pip:
```bash
pip install micropie
```
-This will install MicroPie along with `jinja2` for template rendering and `multipart` for parsing multipart form data.
+This will install MicroPie along with `jinja2` for template rendering and `multipart`/`aiofiles` for parsing multipart form data.
### **Minimal Setup**
For an ultra-minimalistic approach, download the standalone script:
[MicroPie.py](https://raw.githubusercontent.com/patx/micropie/refs/heads/main/MicroPie.py)
-Place it in your project directory, and you are good to go. Note that `jinja2` must be installed separately to use templates and/or `multipart` for handling file uploads, but this *is* optional:
+Place it in your project directory, and you are good to go. Note that `jinja2` must be installed separately to use templates and/or `multipart` & `aiofiles` for handling file uploads, but this *is* optional:
```bash
-pip install jinja2 multipart
+pip install jinja2 multipart aiofiles
```
### **Install an ASGI Web Server**
diff --git a/docs/api.html b/docs/api.html
index 9aad121..ffaa744 100644
--- a/docs/api.html
+++ b/docs/api.html
@@ -1,78 +1,198 @@
<!DOCTYPE html>
-<html>
+<html lang="en">
<head>
- <title>MicroPie API Documentation</title>
- <meta charset="UTF-8">
- <style>
- body { font-family: Arial, sans-serif; line-height: 1.6; margin: 40px; }
- h1, h2, h3 { color: #333; }
- code { background: #f4f4f4; padding: 2px 5px; }
- pre { background: #f4f4f4; padding: 10px; border-left: 5px solid #333; overflow-x: auto; }
- </style>
+ <meta charset="UTF-8">
+ <title>MicroPie API Documentation</title>
+ <style>
+ body {
+ font-family: Arial, sans-serif;
+ margin: 2em;
+ line-height: 1.6;
+ background-color: #f9f9f9;
+ }
+ h1, h2, h3, h4 {
+ color: #333;
+ }
+ h1 {
+ border-bottom: 2px solid #ccc;
+ padding-bottom: 0.5em;
+ }
+ .class-section {
+ background: #fff;
+ border: 1px solid #ddd;
+ padding: 1em;
+ margin-bottom: 2em;
+ border-radius: 4px;
+ }
+ .method, .attribute {
+ margin-bottom: 1em;
+ }
+ code {
+ background-color: #eee;
+ padding: 0.1em 0.3em;
+ border-radius: 3px;
+ }
+ pre {
+ background: #f4f4f4;
+ border: 1px solid #ddd;
+ padding: 1em;
+ overflow-x: auto;
+ }
+ .param-list {
+ margin: 0.5em 0 0.5em 1em;
+ }
+ </style>
</head>
<body>
- <h1>MicroPie API Documentation</h1>
- <p>A simple Python ultra-micro web framework with ASGI support.</p>
+ <h1>MicroPie API Documentation</h1>
+ <p><em>MicroPie: A simple Python ultra‐micro web framework with ASGI support.</em></p>
+ <p>Documentation for all public and private (prefixed with <code>_</code>) methods.</p>
- <h2>Request Class</h2>
- <p>Represents an HTTP request in the MicroPie framework.</p>
- <pre><code>class Request(scope: Dict[str, Any])</code></pre>
+ <!-- ======================================================== -->
+ <!-- Class: Request -->
+ <!-- ======================================================== -->
+ <div class="class-section" id="class-Request">
+ <h2>Class: Request</h2>
+ <p><strong>Description:</strong> Represents an HTTP request in the MicroPie framework.</p>
+
+ <h3>Attributes</h3>
<ul>
- <li><strong>scope</strong>: The ASGI scope dictionary for the request.</li>
- <li><strong>method</strong>: The HTTP method of the request.</li>
- <li><strong>path_params</strong>: URL path parameters.</li>
- <li><strong>query_params</strong>: URL query parameters.</li>
- <li><strong>body_params</strong>: Request body parameters.</li>
- <li><strong>session</strong>: Dictionary for session data.</li>
- <li><strong>files</strong>: Uploaded files in the request.</li>
+ <li><code>scope</code> (Dict[str, Any]): The ASGI scope dictionary for the request.</li>
+ <li><code>method</code> (str): The HTTP method derived from the scope.</li>
+ <li><code>path_params</code> (List[str]): List of URL path parameters.</li>
+ <li><code>query_params</code> (Dict[str, List[str]]): Dictionary of query string parameters.</li>
+ <li><code>body_params</code> (Dict[str, List[str]]): Dictionary of POST/PUT/PATCH body parameters.</li>
+ <li><code>session</code> (Dict[str, Any]): Dictionary for session data.</li>
+ <li><code>files</code> (Dict[str, Any]): Dictionary for uploaded files.</li>
</ul>
- <h2>App Class</h2>
- <p>ASGI application framework for handling HTTP requests.</p>
- <pre><code>class App()</code></pre>
+ <h3>Methods</h3>
+ <div class="method" id="Request.__init__">
+ <h4><code>__init__(self, scope: Dict[str, Any]) -> None</code></h4>
+ <p><strong>Description:</strong> Initialize a new Request instance.</p>
+ <p><strong>Parameters:</strong></p>
+ <ul class="param-list">
+ <li><code>scope</code> (Dict[str, Any]): The ASGI scope dictionary for the request.</li>
+ </ul>
+ </div>
+ </div>
+
+ <!-- ======================================================== -->
+ <!-- Class: App -->
+ <!-- ======================================================== -->
+ <div class="class-section" id="class-App">
+ <h2>Class: App</h2>
+ <p><strong>Description:</strong> ASGI application for handling HTTP requests and WebSocket connections in MicroPie.</p>
- <h3>Properties</h3>
+ <h3>Class Attributes</h3>
<ul>
- <li><code>request</code> - Retrieve the current request instance.</li>
- <li><code>SESSION_TIMEOUT</code> - Session timeout duration (8 hours by default).</li>
- <li><code>sessions</code> - Dictionary storing active session data.</li>
- <li><code>env</code> - Jinja2 environment for template rendering (if installed).</li>
+ <li><code>SESSION_TIMEOUT</code> (int): Session timeout value (8 hours, expressed in seconds).</li>
</ul>
<h3>Methods</h3>
- <pre><code>async def __call__(self, scope, receive, send)</code></pre>
- <p>ASGI callable interface.</p>
- <pre><code>async def _asgi_app(self, scope, receive, send)</code></pre>
- <p>Handles incoming HTTP requests, processes path parameters, query parameters, request body, and calls the appropriate handler function.</p>
+ <div class="method" id="App.__init__">
+ <h4><code>__init__(self) -> None</code></h4>
+ <p><strong>Description:</strong> Initialize a new App instance. If Jinja2 is installed, sets up the template environment and initializes session storage.</p>
+ </div>
- <pre><code>def _parse_cookies(self, cookie_header: str) -> Dict[str, str]</code></pre>
- <p>Parses cookies from the request header and returns them as a dictionary.</p>
+ <div class="method" id="App.request">
+ <h4><code>request(self) -> Request</code></h4>
+ <p><strong>Description:</strong> Retrieve the current request from the context variable.</p>
+ <p><strong>Returns:</strong> The current <code>Request</code> instance.</p>
+ </div>
- <pre><code>async def _parse_multipart(self, reader, boundary)</code></pre>
- <p>Parses multipart form-data including file uploads.</p>
+ <div class="method" id="App.__call__">
+ <h4><code>__call__(self, scope: Dict[str, Any], receive: Callable[[], Awaitable[Dict[str, Any]]], send: Callable[[Dict[str, Any]], Awaitable[None]]) -> None</code></h4>
+ <p><strong>Description:</strong> ASGI callable interface for the server. This method simply delegates to <code>_asgi_app</code>.</p>
+ <p><strong>Parameters:</strong></p>
+ <ul class="param-list">
+ <li><code>scope</code>: The ASGI scope dictionary.</li>
+ <li><code>receive</code>: The callable to receive ASGI events.</li>
+ <li><code>send</code>: The callable to send ASGI events.</li>
+ </ul>
+ </div>
- <pre><code>async def _send_response(self, send, status_code, body, extra_headers=None)</code></pre>
- <p>Sends an HTTP response with the specified status code, response body, and optional extra headers.</p>
+ <div class="method" id="App._asgi_app">
+ <h4><code>_asgi_app(self, scope: Dict[str, Any], receive: Callable[[], Awaitable[Dict[str, Any]]], send: Callable[[Dict[str, Any]], Awaitable[None]]) -> None</code></h4>
+ <p><strong>Description:</strong> ASGI application entry point for handling HTTP requests.</p>
+ <p><strong>Parameters:</strong></p>
+ <ul class="param-list">
+ <li><code>scope</code>: The ASGI scope dictionary.</li>
+ <li><code>receive</code>: The callable to receive ASGI events.</li>
+ <li><code>send</code>: The callable to send ASGI events.</li>
+ </ul>
+ </div>
- <pre><code>def _cleanup_sessions(self)</code></pre>
- <p>Cleans up expired sessions based on the session timeout value.</p>
+ <div class="method" id="App._parse_cookies">
+ <h4><code>_parse_cookies(self, cookie_header: str) -> Dict[str, str]</code></h4>
+ <p><strong>Description:</strong> Parse the Cookie header and return a dictionary of cookie names and values.</p>
+ <p><strong>Parameters:</strong></p>
+ <ul class="param-list">
+ <li><code>cookie_header</code> (str): The raw Cookie header string.</li>
+ </ul>
+ <p><strong>Returns:</strong> A dictionary mapping cookie names to their corresponding values.</p>
+ </div>
- <pre><code>def _redirect(self, location: str) -> Tuple[int, str]</code></pre>
- <p>Generates an HTTP redirect response to the specified location.</p>
+ <div class="method" id="App._parse_multipart">
+ <h4><code>_parse_multipart(self, reader: asyncio.StreamReader, boundary: bytes) -> None</code></h4>
+ <p><strong>Description:</strong> Parse <code>multipart/form-data</code> from the given reader using the specified boundary.</p>
+ <p><strong>Parameters:</strong></p>
+ <ul class="param-list">
+ <li><code>reader</code> (asyncio.StreamReader): Contains the multipart data.</li>
+ <li><code>boundary</code> (bytes): The boundary bytes extracted from the <code>Content-Type</code> header.</li>
+ </ul>
+ <p><strong>Notes:</strong> Requires the <code>multipart</code> and <code>aiofiles</code> packages to be installed.</p>
+ </div>
- <pre><code>async def _render_template(self, name: str, **kwargs)</code></pre>
- <p>Renders a Jinja2 template asynchronously with the provided context variables.</p>
+ <div class="method" id="App._send_response">
+ <h4><code>_send_response(self, send: Callable[[Dict[str, Any]], Awaitable[None]], status_code: int, body: Any, extra_headers: Optional[List[Tuple[str, str]]] = None) -> None</code></h4>
+ <p><strong>Description:</strong> Send an HTTP response using the ASGI <code>send</code> callable.</p>
+ <p><strong>Parameters:</strong></p>
+ <ul class="param-list">
+ <li><code>send</code>: The ASGI send callable.</li>
+ <li><code>status_code</code> (int): The HTTP status code for the response.</li>
+ <li><code>body</code>: The response body (string, bytes, or generator).</li>
+ <li><code>extra_headers</code> (Optional[List[Tuple[str, str]]]): Optional additional header tuples.</li>
+ </ul>
+ </div>
- <h3>ASGI Request Handling</h3>
- <p>The server class processes incoming ASGI requests in the following manner:</p>
- <ul>
- <li>Extracts the request path and determines the appropriate handler function.</li>
- <li>Parses query parameters and request body.</li>
- <li>Handles cookies and session data.</li>
- <li>Processes multipart form data if necessary.</li>
- <li>Executes the handler function and sends the response back.</li>
- </ul>
+ <div class="method" id="App._cleanup_sessions">
+ <h4><code>_cleanup_sessions(self) -> None</code></h4>
+ <p><strong>Description:</strong> Clean up expired sessions based on the <code>SESSION_TIMEOUT</code> value.</p>
+ </div>
+
+ <div class="method" id="App._redirect">
+ <h4><code>_redirect(self, location: str) -> Tuple[int, str]</code></h4>
+ <p><strong>Description:</strong> Generate an HTTP redirect response.</p>
+ <p><strong>Parameters:</strong></p>
+ <ul class="param-list">
+ <li><code>location</code> (str): The URL to redirect to.</li>
+ </ul>
+ <p><strong>Returns:</strong> A tuple containing the HTTP status code (302) and an HTML body for redirection.</p>
+ </div>
+
+ <div class="method" id="App._render_template">
+ <h4><code>_render_template(self, name: str, **kwargs: Any) -> str</code></h4>
+ <p><strong>Description:</strong> Render a template asynchronously using Jinja2.</p>
+ <p><strong>Parameters:</strong></p>
+ <ul class="param-list">
+ <li><code>name</code> (str): The name of the template file.</li>
+ <li><code>**kwargs</code>: Additional keyword arguments passed to the template.</li>
+ </ul>
+ <p><strong>Returns:</strong> The rendered template as a string.</p>
+ <p><strong>Raises:</strong> <code>ImportError</code> if Jinja2 is not installed.</p>
+ </div>
+
+ </div>
+
+ <!-- ======================================================== -->
+ <!-- Footer -->
+ <!-- ======================================================== -->
+ <footer>
+ <p>© Harrison Erd</p>
+ <p>For more details, visit the <a href="https://patx.github.io/micropie" target="_blank">MicroPie website</a>.</p>
+ </footer>
</body>
</html>
diff --git a/setup.py b/setup.py
index 80de66c..ae3adfe 100644
--- a/setup.py
+++ b/setup.py
@@ -25,7 +25,7 @@ Links
from distutils.core import setup
setup(name="MicroPie",
- version="0.9.5.1",
+ version="0.9.6",
description="A ultra micro web framework w/ Jinja2.",
long_description=__doc__,
author="Harrison Erd",
@@ -46,6 +46,6 @@ setup(name="MicroPie",
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Typing :: Typed"],
py_modules=['MicroPie'],
- install_requires=['jinja2'],
+ install_requires=['jinja2', 'multipart', 'aiofiles'],
)