diff --git a/Makefile b/Makefile index c18b2ad..b91a0b8 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PYROBUSTA_VERSION := v0.7.0 +PYROBUSTA_VERSION := v0.8.0 DEVICE ?= u0 SRC_DIR := src diff --git a/README.md b/README.md index f904e5b..dd19563 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ from pyrobusta.server.http_server import HttpServer async def main(): server = HttpServer() - server.start_socket_server() + await server.start_socket_server() while True: await asyncio.sleep(1) @@ -95,7 +95,7 @@ def mem_usage(http_ctx, _): async def main(): server = http_server.HttpServer() - server.start_socket_server() + await server.start_socket_server() while True: await asyncio.sleep(1) diff --git a/dist/pyrobusta/assets/www/concepts.html b/dist/pyrobusta/assets/www/concepts.html new file mode 100644 index 0000000..b079661 --- /dev/null +++ b/dist/pyrobusta/assets/www/concepts.html @@ -0,0 +1,127 @@ + + + + + + + + +

Concepts

+

← Back

+

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

+
+

Table of Contents

+ +
+

Purpose

+

PyRobusta is a lightweight embedded HTTP web server designed for predictable resource usage on constrained systems. +It provides a concise API for building web applications while emphasizing deterministic memory usage, incremental +byte-stream processing, and bounded memory allocations. These design choices reduce memory pressure and improve +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 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. +Together, these components isolate networking, protocol processing, and application logic into distinct layers.

+

HttpServer
(Connection Admission)

HttpConnection
(Connection Management)

HttpEngine
(Request Processing)

Route Handler

+

Components

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentResponsibility
HttpServerConnection management, stream buffer provisioning
HttpConnectionSocket I/O, connection lifecycle (keep-alive, timeout)
HttpEngineIncremental stream parsing, HTTP protocol validation, request routing
Route HandlerApplication-specific processing, response generation
+

Execution Model

+

PyRobusta is based on the uasyncio I/O scheduler implemented in MicroPython. +HttpServer internally creates an asynchronous socket server that accepts client connections +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 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 +the same connection are always processed sequentially.

+

HandlerHttpEngineHttpConnectionHttpServerClientHandlerHttpEngineHttpConnectionHttpServerClientloop[Until request complete]alt[Keep-Alive][Connection closed]New connectionReserve stream buffersCreate async connection taskProcess received bytesNeed more dataRead socketInvoke route handlerReturn responseResponse readySend responseWait for next requestRelease stream buffers

+

Memory Model

+

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 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 +(either through connection closure or timeout). HttpServer automatically closes connections that remain inactive for too long.

+

Stream buffers are preallocated during server initialization based on the configured memory limits. +Their size and number remain fixed throughout the lifetime of the server. Preallocating stream buffers reduces heap +fragmentation, improving memory stability and reducing the risk of runtime allocation failures.

+

HttpEngine minimizes heap allocations by utilizing memoryviews for passing payload arguments to route handlers. +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 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 +sections must fit entirely within the available buffer space, request bodies may be processed incrementally +depending on the transfer encoding. Incremental processing allows request bodies to exceed the buffer size +because they are received as independent processing units. This model allows arbitrarily large uploads to be +processed without buffering the complete request body in memory.

+ +

Architectural Guarantees

+

Ownership

+ +

Processing

+ +

Resource Usage

+ +

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.

+
+

PyRobusta v0.8.0 Web Server

+ + + diff --git a/dist/pyrobusta/assets/www/configuration.html b/dist/pyrobusta/assets/www/configuration.html new file mode 100644 index 0000000..ede073f --- /dev/null +++ b/dist/pyrobusta/assets/www/configuration.html @@ -0,0 +1,131 @@ + + + + + + + + +

Configuration

+

← Back

+

This page documents PyRobusta configuration options, +configuration deployment using mpremote, +and runtime access to configuration values through the +configuration API.

+
+

Table of Contents

+ +
+

Configuration Format & Deployment

+

Configuration overrides can be provided through pyrobusta.env, using standard .env syntax. +pyrobusta.env must be stored in the server root. Inline comments are supported using #.

+
# /pyrobusta.env - Example configuration
+
+socket_max_con=2        # allow two simultaneous socket connections
+http_multipart=False    # turn off multipart parser to lower heap usage
+http_mem_cap=0.05       # limit heap usage of stream buffers to 5% of the total heap
+tls=False               # turn off TLS
+
+

Perform a soft reset and upload pyrobusta.env using mpremote.

+
$ mpremote a0 soft-reset
+$ mpremote a0 cp pyrobusta.env :/pyrobusta.env
+
+

Parameter Description

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionDefault
wifi_ssidName of the Wi-Fi network. When empty, Wi-Fi is not initialized by the built-in wifi.py module.None
wifi_passwordPassword of the Wi-Fi network. When empty, Wi-Fi is not initialized by the built-in wifi.py module.None
http_portPort number for HTTP.80
https_portPort number for HTTPS.443
http_multipartEnables or disables multipart request and response processing. Enabling multipart support increases memory usage.False
http_mem_capFraction of available heap memory reserved for stream buffers. Valid range: (0, 1].0.1
http_served_pathsSpace-separated list of filesystem paths that may be served over HTTP./www /lib/pyrobusta
http_files_apiEnables or disables the file management API endpoint (/files), allowing upload, download, and listing of files.False
socket_max_conMaximum number of simultaneous socket connections.2
tlsEnables or disables TLS. When enabled, cert.der and key.der must be installed at the server root.False
log_levelLogging level. Can be one of: warning, info, debug.info
+

Configuration API

