patx/micropie
add more documentation, work in progress
Commit 9aed376 · patx · 2025-08-15T01:54:08-04:00
Comments
No comments yet.
Diff
diff --git a/docs/docs/explanation/architecture.rst b/docs/docs/explanation/architecture.rst
new file mode 100644
index 0000000..a55c720
--- /dev/null
+++ b/docs/docs/explanation/architecture.rst
@@ -0,0 +1,95 @@
+Architecture and design
+=======================
+
+MicroPie is intentionally small. Its goal is to provide just enough
+functionality to build asynchronous web applications without locking
+you into a rigid structure. This section discusses some of the key
+design choices and how they influence the API.
+
+ASGI application
+----------------
+
+MicroPie is built on top of the `ASGI`_ specification, which defines
+how Python applications communicate with asynchronous servers. Your
+application subclass inherits from :class:`~micropie.App` and
+implements the ASGI call interface via the ``__call__`` method. The
+framework inspects the ``scope['type']`` field to dispatch to HTTP,
+WebSocket or lifespan handling code.
+
+Routing by naming
+-----------------
+
+Unlike frameworks that require you to declare routes in advance,
+MicroPie derives routing information from the names of your methods.
+The first segment of the path becomes the handler name. This allows
+you to add new endpoints simply by defining a new method on your
+application subclass. The remaining path segments, query string
+parameters and form data are mapped to arguments via function
+signature inspection. This approach reduces boilerplate but may
+surprise users expecting explicit routing. You can always regain
+control by writing a custom middleware to map paths to handlers or by
+checking ``self.request.method`` and ``self.request.path_params`` inside
+your handlers.
+
+Context‑local request
+---------------------
+
+MicroPie stores the current request in a :mod:`contextvars`
+ContextVar. This allows you to access ``self.request`` within your
+handler as well as from deeper helper functions without passing the
+request around explicitly. The context variable is reset at the end
+of each request to avoid leaking state.
+
+Session management
+------------------
+
+Sessions are stored in a pluggable back‑end and identified by a
+random ``session_id`` cookie. The default in‑memory back‑end keeps
+session data in dictionaries keyed by session ID and updates a last
+access timestamp to implement expiration. You can customise the
+back‑end by providing your own implementation of
+:class:`~micropie.SessionBackend`. MicroPie saves sessions after a
+handler returns, only if ``self.request.session`` is non‑empty, to
+avoid creating unnecessary cookies.
+
+Middleware pipeline
+-------------------
+
+The middleware hooks allow you to intercept requests before and after
+handlers. Middleware can modify the request object, provide early
+responses (useful for authentication), and alter the final response.
+For WebSockets, separate middleware hooks run before the connection
+handler starts and after it finishes.
+
+Streaming and SSE support
+-------------------------
+
+Handlers may return an iterator or asynchronous generator to stream
+data to the client. MicroPie detects such responses and iterates
+over them, sending each chunk to the client. When using server‑sent
+events (SSE) there is an additional challenge: if the client
+disconnects, the iterator must be cancelled. MicroPie wraps SSE
+responses in a small loop that listens for disconnect events and
+cancels the generator accordingly. Remember to include a
+``Content‑Type: text/event-stream`` header when sending SSE.
+
+Error handling
+--------------
+
+MicroPie automatically handles common error cases. Requests for
+unknown routes result in a ``404 Not Found``. If a required
+parameter is missing, MicroPie responds with ``400 Bad Request``.
+Unhandled exceptions inside handlers produce a ``500 Internal Server
+Error`` and are printed to standard error. You can override these
+behaviours via middleware.
+
+Extensibility
+-------------
+
+The minimalist core is designed to be extended. You can mount your
+application behind other ASGI middleware, integrate additional
+protocols like Socket.IO, or implement your own session storage. The
+framework imposes few constraints so that you remain in control of
+your stack.
+
+.. _ASGI: https://asgi.readthedocs.io/
\ No newline at end of file
diff --git a/docs/docs/explanation/index.rst b/docs/docs/explanation/index.rst
new file mode 100644
index 0000000..0d3ac07
--- /dev/null
+++ b/docs/docs/explanation/index.rst
@@ -0,0 +1,13 @@
+Explanations
+============
+
+The explanation section explores concepts and design decisions behind
+MicroPie. It answers “why” questions and provides deeper context.
+For hands‑on instructions see the tutorials and how‑to guides; for
+precise definitions consult the reference.
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Explanations
+
+ architecture
\ No newline at end of file
diff --git a/docs/docs/glossary.rst b/docs/docs/glossary.rst
new file mode 100644
index 0000000..c02a776
--- /dev/null
+++ b/docs/docs/glossary.rst
@@ -0,0 +1,44 @@
+Glossary
+========
+
+This glossary defines terms used throughout the MicroPie documentation.
+
+.. glossary::
+
+ ASGI
+ The Asynchronous Server Gateway Interface. A standard for
+ asynchronous communication between Python applications and
+ servers. MicroPie applications implement the ASGI call
+ interface. See the `ASGI specification <https://asgi.readthedocs.io>`_.
+
+ route handler
+ A method on your :class:`~micropie.App` subclass that handles an
+ incoming HTTP request. The first segment of the URL path maps to
+ the handler name, and remaining segments or query parameters map
+ to arguments. Handlers may be synchronous or asynchronous.
+
+ session
+ A dictionary of data associated with a client across multiple
+ requests. MicroPie stores the session in a pluggable back‑end
+ and identifies it using a ``session_id`` cookie. See
+ :doc:`howto/sessions`.
+
+ middleware
+ Code that runs before and/or after a request handler. Middleware
+ can short‑circuit requests, modify responses, enforce security
+ policies or implement custom routing. MicroPie defines
+ :class:`~micropie.HttpMiddleware` and
+ :class:`~micropie.WebSocketMiddleware` base classes.
+
+ WebSocket
+ A persistent, bidirectional communication channel over TCP. In
+ MicroPie, WebSocket handlers are methods prefixed with ``ws_``.
+ They receive an instance of :class:`~micropie.WebSocket` and
+ optionally additional parameters. See :doc:`tutorial/websockets`.
+
+ context variable
+ An object from the :mod:`contextvars` module that stores data
+ local to the current asynchronous context. MicroPie stores the
+ current request in a context variable so it is accessible via
+ :meth:`~micropie.App.request` without passing it through your
+ function calls.
\ No newline at end of file
diff --git a/docs/docs/howto/index.rst b/docs/docs/howto/index.rst
new file mode 100644
index 0000000..6d3e1a2
--- /dev/null
+++ b/docs/docs/howto/index.rst
@@ -0,0 +1,19 @@
+How‑to guides
+=============
+
+How‑to guides provide short recipes for solving a specific problem with
+MicroPie. They assume that you already know the basics covered in
+the tutorials. Each guide focuses on performing a single task. See
+the :doc:`tutorials <../tutorial/index>` for a more gentle
+introduction.
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Guides
+
+ sessions
+ middleware
+ templates
+ streaming
+ static_files
+ socketio
\ No newline at end of file
diff --git a/docs/docs/howto/middleware.rst b/docs/docs/howto/middleware.rst
new file mode 100644
index 0000000..a815318
--- /dev/null
+++ b/docs/docs/howto/middleware.rst
@@ -0,0 +1,93 @@
+Writing middleware
+==================
+
+Middleware allows you to insert code that runs before and after your
+handlers. It is useful for cross‑cutting concerns such as logging,
+authentication, rate limiting or explicit routing. MicroPie defines
+separate middleware classes for HTTP and WebSocket connections.
+
+HTTP middleware
+---------------
+
+To create HTTP middleware, subclass :class:`~micropie.HttpMiddleware`
+and implement two asynchronous methods:
+
+* :meth:`~micropie.HttpMiddleware.before_request` – called before the
+ handler is executed. If this method returns a dictionary with
+ ``status_code`` and ``body`` keys, MicroPie immediately sends that
+ response and skips calling the handler. You can also provide
+ additional headers via a ``headers`` key.
+
+* :meth:`~micropie.HttpMiddleware.after_request` – called after the
+ handler has returned a response but before it is sent to the client.
+ You can modify the status code, body or headers by returning a
+ dictionary with updated values.
+
+Register your middleware by appending an instance to
+:attr:`~micropie.App.middlewares` on your application:
+
+.. code-block:: python
+
+ from micropie import App, HttpMiddleware
+
+ class LoggingMiddleware(HttpMiddleware):
+ async def before_request(self, request):
+ print(f"Incoming request: {request.method} {request.scope['path']}")
+ # returning None continues processing
+
+ async def after_request(self, request, status_code, body, headers):
+ print(f"Response status: {status_code}")
+ # returning None uses the original response
+
+ class MyApp(App):
+ async def index(self):
+ return "Hello, world!"
+
+ app = MyApp()
+ app.middlewares.append(LoggingMiddleware())
+
+WebSocket middleware
+--------------------
+
+WebSocket middleware must subclass :class:`~micropie.WebSocketMiddleware`
+and implement two methods:
+
+* :meth:`~micropie.WebSocketMiddleware.before_websocket` – called
+ before a WebSocket handler runs. If this method returns a
+ dictionary with ``code`` and ``reason``, MicroPie closes the
+ connection with the given code and reason.
+
+* :meth:`~micropie.WebSocketMiddleware.after_websocket` – called after
+ the WebSocket handler completes. Use this to perform cleanup.
+
+Example:
+
+.. code-block:: python
+
+ from micropie import App, WebSocketMiddleware
+
+ class RejectAnonymous(WebSocketMiddleware):
+ async def before_websocket(self, request):
+ # Reject connections without a "user" query parameter
+ if "user" not in request.query_params:
+ return {"code": 1008, "reason": "User name required"}
+
+ async def after_websocket(self, request):
+ print("WebSocket closed")
+
+ class MyApp(App):
+ async def ws_chat(self, ws, user):
+ await ws.accept()
+ await ws.send_text(f"Welcome, {user}!")
+ await ws.close()
+
+ app = MyApp()
+ app.ws_middlewares.append(RejectAnonymous())
+
+Explicit routing and other patterns
+----------------------------------
+
+You can implement custom routing schemes by writing middleware that
+parses the incoming path and sets ``request._route_handler`` or
+``request._ws_route_handler`` accordingly. See the examples in the
+``examples/middleware`` directory for a complete implementation.
\ No newline at end of file
diff --git a/docs/docs/howto/sessions.rst b/docs/docs/howto/sessions.rst
new file mode 100644
index 0000000..b29f6de
--- /dev/null
+++ b/docs/docs/howto/sessions.rst
@@ -0,0 +1,84 @@
+Managing sessions
+=================
+
+MicroPie includes simple session management built on top of cookies. A
+session stores data associated with a client across multiple requests.
+Sessions are useful for keeping track of login state, counters and
+temporary information.
+
+Enabling sessions
+-----------------
+
+By default, MicroPie uses an in‑memory session back‑end implemented by
+:class:`~micropie.InMemorySessionBackend`. Each session is identified
+by a ``session_id`` cookie. When the client makes a request, the
+cookie is read and the session data is loaded from the back‑end. When
+you modify the session dictionary, MicroPie writes the updated
+session back at the end of the request and issues a ``Set‑Cookie``
+header if needed.
+
+In order to use sessions, simply read and write the
+:attr:`~micropie.Request.session` dictionary in your handler. The
+following example counts the number of visits for each client:
+
+.. code-block:: python
+
+ from micropie import App
+
+ class MyApp(App):
+ async def index(self):
+ if "visits" not in self.request.session:
+ self.request.session["visits"] = 1
+ else:
+ self.request.session["visits"] += 1
+ return f"You have visited {self.request.session['visits']} times."
+
+ app = MyApp()
+
+Custom session back‑ends
+------------------------
+
+The in‑memory back‑end stores all sessions in a Python dictionary. It
+is suitable for development but will lose data when the process
+terminates and cannot be shared across worker processes. To persist
+sessions in a database or cache, implement the abstract
+:class:`~micropie.SessionBackend` interface:
+
+.. code-block:: python
+
+ from micropie import SessionBackend
+
+ class DatabaseSessionBackend(SessionBackend):
+ async def load(self, session_id: str) -> dict:
+ # fetch the session from your database or cache
+ data = await get_session_from_db(session_id)
+ return data or {}
+
+ async def save(self, session_id: str, data: dict, timeout: int) -> None:
+ # store the session with an expiration timeout
+ await save_session_to_db(session_id, data, timeout)
+
+Assign your back‑end when constructing your application:
+
+.. code-block:: python
+
+ backend = DatabaseSessionBackend()
+ app = MyApp(session_backend=backend)
+
+Expiring sessions
+-----------------
+
+The global constant :data:`micropie.SESSION_TIMEOUT` controls how long
+a session remains valid (in seconds). The default is eight hours.
+Back‑ends may choose to enforce this timeout in whatever manner makes
+sense for their storage layer.
+
+Security considerations
+-----------------------
+
+Sessions are sent to the client as cookies and should be treated as
+untrusted input. Avoid storing sensitive information directly in the
+session. If you implement your own back‑end, ensure that session
+identifiers are random, unique and that the cookie includes the
+``HttpOnly``, ``Secure`` and ``SameSite=Lax`` directives. The
+in‑memory back‑end provided by MicroPie already issues these flags.
\ No newline at end of file
diff --git a/docs/docs/howto/socketio.rst b/docs/docs/howto/socketio.rst
new file mode 100644
index 0000000..02b5bbe
--- /dev/null
+++ b/docs/docs/howto/socketio.rst
@@ -0,0 +1,54 @@
+Integrating Socket.IO
+=====================
+
+MicroPie’s built‑in WebSocket support provides a simple API for
+bidirectional communication. If you require advanced features such
+as automatic reconnection, broadcasting to rooms, namespaces, or
+fallback transports for browsers that do not support WebSockets, you
+can integrate `python‑socketio <https://python-socketio.readthedocs.io>`_.
+
+Socket.IO runs as an ASGI application, so you can mount it
+alongside your MicroPie app and serve both under the same Uvicorn
+server. The following example shows how to set up a basic chat
+application using Socket.IO with MicroPie.
+
+.. code-block:: python
+
+ import socketio
+ from micropie import App
+
+ # Create a Socket.IO server instance (ASGI compatible)
+ sio = socketio.AsyncServer(async_mode="asgi")
+ socket_app = socketio.ASGIApp(sio)
+
+ # Create your MicroPie application
+ class MyApp(App):
+ async def index(self):
+ # Serve an HTML page with Socket.IO client code (omitted)
+ return await self._render_template("chat.html")
+
+ app = MyApp()
+
+ # Uvicorn accepts a single callable; wrap both apps in a simple
+ # dispatcher that routes to Socket.IO on the /socket.io path
+ async def application(scope, receive, send):
+ if scope["path"].startswith("/socket.io"):
+ await socket_app(scope, receive, send)
+ else:
+ await app(scope, receive, send)
+
+Run the dispatcher with Uvicorn:
+
+.. code-block:: console
+
+ $ uvicorn app:application
+
+On the client side, connect to ``/socket.io`` as usual using the
+Socket.IO JavaScript client. All other paths are handled by your
+MicroPie routes. See the ``examples/socketio`` directory in the
+MicroPie source distribution for a more complete implementation that
+includes HTML and JavaScript code.
+
+Note that python‑socketio may install its own dependencies. Consult
+the Socket.IO documentation for configuration options such as
+message queue back‑ends and authentication.
\ No newline at end of file
diff --git a/docs/docs/howto/static_files.rst b/docs/docs/howto/static_files.rst
new file mode 100644
index 0000000..38933cd
--- /dev/null
+++ b/docs/docs/howto/static_files.rst
@@ -0,0 +1,45 @@
+Serving static files
+====================
+
+MicroPie deliberately avoids bundling a static file server. Serving
+static content such as images, CSS and JavaScript can be delegated to
+a specialised middleware or a separate web server. This approach
+keeps the core framework small and flexible.
+
+Using a static file middleware
+------------------------------
+
+To serve static files from within your MicroPie application, you can
+use third‑party libraries such as
+`ServeStatic <https://archmonger.github.io/ServeStatic/latest/>`_ or
+Starlette’s `StaticFiles` class. The example below uses
+ServeStatic to serve files from a ``static`` directory:
+
+.. code-block:: python
+
+ from micropie import App
+ from servestatic import ServeStatic
+
+ class MyApp(App):
+ async def index(self):
+ return "Hello, world!"
+
+ app = MyApp()
+
+ # Mount the static file handler at the /static path
+ app_with_static = ServeStatic(app, directory="static", path_prefix="/static")
+
+When running your application, pass ``app_with_static`` to your ASGI
+server instead of ``app``. Requests to paths under ``/static`` will
+be served from your ``static`` directory, while all other requests
+fall through to your MicroPie routes.
+
+Using a reverse proxy
+---------------------
+
+Alternatively, you can serve static files via a dedicated HTTP server
+such as Nginx or Caddy. In production deployments this is often the
+preferred option, as a dedicated server can handle caching and
+compression. Configure your web server to serve the ``/static``
+prefix directly from the filesystem and forward all other requests to
+your ASGI server running MicroPie.
\ No newline at end of file
diff --git a/docs/docs/howto/streaming.rst b/docs/docs/howto/streaming.rst
new file mode 100644
index 0000000..b231ce3
--- /dev/null
+++ b/docs/docs/howto/streaming.rst
@@ -0,0 +1,66 @@
+Streaming responses
+===================
+
+For long‑running tasks or large responses it is often desirable to
+stream data to the client in chunks rather than return a single
+buffer. MicroPie supports streaming by allowing your handler to
+return an iterator or asynchronous generator. Each chunk yielded by
+the generator is sent as part of the response body. When the
+generator finishes, MicroPie automatically sends an empty chunk to
+signal completion.
+
+Example: number stream
+----------------------
+
+The following handler streams a sequence of numbers to the client one
+per second:
+
+.. code-block:: python
+
+ from micropie import App
+ import asyncio
+
+ class MyApp(App):
+ async def numbers(self):
+ async def generator():
+ for i in range(5):
+ yield f"Number: {i}\n"
+ await asyncio.sleep(1)
+ return generator()
+
+ app = MyApp()
+
+Visiting ``/numbers`` will produce output like:
+
+.. code-block:: text
+
+ Number: 0
+ Number: 1
+ Number: 2
+ Number: 3
+ Number: 4
+
+MicroPie automatically sets ``Content‑Type`` to ``text/html`` if you
+do not specify a content type. You can override or add headers by
+returning a tuple ``(status_code, body, headers)`` and making the
+generator the body.
+
+Handling client disconnects
+---------------------------
+
+If the client disconnects while a generator is running, MicroPie
+cancels the generator and closes the underlying response. This
+behaviour is important for long‑lived streams such as server‑sent
+events (SSE). When writing a generator, use ``try``/``finally`` or
+``async with`` to perform cleanup when the stream is cancelled.
+
+Server‑sent events
+------------------
+
+MicroPie treats any iterable response body as a general stream.
+Implementing true server‑sent events requires you to set the
+``Content‑Type`` header to ``text/event-stream`` and send
+properly‑formatted SSE messages (``data: ...\n\n``). Additionally,
+MicroPie includes a patch that handles early disconnection of SSE
+clients in its HTTP handling code. For a full example, see the
+``examples/sse`` directory in the MicroPie source distribution.
\ No newline at end of file
diff --git a/docs/docs/howto/templates.rst b/docs/docs/howto/templates.rst
new file mode 100644
index 0000000..57b5176
--- /dev/null
+++ b/docs/docs/howto/templates.rst
@@ -0,0 +1,71 @@
+Rendering templates
+==================
+
+MicroPie can render HTML templates using the Jinja2 template engine.
+Templates allow you to separate presentation from code by placing
+HTML in separate files. Template rendering is optional; you can use
+MicroPie without Jinja2 installed, but the
+:meth:`~micropie.App._render_template` method will not be available.
+
+Installing Jinja2
+-----------------
+
+If you installed MicroPie with the ``[standard]`` extra, Jinja2 is
+already available. Otherwise install it with pip:
+
+.. code-block:: console
+
+ $ pip install jinja2
+
+Creating templates
+------------------
+
+MicroPie looks for templates in a directory called ``templates`` in
+your current working directory. Use normal Jinja2 syntax in your
+templates. Here is a simple ``templates/index.html`` file:
+
+.. code-block:: html
+
+ <!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>{{ title }}</title>
+ </head>
+ <body>
+ <h1>{{ message }}</h1>
+ </body>
+ </html>
+
+Rendering a template
+--------------------
+
+To render a template, call :meth:`~micropie.App._render_template` from
+within an asynchronous handler. The method returns a string containing
+the rendered HTML. Because template loading and rendering may block,
+MicroPie runs it in a background thread using ``asyncio.to_thread``.
+
+.. code-block:: python
+
+ from micropie import App
+
+ class MyApp(App):
+ async def index(self):
+ return await self._render_template(
+ "index.html",
+ title="Welcome",
+ message="Hello from MicroPie!",
+ )
+
+ app = MyApp()
+
+When you visit ``/`` in your browser, MicroPie returns the rendered
+HTML with a ``Content‑Type`` of ``text/html; charset=utf‑8``.
+
+Template variables
+------------------
+
+You can pass arbitrary keyword arguments to `_render_template`. These
+become variables in the Jinja2 template. For more information on
+creating complex templates, including inheritance and control flow,
+refer to the `Jinja2 documentation <https://jinja.palletsprojects.com>`_.
\ No newline at end of file
diff --git a/docs/docs/index.rst b/docs/docs/index.rst
new file mode 100644
index 0000000..3ab55be
--- /dev/null
+++ b/docs/docs/index.rst
@@ -0,0 +1,56 @@
+.. MicroPie documentation master file, created for the MicroPie ASGI framework.
+
+MicroPie ASGI Web Framework
+===========================
+
+MicroPie is a small, asynchronous web framework built on top of the
+ASGI specification. It is designed to be simple to learn and easy to
+extend while still providing the features necessary to build modern web
+applications. MicroPie offers automatic URL routing, pluggable
+session back‑ends, middleware hooks, WebSocket support and optional
+template rendering. Its focus on minimalism makes it a good choice for
+lightweight services, APIs and educational projects.
+
+This documentation is organized according to the
+Diátaxis documentation framework. That framework separates documentation into
+four distinct types:
+
+* **Tutorials** – step‑by‑step introductions that teach you how to
+ accomplish a task. Tutorials should avoid deep explanations and help
+ you get started quickly.
+* **How‑to guides** – recipes focused on solving a particular problem.
+ Guides assume you already know the basics and want to apply MicroPie
+ to a concrete task.
+* **Reference** – authoritative descriptions of classes, functions and
+ modules. These documents are factual and concise.
+* **Explanation** – discussions that explore the rationale behind
+ design choices and deeper concepts in MicroPie.
+
+In addition to these four types, a small glossary collects
+terminology used throughout the framework. If you are new to
+MicroPie, start with the tutorial. If you want to accomplish a
+specific task, head to the how‑to guides. If you need to look up a
+particular method or class, consult the reference manual. For
+background and design considerations, see the explanation section.
+
+.. _Diátaxis documentation framework: https://diataxis.fr/
+
+Contents
+========
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Documentation
+
+ tutorial/index
+ howto/index
+ reference/index
+ explanation/index
+ glossary
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/docs/reference/app.rst b/docs/docs/reference/app.rst
new file mode 100644
index 0000000..7fbad67
--- /dev/null
+++ b/docs/docs/reference/app.rst
@@ -0,0 +1,78 @@
+App class
+=========
+
+.. _app-class:
+
+The :class:`~micropie.App` class is the core of a MicroPie application.
+It implements the ASGI call interface and dispatches HTTP, WebSocket
+and lifespan events to your handlers. Subclass :class:`~micropie.App`
+and add public methods to create route handlers. Instantiate your
+subclass and pass the resulting object to an ASGI server such as
+Uvicorn.
+
+Constructor
+-----------
+
+.. class:: App(session_backend=None)
+
+ Create a new application. If *session_backend* is provided it must
+ be an instance of :class:`~micropie.SessionBackend`. When omitted,
+ MicroPie uses an in‑memory back‑end.
+
+Attributes
+----------
+
+.. attribute:: middlewares
+
+ A list of :class:`~micropie.HttpMiddleware` instances. Middlewares
+ run before and after every HTTP request. Append a middleware
+ instance to enable it. See :doc:`../howto/middleware` for examples.
+
+.. attribute:: ws_middlewares
+
+ A list of :class:`~micropie.WebSocketMiddleware` instances used for
+ WebSocket connections.
+
+.. attribute:: startup_handlers
+
+ A list of asynchronous callables that run during the ASGI
+ ``lifespan.startup`` event. Use this to set up resources such as
+ database connections. See :doc:`../tutorial/quickstart` for an
+ example.
+
+.. attribute:: shutdown_handlers
+
+ A list of asynchronous callables that run during the ASGI
+ ``lifespan.shutdown`` event. Use this to clean up resources.
+
+Methods
+-------
+
+.. method:: __call__(scope, receive, send)
+
+ The ASGI entry point. Dispatches to HTTP, WebSocket or lifespan
+ handlers based on ``scope['type']``. You normally do not call this
+ directly; the ASGI server calls it for you.
+
+.. method:: request
+
+ Return the current :class:`~micropie.Request` object. This is
+ stored in a context variable so that it is available throughout
+ asynchronous callbacks.
+
+.. method:: _redirect(location, extra_headers=None)
+
+ Return a tuple ``(302, '', headers)`` representing an HTTP redirect
+ to *location*. Use this helper in your handlers to redirect the
+ client.
+
+.. method:: _render_template(name, **kwargs)
+
+ Render a Jinja2 template asynchronously. Returns a string
+ containing the rendered output. Requires Jinja2 to be installed.
+ See :doc:`../howto/templates` for details.
+
+Additionally, MicroPie defines several private helper methods such as
+``_parse_cookies`` and ``_send_response``. These are considered
+internal and not part of the public API. They may change without
+notice.
\ No newline at end of file
diff --git a/docs/docs/reference/exceptions.rst b/docs/docs/reference/exceptions.rst
new file mode 100644
index 0000000..fd1454c
--- /dev/null
+++ b/docs/docs/reference/exceptions.rst
@@ -0,0 +1,18 @@
+Exceptions
+==========
+
+MicroPie defines a small number of exceptions. The most commonly
+encountered is :class:`~micropie.ConnectionClosed`, used for WebSocket
+disconnections.
+
+ConnectionClosed
+----------------
+
+.. class:: ConnectionClosed
+
+ Raised by :meth:`~micropie.WebSocket.receive_text` and
+ :meth:`~micropie.WebSocket.receive_bytes` when the client closes
+ the WebSocket connection. Catch this exception to detect
+ disconnection and exit your handler gracefully. The exception does
+ not carry any attributes; the WebSocket connection has already been
+ closed when it is raised.
\ No newline at end of file
diff --git a/docs/docs/reference/index.rst b/docs/docs/reference/index.rst
new file mode 100644
index 0000000..40095d3
--- /dev/null
+++ b/docs/docs/reference/index.rst
@@ -0,0 +1,18 @@
+API reference
+=============
+
+The reference manual provides detailed descriptions of the classes and
+functions exposed by MicroPie. These documents are factual and
+succinct【730263613588085†L336-L339】. For tutorials and recipes, see the
+:doc:`../tutorial/index` and :doc:`../howto/index` sections.
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Reference
+
+ app
+ request
+ websocket
+ middleware
+ session
+ exceptions
\ No newline at end of file
diff --git a/docs/docs/reference/middleware.rst b/docs/docs/reference/middleware.rst
new file mode 100644
index 0000000..4754f9e
--- /dev/null
+++ b/docs/docs/reference/middleware.rst
@@ -0,0 +1,51 @@
+Middleware classes
+==================
+
+MicroPie defines abstract base classes for writing middleware. Use
+these classes to intercept requests, implement custom routing,
+authentication, logging and other cross‑cutting concerns. See the
+:doc:`../howto/middleware` guide for practical examples.
+
+HttpMiddleware
+--------------
+
+.. class:: HttpMiddleware
+
+ Base class for HTTP middleware. Subclasses must implement two
+ asynchronous methods:
+
+ .. method:: before_request(request)
+
+ Called before the HTTP handler runs. *request* is the
+ :class:`~micropie.Request` object. Return a dictionary
+ ``{"status_code": status, "body": body, "headers": headers}``
+ to short‑circuit the request and send a response immediately. To
+ continue processing, return ``None``.
+
+ .. method:: after_request(request, status_code, response_body, extra_headers)
+
+ Called after the handler has completed but before the response
+ is sent. You may return a dictionary with updated
+ ``status_code``, ``body`` and ``headers``. Return ``None`` to
+ leave the response unchanged.
+
+WebSocketMiddleware
+-------------------
+
+.. class:: WebSocketMiddleware
+
+ Base class for WebSocket middleware. Subclasses must implement
+ these methods:
+
+ .. method:: before_websocket(request)
+
+ Called before a WebSocket handler runs. *request* is a
+ :class:`~micropie.WebSocketRequest` object. Return a dictionary
+ ``{"code": code, "reason": reason}`` to reject the connection
+ with the given close code and reason. Return ``None`` to allow
+ the connection.
+
+ .. method:: after_websocket(request)
+
+ Called after the WebSocket handler completes. Use this to
+ release resources or log activity. Return value is ignored.
\ No newline at end of file
diff --git a/docs/docs/reference/request.rst b/docs/docs/reference/request.rst
new file mode 100644
index 0000000..332b8e7
--- /dev/null
+++ b/docs/docs/reference/request.rst
@@ -0,0 +1,80 @@
+Request objects
+===============
+
+.. _request-object:
+
+MicroPie provides two request classes: :class:`~micropie.Request` for
+HTTP requests and :class:`~micropie.WebSocketRequest` for WebSocket
+connections. You access the current request via
+:meth:`~micropie.App.request` on your application instance or by using
+the context variable in lower‑level code.
+
+Request class
+-------------
+
+.. class:: Request(scope)
+
+ Represents an incoming HTTP request. The *scope* argument is the
+ ASGI scope dictionary provided by the server. You should not
+ instantiate this class yourself. MicroPie creates one per
+ request and stores it in a context variable.
+
+ .. attribute:: scope
+
+ The original ASGI scope for the request.
+
+ .. attribute:: method
+
+ The HTTP method, such as ``GET`` or ``POST``.
+
+ .. attribute:: path_params
+
+ A list of positional path parameters. See
+ :doc:`../tutorial/routing` for details on parameter mapping.
+
+ .. attribute:: query_params
+
+ A ``dict`` mapping each query parameter to a list of values.
+ For convenience, use ``self.request.query_params['name'][0]`` to
+ obtain the first value.
+
+ .. attribute:: body_params
+
+ A ``dict`` mapping form field names to lists of values. For
+ JSON requests, body parameters are derived from the top‑level
+ object; for ``application/x-www-form-urlencoded`` forms they are
+ parsed using :func:`urllib.parse.parse_qs`.
+
+ .. attribute:: get_json
+
+ The JSON body parsed into a Python object. Only populated when
+ the request contains valid JSON with content type
+ ``application/json``.
+
+ .. attribute:: session
+
+ A ``dict`` for storing per‑client data across requests. See
+ :doc:`../howto/sessions`.
+
+ .. attribute:: files
+
+ A ``dict`` of uploaded files for multipart/form‑data requests.
+ Each entry is a mapping containing keys ``filename``,
+ ``content_type`` and ``content``, where ``content`` is an
+ ``asyncio.Queue`` yielding file chunks as bytes. See
+ :doc:`../tutorial/routing` for information on awaiting file fields.
+
+ .. attribute:: headers
+
+ A case‑insensitive mapping of header names to values, decoded as
+ UTF‑8 with invalid bytes replaced. Header names are lowercased.
+
+WebSocketRequest class
+----------------------
+
+.. class:: WebSocketRequest(scope)
+
+ Inherits from :class:`~micropie.Request` and represents a WebSocket
+ connection request. All attributes of :class:`~micropie.Request`
+ apply. For WebSocket handlers the request is accessible via
+ ``self.request`` inside the handler or via the context variable.
\ No newline at end of file
diff --git a/docs/docs/reference/session.rst b/docs/docs/reference/session.rst
new file mode 100644
index 0000000..1804ae2
--- /dev/null
+++ b/docs/docs/reference/session.rst
@@ -0,0 +1,63 @@
+Session back‑ends
+=================
+
+MicroPie abstracts session storage behind the :class:`~micropie.SessionBackend`
+interface. Different back‑ends may store session data in memory,
+databases, caches or external services. A session is a dictionary of
+key/value pairs associated with a ``session_id`` cookie.
+
+SessionBackend
+--------------
+
+.. class:: SessionBackend
+
+ Abstract base class for session storage. Implementations must
+ provide the following asynchronous methods:
+
+ .. method:: load(session_id)
+
+ Load session data for the given *session_id*. Return a
+ dictionary of session data or an empty dictionary if the session
+ does not exist or has expired.
+
+ .. method:: save(session_id, data, timeout)
+
+ Persist the *data* dictionary for the given *session_id* with
+ an expiry time of *timeout* seconds.
+
+InMemorySessionBackend
+----------------------
+
+.. class:: InMemorySessionBackend
+
+ Concrete implementation of :class:`SessionBackend` that stores
+ sessions in memory. This back‑end is appropriate for development
+ and testing but does not persist data across process restarts and
+ cannot be shared among worker processes.
+
+ .. method:: __init__()
+
+ Create an empty in‑memory session store.
+
+ .. method:: load(session_id)
+
+ Return the session dictionary if it exists and has not expired
+ according to :data:`micropie.SESSION_TIMEOUT`, otherwise return
+ an empty dictionary.
+
+ .. method:: save(session_id, data, timeout)
+
+ Store *data* under *session_id* and update the last access time.
+
+SESSION_TIMEOUT
+---------------
+
+.. data:: SESSION_TIMEOUT
+
+ The default session expiration time in seconds (eight hours). You
+ can override this constant in your own code before importing
+ MicroPie or by passing a different timeout when saving sessions in
+ a custom back‑end.
+
+See also the :doc:`../howto/sessions` guide for examples of using and
+implementing session back‑ends.
\ No newline at end of file
diff --git a/docs/docs/reference/websocket.rst b/docs/docs/reference/websocket.rst
new file mode 100644
index 0000000..78b84a2
--- /dev/null
+++ b/docs/docs/reference/websocket.rst
@@ -0,0 +1,74 @@
+WebSocket class
+===============
+
+.. _websocket-reference:
+
+The :class:`~micropie.WebSocket` class encapsulates a WebSocket
+connection. MicroPie constructs an instance for each WebSocket
+request and passes it as the first argument to your WebSocket handler.
+
+Constructor
+-----------
+
+.. class:: WebSocket(receive, send)
+
+ Create a new WebSocket wrapper around the ASGI ``receive`` and
+ ``send`` callables. You do not instantiate this class yourself;
+ MicroPie does so internally.
+
+Methods
+-------
+
+.. method:: accept(subprotocol=None, session_id=None)
+
+ Accept the WebSocket connection. You must call this method before
+ sending or receiving messages. If you provide a *session_id*,
+ MicroPie sets a ``session_id`` cookie during the handshake. The
+ optional *subprotocol* argument specifies a negotiated subprotocol.
+
+.. method:: receive_text()
+
+ Await a text message from the client. Returns a string. Raises
+ :class:`~micropie.ConnectionClosed` if the client has disconnected or
+ ``ValueError`` if a binary message is received.
+
+.. method:: receive_bytes()
+
+ Await a binary message from the client. Returns bytes. Raises
+ :class:`~micropie.ConnectionClosed` if the client has disconnected or
+ ``ValueError`` if a text message is received.
+
+.. method:: send_text(data)
+
+ Send a text message to the client. Raises ``RuntimeError`` if you
+ have not called :meth:`accept`.
+
+.. method:: send_bytes(data)
+
+ Send a binary message to the client. Raises ``RuntimeError`` if
+ the connection has not been accepted.
+
+.. method:: close(code=1000, reason=None)
+
+ Close the WebSocket connection. By default uses code 1000
+ (normal closure). The optional *reason* is sent to the client.
+
+Attributes
+----------
+
+.. attribute:: accepted
+
+ ``True`` if the WebSocket has been accepted.
+
+.. attribute:: session_id
+
+ The session ID set during the handshake, or ``None`` if not set.
+
+Exceptions
+----------
+
+.. class:: ConnectionClosed
+
+ Raised by :meth:`receive_text` and :meth:`receive_bytes` when the
+ client has closed the connection. Catch this exception in your
+ handlers to perform cleanup after a disconnect.
\ No newline at end of file
diff --git a/docs/docs/tutorial/index.rst b/docs/docs/tutorial/index.rst
new file mode 100644
index 0000000..93f2c06
--- /dev/null
+++ b/docs/docs/tutorial/index.rst
@@ -0,0 +1,16 @@
+Tutorials
+=========
+
+The tutorials walk you through the basics of writing a MicroPie
+application. They are intended for readers who have never used
+MicroPie before. Each tutorial builds upon the previous one and
+demonstrates a specific aspect of the framework. Tutorials should
+present clear, logical steps and avoid long theoretical discussions.
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Tutorials
+
+ quickstart
+ routing
+ websockets
diff --git a/docs/docs/tutorial/quickstart.rst b/docs/docs/tutorial/quickstart.rst
new file mode 100644
index 0000000..9531525
--- /dev/null
+++ b/docs/docs/tutorial/quickstart.rst
@@ -0,0 +1,80 @@
+Quick start
+===========
+
+This tutorial shows you how to install MicroPie and run a very simple
+application. By the end you will be able to serve an HTTP
+endpoint that returns plain text. This section assumes that you
+already have Python 3.8 or later installed.
+
+Installation
+------------
+
+MicroPie is distributed on the Python Package Index (PyPI). The
+recommended way to install it is via ``pip`` with the standard
+optional dependencies:
+
+.. code-block:: console
+
+ $ pip install micropie[standard]
+
+This command installs MicroPie along with the Jinja2 template engine
+and the ``multipart`` parser for handling file uploads. If you want
+the fastest JSON parsing and a preconfigured ASGI server, you can
+install all optional extras:
+
+.. code-block:: console
+
+ $ pip install micropie[all]
+
+See :ref:`reference/session` for details about session back‑ends and
+the :ref:`howto/templates` guide for information on templates.
+
+Writing your first application
+------------------------------
+
+Create a file called ``app.py`` with the following contents:
+
+.. code-block:: python
+
+ from micropie import App
+
+ class MyApp(App):
+ async def index(self):
+ return "Welcome to MicroPie ASGI."
+
+ app = MyApp()
+
+MicroPie applications are defined by subclassing :class:`~micropie.App`.
+Each public method of your subclass becomes a potential route handler
+automatically mapped to an HTTP path. The special method ``index``
+handles requests to the root path ``/``. Handlers may be asynchronous
+(``async def``) or synchronous functions and can return strings,
+bytes, JSON‑serialisable objects, tuples of status and body, or an
+async generator for streaming responses.
+
+Running the application
+-----------------------
+
+MicroPie itself is not a web server; it is an ASGI application. To
+run it you will need an ASGI server such as Uvicorn, Hypercorn or
+Daphne. Install Uvicorn if you have not already:
+
+.. code-block:: console
+
+ $ pip install uvicorn
+
+Start your application with Uvicorn:
+
+.. code-block:: console
+
+ $ uvicorn app:app
+
+By default Uvicorn listens on ``http://127.0.0.1:8000``. Open that URL
+in your browser and you should see the text “Welcome to MicroPie ASGI.”
+
+What’s next?
+------------
+
+Now that you have a running MicroPie application, proceed to the
+:doc:`routing <routing>` tutorial to learn how URL paths map to
+methods and how to pass parameters to your handlers.
\ No newline at end of file
diff --git a/docs/docs/tutorial/routing.rst b/docs/docs/tutorial/routing.rst
new file mode 100644
index 0000000..d4a3be5
--- /dev/null
+++ b/docs/docs/tutorial/routing.rst
@@ -0,0 +1,126 @@
+Routing and handlers
+====================
+
+MicroPie maps incoming HTTP requests to methods on your :class:`~micropie.App`
+subclass. This section explains how the mapping works and how your
+handlers receive input from the URL path, query strings, form data and
+sessions.
+
+URL to function mapping
+-----------------------
+
+When an HTTP request arrives, MicroPie extracts the path portion of the
+URL and uses the first path segment to determine which method should
+handle the request. For example, a GET request to ``/greet`` calls
+``greet`` on your :class:`~micropie.App` subclass. A request to the
+root URL ``/`` calls ``index``. Only methods that do not start with
+an underscore are exposed as route handlers. Prefacing a method name
+with an underscore makes it private and hides it from external
+requests.
+
+Handlers may be synchronous or asynchronous functions. They return
+either a string, bytes, a JSON‑serialisable object, a tuple of
+``(status_code, body)`` or ``(status_code, body, headers)``, or an
+iterator/generator for streaming responses. See
+:doc:`howto/streaming` for details on streaming.
+
+Parameters and arguments
+------------------------
+
+MicroPie automatically populates handler arguments from several
+sources in the following order:
+
+1. **Path parameters** – Additional path segments after the first map to
+ positional parameters. For example:
+
+ .. code-block:: python
+
+ class AppExample(App):
+ async def greet(self, name):
+ return f"Hello, {name}!"
+
+ Visiting ``/greet/Alice`` passes ``"Alice"`` as the ``name`` argument.
+ You can also use ``*args`` to capture an arbitrary number of path
+ segments.
+
+2. **Query parameters** – Named parameters in the query string fill
+ keyword arguments when path parameters are exhausted. For example,
+ ``/greet?name=Alice`` also passes ``"Alice"`` to the ``name``
+ parameter.
+
+3. **Body/form data** – For POST, PUT and PATCH requests, form fields
+ populate handler arguments. JSON bodies are decoded into a
+ dictionary of key/value pairs.
+
+4. **Files** – If the request is multipart/form‑data, uploaded files
+ appear in :attr:`~micropie.Request.files`. You can declare a file
+ argument in your handler signature to receive a file object.
+
+5. **Session data** – Values stored in :attr:`~micropie.Request.session`
+ fill remaining parameters. See :doc:`howto/sessions` for details.
+
+6. **Default values** – If no other source provides a value, default
+ values in your function signature are used.
+
+If MicroPie cannot determine a value for a required parameter, it
+returns a ``400 Bad Request``.
+
+Examples
+--------
+
+The following examples illustrate common patterns:
+
+.. code-block:: python
+
+ class MyApp(App):
+ async def greet(self, name="Guest"):
+ """Return a greeting for a named visitor.
+
+ If the ``name`` argument is not provided via the path or
+ query parameters, ``"Guest"`` is used as a default.
+ """
+ return f"Hello, {name}!"
+
+ async def add(self, x, y):
+ """Add two numbers provided via path segments.
+
+ Example: ``/add/2/3`` returns ``5``.
+ """
+ return str(int(x) + int(y))
+
+ async def submit(self, username="Anonymous"):
+ """Handle a form submission.
+
+ This handler accepts POST requests and uses the
+ ``username`` field from the request body.
+ """
+ return f"Form submitted by: {username}"
+
+See :ref:`request-object` for the attributes available on the current
+request and :ref:`app-class` for details about the ``App`` class.
+
+HTTP methods and responses
+--------------------------
+
+Handlers run for any HTTP method unless you explicitly check
+``self.request.method`` inside the handler. It is your
+responsibility to dispatch based on the method if your endpoint should
+behave differently for GET and POST. Handlers may return:
+
+* A string or bytes – sent as the body of the response with a
+ ``200 OK`` status.
+* A JSON‑serialisable object – automatically encoded into JSON and
+ returned with a ``Content‑Type`` of ``application/json``.
+* A tuple ``(status_code, body)`` – sets the HTTP status and body.
+* A tuple ``(status_code, body, headers)`` – sets status, body and
+ additional headers. Headers should be a list of ``(name, value)`` pairs.
+* A generator or async generator – streams chunks to the client. Use
+ this for large responses or server‑sent events.
+
+Advanced routing
+----------------
+
+You can implement explicit routing, path prefixing or complex
+dispatching by writing a custom :class:`~micropie.HttpMiddleware`. See
+the :doc:`howto/middleware` guide and the examples in the
+``examples/explicit_routing`` directory of the source distribution.
\ No newline at end of file
diff --git a/docs/docs/tutorial/websockets.rst b/docs/docs/tutorial/websockets.rst
new file mode 100644
index 0000000..e4197a2
--- /dev/null
+++ b/docs/docs/tutorial/websockets.rst
@@ -0,0 +1,88 @@
+WebSocket support
+=================
+
+MicroPie includes first‑class support for WebSockets. WebSockets are a
+persistent, bidirectional communication channel over a single TCP
+connection. They enable real‑time applications such as chat, live
+notifications and collaborative editing.
+
+Mapping WebSocket routes
+------------------------
+
+WebSocket handlers live on the same :class:`~micropie.App` subclass
+that serves your HTTP routes. The naming convention is similar to
+HTTP handlers, but with a ``ws_`` prefix. For example, a method
+called ``ws_chat`` handles WebSocket connections at ``/chat``. If the
+path is empty, ``ws_index`` handles the root WebSocket route.
+
+When a WebSocket connection is initiated, MicroPie constructs a
+:class:`~micropie.WebSocket` object and passes it as the first
+argument to your handler, along with any path or query parameters. A
+typical handler must call :meth:`~micropie.WebSocket.accept` to accept
+the connection and then use :meth:`~micropie.WebSocket.receive_text`
+and :meth:`~micropie.WebSocket.send_text` (or their ``bytes``
+counterparts) to communicate.
+
+Example: echo server
+--------------------
+
+The example below implements a simple echo server. The server
+accepts the WebSocket connection and then echoes back whatever text it
+receives. When the client disconnects, the handler finishes.
+
+.. code-block:: python
+
+ from micropie import App
+
+ class MyApp(App):
+ async def ws_echo(self, ws):
+ # Accept the connection; without this call, MicroPie will
+ # automatically reject the WebSocket.
+ await ws.accept()
+ while True:
+ try:
+ message = await ws.receive_text()
+ except ConnectionClosed:
+ # Client closed the connection
+ break
+ # Send the same message back to the client
+ await ws.send_text(f"Echo: {message}")
+
+ app = MyApp()
+
+To test this endpoint, run your application and connect to
+``ws://127.0.0.1:8000/echo`` using a WebSocket client. Most modern
+browsers support WebSockets via JavaScript.
+
+Accessing query and path parameters
+-----------------------------------
+
+Just like HTTP handlers, WebSocket handlers receive path and query
+parameters. Additional positional arguments come from the path, and
+keyword arguments are filled from the query string or session. For
+example:
+
+.. code-block:: python
+
+ class MyApp(App):
+ async def ws_greet(self, ws, name="Guest"):
+ await ws.accept()
+ await ws.send_text(f"Hello {name}!")
+ await ws.close()
+
+Connecting to ``ws://localhost:8000/greet/Alice`` sends back
+``"Hello Alice!"``. Connecting to ``ws://localhost:8000/greet?name=Bob``
+sends ``"Hello Bob!"``. See :ref:`websocket-reference` for the
+complete API of the :class:`~micropie.WebSocket` class.
+
+Integrating Socket.IO
+---------------------
+
+MicroPie focuses on the core WebSocket protocol. If you need more
+advanced real‑time features—automatic reconnection, broadcasting, or
+fallback transports such as polling—you can integrate
+`python‑socketio <https://python-socketio.readthedocs.io>`_. See the
+``examples/socketio`` directory in the MicroPie source distribution for
+sample code and instructions. When integrating Socket.IO, you still
+run your MicroPie application behind an ASGI server and attach the
+Socket.IO server as additional middleware or route.
\ No newline at end of file
diff --git a/micropie.py b/micropie.py
index 5f53d18..dded65d 100644
--- a/micropie.py
+++ b/micropie.py
@@ -467,13 +467,12 @@ class App:
await self._send_response(send, 400, "400 Bad Request: Missing boundary")
return
# Start TRUE streaming parser in background, populate request.* live
- # Tune file_queue_maxsize as desired (e.g., 512 ≈ ~32MB if ~64KB chunks)
parse_task = asyncio.create_task(
self._parse_multipart_into_request(
receive,
boundary_match.group(1).encode("utf-8"),
request,
- file_queue_maxsize=512,
+ file_queue_maxsize=2048,
)
)
else:
@@ -818,7 +817,7 @@ class App:
boundary: bytes,
request: "Request",
*,
- file_queue_maxsize: int = 100000
+ file_queue_maxsize: int = 2048
) -> None:
"""
Parse multipart directly from ASGI receive() and populate