Installation
------------
The installation method is different depending on the version of Python.
CPython Installation
~~~~~~~~~~~~~~~~~~~~
For use with standard Python (CPython) projects, Microdot and all of its core
extensions are installed with ``pip``::
pip install microdot
MicroPython Installation
~~~~~~~~~~~~~~~~~~~~~~~~
For MicroPython, the recommended approach is to manually copy the necessary
source files from the
`GitHub repository `_
into your device, ideally after
`compiling `_
them to *.mpy* files. These source files can also be
`frozen `_
and incorporated into a custom MicroPython firmware.
Use the following guidelines to know what files to copy:
* For a minimal setup with only the base web server functionality, copy
`microdot.py `_
into your project.
* For a configuration that includes one or more optional extensions, create a
*microdot* directory in your device and copy the following files:
* `__init__.py `_
* `microdot.py `_
* any needed `extensions `_.
Getting Started
---------------
This section describes the main features of Microdot in an informal manner.
For detailed reference information, consult the :ref:`API Reference`.
If you are familiar with releases of Microdot before 2.x, review the
:ref:`Migration Guide `.
A Simple Microdot Web Server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following is an example of a simple web server::
from microdot import Microdot
app = Microdot()
@app.route('/')
async def index(request):
return 'Hello, world!'
app.run()
The script imports the :class:`Microdot ` class and creates
an application instance from it.
The application instance provides a :func:`route() `
decorator, which is used to define one or more routes, as needed by the
application.
The ``route()`` decorator takes the path portion of the URL as an
argument, and maps it to the decorated function, so that the function is called
when the client requests the URL.
When the function is called, it is passed a :class:`Request `
object as an argument, which provides access to the information passed by the
client. The value returned by the function is sent back to the client as the
response.
Microdot is an asynchronous framework that uses the ``asyncio`` package. Route
handler functions can be defined as ``async def`` or ``def`` functions, but
``async def`` functions are recommended for performance.
The :func:`run() ` method starts the application's web
server on port 5000 by default, and creates its own asynchronous loop. This
method blocks while it waits for connections from clients.
For some applications it may be necessary to run the web server alongside other
asynchronous tasks, on an already running loop. In that case, instead of
``app.run()`` the web server can be started by invoking the
:func:`start_server() ` coroutine as shown in
the following example::
import asyncio
from microdot import Microdot
app = Microdot()
@app.route('/')
async def index(request):
return 'Hello, world!'
async def main():
# start the server in a background task
server = asyncio.create_task(app.start_server())
# ... do other asynchronous work here ...
# cleanup before ending the application
await server
asyncio.run(main())
Running with CPython
^^^^^^^^^^^^^^^^^^^^
.. list-table::
:align: left
* - Required Microdot source files
- | `microdot.py `_
* - Required external dependencies
- | None
* - Examples
- | `hello.py `_
When using CPython, you can start the web server by running the script that
has the ``app.run()`` call at the bottom::
python main.py
After starting the script, open a web browser and navigate to
*http://localhost:5000/* to access the application at the default address for
the Microdot web server. From other computers in the same network, use the IP
address or hostname of the computer running the script instead of
``localhost``.
Running with MicroPython
^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:align: left
* - Required Microdot source files
- | `microdot.py `_
* - 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 the required Microdot files, as defined
in the :ref:`MicroPython Installation` section.
MicroPython will automatically run *main.py* when the device is powered on, so
the web server will automatically start. The application can be accessed on
port 5000 at the device's IP address. As indicated above, the port can be
changed by passing the ``port`` argument to the ``run()`` method.
.. note::
Microdot does not configure the network interface of the device in which it
is running. If your device requires a network connection to be made in
advance, for example to a Wi-Fi access point, this must be configured before
the ``run()`` method is invoked.
Web Server Configuration
^^^^^^^^^^^^^^^^^^^^^^^^
The :func:`run() ` and
:func:`start_server() ` methods support a few
arguments to configure the web server.
- ``port``: The port number to listen on. Pass the desired port number in this
argument to use a port different than the default of 5000. For example::
app.run(port=6000)
- ``host``: The IP address of the network interface to listen on. By default
the server listens on all available interfaces. To listen only on the local
loopback interface, pass ``'127.0.0.1'`` as value for this argument.
- ``debug``: when set to ``True``, the server ouputs logging information to the
console. The default is ``False``.
- ``ssl``: an ``SSLContext`` instance that configures the server to use TLS
encryption, or ``None`` to disable TLS use. The default is ``None``. The
following example demonstrates how to configure the server with an SSL
certificate stored in *cert.pem* and *key.pem* files::
import ssl
# ...
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
sslctx.load_cert_chain('cert.pem', 'key.pem')
app.run(port=4443, debug=True, ssl=sslctx)
.. note::
When using CPython, the certificate and key files must be given in PEM
format. When using MicroPython, these files must be given in DER format.
Defining Routes
~~~~~~~~~~~~~~~
The :func:`route() ` decorator is used to associate an
application URL with the function that handles it. The only required argument
to the decorator is the path portion of the URL.
The following example creates a route for the root URL of the application::
@app.route('/')
async def index(request):
return 'Hello, world!'
When a client requests the root URL (for example, *http://localhost:5000/*),
Microdot will call the ``index()`` function, passing it a
:class:`Request ` object. The return value of the function
is the response that is sent to the client.
Below is another example, this one with a route for a URL with two components
in its path::
@app.route('/users/active')
async def active_users(request):
return 'Active users: Susan, Joe, and Bob'
The complete URL that maps to this route is
*http://localhost:5000/users/active*.
An application can include multiple routes. Microdot uses the path portion of
the URL to determine the correct route function to call for each incoming
request.
Choosing the HTTP Method
^^^^^^^^^^^^^^^^^^^^^^^^
All the example routes shown above are associated with ``GET`` requests, which
are the default. Applications often need to define routes for other HTTP
methods, such as ``POST``, ``PUT``, ``PATCH`` and ``DELETE``. The ``route()``
decorator takes a ``methods`` optional argument, in which the application can
provide a list of HTTP methods that the route should be associated with on the
given path.
The following example defines a route that handles ``GET`` and ``POST``
requests within the same function::
@app.route('/invoices', methods=['GET', 'POST'])
async def invoices(request):
if request.method == 'GET':
return 'get invoices'
elif request.method == 'POST':
return 'create an invoice'
As an alternative to the example above, in which a single function is used to
handle multiple HTTP methods, sometimes it may be desirable to write a separate
function for each HTTP method. The above example can be implemented with two
routes as follows::
@app.route('/invoices', methods=['GET'])
async def get_invoices(request):
return 'get invoices'
@app.route('/invoices', methods=['POST'])
async def create_invoice(request):
return 'create an invoice'
Microdot provides the :func:`get() `,
:func:`post() `, :func:`put() `,
:func:`patch() `, and
:func:`delete() ` decorators as shortcuts for the
corresponding HTTP methods. The two example routes above can be written more
concisely with them::
@app.get('/invoices')
async def get_invoices(request):
return 'get invoices'
@app.post('/invoices')
async def create_invoice(request):
return 'create an invoice'
Including Dynamic Components in the URL Path
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The examples shown above all use hardcoded URL paths. Microdot also supports
the definition of routes that have dynamic components in the path. For example,
the following route associates all URLs that have a path following the pattern
*http://localhost:5000/users/* with the ``get_user()`` function::
@app.get('/users/')
async def get_user(request, username):
return 'User: ' + username
As shown in the example, a path component that is enclosed in angle brackets
is considered a placeholder. Microdot accepts any values for that portion of
the URL path, and passes the value received to the function as an argument
after the request object.
Routes are not limited to a single dynamic component. The following route shows
how multiple dynamic components can be included in the path::
@app.get('/users//')
async def get_user(request, firstname, lastname):
return 'User: ' + firstname + ' ' + lastname
Dynamic path components are considered to be strings by default. An explicit
type can be specified as a prefix, separated from the dynamic component name by
a colon. The following route has two dynamic components declared as an integer
and a string respectively::
@app.get('/users//')
async def get_user(request, id, username):
return 'User: ' + username + ' (' + str(id) + ')'
If a dynamic path component is defined as an integer, the value passed to the
route function is also an integer. If the client sends a value that is not an
integer in the corresponding section of the URL path, then the URL will not
match and the route will not be called.
A special type ``path`` can be used to capture the remainder of the path as a
single argument. The difference between an argument of type ``path`` and one of
type ``string`` is that the latter stops capturing when a ``/`` appears in the
URL::
@app.get('/tests/')
async def get_test(request, path):
return 'Test: ' + path
The ``re`` type allows the application to provide a custom regular expression
for the dynamic component. The next example defines a route that only matches
usernames that begin with an upper or lower case letter, followed by a sequence
of letters or numbers::
@app.get('/users/')
async def get_user(request, username):
return 'User: ' + username
The ``re`` type returns the URL component as a string, which sometimes may not
be the most convenient. To convert a path component to something more
meaningful than a string, the application can register a custom URL component
type and provide a parser function that performs the conversion. In the
following example, a ``hex`` custom type is registered to automatically
convert hex numbers given in the path to numbers::
from microdot import URLPattern
URLPattern.register_type('hex', parser=lambda value: int(value, 16))
@app.get('/users/')
async def get_user(request, user_id):
user = get_user_by_id(user_id)
# ...
In addition to the parser, the custom URL component can include a pattern,
given as a regular expression. When a pattern is provided, the URL component
will only match if the regular expression matches the value passed in the URL.
The ``hex`` example above can be expanded with a pattern as follows::
URLPattern.register_type('hex', pattern='[0-9a-fA-F]+',
parser=lambda value: int(value, 16))
In cases where a pattern isn't provided, or when the pattern is unable to
filter out all invalid values, the parser function can return ``None`` to
indicate a failed match. The next example shows how the parser for the ``hex``
type can be expanded to do that::
def hex_parser(value):
try:
return int(value, 16)
except ValueError:
return None
URLPattern.register_type('hex', parser=hex_parser)
.. note::
Dynamic path components are passed to route functions as keyword arguments,
so the names of the function arguments must match the names declared in the
path specification.
Before and After Request Handlers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
It is common for applications to need to perform one or more actions before a
request is handled. Examples include authenticating and/or authorizing the
client, opening a connection to a database, or checking if the requested
resource can be obtained from a cache. The
:func:`before_request() ` decorator registers
a function to be called before the request is dispatched to the route function.
The following example registers a before-request handler that ensures that the
client is authenticated before the request is handled::
@app.before_request
async def authenticate(request):
user = authorize(request)
if not user:
return 'Unauthorized', 401
request.g.user = user
Before-request handlers receive the request object as an argument. If the
function returns a value, Microdot sends it to the client as the response, and
does not invoke the route function. This gives before-request handlers the
power to intercept a request if necessary. The example above uses this
technique to prevent an unauthorized user from accessing the requested
route.
After-request handlers registered with the
:func:`after_request() ` decorator are called
after the route function returns a response. Their purpose is to perform any
common closing or cleanup tasks. The next example shows a combination of
before- and after-request handlers that print the time it takes for a request
to be handled::
@app.before_request
async def start_timer(request):
request.g.start_time = time.time()
@app.after_request
async def end_timer(request, response):
duration = time.time() - request.g.start_time
print(f'Request took {duration:0.2f} seconds')
After-request handlers receive the request and response objects as arguments,
and they can return a modified response object to replace the original. If
no value is returned from an after-request handler, then the original response
object is used.
The after-request handlers are only invoked for successful requests. The
:func:`after_error_request() `
decorator can be used to register a function that is called after an error
occurs. The function receives the request and the error response and is
expected to return an updated response object after performing any necessary
cleanup.
.. note::
The :ref:`request.g ` object used in many of the above
examples is a special object that allows the before- and after-request
handlers, as well as the route function to share data during the life of the
request.
Error Handlers
^^^^^^^^^^^^^^
When an error occurs during the handling of a request, Microdot ensures that
the client receives an appropriate error response. Some of the common errors
automatically handled by Microdot are:
- 400 for malformed requests.
- 404 for URLs that are unknown.
- 405 for URLs that are known, but not implemented for the requested HTTP
method.
- 413 for requests that are larger than the allowed size.
- 500 when the application raises an unhandled exception.
While the above errors are fully complaint with the HTTP specification, the
application might want to provide custom responses for them. The
:func:`errorhandler() ` decorator registers
functions to respond to specific error codes. The following example shows a
custom error handler for 404 errors::
@app.errorhandler(404)
async def not_found(request):
return {'error': 'resource not found'}, 404
The ``errorhandler()`` decorator has a second form, in which it takes an
exception class as an argument. Microdot will invoke the handler when an
unhandled exception that is an instance of the given class is raised. The next
example provides a custom response for division by zero errors::
@app.errorhandler(ZeroDivisionError)
async def division_by_zero(request, exception):
return {'error': 'division by zero'}, 500
When the raised exception class does not have an error handler defined, but
one or more of its parent classes do, Microdot makes an attempt to invoke the
most specific handler.
Mounting a Sub-Application
^^^^^^^^^^^^^^^^^^^^^^^^^^
Small Microdot applications can be written as a single source file, but this
is not the best option for applications that pass a certain size. To make it
simpler to write large applications, Microdot supports the concept of
sub-applications that can be "mounted" on a larger application, possibly with
a common URL prefix applied to all of its routes. For developers familiar with
the Flask framework, this is a similar concept to Flask's blueprints.
Consider, for example, a *customers.py* sub-application that implements
operations on customers::
from microdot import Microdot
customers_app = Microdot()
@customers_app.get('/')
async def get_customers(request):
# return all customers
@customers_app.post('/')
async def new_customer(request):
# create a new customer
Similar to the above, the *orders.py* sub-application implements operations on
customer orders::
from microdot import Microdot
orders_app = Microdot()
@orders_app.get('/')
async def get_orders(request):
# return all orders
@orders_app.post('/')
async def new_order(request):
# create a new order
Now the main application, which is stored in *main.py*, can import and mount
the sub-applications to build the larger combined application::
from microdot import Microdot
from customers import customers_app
from orders import orders_app
def create_app():
app = Microdot()
app.mount(customers_app, url_prefix='/customers')
app.mount(orders_app, url_prefix='/orders')
return app
app = create_app()
app.run()
The resulting application will have the customer endpoints available at
*/customers/* and the order endpoints available at */orders/*.
.. note::
During the handling of a request, the
:attr:`Request.url_prefix ` attribute is
set to the URL prefix under which the sub-application was mounted, or an
empty string if the endpoint did not come from a sub-application or the
sub-application was mounted without a URL prefix. It is possible to issue a
redirect that is relative to the sub-application as follows::
return redirect(request.url_prefix + '/relative-url')
When mounting an application as shown above, before-request, after-request and
error handlers defined in the sub-application are copied over to the main
application at mount time. Once installed in the main application, these
handlers will apply to the whole application and not just the sub-application
in which they were created.
The :func:`mount() ` method has a ``local`` argument
that defaults to ``False``. When this argument is set to ``True``, the
before-request, after-request and error handlers defined in the sub-application
will only apply to the sub-application.
Shutting Down the Server
^^^^^^^^^^^^^^^^^^^^^^^^
Web servers are designed to run forever, and are often stopped by sending them
an interrupt signal. But having a way to gracefully stop the server is
sometimes useful, especially in testing environments. Microdot provides a
:func:`shutdown() ` method that can be invoked
during the handling of a route to gracefully shut down the server when that
request completes. The next example shows how to use this feature::
@app.get('/shutdown')
async def shutdown(request):
request.app.shutdown()
return 'The server is shutting down...'
The request that invokes the ``shutdown()`` method will complete, and then the
server will not accept any new requests and stop once any remaining requests
complete. At this point the ``app.run()`` call will return.
The Request Object
~~~~~~~~~~~~~~~~~~
The :class:`Request ` object encapsulates all the information
passed by the client. It is passed as an argument to route handlers, as well as
to before-request, after-request and error handlers.
Request Attributes
^^^^^^^^^^^^^^^^^^
The request object provides access to the request attributes, including:
- :attr:`method `: The HTTP method of the request.
- :attr:`path `: The path of the request.
- :attr:`args `: The query string parameters of the
request, as a :class:`MultiDict ` object.
- :attr:`headers `: The headers of the request, as a
dictionary.
- :attr:`cookies `: The cookies that the client sent
with the request, as a dictionary.
- :attr:`content_type `: The content type
specified by the client, or ``None`` if no content type was specified.
- :attr:`content_length `: The content
length of the request, or 0 if no content length was specified.
- :attr:`json `: The parsed JSON data in the request
body. See :ref:`below ` for additional details.
- :attr:`form `: The parsed form data in the request
body, as a dictionary. See :ref:`below