+

Configuration values can be accessed through the +pyrobusta.utils.config module. +Values are loaded from pyrobusta.env during server initialization. +Configuration values can be retrieved using +get_config() together with one of the +predefined CONF_* constants.

+

After initialization, configuration values are retrieved from an internal cache. +The cached values are normalized to their expected runtime types to avoid repeated +parsing of environment strings.

+

Configuration values are treated as immutable during runtime. +Changes are applied only when the configuration cache is reloaded. +The configuration cache can be reloaded by calling +read_config(), which re-reads pyrobusta.env +and rebuilds the internal normalized cache.

+
from pyrobusta.utils.config import get_config, CONF_TLS
+
+@HttpEngine.route("/tls", "GET")
+def tls_status(http_ctx, _):
+    enabled = get_config(CONF_TLS)
+    return "text/plain", f"TLS enabled: {enabled}"
+
+
+

PyRobusta v0.8.0 Web Server

+ + + diff --git a/dist/pyrobusta/assets/www/examples.html b/dist/pyrobusta/assets/www/examples.html deleted file mode 100644 index fd5d501..0000000 --- a/dist/pyrobusta/assets/www/examples.html +++ /dev/null @@ -1,208 +0,0 @@ - - - - - - PyRobusta Home - - - - -

Getting Started

- - ← Back - -

This page presents useful examples to configure your server.

- -
- -

Server configuration

- -

- -
- -

Demo Application

-

The below application demonstrates common use cases for handling headers, status codes, query parameters, and wilcard URLs.

-
    -
  1. /version returns the version of the application. -
    Optionally, the server version is also included in the response if the 'detailed' query parameter is set to true. -
  2. -
  3. /app/version or /server/version returns the designated version string, handled by a single route handler with a wildcard URL. -
  4. -
- - - -

Soft reset the device and upload app.py and boot.py with mpremote.

- - -

Hard reset the device to start the application and connect over REPL.

- - -

Use curl to test your application.

- - - - - \ No newline at end of file diff --git a/dist/pyrobusta/assets/www/file_server.html b/dist/pyrobusta/assets/www/file_server.html new file mode 100644 index 0000000..a54fe4f --- /dev/null +++ b/dist/pyrobusta/assets/www/file_server.html @@ -0,0 +1,145 @@ + + + + + + + + +

File Server API

+

← Back

+

This API provides file management capabilities, allowing clients to upload, +retrieve, and manage files through various HTTP methods. +http_files_api must be set to True in +pyrobusta.env to enable this API.

+
+

Table of Contents

+ +
+

Summary

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodPathDescription
GET/files/{path}Lists or retrieves metadata about files.
PUT/files/{file path}Uploads or overwrites a file at the specified path.
POST/filesUploads multiple files in multipart/form-data.
DELETE/files/{file path}Deletes a file at the specified path.
+

File Retrieval / Listing

+

Endpoint: GET /files/{path}

+

This method allows general file system interaction, enabling operations +such as listing directory contents, retrieving metadata, and downloading +files.

+ +

Example Request

+
$ curl 192.168.1.100/files/www
+[
+    {"path": "/www/examples.html", "created": "90", "size": "4507"},
+    {"path": "/www/index.html", "created": "91", "size": "1198"}
+]
+
+

File Upload / Overwrite

+

Endpoint: PUT /files/{file path}

+

This method uploads a file or overwrites an existing file at a specific +path. The upload path is restricted to +/www/user_data.

+ +

Example Request

+
$ curl -X PUT --data 'This is a test.' \
+http://192.168.1.100/files/www/user_data/test.txt
+OK
+
+$ curl 192.168.1.100/files/www/user_data/test.txt
+This is a test.
+
+

Bulk File Upload

+

Endpoint: POST /files

+

This method handles general file uploads, designed for uploading multiple +files with per-file chunking supported. Only +multipart/form-data is accepted.

+

Uploads are restricted to /www/user_data. The +Content-Disposition header only needs to specify the file +name; the upload directory is prepended automatically.

+

http_multipart must be set to True in the +configuration to use this method.

+ +

Example Request

+
$ echo "File 1 content" > /tmp/upload-1.txt
+$ echo "File 2 content" > /tmp/upload-2.txt
+
+$ curl -X POST \
+    --form file1='@/tmp/upload-1.txt' \
+    --form file2='@/tmp/upload-2.txt' \
+    http://192.168.1.100/files
+
+$ curl 192.168.1.100/files/www/user_data
+[
+    {"path": "/www/user_data/upload-1.txt", "created": "418", "size": "15"},
+    {"path": "/www/user_data/upload-2.txt", "created": "418", "size": "15"}
+]
+
+

File Delete

+

Endpoint: DELETE /files/{file path}

+

This method deletes a file at a specific path. The path is restricted to +/www/user_data.

+ +

Example Request

+
$ curl -X DELETE \
+192.168.1.100/files/www/user_data/test.txt
+
+
+

PyRobusta v0.8.0 Web Server

+ + + diff --git a/dist/pyrobusta/assets/www/index.html b/dist/pyrobusta/assets/www/index.html index b3be74b..78418ba 100644 --- a/dist/pyrobusta/assets/www/index.html +++ b/dist/pyrobusta/assets/www/index.html @@ -1,55 +1,32 @@ - - + + - - - PyRobusta Home - + + -

PyRobusta Home

+

PyRobusta Home

+
The server is running correctly and is ready to serve content.
-

The server is running correctly and is ready to serve content.

- -
- -

Available Resources

- - - +

