Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ toolchain:
.PHONY: build
build: $(MPY_TARGETS) $(INIT_TARGETS)
@mkdir -p $(BUILD_DIR)
$(MAKE) docs

# Compile .py -> .mpy
$(BUILD_DIR)/%.mpy: $(SRC_DIR)/%.py
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ from pyrobusta.server.http_server import HttpServer

async def main():
server = HttpServer()
asyncio.create_task(server.start_socket_server())
server.start_socket_server()
while True:
await asyncio.sleep(1)

Expand Down Expand Up @@ -95,7 +95,7 @@ def mem_usage(http_ctx, _):

async def main():
server = http_server.HttpServer()
asyncio.create_task(server.start_socket_server())
server.start_socket_server()
while True:
await asyncio.sleep(1)

Expand Down
22 changes: 13 additions & 9 deletions docs/application_development/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[← Back](index.md)

This chapter introduces the architectural concepts and design principles that underpin PyRobusta.
Detailed information about configuration, routing, and request handling is covered in the correspondingchapters.
Detailed information about configuration, routing, and request handling is covered in the corresponding chapters.

---

Expand All @@ -28,7 +28,7 @@ long-term reliability on devices with limited RAM.
## System Overview

PyRobusta is organized into a small number of runtime components, each with well-defined responsibilities.
`HttpServer` admits new connections and provisions stream buffers from a shared resource pool.
`HttpServer` accepts new connections and provisions stream buffers from a shared resource pool.
`HttpConnection` manages the lifetime of a client connection, including request and response streams and socket I/O.
`HttpEngine` dispatches requests and ensures protocol correctness with incremental request
parsing.
Expand All @@ -38,7 +38,7 @@ Together, these components isolate networking, protocol processing, and applicat
flowchart LR
server["HttpServer<br>(Connection Admission)"] --> con["HttpConnection<br>(Connection Management)<br>"]
con --> parser["HttpEngine<br>(Request Processing)"]
parser --> app["Application Handler"]
parser --> app["Route Handler"]
```

### Components
Expand All @@ -47,7 +47,7 @@ flowchart LR
| HttpServer | Connection management, stream buffer provisioning |
| HttpConnection | Socket I/O, connection lifecycle (keep-alive, timeout) |
| HttpEngine | Incremental stream parsing, HTTP protocol validation, request routing |
| Application Handler | Application-specific processing, response generation |
| Route Handler | Application-specific processing, response generation |

## Execution Model

Expand All @@ -56,7 +56,7 @@ PyRobusta is based on the uasyncio I/O scheduler implemented in MicroPython.
and associates each connection with a `StreamReader` and `StreamWriter` pair for network I/O.

Each client connection executes as an independent asynchronous task, allowing multiple connections
to make progress concurrently. `HttpServer` maintains the set of active connections and admits
to make progress concurrently. `HttpServer` maintains the set of active connections and accepts
new connections until the configured maximum number of concurrent connections is reached. `HttpServer`
closes idle connections after the configured timeout to reclaim resources. Because each connection is
processed independently, slow clients do not block unrelated connections. However, requests within
Expand Down Expand Up @@ -97,7 +97,7 @@ sequenceDiagram
PyRobusta ensures bounded memory usage by allocating a fixed number of per-connection stream buffer pairs from
a shared buffer pool. The maximum number of concurrent connections determines the size of this pool.
**Because stream buffers have a fixed size, memory usage per connection is bounded. Memory required
for normal request processing is reserved when a connection is admitted, rather than during request handling.**
for normal request processing is reserved when a connection is accepted, rather than during request handling.**

For each new connection, the server reserves a request and response stream buffer from the shared buffer pool.
When all stream buffers are reserved, new connections are blocked until a buffer pair becomes available
Expand All @@ -111,8 +111,8 @@ fragmentation, improving memory stability and reducing the risk of runtime alloc
Memoryviews share the same underlying bytearray with stream buffers used for socket I/O, allowing application code
to access request bodies without copying the underlying data.

Incremental request parsing requires buffers that support incremental consumption of parsed data while new bytes
continue to arrive. PyRobusta therefore uses stream buffers designed for incremental processing rather than
Incremental request parsing requires stream buffers that allow consumed data to be discarded while additional
bytes continue to arrive. PyRobusta therefore uses stream buffers designed for incremental processing rather than
fixed byte buffers without consumption semantics.

Request processing is constrained by the size of the per-connection stream buffer. While request lines and header
Expand All @@ -130,14 +130,18 @@ processed without buffering the complete request body in memory.**
### Ownership
* Every request is processed by exactly one `HttpConnection`.
* Every request is dispatched through `HttpEngine`.
* Connections are admitted only when the required stream buffers are available.
* Connections are accepted only when the required stream buffers are available.
* Each active connection owns a dedicated request and response stream buffer.

### Processing
* For streaming request bodies, protocol parsing and application callbacks may be interleaved, allowing data to be processed incrementally.
* Application callbacks are invoked only after the corresponding protocol state has been successfully validated.
* All protocol errors are converted into HTTP error responses.

### Resource Usage
* Stream buffers are allocated during server initialization.
* Memory usage remains bounded by the configured number and size of stream buffers.

Together, these architectural decisions enable PyRobusta to handle HTTP requests with predictable resource usage,
incremental request processing, and deterministic behavior suitable for resource-constrained embedded systems.

Expand Down
2 changes: 1 addition & 1 deletion docs/application_development/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Then continue with the remaining guides in the order listed below.

## Available Resources

* [Introduction](introduction.md)
* [Getting Started](introduction.md)
* [Concepts](concepts.md)
* [Configuration](configuration.md)
* [Routing](routing.md)
Expand Down
90 changes: 20 additions & 70 deletions docs/application_development/introduction.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Introduction
# Getting Started

[← Back](index.md)

Expand All @@ -8,84 +8,47 @@ This page provides practical examples for using your server.

## Table of Contents

* [Introduction](#introduction)
* [Getting Started](#getting-started)
+ [Demo Application](#demo-application)
+ [Deployment with mpremote](#deployment-with-mpremote)

---

## Demo Application

The following application demonstrates common use cases for handling headers, status codes, query parameters, and wildcard routes.

1. **/version** returns the version of the application.

The server version can optionally be included in the response by setting the detailed query parameter to true.
2. **/app/version** or **/server/version** returns the designated version string, handled by a single route handler with a wildcard URL.
The following application demonstrates the basic structure of a PyRobusta application, including route registration,
response generation, and server initialization. The application implements a simple HTTP API that returns the application version.
The example includes boot.py, which starts the application, and app.py, which registers routes and starts the HTTP server.

```
# /app.py

