patx/micropie
update api readme for new multipart parser
Commit 263d889 · patx · 2025-06-10T04:56:51-04:00
Comments
No comments yet.
Diff
diff --git a/README.md b/README.md
index b5e24fc..8349fc0 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ Install MicroPie with all optional dependencies via pip:
```bash
pip install micropie[standard]
```
-This will install MicroPie along with `jinja2` for template rendering, and `multipart`/`aiofiles` for parsing multipart form data.
+This will install MicroPie along with `jinja2` for template rendering, and `multipart` for parsing multipart form data.
If you would like to install **all** optional dependencies (everything from `standard` plus `orjson` and `uvicorn`) you can run:
```bash
@@ -46,9 +46,9 @@ 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 the `_render_template` method and/or `multipart` & `aiofiles` for handling file uploads (the `_parse_multipart` method), but this *is* optional and you can use MicroPie without them. To install the optional dependencies use:
+Place it in your project directory, and you are good to go. Note that `jinja2` must be installed separately to use the `_render_template` method and/or `multipart` for handling file data (the `_parse_multipart` method), but this *is* optional and you can use MicroPie without them. To install the optional dependencies use:
```bash
-pip install jinja2 multipart aiofiles
+pip install jinja2 multipart
```
By default MicroPie will use the `json` library from Python's standard library. If you need faster performance you can use `orjson`. MicroPie *will* use `orjson` *if installed* by default. If it is not installed, MicroPie will fallback to `json`. This means with or without `orjson` installed MicroPie will still handle JSON requests/responses the same. To install `orjson` and take advantage of it's performance, use:
@@ -312,7 +312,7 @@ Represents an HTTP request in the MicroPie framework.
- `body_params`: Dictionary of body parameters.
- `get_json`: JSON request body object.
- `session`: Dictionary of session data.
-- `files`: Dictionary of uploaded files.
+- `files`: Dictionary of multipart data/streamed content.
- `headers`: Dictionary of headers.
## Application Base
@@ -341,10 +341,14 @@ The main ASGI application class for handling HTTP requests in MicroPie.
- `_parse_cookies(cookie_header: str) -> Dict[str, str]`
- Parses the Cookie header and returns a dictionary of cookie names and values.
-- `_parse_multipart(reader: asyncio.StreamReader, boundary: bytes)`
- - Parses multipart/form-data from the given reader using the specified boundary.
- - *Requires*: `multipart` and `aiofiles`
-
+- `_parse_multipart(reader: asyncio.StreamReader, boundary: bytes) -> Tuple[Dict[str, List[str]], Dict[str, Dict[str, Any]]]`
+ - Asynchronously parses multipart/form-data from the given reader using the specified boundary. Returns a tuple of two dictionaries: `form_data` (text fields as key-value pairs) and `files` (file fields with metadata). Each file entry in `files` contains:
+ - `filename`: The original filename of the uploaded file.
+ - `content_type`: The MIME type of the file (defaults to `application/octet-stream`).
+ - `content`: An `asyncio.Queue` containing chunks of file data as bytes, with a `None` sentinel signaling the end of the stream.
+ - Handlers can consume the file data by iterating over the queue (e.g., using `await queue.get()`).
+ - *Requires:* `multipart`
+
- `_send_response(send: Callable[[Dict[str, Any]], Awaitable[None]], status_code: int, body: Any, extra_headers: Optional[List[Tuple[str, str]]] = None) -> None`
- Sends an HTTP response using the ASGI send callable.
diff --git a/examples/file_uploads/app.py b/examples/file_uploads/app.py
index 3efa239..98228a6 100644
--- a/examples/file_uploads/app.py
+++ b/examples/file_uploads/app.py
@@ -1,4 +1,5 @@
import os
+import re
import aiofiles
from MicroPie import App
@@ -6,25 +7,6 @@ UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True) # Ensure directory exists
MAX_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB
-
-class MaxUploadSizeMiddleware(HttpMiddleware):
- async def before_request(self, request):
- # Check if we're dealing with a POST, PUT, or PATCH request
- if request.method in ("POST", "PUT", "PATCH"):
- content_length = request.headers.get("content-length")
- # Make sure the file is not too large
- if int(content_length) > MAX_UPLOAD_SIZE:
- return {
- "status_code": 413,
- "body": "413 Payload Too Large: Uploaded file exceeds size limit."
- }
- # If the check passes, return None to continue processing.
- return None
-
- async def after_request(self, request, status_code, response_body, extra_headers):
- return None
-
-
class Root(App):
async def index(self):
@@ -35,21 +17,28 @@ class Root(App):
</form>"""
async def upload(self, file):
- filename = file["filename"]
- queue = file["content"]
- total_bytes = 0
- filepath = os.path.join(UPLOAD_DIR, filename)
-
- async with aiofiles.open(filepath, "wb") as f:
- while True:
- chunk = await queue.get()
- if chunk is None:
- break
- await f.write(chunk)
- total_bytes += len(chunk)
-
- return 200, f"Uploaded {filename} ({total_bytes} bytes) to {filepath}"
-
+ try:
+ filename = file["filename"]
+ # Sanitize filename
+ safe_filename = re.sub(r'[^\w\.-]', '_', os.path.basename(filename))
+ queue = file["content"]
+ total_bytes = 0
+ filepath = os.path.join(UPLOAD_DIR, safe_filename)
+
+ async with aiofiles.open(filepath, "wb") as f:
+ while True:
+ chunk = await queue.get()
+ if chunk is None:
+ break
+ if total_bytes + len(chunk) > MAX_UPLOAD_SIZE:
+ await aiofiles.os.remove(filepath) # Clean up partial file
+ return 400, "File exceeds maximum size of 100MB"
+ await f.write(chunk)
+ total_bytes += len(chunk)
+
+ return 200, f"Uploaded {safe_filename} ({total_bytes} bytes) to {filepath}"
+ except Exception as e:
+ print(f"Upload error: {e}")
+ return 500, f"Failed to upload {filename}: {str(e)}"
app = Root()
-
diff --git a/pyproject.toml b/pyproject.toml
index 6d2644d..f18e29b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
[project]
name = "MicroPie"
-version = "0.10"
+version = "0.11"
description = "An ultra micro ASGI web framework"
keywords = ["micropie", "asgi", "microframework", "http"]
readme = "README.md"
@@ -18,8 +18,8 @@ classifiers = [
]
[project.optional-dependencies]
-standard = ["jinja2", "multipart", "aiofiles"]
-all = ["jinja2", "multipart", "aiofiles", "orjson", "uvicorn"]
+standard = ["jinja2", "multipart"]
+all = ["jinja2", "multipart", "orjson", "uvicorn"]
[project.urls]
Homepage = "https://patx.github.io/micropie"