This page collects user guides for configuring the server and developing applications with it. +If you're new to the project, start with the introduction, which provides a hands-on application example. +Then continue with the remaining guides in the order listed below.

+
+

Available Resources

+ +
+

PyRobusta v0.8.0 Web Server

- \ No newline at end of file + diff --git a/dist/pyrobusta/assets/www/introduction.html b/dist/pyrobusta/assets/www/introduction.html new file mode 100644 index 0000000..b6b1d97 --- /dev/null +++ b/dist/pyrobusta/assets/www/introduction.html @@ -0,0 +1,94 @@ + + + + + + + + +

Getting Started

+

← Back

+

This page provides practical examples for using your server.

+
+

Table of Contents

+ +
+

Demo Application

+

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.

+
import asyncio
+
+from pyrobusta.server import http_server
+from pyrobusta.protocol.http import HttpEngine
+
+APP_VERSION = "v0.1.0"
+
+@HttpEngine.route("/version", "GET")
+def version(http_ctx, _):
+    if http_ctx.headers.get("accept") == "application/json":
+        return "application/json", {
+            "version": APP_VERSION
+        }
+
+    return "text/plain", f"{APP_VERSION}\n"
+
+async def main():
+    server = http_server.HttpServer()
+    await server.start_socket_server()
+
+    while True:
+        await asyncio.sleep(1)
+
+
# /boot.py
+# This file is executed on every boot
+
+import machine
+from os import listdir
+
+from pyrobusta.connectivity import wifi
+
+connected = wifi.initialize()
+if connected and not machine.reset_cause() == machine.SOFT_RESET:
+    if "app.py" in listdir():
+        import app
+
+        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.

+
$ mpremote a0 soft-reset
+$ mpremote a0 cp app.py :/app.py
+$ mpremote a0 cp boot.py :/boot.py
+
+

Perform a hard reset to start the application and connect to the REPL.

+
$ mpremote a0 reset repl
+Connected to MicroPython at /dev/ttyACM0
+...
+[INFO] pyrobusta.con.wifi: network b'Home-Wi-Fi' found!
+[INFO] pyrobusta.con.wifi: connected, available at 192.168.1.101
+[WARN] pyrobusta.server.http_server.init_pools: low-memory mode with reduced buffer size
+[INFO] pyrobusta.server.http_server.init_pools: 2 connection(s) allowed
+[INFO] pyrobusta.server.http_server: started
+
+# You can now reach the device at 192.168.1.101 (replace with your IP)
+# Press Ctrl-x to exit
+
+

Use curl to test the application.

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

PyRobusta v0.8.0 Web Server

+ + + diff --git a/dist/pyrobusta/assets/www/request.html b/dist/pyrobusta/assets/www/request.html new file mode 100644 index 0000000..7aed513 --- /dev/null +++ b/dist/pyrobusta/assets/www/request.html @@ -0,0 +1,189 @@ + + + + + + + + +

Request Processing

+

← Back

+

This page describes how route handlers receive and process incoming HTTP requests.

+
+

Table of Contents

+ +
+

Query Parameters

+

Handler functions take an HTTP context and payload as arguments. +While handler functions cannot define additional arguments, extra input can be provided +through query parameters. Query parameters appear after the path component of a URL and +are introduced by a question mark (?), for example:

+

http://192.168.1.101/path/to/resource?param-key=param-value

+

Query parameters use the same encoding rules as the application/x-www-form-urlencoded format. +Multiple query parameters are separated by the ampersand (&). Percent-encoded characters +(for example, %2F representing /) are decoded automatically by the server.

+

Query parameters can be retrieved using the get_query_param() function, which accepts two +arguments: the name of the key and an optional default value. The function returns a string +that can be further processed by the application.

+
from pyrobusta.protocol.http import HttpEngine
+
+@HttpEngine.route("/app/resource", "GET")
+def query_param_handler(http_ctx, _):
+    # Handler for /app/resource?detailed=true/false
+
+    is_detailed = False
+
+    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"
+
+    is_detailed = param == "true"
+
+    resource = "resource content\n"
+    if is_detailed:
+        resource = "detailed " + resource
+
+    return "text/plain", resource
+
+

Request Headers

+

Headers received in a request are available in the headers attribute of the HTTP context. +The headers attribute is a dictionary of key-value pairs. Header names and values are exposed as strings. +As a convenience, the Content-Length header is automatically converted to an integer because it is frequently used for +payload size calculations. Headers are normalized to lower case, so the key "Content-Length" is equivalent to the key +"content-length".

+

Request headers must contain only a subset of US-ASCII characters:

+ +
from pyrobusta.protocol.http import HttpEngine
+
+@HttpEngine.route("/app", "GET")
+def app(http_ctx, _):
+    if http_ctx.headers.get("accept", "*/*") in ("text/plain", "*/*"):
+        return "text/plain", "App response\n"
+    elif http_ctx.headers["accept"] == "application/json":
+        return "application/json", {"response": "App response"}
+    raise ValueError("Unsupported accept header")
+
+

Request Bodies

+

A request payload is passed as the second positional argument of the +route handler. Unless the request uses streaming or multipart encoding, the payload +is provided as a memoryview to avoid unnecessary memory allocations. Deserialization must be done +by the user application.

+
import json
+
+from pyrobusta.protocol.http import HttpEngine
+
+@HttpEngine.route("/app", "GET")
+def app(http_ctx, _):
+    return "text/plain", "GET request without payload"
+
+@HttpEngine.route("/app", "POST")
+def app(http_ctx, payload):
+    data = json.loads(bytes(payload))
+    return "text/plain", "POST request with payload"
+
+

Streamed Requests

+