import asyncio

from pyrobusta.server import http_server
from pyrobusta.protocol.http import HttpEngine
from pyrobusta.utils.config import PYROBUSTA_VERSION

APP_VERSION = "v0.0.1"
APP_VERSION = "v0.1.0"

@HttpEngine.route("/version", "GET")
def version(http_ctx, _):
include_server_version = False

if http_ctx.query:
is_detailed = http_ctx.get_query_param(
"detailed", default="false"
).lower()

if is_detailed not in ("true", "false"):
http_ctx.terminate(400)
return "text/plain", "Invalid query"

include_server_version = is_detailed == "true"

if http_ctx.headers.get("accept") == "application/json":
version_dict = {"app_version": APP_VERSION}
if include_server_version:
version_dict["server_version"] = PYROBUSTA_VERSION
return "application/json", version_dict

version_text = f"app_version: {APP_VERSION}\n"
if include_server_version:
version_text += f"server_version: {PYROBUSTA_VERSION}\n"
return "text/plain", version_text

@HttpEngine.route("/{app_or_server}/version", "GET")
def version(http_ctx, _):
resource = http_ctx.path_segment(0)

if resource not in ("app", "server"):
http_ctx.terminate(404)
return "text/plain", "Not found"
return "application/json", {
"version": APP_VERSION
}

version_string = APP_VERSION if resource == "app" else PYROBUSTA_VERSION

if http_ctx.headers.get("accept") == "application/json":
return "application/json", {"version": version_string}

return "text/plain", f"{version_string}\n"
return "text/plain", f"{APP_VERSION}\n"

async def main():
server = http_server.HttpServer()
asyncio.create_task(server.start_socket_server())
server.start_socket_server()

while True:
await asyncio.sleep(1)
```

```
# /boot.py
# This file is executed on every boot

# This file is executed on every boot (including wake-boot from deepsleep)
import machine
from os import listdir

Expand All @@ -99,9 +62,12 @@ if connected and not machine.reset_cause() == machine.SOFT_RESET:
asyncio.run(app.main())
```

In the example, `boot.py` conditionally starts the server when no REPL session is active.
This allows `mpremote` to connect after a soft reset and upload files during development.

## Deployment with mpremote

Perform a soft reset and upload app.py and boot.py using mpremote.
Perform a soft reset and upload app.py and boot.py using `mpremote`.

```
$ mpremote a0 soft-reset
Expand Down Expand Up @@ -129,26 +95,10 @@ Use curl to test the application.

