diff --git a/docs/api.rst b/docs/api.rst
index 2c88bf7..242e144 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -4,92 +4,71 @@ API Reference
``microdot`` module
-------------------
-The ``microdot`` module defines a few classes that help implement HTTP-based
-servers for MicroPython and standard Python, with multithreading support for
-Python interpreters that support it.
-
-``Microdot`` class
-~~~~~~~~~~~~~~~~~~
-
.. autoclass:: microdot.Microdot
:members:
-``Request`` class
-~~~~~~~~~~~~~~~~~
-
.. autoclass:: microdot.Request
:members:
-``Response`` class
-~~~~~~~~~~~~~~~~~~
-
.. autoclass:: microdot.Response
:members:
-``MultiDict`` class
-~~~~~~~~~~~~~~~~~~~
-
.. autoclass:: microdot.MultiDict
:members:
``microdot_asyncio`` module
---------------------------
-The ``microdot_asyncio`` module defines a few classes that help implement
-HTTP-based servers for MicroPython and standard Python that use ``asyncio``
-and coroutines.
-
-``Microdot`` class
-~~~~~~~~~~~~~~~~~~
-
.. autoclass:: microdot_asyncio.Microdot
:inherited-members:
:members:
-``Request`` class
-~~~~~~~~~~~~~~~~~
-
.. autoclass:: microdot_asyncio.Request
:inherited-members:
:members:
-``Response`` class
-~~~~~~~~~~~~~~~~~~
-
.. autoclass:: microdot_asyncio.Response
:inherited-members:
:members:
-``microdot_test_client`` module
--------------------------------
+``microdot_utemplate`` module
+-----------------------------
-The ``microdot_test_client`` module defines a test client that can be used to
-create automated tests for the Microdot server.
+.. automodule:: microdot_utemplate
+ :members:
-``TestClient`` class
-~~~~~~~~~~~~~~~~~~~~
+``microdot_jinja`` module
+-------------------------
-.. autoclass:: microdot_test_client.TestClient
+.. automodule:: microdot_jinja
:members:
-``TestResponse`` class
-~~~~~~~~~~~~~~~~~~~~~~
+``microdot_session`` module
+---------------------------
+
+.. automodule:: microdot_session
+ :members:
+
+``microdot_test_client`` module
+-------------------------------
+
+.. autoclass:: microdot_test_client.TestClient
+ :members:
.. autoclass:: microdot_test_client.TestResponse
:members:
-``microdot_wsgi`` module
-------------------------
+``microdot_asyncio_test_client`` module
+---------------------------------------
-The ``microdot_wsgi`` module provides an extended ``Microdot`` class that
-implements the WSGI protocol and can be used with a compliant WSGI web server
-such as `Gunicorn `_ or
-`uWSGI `_. Since there are
-no WSGI web servers available for MicroPython, this support is currently
-limited to standard Python.
+.. autoclass:: microdot_asyncio_test_client.TestClient
+ :members:
-``Microdot`` class
-~~~~~~~~~~~~~~~~~~
+.. autoclass:: microdot_asyncio_test_client.TestResponse
+ :members:
+
+``microdot_wsgi`` module
+------------------------
.. autoclass:: microdot_wsgi.Microdot
:members:
@@ -98,15 +77,6 @@ limited to standard Python.
``microdot_asgi`` module
------------------------
-The ``microdot_asgi`` module provides an extended ``Microdot`` class that
-implements the ASGI protocol and can be used with a compliant ASGI server such
-as `Uvicorn `_. Since there are no ASGI web servers
-available for MicroPython, this support is currently limited to standard
-Python.
-
-``Microdot`` class
-~~~~~~~~~~~~~~~~~~
-
.. autoclass:: microdot_asgi.Microdot
:members:
:exclude-members: shutdown, run
diff --git a/docs/conf.py b/docs/conf.py
index 61204be..986fe2d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -13,7 +13,7 @@
import os
import sys
sys.path.insert(0, os.path.abspath('../src'))
-
+sys.path.insert(1, os.path.abspath('../libs/common'))
# -- Project information -----------------------------------------------------
diff --git a/docs/extensions.rst b/docs/extensions.rst
index 481f4a4..b3e425b 100644
--- a/docs/extensions.rst
+++ b/docs/extensions.rst
@@ -22,10 +22,13 @@ Asynchronous Support with ``asyncio``
- | CPython: None
| MicroPython: `uasyncio `_
+ * - Examples
+ - | `hello_async.py `_
+
Microdot can be extended to use an asynchronous programming model based on the
``asyncio`` package. When the :class:`Microdot `
class is imported from the ``microdot_asyncio`` package, an asynchronous server
-is used.
+is used, and handlers can be defined as coroutines.
The example that follows uses ``asyncio`` coroutines for concurrency::
@@ -42,8 +45,14 @@ The example that follows uses ``asyncio`` coroutines for concurrency::
Rendering HTML Templates
~~~~~~~~~~~~~~~~~~~~~~~~
+Many web applications use HTML templates for rendering content to clients.
+Microdot includes extensions to render templates with the
+`utemplate `_ package on CPython and
+MicroPython, and with `Jinja `_ only on
+CPython.
+
Using the uTemplate Engine
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
+^^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:align: left
@@ -58,6 +67,32 @@ Using the uTemplate Engine
* - Required external dependencies
- | `utemplate `_
+ * - Examples
+ - | `hello_utemplate.py `_
+ | `hello_utemplate_async.py `_
+
+The :func:`render_template ` function is
+used to render HTML templates with the uTemplate engine. The first argument is
+the template filename, relative to the templates directory, which is
+*templates* by default. Any additional arguments are passed to the template
+engine to be used as arguments.
+
+Example::
+
+ from microdot_utemplate import render_template
+
+ @app.get('/')
+ def index(req):
+ return render_template('index.html')
+
+The default location from where templates are loaded is the *templates*
+subdirectory. This location can be changed with the
+:func:`init_templates ` function::
+
+ from microdot_utemplate import init_templates
+
+ init_templates('my_templates')
+
Using the Jinja Engine
^^^^^^^^^^^^^^^^^^^^^^
@@ -74,6 +109,34 @@ Using the Jinja Engine
* - Required external dependencies
- | `Jinja2 `_
+ * - Examples
+ - | `hello_jinja.py `_
+
+The :func:`render_template ` function is used
+to render HTML templates with the Jinja engine. The first argument is the
+template filename, relative to the templates directory, which is *templates* by
+default. Any additional arguments are passed to the template engine to be used
+as arguments.
+
+Example::
+
+ from microdot_jinja import render_template
+
+ @app.get('/')
+ def index(req):
+ return render_template('index.html')
+
+The default location from where templates are loaded is the *templates*
+subdirectory. This location can be changed with the
+:func:`init_templates ` function::
+
+ from microdot_jinja import init_templates
+
+ init_templates('my_templates')
+
+.. note::
+ The Jinja extension is not compatible with MicroPython.
+
Maintaing Secure User Sessions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -89,11 +152,64 @@ Maintaing Secure User Sessions
* - Required external dependencies
- | CPython: `PyJWT `_
- | MicroPython: `ujwt.py `_,
+ | MicroPython: `jwt.py `_,
`hmac `_,
`hashlib `_,
`warnings `_
+ * - Examples
+ - | `login.py `_
+
+The session extension provides a secure way for the application to maintain
+user sessions. The session is stored as a signed cookie in the client's
+browser, in `JSON Web Token (JWT) `_
+format.
+
+To work with user sessions, the application first must configure the secret key
+that will be used to sign the session cookies. It is very important that this
+key is kept secret. An attacker who is in possession of this key can generate
+valid user session cookies with any contents.
+
+To set the secret key, use the :func:`set_session_secret_key ` function::
+
+ from microdot_session import set_session_secret_key
+
+ set_session_secret_key('top-secret!')
+
+To :func:`get_session `,
+:func:`update_session ` and
+:func:`delete_session ` functions are used
+inside route handlers to retrieve, store and delete session data respectively.
+The :func:`with_session ` decorator is provided
+as a convenient way to retrieve the session at the start of a route handler.
+
+Example::
+
+ from microdot import Microdot
+ from microdot_session import set_session_secret_key, with_session, \
+ update_session, delete_session
+
+ app = Microdot()
+ set_session_secret_key('top-secret')
+
+ @app.route('/', methods=['GET', 'POST'])
+ @with_session
+ def index(req, session):
+ username = session.get('username')
+ if req.method == 'POST':
+ username = req.form.get('username')
+ update_session(req, {'username': username})
+ return redirect('/')
+ if username is None:
+ return 'Not logged in'
+ else:
+ return 'Logged in as ' + username
+
+ @app.post('/logout')
+ def logout(req):
+ delete_session(req)
+ return redirect('/')
+
Test Client
~~~~~~~~~~~
@@ -110,9 +226,69 @@ Test Client
* - Required external dependencies
- | None
+The Microdot Test Client is a utility class that can be used during testing to
+send requests into the application.
+
+Example::
+
+ from microdot import Microdot
+ from microdot_test_client import TestClient
+
+ app = Microdot()
+
+ @app.route('/')
+ def index(req):
+ return 'Hello, World!'
+
+ def test_app():
+ client = TestClient(app)
+ response = client.get('/')
+ assert response.text == 'Hello, World!'
+
+See the documentation for the :class:`TestClient `
+class for more details.
+
+Asynchronous Test Client
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. list-table::
+ :align: left
+
+ * - Compatibility
+ - | CPython & MicroPython
+
+ * - Required Microdot source files
+ - | `microdot.py `_
+ | `microdot_asyncio.py `_
+ | `microdot_test_client.py `_
+ | `microdot_asyncio_test_client.py `_
+
+ * - Required external dependencies
+ - | None
+
+Similar to the :class:`TestClient ` class
+above, but for asynchronous applications.
+
+Example usage::
+
+ from microdot_asyncio_test_client import TestClient
+
+ async def test_app():
+ client = TestClient(app)
+ response = await client.get('/')
+ assert response.text == 'Hello, World!'
+
+See the :class:`reference documentation `
+for details.
+
Deploying on a Production Web Server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The ``Microdot`` class creates its own simple web server. This is enough for an
+application deployed with MicroPython, but when using CPython it may be useful
+to use a separate, battle-tested web server. To address this need, Microdot
+provides extensions that implement the WSGI and ASGI protocols.
+
Using a WSGI Web Server
^^^^^^^^^^^^^^^^^^^^^^^
@@ -129,6 +305,34 @@ Using a WSGI Web Server
* - Required external dependencies
- | A WSGI web server, such as `Gunicorn `_.
+ * - Examples
+ - | `hello_wsgi.py `_
+
+
+The ``microdot_wsgi`` module provides an extended ``Microdot`` class that
+implements the WSGI protocol and can be used with a compliant WSGI web server
+such as `Gunicorn `_ or
+`uWSGI `_.
+
+To use a WSGI web server, the application must import the
+:class:`Microdot ` class from the ``microdot_wsgi``
+module::
+
+ from microdot_wsgi import Microdot
+
+ app = Microdot()
+
+ @app.route('/')
+ def index(req):
+ return 'Hello, World!'
+
+The ``app`` application instance created from this class is a WSGI application
+that can be used with any complaint WSGI web server. If the above application
+is stored in a file called *test.py*, then the following command runs the
+web application using the Gunicorn web server::
+
+ gunicorn test:app
+
Using an ASGI Web Server
^^^^^^^^^^^^^^^^^^^^^^^^
@@ -146,3 +350,29 @@ Using an ASGI Web Server
* - Required external dependencies
- | An ASGI web server, such as `Uvicorn `_.
+ * - Examples
+ - | `hello_asgi.py `_
+
+The ``microdot_asgi`` module provides an extended ``Microdot`` class that
+implements the ASGI protocol and can be used with a compliant ASGI server such
+as `Uvicorn `_.
+
+To use an ASGI web server, the application must import the
+:class:`Microdot ` class from the ``microdot_asgi``
+module::
+
+ from microdot_asgi import Microdot
+
+ app = Microdot()
+
+ @app.route('/')
+ async def index(req):
+ return 'Hello, World!'
+
+The ``app`` application instance created from this class is an ASGI application
+that can be used with any complaint ASGI web server. If the above application
+is stored in a file called *test.py*, then the following command runs the
+web application using the Uvicorn web server::
+
+ uvicorn test:app
+
diff --git a/docs/intro.rst b/docs/intro.rst
index dc6d634..312ec7c 100644
--- a/docs/intro.rst
+++ b/docs/intro.rst
@@ -67,6 +67,9 @@ Running with CPython
* - Required external dependencies
- | None
+ * - Examples
+ - | `hello.py `_
+
When using CPython, you can start the web server by running the script that
defines and runs the application instance::
@@ -89,6 +92,10 @@ Running with MicroPython
* - Required external dependencies
- | None
+ * - Examples
+ - | `hello.py `_
+ | `gpio.py `_
+
When using MicroPython, you can upload a *main.py* file containing the web
server code to your device along with *microdot.py*. MicroPython will
automatically run *main.py* when the device is powered on, so the web server
@@ -618,8 +625,8 @@ Example::
return {'hello': 'world'}
.. note::
- JSON responses are sent with the ``Content-Type`` header set to
- ``application/json``.
+ A ``Content-Type`` header set to ``application/json`` is automatically added
+ to the response.
Redirects
^^^^^^^^^
@@ -694,9 +701,9 @@ default content type::
Setting Cookies
^^^^^^^^^^^^^^^
-Many web application rely on cookies to maintain client state between requests.
-Cookies can be set with the ``Set-Cookie`` header in the response, but since
-this is such a common practice, Microdot provides the
+Many web applications rely on cookies to maintain client state between
+requests. Cookies can be set with the ``Set-Cookie`` header in the response,
+but since this is such a common practice, Microdot provides the
:func:`set_cookie() ` method in the response
object to add a properly formatted cookie header to the response.
@@ -719,7 +726,7 @@ Another option is to create a response object directly in the route function::
@app.get('/')
def index(request):
- response = Response('Hello, World!'))
+ response = Response('Hello, World!')
response.set_cookie('name', 'value')
return response
diff --git a/src/microdot_jinja.py b/src/microdot_jinja.py
index 6656fd9..533ef1d 100644
--- a/src/microdot_jinja.py
+++ b/src/microdot_jinja.py
@@ -18,6 +18,15 @@ def init_templates(template_dir='templates'):
def render_template(template, *args, **kwargs):
+ """Render a template.
+
+ :param template: The filename of the template to render, relative to the
+ configured template directory.
+ :param args: Positional arguments to be passed to the render engine.
+ :param kwargs: Keyword arguments to be passed to the render engine.
+
+ The return value is a string with the rendered template.
+ """
if _jinja_env is None: # pragma: no cover
init_templates()
template = _jinja_env.get_template(template)
diff --git a/src/microdot_session.py b/src/microdot_session.py
index 6f565f3..430577c 100644
--- a/src/microdot_session.py
+++ b/src/microdot_session.py
@@ -4,11 +4,22 @@
def set_session_secret_key(key):
+ """Set the secret key for signing user sessions.
+
+ :param key: The secret key, as a string or bytes object.
+ """
global secret_key
secret_key = key
def get_session(request):
+ """Retrieve the user session.
+
+ :param request: The client request.
+
+ The return value is a dictionary with the data stored in the user's
+ session, or ``{}`` if the session data is not available or invalid.
+ """
global secret_key
if not secret_key:
raise ValueError('The session secret key is not configured')
@@ -24,6 +35,14 @@ def get_session(request):
def update_session(request, session):
+ """Update the user session.
+
+ :param request: The client request.
+ :param session: A dictionary with the update session data for the user.
+
+ Calling this function adds a cookie with the updated session to the request
+ currently being processed.
+ """
if not secret_key:
raise ValueError('The session secret key is not configured')
@@ -36,6 +55,13 @@ def _update_session(request, response):
def delete_session(request):
+ """Remove the user session.
+
+ :param request: The client request.
+
+ Calling this function adds a cookie removal header to the request currently
+ being processed.
+ """
@request.after_request
def _delete_session(request, response):
response.set_cookie('session', '', http_only=True,
@@ -44,6 +70,19 @@ def _delete_session(request, response):
def with_session(f):
+ """Decorator that passes the user session to the route handler.
+
+ The session dictionary is passed to the decorated function as an argument
+ after the request object. Example::
+
+ @app.route('/')
+ @with_session
+ def index(request, session):
+ return 'Hello, World!'
+
+ Note that the decorator does not save the session. To update the session,
+ call the :func:`update_session ` function.
+ """
def wrapper(request, *args, **kwargs):
return f(request, get_session(request), *args, **kwargs)
diff --git a/src/microdot_utemplate.py b/src/microdot_utemplate.py
index cd767e2..ccef608 100644
--- a/src/microdot_utemplate.py
+++ b/src/microdot_utemplate.py
@@ -19,6 +19,15 @@ def init_templates(template_dir='templates', loader_class=recompile.Loader):
def render_template(template, *args, **kwargs):
+ """Render a template.
+
+ :param template: The filename of the template to render, relative to the
+ configured template directory.
+ :param args: Positional arguments to be passed to the render engine.
+ :param kwargs: Keyword arguments to be passed to the render engine.
+
+ The return value is an iterator that returns sections of rendered template.
+ """
if _loader is None: # pragma: no cover
init_templates()
render = _loader.load(template)