PyRobusta supports streaming requests by processing individual +chunks of the request body as they are received. To enforce bounded memory usage, +request chunks are processed individually by calling registered route handlers +for each chunk received. As a result, the application must process the request body +incrementally rather than assuming the full payload is available at once.

+
import pyrobusta.server.http_server as http_server
+from pyrobusta.protocol.http import HttpEngine
+from pyrobusta.utils.helpers import normalize_path
+
+@HttpEngine.route("/app/chunks", "POST")
+def upload_chunks(http_ctx, payload: bytes):
+    """
+    Route handler for demonstrating chunked transfer encoding.
+    """
+    if not http_ctx.is_chunked():
+        http_ctx.terminate(400)
+        return "text/plain", "Bad request"
+
+    if payload:
+        # Wait for more chunks before setting response status
+        with open(normalize_path("/tmp/chunks.txt"), "ab") as f:
+            f.write(payload)
+        return
+
+    # Last (empty) chunk received
+    http_ctx.terminate(201)
+    return "text/plain", "OK"
+
+async def main():
+    server = http_server.HttpServer()
+    await server.start_socket_server()
+    while True:
+        await asyncio.sleep(1)
+
+

Multipart Requests

+

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 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
+from pyrobusta.protocol.http import HttpEngine
+from pyrobusta.utils.helpers import normalize_path
+
+@HttpEngine.route("/app/parts", "POST")
+def handle_parts(http_ctx, payload: tuple):
+    if not http_ctx.is_multipart():
+        http_ctx.terminate(400)
+        return "text/plain", "Multipart request required"
+
+    _part_headers, part_body = payload
+
+    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)
+
+    # Finalize uploads
+    if http_ctx.mp_is_last:
+        rename(tmp_path, target_path)
+        http_ctx.terminate(201)
+        return "text/plain", "OK"
+
+async def main():
+    mkdir(normalize_path("/tmp"))
+    server = http_server.HttpServer()
+    await server.start_socket_server()
+    while True:
+        await asyncio.sleep(1)
+
+
+

PyRobusta v0.8.0 Web Server

+ + + diff --git a/dist/pyrobusta/assets/www/response.html b/dist/pyrobusta/assets/www/response.html new file mode 100644 index 0000000..42f3be9 --- /dev/null +++ b/dist/pyrobusta/assets/www/response.html @@ -0,0 +1,213 @@ + + + + + + + + +

Response Processing

+

← Back

+

Response processing controls how route handlers construct HTTP responses. +This includes setting status codes, configuring response headers, serializing response bodies, +and generating streamed or multipart responses.

+
+

Table of Contents

+ +
+

Status Codes

+

Route handlers may optionally set the status code of the HTTP response. +If unspecified, the server defaults to HTTP 200. The status code can be overridden +through the terminate() method of the HTTP context.

+

The terminate() method sets the HTTP status code and transitions the request into a +terminal state. Execution of the handler continues, but the response is considered +finalized. Route handlers can still return a response body.

+
from pyrobusta.protocol.http import HttpEngine
+
+@HttpEngine.route("/app/{resource}", "GET")
+def handler(http_ctx, _):
+    resource = http_ctx.path_segment(1)
+
+    if resource == "items":
+        # Default HTTP 200
+        return "application/json", ["item-1", "item-2"]
+
+    if resource == "version":
+        # Default HTTP 200
+        return "text/plain", "v0.1.0"
+
+    http_ctx.terminate(404)
+    return "text/plain", "Resource not found"
+
+

Response Headers

+

Response headers and response bodies can be configured through methods exposed by the HTTP context +(set_response_header(), set_response_body()). +Alternatively, route handlers may return a (content_type, body) tuple.

+
from pyrobusta.protocol.http import HttpEngine
+
+@HttpEngine.route("/app", "POST")
+def handler(http_ctx, payload):
+    if not payload:
+        http_ctx.set_response_header(b"x-error", b"empty-payload")
+        http_ctx.terminate(400)
+        return "text/plain", "Missing payload"
+
+    # Set a custom response header
+    http_ctx.set_response_header(b"x-app-version", b"1.0")
+
+    return "text/plain", "Request processed"
+
+

Cache Control

+

PyRobusta implements a simple caching policy. +Unless overridden by the application, all HTTP responses include the +header Cache-Control: no-store. +Conditional requests and cache validation mechanisms are not supported. +This includes ETag, Last-Modified, +If-None-Match, and If-Modified-Since. +This design reduces implementation complexity and avoids additional +filesystem metadata lookups on resource-constrained devices.

+

Content Types & Serialization

+

PyRobusta can automatically serialize a limited set of built-in types and data structures. +Unsupported types must be serialized by the application before being returned as either a string or a bytes-like +object. The following response body types are currently supported:

+ +

For non-streamed responses, the entire response body must exist in memory before it is +transmitted. As a result, the maximum response size is limited by the available heap. In the meantime, a +response buffer has a fixed size depending on the configuration. Internally, response bodies are wrapped +in a BytesIO object so that both fixed-size and streamed responses can be written through the same +buffer-oriented interface. Each response body returned by a route handler has a known size, allowing the +content-length header to be filled by the server.

+

Streamed Responses

+

Responses can be streamed in chunks when the application cannot determine the size of the response body in advance. +Such responses must use chunked transfer encoding, indicated by the Transfer-Encoding header. With chunked encoding, +the Content-Length header must be omitted; instead the size of each chunk must be indicated as the chunks are sent. +The server automatically generates the required chunk metadata.

+