```
$ curl "http://192.168.1.101/version"
app_version: v0.0.1

$ curl "http://192.168.1.101/version?detailed=True"
app_version: v0.0.1
server_version: v0.5.0

$ curl -H "Accept: application/json" "http://192.168.1.101/version?detailed=True"
{"server_version": "v0.5.0", "app_version": "v0.0.1"}

$ curl "http://192.168.1.101/app/version"
v0.0.1

$ curl "http://192.168.1.101/server/version"
v0.5.0

$ curl -H "Accept: application/json" "http://192.168.1.101/server/version"
{"version": "v0.5.0"}
v0.1.0

$ curl 192.168.1.101/application/version
Not found
$ curl -H "Accept: application/json" "http://192.168.1.101/version"
{"version": "v0.1.0"}
```

---
Expand Down
61 changes: 28 additions & 33 deletions docs/application_development/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,15 @@ def query_param_handler(http_ctx, _):

is_detailed = False

if http_ctx.query:
param = http_ctx.get_query_param(
"detailed", default="false"
).lower()
param = http_ctx.get_query_param(
"detailed", default="false"
).lower()

if param not in ("true", "false"):
http_ctx.terminate(400)
return "text/plain", "Invalid query"
if param not in ("true", "false"):
http_ctx.terminate(400)
return "text/plain", "Invalid query"

is_detailed = param == "true"
is_detailed = param == "true"

resource = "resource content\n"
if is_detailed:
Expand Down Expand Up @@ -142,28 +141,34 @@ def upload_chunks(http_ctx, payload: bytes):

async def main():
server = http_server.HttpServer()
asyncio.create_task(server.start_socket_server())
await server.start_socket_server()
while True:
await asyncio.sleep(1)
```

## Multipart Requests

Multipart requests allow clients to send composite payloads with
the option of varying content metadata or multiple resources in a single request.
Similar to streamed requests, multipart requests are processed one part at a time.
The route handler is invoked once for each part. Unlike regular request bodies, multipart
parts are parsed by the server before being passed to the application. Peprocessed parts
consist of headers (dictionary) and the raw part body (bytes), passed as a tuple.
Multipart requests allow clients to send multiple data parts within a single HTTP request.
Multipart requests are delivered to the application one part at a time.
The same route handler instance is invoked for each multipart segment of the same request.
Unlike regular request bodies, multipart parts are parsed by the server before being passed
to the application. Preprocessed parts consist of headers (dictionary) and the raw part body
(bytes), passed as a tuple.

**Multipart state tracking**
The HTTP context exposes the boolean attributes
`mp_is_first` and
`mp_is_last`
to identify the first and final part of a multipart request. This allows stateful processing
of multiple parts belonging to the same request.
to identify the first and final part of a multipart request. This allows the handler to maintain per-request
state across multiple parts of the same multipart request.

```
"""
This example demonstrates end-to-end processing of a multipart
upload using request-scoped temporary file buffering.
"""

import asyncio
from os import listdir, remove, rename, mkdir

import pyrobusta.server.http_server as http_server
Expand All @@ -172,25 +177,14 @@ from pyrobusta.utils.helpers import normalize_path

@HttpEngine.route("/app/parts", "POST")
def handle_parts(http_ctx, payload: tuple):
"""
Route handler for demonstrating multipart processing.
"""
if not http_ctx.is_multipart():
http_ctx.terminate(400)
return "text/plain", "Bad request"

part_headers, part_body = payload
return "text/plain", "Multipart request required"

tmp_dir = normalize_path("/tmp")
tmp_path = normalize_path("/tmp/parts.txt")
target_path = normalize_path("/www/user_data/parts.txt")
_part_headers, part_body = payload

# Clean stale partial uploads
if http_ctx.mp_is_first:
if not "tmp" in listdir(normalize_path("/")):
mkdir(tmp_dir)
if "parts.txt" in listdir(tmp_dir):
remove(tmp_path)
tmp_path = normalize_path(f"/tmp/parts.{http_ctx.id}.txt")
target_path = normalize_path(f"/www/user_data/parts.{http_ctx.id}.txt")

with open(tmp_path, "ab") as f:
f.write(part_body)
Expand All @@ -202,8 +196,9 @@ def handle_parts(http_ctx, payload: tuple):
return "text/plain", "OK"

async def main():
mkdir(normalize_path("/tmp"))
server = http_server.HttpServer()
asyncio.create_task(server.start_socket_server())
await server.start_socket_server()
while True:
await asyncio.sleep(1)
```
Expand Down
Loading
Loading