When chunked transfer encoding is enabled, the server automatically generates the required encoding format. +The application must assign a generator function to http_ctx.resp_handler. The server then invokes the generator +when data is ready to be transmitted. The generator may be resumed multiple times until the stream is complete. +The following requirements must be fulfilled by the generator:

+
    +
  1. the generator function must accept a single response buffer argument (tx) used to write response data
  2. +
  3. the generator yields False after producing a chunk while additional data remains
  4. +
  5. the generator yields True exactly once to indicate that the stream is complete
  6. +
  7. the generator verifies the writable capacity of the buffer and + writes at most that much data to the buffer before yielding
  8. +
+
# /app.py
+import asyncio
+
+from pyrobusta.server import http_server
+from pyrobusta.protocol.http import HttpEngine
+
+@HttpEngine.route("/stream", "GET")
+def stream_handler(http_ctx, _):
+
+    def generate_chunks(tx):
+        for i in range(10):
+            data = b"data: chunk %d\n\n" % i
+            written = 0
+            while written < len(data):
+                to_write = tx.capacity - tx.size()
+                # Defensive check; buffer should never be full here
+                if not to_write:
+                    raise BufferError()
+                tx.write(data[written : written + to_write])
+                written += to_write
+                yield False
+        yield True
+
+    http_ctx.set_response_header(b"transfer-encoding", b"chunked")
+    http_ctx.set_response_header(b"content-type", b"text/event-stream")
+    http_ctx.resp_handler = generate_chunks
+
+async def main():
+    server = http_server.HttpServer()
+    await server.start_socket_server()
+    while True:
+        await asyncio.sleep(1)
+
+
$ curl 192.168.1.101/stream
+data: chunk 0
+
+data: chunk 1
+
+data: chunk 2
+
+...
+
+data: chunk 9
+
+

Streamed Multipart Responses

+

Multipart responses allow a single HTTP response to contain multiple independently typed payloads, +each with its own headers and body. Similar to streamed responses, PyRobusta uses response producer +functions to generate multipart response parts on demand. Because the total response size is not known +in advance, the server automatically uses chunked transfer encoding for multipart responses. It is +explicitly enabled in the example below for completeness.

+

Routes producing streamed multipart responses must satisfy the following requirements:

+
    +
  1. the route must return a tuple containing the multipart content type and a callable response producer
  2. +
  3. the response producer must return a tuple containing the content type and payload of each part
  4. +
  5. after producing the final part, the response producer must return None
  6. +
+
# /app.py
+import asyncio
+
+from pyrobusta.server import http_server
+from pyrobusta.protocol.http import HttpEngine
+
+def multipart_response(num_responses, part_size):
+    i = 0
+    def response_producer():
+        nonlocal i
+        i += 1
+        if i > num_responses:
+            return None
+        return "text/plain", b"X" * part_size
+    return response_producer
+
+@HttpEngine.route("/multipart", "GET")
+def multipart_handler(http_ctx, _):
+    http_ctx.set_response_header(b"transfer-encoding", b"chunked")
+    part_count = int(http_ctx.headers.get("x-part-count", 1))
+    part_size = int(http_ctx.headers.get("x-part-size", 1024))
+    return "multipart/form-data", multipart_response(part_count, part_size)
+
+async def main():
+    server = http_server.HttpServer()
+    await server.start_socket_server()
+    while True:
+        await asyncio.sleep(1)
+
+

Try the X-Part-Count and X-Part-Size headers to arbitrarily configure the response size.

+
$ curl -H "X-Part-Count: 3" -H "X-Part-Size: 10" 192.168.1.101/multipart
+--pyrobusta-boundary
+content-type:text/plain
+
+XXXXXXXXXX
+--pyrobusta-boundary
+content-type:text/plain
+
+XXXXXXXXXX
+--pyrobusta-boundary
+content-type:text/plain
+
+XXXXXXXXXX
+--pyrobusta-boundary--
+
+
+

PyRobusta v0.8.0 Web Server

+ + + diff --git a/dist/pyrobusta/assets/www/routing.html b/dist/pyrobusta/assets/www/routing.html new file mode 100644 index 0000000..8d8e415 --- /dev/null +++ b/dist/pyrobusta/assets/www/routing.html @@ -0,0 +1,136 @@ + + + + + + + + +

Routing

+

← Back

+

Routing maps incoming HTTP requests to application-defined route handlers. +This page describes how routes are defined, how route handlers receive requests, and how wildcard routes +can be used to match dynamic URL paths.

+
+

Table of Contents

+ +
+

Route Definitions

+

Routes map HTTP requests to server-side handler functions that +process requests and manage resources. Similar to common web frameworks, PyRobusta +utilizes function decorators to map handler functions to URL paths. A route handler +can only be mapped to a single URL path and HTTP method. The same URL path may be +associated with multiple route handlers provided that each handler uses a different +HTTP method.

+
from pyrobusta.protocol.http import HttpEngine
+
+@HttpEngine.route("/app/resource", "GET")
+def get_handler(http_ctx, _):
+    return "text/plain", "resource content\n"
+
+@HttpEngine.route("/app/resource", "POST")
+def post_handler(http_ctx, payload):
+    return "text/plain", "payload processed\n"
+
+

Route Handlers

+

In PyRobusta, a route handler is a synchronous function registered to a +specific URL path and HTTP method. PyRobusta invokes a route handler whenever it receives a +request whose URL path and HTTP method match the registered route. Route handlers must accept +exactly two positional arguments:

+ +

HTTP Context

+

The HTTP context is an instance of the HttpEngine class that exposes the +public API used to inspect requests and construct responses. The HTTP context provides public +methods and attributes that enable user applications to process headers and structure responses. +By convention, non-public attributes and methods are prefixed with an underscore.

+

Apart from the public API, the HTTP context also encapsulates the state associated with the current +request and response exchange. Internally, the HTTP context ensures protocol correctness and assists +with request routing.

+

Request Body

+

Depending on the request type, the body argument may contain either the complete +request body or a partial payload chunk. Partial request bodies are passed to route handlers +when the request uses multipart encoding or chunked transfer encoding, with each chunk of the payload +fed incrementally to the route handler. Such request processing is documented in the +Request Processing guide.

+

Wildcard Routes

+

Wildcard routes use placeholders in one or more segments of a URL path. +These placeholders match varying path values, allowing multiple URL paths to be mapped to +a single handler function. A placeholder matches a single path segment by default. Alternatively, +a placeholder can match multiple segments by using the :path suffix. For example:

+

/path/to/{resource:path}

+

Placeholders that match multiple path segments are only allowed at the end of a route. For example, +/path/{to:path}/resource is disallowed because it would result in ambiguous route resolution.

+
from pyrobusta.protocol.http import HttpEngine
+
+sensor_values = {
+    "dht22": {
+        "temperature_c": 22,
+        "rel_hum": 45
+    }
+}
+
+@HttpEngine.route("/sensors/{name}/values/{value_name}", "GET")
+def wildcard_url_handler(http_ctx, _):
+    sensor_name = http_ctx.path_segment(1)
+    sensor_value = http_ctx.path_segment(3)
+
+    if sensor_name not in sensor_values:
+        http_ctx.terminate(404)
+        return "text/plain", "Not found"
+
+    values = sensor_values[sensor_name]
+
+    if sensor_value not in values:
+        http_ctx.terminate(404)
+        return "text/plain", "Not found"
+
+    return "text/plain", str(values[sensor_value])
+
+

Route Registration & Deregistration

+

By convention, user applications should use decorators +to register route handlers in most cases. This approach ensures that routes are registered +during application initialization, and routes remain available for the lifetime of the application. +The public API exposed by HttpEngine allows route registration and deregistration +during an application's lifecycle, enabling applications to dynamically expose or +remove functionality at runtime.

+
from pyrobusta.protocol.http import HttpEngine
+
+sensor_value = {
+    "temperature_c": 22,
+    "rel_hum": 45
+}
+
+def sensor_handler(http_ctx, _):
+    return "application/json",  sensor_value
+
+@HttpEngine.route("/sensor/enabled", "PUT")
+def enable_sensor(http_ctx, enabled):
+    enabled = enabled.decode().lower()
+
+    if enabled not in ("true", "false"):
+        http_ctx.terminate(400)
+        return "text/plain", "Invalid value"
+
+    is_enabled = enabled == "true"
+
+    if is_enabled:
+        HttpEngine.register(f"/sensor/value", sensor_handler, "GET")
+    else:
+        HttpEngine.deregister(f"/sensor/value", "GET")
+
+    return "text/plain", "OK"
+
+
+

PyRobusta v0.8.0 Web Server

+ + + diff --git a/dist/pyrobusta/assets/www/static_content.html b/dist/pyrobusta/assets/www/static_content.html new file mode 100644 index 0000000..801e137 --- /dev/null +++ b/dist/pyrobusta/assets/www/static_content.html @@ -0,0 +1,112 @@ + + + + + + + + +

Static Content

+

← Back

+

PyRobusta serves static content directly from the filesystem. This page +describes the directory structure, file resolution rules, and MIME type +handling used by the server.

+
+

Table of Contents

+ +
+

Static File Serving

+

Files stored under /www are served as static content. +Requests to the server root (/) return the default landing page (index.html). +For static content requests, the server automatically prepends /www to the requested path before +resolving the corresponding file on the filesystem.

+

When the file management API is enabled (http_files_api=True), +additional filesystem locations can be exposed through the http_served_paths +configuration option. See the Server Configuration +and File Server API guide for additional details.

+

Directory Structure

+
root/
+├── www                     document root for static content
+│   ├── index.html
+│   ├── introduction.html
+│   ├── ...
+│   └── user_data           root for user uploads
+│       └── ...
+├── lib                     root for installed MIP packages
+│   ├── pyrobusta
+│   │   ├── bindings
+│   │   ├── connectivity
+│   │   └── ...
+│   └── <other packages>
+├── cert.der                TLS certificate
+└── key.der                 TLS key
+
+

MIME Type Handling

+

PyRobusta automatically determines the Content-Type header for +static files based on their filename extension. The selected content type depends on the file extension. +Unknown extensions are mapped to application/octet-stream. +The following mapping between extensions and content types is maintained by the server:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExtensionContent-Type header
.htmltext/html
.csstext/css
.jsapplication/javascript
.jsonapplication/json
.icoimage/x-icon
.jpegimage/jpeg
.jpgimage/jpeg
.pngimage/png
.txttext/plain
.gifimage/gif
.raw, unknown extensionsapplication/octet-stream
+
+

PyRobusta v0.8.0 Web Server

+ + + diff --git a/dist/pyrobusta/assets/www/style.css b/dist/pyrobusta/assets/www/style.css new file mode 100644 index 0000000..db12f2b --- /dev/null +++ b/dist/pyrobusta/assets/www/style.css @@ -0,0 +1,101 @@ +:root { + --bg: #ffffff; + --fg: #1a1a1a; + --muted: #666; + --link: #0b5fff; + --code-bg: #f5f5f5; + --border: #e5e5e5; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0f1115; + --fg: #e6e6e6; + --muted: #9aa0a6; + --link: #7aa2ff; + --code-bg: #1a1d23; + --border: #2a2f3a; + } +} + +body { + font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; + max-width: 850px; + margin: 40px auto; + padding: 0 16px; + background: var(--bg); + color: var(--fg); + line-height: 1.6; +} + +h1, h2, h3 { + line-height: 1.25; + margin-top: 1.4em; +} + +a { + color: var(--link); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +code { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.9em; +} + +pre { + background: var(--code-bg); + padding: 12px; + overflow-x: auto; + border-radius: 6px; + border: 1px solid var(--border); +} + +:not(pre) > code { + background: var(--code-bg); + padding: 2px 5px; + border-radius: 4px; +} + +pre > code { + display: block; +} + +table { + border-collapse: collapse; + width: 100%; +} + +th, td { + border: 1px solid var(--border); + padding: 6px 10px; + text-align: left; +} + +blockquote { + border-left: 3px solid var(--border); + padding-left: 12px; + color: var(--muted); + margin-left: 0; +} + +.build-note { + padding: 10px; + margin: 12px 0; + border-left: 3px solid #888; + background: #f6f6f6; + font-size: 0.9em; + color: #444; +} + +@media (prefers-color-scheme: dark) { + .build-note { + background: #1a1d23; + border-left-color: #777; + color: #bbb; + } +} \ No newline at end of file diff --git a/dist/pyrobusta/bindings/http_connection.mpy b/dist/pyrobusta/bindings/http_connection.mpy index 6fd9c1b..8d77afb 100644 Binary files a/dist/pyrobusta/bindings/http_connection.mpy and b/dist/pyrobusta/bindings/http_connection.mpy differ diff --git a/dist/pyrobusta/protocol/http.mpy b/dist/pyrobusta/protocol/http.mpy index b991b95..5879026 100644 Binary files a/dist/pyrobusta/protocol/http.mpy and b/dist/pyrobusta/protocol/http.mpy differ diff --git a/dist/pyrobusta/protocol/http_file_server.mpy b/dist/pyrobusta/protocol/http_file_server.mpy index f952c3b..1c30f91 100644 Binary files a/dist/pyrobusta/protocol/http_file_server.mpy and b/dist/pyrobusta/protocol/http_file_server.mpy differ diff --git a/dist/pyrobusta/protocol/http_multipart.mpy b/dist/pyrobusta/protocol/http_multipart.mpy index 41790a5..d5ad156 100644 Binary files a/dist/pyrobusta/protocol/http_multipart.mpy and b/dist/pyrobusta/protocol/http_multipart.mpy differ diff --git a/dist/pyrobusta/server/http_server.mpy b/dist/pyrobusta/server/http_server.mpy index 295506e..542660d 100644 Binary files a/dist/pyrobusta/server/http_server.mpy and b/dist/pyrobusta/server/http_server.mpy differ diff --git a/dist/pyrobusta/utils/config.mpy b/dist/pyrobusta/utils/config.mpy index 0001451..cf1ccad 100644 Binary files a/dist/pyrobusta/utils/config.mpy and b/dist/pyrobusta/utils/config.mpy differ diff --git a/dist/pyrobusta/utils/helpers.mpy b/dist/pyrobusta/utils/helpers.mpy index b304126..38a8103 100644 Binary files a/dist/pyrobusta/utils/helpers.mpy and b/dist/pyrobusta/utils/helpers.mpy differ diff --git a/dist/pyrobusta/utils/patch.mpy b/dist/pyrobusta/utils/patch.mpy new file mode 100644 index 0000000..ccb4f2d Binary files /dev/null and b/dist/pyrobusta/utils/patch.mpy differ diff --git a/docs/application_development/concepts.md b/docs/application_development/concepts.md index 2c75c29..50ce84b 100644 --- a/docs/application_development/concepts.md +++ b/docs/application_development/concepts.md @@ -147,4 +147,4 @@ incremental request processing, and deterministic behavior suitable for resource --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/configuration.md b/docs/application_development/configuration.md index 46c2d5c..6eb8582 100644 --- a/docs/application_development/configuration.md +++ b/docs/application_development/configuration.md @@ -85,4 +85,4 @@ def tls_status(http_ctx, _): --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/error_handling.md b/docs/application_development/error_handling.md index d668ab7..f08be8b 100644 --- a/docs/application_development/error_handling.md +++ b/docs/application_development/error_handling.md @@ -29,4 +29,4 @@ This page is work in progress. Refer to the [main page](index.md) for available --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/file_server.md b/docs/application_development/file_server.md index a2be14c..b605dc2 100644 --- a/docs/application_development/file_server.md +++ b/docs/application_development/file_server.md @@ -136,4 +136,4 @@ $ curl -X DELETE \ --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/index.md b/docs/application_development/index.md index 8731cb7..f97dcd5 100644 --- a/docs/application_development/index.md +++ b/docs/application_development/index.md @@ -2,7 +2,7 @@ This page collects user guides for configuring the server and developing applications with it. -If you're new to the project, start with the Introduction, which provides a hands-on application example. +If you're new to the project, start with the introduction, which provides a hands-on application example. Then continue with the remaining guides in the order listed below. --- @@ -21,4 +21,4 @@ Then continue with the remaining guides in the order listed below. --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/introduction.md b/docs/application_development/introduction.md index 02f53b4..e7e3fcd 100644 --- a/docs/application_development/introduction.md +++ b/docs/application_development/introduction.md @@ -39,7 +39,7 @@ def version(http_ctx, _): async def main(): server = http_server.HttpServer() - server.start_socket_server() + await server.start_socket_server() while True: await asyncio.sleep(1) @@ -103,4 +103,4 @@ $ curl -H "Accept: application/json" "http://192.168.1.101/version" --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/performance.md b/docs/application_development/performance.md index 645d9c1..9439173 100644 --- a/docs/application_development/performance.md +++ b/docs/application_development/performance.md @@ -23,4 +23,4 @@ This page is work in progress. Refer to the [main page](index.md) for available --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/reference.md b/docs/application_development/reference.md index 4187b83..b934d48 100644 --- a/docs/application_development/reference.md +++ b/docs/application_development/reference.md @@ -26,4 +26,4 @@ This page is work in progress. Refer to the [main page](index.md) for available --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/request.md b/docs/application_development/request.md index daa8769..11a986d 100644 --- a/docs/application_development/request.md +++ b/docs/application_development/request.md @@ -205,4 +205,4 @@ async def main(): --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/response.md b/docs/application_development/response.md index d555e86..e585e3b 100644 --- a/docs/application_development/response.md +++ b/docs/application_development/response.md @@ -231,4 +231,4 @@ XXXXXXXXXX --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/routing.md b/docs/application_development/routing.md index c163af8..3c9ec18 100644 --- a/docs/application_development/routing.md +++ b/docs/application_development/routing.md @@ -148,4 +148,4 @@ def enable_sensor(http_ctx, enabled): --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/security.md b/docs/application_development/security.md index 2beeb55..5f7038d 100644 --- a/docs/application_development/security.md +++ b/docs/application_development/security.md @@ -26,4 +26,4 @@ This page is work in progress. Refer to the [main page](index.md) for available --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/server_lifecycle.md b/docs/application_development/server_lifecycle.md index 4acd516..7ec47c3 100644 --- a/docs/application_development/server_lifecycle.md +++ b/docs/application_development/server_lifecycle.md @@ -26,4 +26,4 @@ This page is work in progress. Refer to the [main page](index.md) for available --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/application_development/static_content.md b/docs/application_development/static_content.md index 27238ea..713657e 100644 --- a/docs/application_development/static_content.md +++ b/docs/application_development/static_content.md @@ -72,4 +72,4 @@ The following mapping between extensions and content types is maintained by the --- -PyRobusta v0.7.0 Web Server +PyRobusta v0.8.0 Web Server diff --git a/docs/img/home_page.png b/docs/img/home_page.png index 929d8e2..93d70e5 100644 Binary files a/docs/img/home_page.png and b/docs/img/home_page.png differ diff --git a/package.json b/package.json index dc36327..09a56af 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "v0.7.0", + "version": "v0.8.0", "urls": [ [ "pyrobusta/connectivity/wifi.mpy", @@ -17,13 +17,45 @@ "pyrobusta/transport/connection.mpy", "github:szeka9/PyRobusta/dist/pyrobusta/transport/connection.mpy" ], + [ + "pyrobusta/assets/www/style.css", + "github:szeka9/PyRobusta/dist/pyrobusta/assets/www/style.css" + ], + [ + "pyrobusta/assets/www/request.html", + "github:szeka9/PyRobusta/dist/pyrobusta/assets/www/request.html" + ], [ "pyrobusta/assets/www/index.html", "github:szeka9/PyRobusta/dist/pyrobusta/assets/www/index.html" ], [ - "pyrobusta/assets/www/examples.html", - "github:szeka9/PyRobusta/dist/pyrobusta/assets/www/examples.html" + "pyrobusta/assets/www/configuration.html", + "github:szeka9/PyRobusta/dist/pyrobusta/assets/www/configuration.html" + ], + [ + "pyrobusta/assets/www/file_server.html", + "github:szeka9/PyRobusta/dist/pyrobusta/assets/www/file_server.html" + ], + [ + "pyrobusta/assets/www/introduction.html", + "github:szeka9/PyRobusta/dist/pyrobusta/assets/www/introduction.html" + ], + [ + "pyrobusta/assets/www/routing.html", + "github:szeka9/PyRobusta/dist/pyrobusta/assets/www/routing.html" + ], + [ + "pyrobusta/assets/www/concepts.html", + "github:szeka9/PyRobusta/dist/pyrobusta/assets/www/concepts.html" + ], + [ + "pyrobusta/assets/www/static_content.html", + "github:szeka9/PyRobusta/dist/pyrobusta/assets/www/static_content.html" + ], + [ + "pyrobusta/assets/www/response.html", + "github:szeka9/PyRobusta/dist/pyrobusta/assets/www/response.html" ], [ "pyrobusta/utils/helpers.mpy", @@ -41,6 +73,10 @@ "pyrobusta/utils/logging.mpy", "github:szeka9/PyRobusta/dist/pyrobusta/utils/logging.mpy" ], + [ + "pyrobusta/utils/patch.mpy", + "github:szeka9/PyRobusta/dist/pyrobusta/utils/patch.mpy" + ], [ "pyrobusta/utils/assets.mpy", "github:szeka9/PyRobusta/dist/pyrobusta/utils/assets.mpy" diff --git a/src/pyrobusta/utils/config.py b/src/pyrobusta/utils/config.py index 6c17576..1acb070 100644 --- a/src/pyrobusta/utils/config.py +++ b/src/pyrobusta/utils/config.py @@ -14,7 +14,7 @@ def const(n): # pylint: disable=C0116 from .helpers import normalize_path -PYROBUSTA_VERSION = "v0.7.0" +PYROBUSTA_VERSION = "v0.8.0" CONFIG_LOCATION = "pyrobusta.env" # -------------------------------------------