Getting Started documentation chapter
This commit is contained in:
@@ -28,6 +28,7 @@ author = 'Miguel Grinberg'
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autosectionlabel',
|
||||
'sphinx.ext.autodoc',
|
||||
]
|
||||
|
||||
|
||||
132
docs/extensions.rst
Normal file
132
docs/extensions.rst
Normal file
@@ -0,0 +1,132 @@
|
||||
Core Extensions
|
||||
---------------
|
||||
|
||||
Microdot is a highly extensible web application framework. The extensions
|
||||
described in this section are maintained as part of the Microdot project and
|
||||
can be obtained from the same source code repository.
|
||||
|
||||
Asynchronous Support with ``asyncio``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. list-table::
|
||||
:align: left
|
||||
|
||||
* - Compatibility
|
||||
- | CPython & MicroPython
|
||||
|
||||
* - Required Microdot source files
|
||||
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
|
||||
| `microdot_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
|
||||
|
||||
* - Required external dependencies
|
||||
- | CPython: None
|
||||
| MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_
|
||||
|
||||
Microdot can be extended to use an asynchronous programming model based on the
|
||||
``asyncio`` package. When the :class:`Microdot <microdot_asyncio.Microdot>`
|
||||
class is imported from the ``microdot_asyncio`` package, an asynchronous server
|
||||
is used.
|
||||
|
||||
The example that follows uses ``asyncio`` coroutines for concurrency::
|
||||
|
||||
from microdot_asyncio import Microdot
|
||||
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
async def hello(request):
|
||||
return 'Hello, world!'
|
||||
|
||||
app.run()
|
||||
|
||||
Rendering HTML Templates
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Using the uTemplate Engine
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. list-table::
|
||||
:align: left
|
||||
|
||||
* - Compatibility
|
||||
- | CPython & MicroPython
|
||||
|
||||
* - Required Microdot source files
|
||||
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
|
||||
| `microdot_utemplate.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_utemplate.py>`_
|
||||
|
||||
* - Required external dependencies
|
||||
- | `utemplate <https://github.com/pfalcon/utemplate/tree/master/utemplate>`_
|
||||
|
||||
Using the Jinja Engine
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. list-table::
|
||||
:align: left
|
||||
|
||||
* - Compatibility
|
||||
- | CPython only
|
||||
|
||||
* - Required Microdot source files
|
||||
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
|
||||
| `microdot_jinja.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_jinja.py>`_
|
||||
|
||||
* - Required external dependencies
|
||||
- | `Jinja2 <https://jinja.palletsprojects.com/>`_
|
||||
|
||||
Maintaing Secure User Sessions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. list-table::
|
||||
:align: left
|
||||
|
||||
* - Compatibility
|
||||
- | CPython & MicroPython
|
||||
|
||||
* - Required Microdot source files
|
||||
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
|
||||
| `microdot_session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_session.py>`_
|
||||
|
||||
* - Required external dependencies
|
||||
- | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
|
||||
| MicroPython: `ujwt.py <https://github.com/miguelgrinberg/micropython-lib/blob/ujwt-module/python-ecosys/ujwt/ujwt.py>`_,
|
||||
`hmac <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_,
|
||||
`hashlib <https://github.com/miguelgrinberg/micropython-lib/blob/ujwt-module/python-stdlib/hashlib>`_,
|
||||
`warnings <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/warnings/warnings.py>`_
|
||||
|
||||
Deploying on a Production Web Server
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Using a WSGI Web Server
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. list-table::
|
||||
:align: left
|
||||
|
||||
* - Compatibility
|
||||
- | CPython only
|
||||
|
||||
* - Required Microdot source files
|
||||
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
|
||||
| `microdot_wsgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_wsgi.py>`_
|
||||
|
||||
* - Required external dependencies
|
||||
- | A WSGI web server, such as `Gunicorn <https://gunicorn.org/>`_.
|
||||
|
||||
Using an ASGI Web Server
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. list-table::
|
||||
:align: left
|
||||
|
||||
* - Compatibility
|
||||
- | CPython only
|
||||
|
||||
* - Required Microdot source files
|
||||
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
|
||||
| `microdot_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
|
||||
| `microdot_asgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asgi.py>`_
|
||||
|
||||
* - Required external dependencies
|
||||
- | An ASGI web server, such as `Uvicorn <https://uvicorn.org/>`_.
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
Microdot
|
||||
========
|
||||
|
||||
*"The impossibly small web framework for Python and MicroPython"*
|
||||
|
||||
Microdot is a minimalistic Python web framework inspired by
|
||||
`Flask <https://flask.palletsprojects.com/>`_, and designed to run on
|
||||
systems with limited resources such as microcontrollers. It runs on standard
|
||||
@@ -15,6 +17,7 @@ Python and on `MicroPython <https://micropython.org>`_.
|
||||
:maxdepth: 3
|
||||
|
||||
intro
|
||||
extensions
|
||||
api
|
||||
|
||||
* :ref:`genindex`
|
||||
|
||||
722
docs/intro.rst
722
docs/intro.rst
@@ -1,43 +1,731 @@
|
||||
Installation
|
||||
------------
|
||||
|
||||
Microdot can be installed with ``pip``::
|
||||
For standard Python (CPython) projects, Microdot and all of its core extensions
|
||||
can be installed with ``pip``::
|
||||
|
||||
pip install microdot
|
||||
|
||||
For MicroPython, you can install with ``upip``.
|
||||
For MicroPython, you can install it with ``upip`` if that option is available,
|
||||
but the recommended approach is to manually copy *microdot.py* and any
|
||||
desired optional extension source files from the
|
||||
`GitHub repository <https://github.com/miguelgrinberg/microdot/tree/main/src>`_
|
||||
into your device, possibly after
|
||||
`compiling <https://docs.micropython.org/en/latest/reference/mpyfiles.html>`_
|
||||
them to *.mpy* files. These source files can also be
|
||||
`frozen <https://docs.micropython.org/en/latest/develop/optimizations.html?highlight=frozen#frozen-bytecode>`_
|
||||
and incorporated into a custom MicroPython firmware.
|
||||
|
||||
On platforms where ``pip`` or ``upip`` are not viable options, you can manually
|
||||
copy and install the ``microdot.py`` and ``microdot_asyncio.py`` source files
|
||||
from the `GitHub reposutory <https://github.com/miguelgrinberg/microdot>`_
|
||||
into your project directory.
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
Examples
|
||||
--------
|
||||
This section describes the main features of Microdot in an informal manner. For
|
||||
detailed reference information, consult the :ref:`API Reference`.
|
||||
|
||||
The following is an example of a standard single or multi-threaded web
|
||||
server::
|
||||
A Simple Microdot Web Server
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The following is an example of a simple web server::
|
||||
|
||||
from microdot import Microdot
|
||||
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
def hello(request):
|
||||
def index(request):
|
||||
return 'Hello, world!'
|
||||
|
||||
app.run()
|
||||
|
||||
Microdot also supports the asynchronous model and can be used under
|
||||
``asyncio``. The example that follows is equivalent to the one above, but uses
|
||||
coroutines for concurrency::
|
||||
The script imports the :class:`Microdot <microdot.Microdot>` class and creates
|
||||
an application instance from it.
|
||||
|
||||
from microdot_asyncio import Microdot
|
||||
The application instance provides a :func:`route() <microdot.Microdot.route>`
|
||||
decorator, which is used to define one or more routes, as needed by the
|
||||
application.
|
||||
|
||||
app = Microdot()
|
||||
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. The function is passed a
|
||||
:class:`Request <microdot.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.
|
||||
|
||||
The :func:`run() <microdot.Microdot.run>` method starts the application's web
|
||||
server on port 5000 (or the port number passed in the ``port`` argument). This
|
||||
method blocks while it waits for connections from clients.
|
||||
|
||||
Running with CPython
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. list-table::
|
||||
:align: left
|
||||
|
||||
* - Required Microdot source files
|
||||
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
|
||||
|
||||
* - Required external dependencies
|
||||
- | None
|
||||
|
||||
When using CPython, you can start the web server by running the script that
|
||||
defines and runs the application instance::
|
||||
|
||||
python main.py
|
||||
|
||||
While the script is running, you can open a web browser and navigate to
|
||||
*http://localhost:5000/*, which is 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 <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
|
||||
|
||||
* - Required external dependencies
|
||||
- | None
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
Defining Routes
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
The :func:`route() <microdot.Microdot.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 hello(request):
|
||||
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 <microdot.Request>` object. The return value of the function
|
||||
is the response that is sent to the client.
|
||||
|
||||
Below is a another example, this one with a route for a URL with two components
|
||||
in its path::
|
||||
|
||||
@app.route('/users/active')
|
||||
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. But
|
||||
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'])
|
||||
def invoices(request):
|
||||
if request.method == 'GET':
|
||||
return 'get invoices'
|
||||
elif request.method == 'POST':
|
||||
return 'create an invoice'
|
||||
|
||||
In cases like the above, where a single URL is used to handle multiple HTTP
|
||||
methods, 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'])
|
||||
def get_invoices(request):
|
||||
return 'get invoices'
|
||||
|
||||
@app.route('/invoices', methods=['POST'])
|
||||
def create_invoice(request):
|
||||
return 'create an invoice'
|
||||
|
||||
Microdot provides the :func:`get() <microdot.Microdot.get>`,
|
||||
:func:`post() <microdot.Microdot.post>`, :func:`put() <microdot.Microdot.put>`,
|
||||
:func:`patch() <microdot.Microdot.patch>`, and
|
||||
:func:`delete() <microdot.Microdot.delete>` decorator shortcuts as well. The
|
||||
two example routes above can be written more concisely with them::
|
||||
|
||||
@app.get('/invoices')
|
||||
def get_invoices(request):
|
||||
return 'get invoices'
|
||||
|
||||
@app.post('/invoices')
|
||||
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/<username>* with the ``get_user()`` function::
|
||||
|
||||
@app.get('/users/<username>')
|
||||
def get_user(request, username):
|
||||
return 'User: ' + username
|
||||
|
||||
As shown in the example, a path components that is enclosed in angle brackets
|
||||
is considered dynamic. Microdot accepts any values for that section 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/<firstname>/<lastname>')
|
||||
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/<int:id>/<string:username>')
|
||||
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::
|
||||
|
||||
@app.get('/tests/<path:path>')
|
||||
def get_test(request, path):
|
||||
return 'Test: ' + path
|
||||
|
||||
For the most control, 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/<re:[a-zA-Z][a-zA-Z0-9]*:username>')
|
||||
def get_user(request, username):
|
||||
return 'User: ' + username
|
||||
|
||||
.. 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() <microdot.Microdot.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
|
||||
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
|
||||
resource.
|
||||
|
||||
After request handlers registered with the
|
||||
:func:`after_request() <microdot.Microdot.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
|
||||
def start_timer(request):
|
||||
request.g.start_time = time.time()
|
||||
|
||||
@ap.after_request
|
||||
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.
|
||||
The function can return a modified response object to replace the original. If
|
||||
the function does not return a value, then the original response object is
|
||||
used.
|
||||
|
||||
.. note::
|
||||
The :ref:`request.g <The "g" Object>` object is a special object that allows
|
||||
the before and after request handlers, as well sa 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 not defined.
|
||||
- 405 for URLs that are defined, but not for the requested HTTP method.
|
||||
- 413 for requests that are larger than the allowed size.
|
||||
- 500 when the application raises an 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() <microdot.Microdot.errorhandler>` decorator registers
|
||||
a functions to respond to specific error codes. The following example shows a
|
||||
custom error handler for 404 errors::
|
||||
|
||||
@app.errorhandler(404)
|
||||
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 then invoke the handler when an
|
||||
exception of that class is raised. The next example provides a custom response
|
||||
for division by zero errors::
|
||||
|
||||
@app.errorhandler(ZeroDivisionError)
|
||||
def division_by_zero(request):
|
||||
return {'error': 'division by zero'}, 500
|
||||
|
||||
Mounting a Sub-Application
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Small Microdot applications can be written an a single source file, but this
|
||||
is not the best option for applications that past 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.
|
||||
|
||||
Consider, for example, a *customers.py* sub-application that implements
|
||||
operations on customers::
|
||||
|
||||
from microdot import Microdot
|
||||
|
||||
customers_app = Microdot()
|
||||
|
||||
@customers_app.get('/')
|
||||
def get_customers(request):
|
||||
# return all customers
|
||||
|
||||
@customers_app.post('/')
|
||||
def new_customer(request):
|
||||
# create a new customer
|
||||
|
||||
In the same way, the *orders.py* sub-application implements operations on
|
||||
customer orders::
|
||||
|
||||
from microdot import Microdot
|
||||
|
||||
orders_app = Microdot()
|
||||
|
||||
@orders_app.get('/')
|
||||
def get_orders(request):
|
||||
# return all orders
|
||||
|
||||
@orders_app.post('/')
|
||||
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 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::
|
||||
Before request, after request and error handlers defined in the
|
||||
sub-application are also 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.
|
||||
|
||||
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() <microdot.Microdot.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')
|
||||
def shutdown(request):
|
||||
request.app.shutdown()
|
||||
return 'The server is shutting down...'
|
||||
|
||||
The Request Object
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The :class:`Request <microdot.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 <microdot.Request.method>`: The HTTP method of the request.
|
||||
- :attr:`path <microdot.Request.path>`: The path of the request.
|
||||
- :attr:`args <microdot.Request.args>`: The query string parameters of the
|
||||
request, as a :class:`MultiDict <microdot.MultiDict>` object.
|
||||
- :attr:`headers <microdot.Request.headers>`: The headers of the request, as a
|
||||
dictionary.
|
||||
- :attr:`cookies <microdot.Request.cookies>`: The cookies that the client sent
|
||||
with the request, as a dictionary.
|
||||
- :attr:`content_type <microdot.Request.content_type>`: The content type
|
||||
specified by the client, or ``None`` if no content type was specified.
|
||||
- :attr:`content_length <microdot.Request.content_length>`: The content
|
||||
length of the request, or 0 if no content length was specified.
|
||||
- :attr:`client_addr <microdot.Request.client_addr>`: The network address of
|
||||
the client, as a tuple (host, port).
|
||||
- :attr:`app <microdot.Request.app>`: The application instance that created the
|
||||
request.
|
||||
|
||||
JSON Payloads
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
When the client sends a request that contains JSON data in the body, the
|
||||
application can access the parsed JSON data using the
|
||||
:attr:`json <microdot.Request.json>` attribute. The following example shows how
|
||||
to use this attribute::
|
||||
|
||||
@app.post('/customers')
|
||||
def create_customer(request):
|
||||
customer = request.json
|
||||
# do something with customer
|
||||
return {'success': True}
|
||||
|
||||
.. note::
|
||||
The client must set the ``Content-Type`` header to ``application/json`` for
|
||||
the ``json`` attribute of the request object to be populated.
|
||||
|
||||
URLEncoded Form Data
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The request object also supports standard HTML form submissions through the
|
||||
:attr:`form <microdot.Request.form>` attribute, which presents the form data
|
||||
as a :class:`MultiDict <microdot.MultiDict>` object. Example::
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index(req):
|
||||
name = 'Unknown'
|
||||
if req.method == 'POST':
|
||||
name = req.form.get('name')
|
||||
return f'Hello {name}'
|
||||
|
||||
.. note::
|
||||
Form submissions are only parsed when the ``Content-Type`` header is set by
|
||||
the client to ``application/x-www-form-urlencoded``. Form submissions using
|
||||
the ``multipart/form-data`` content type are currently not supported.
|
||||
|
||||
Accessing the Raw Request Body
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
For cases in which neither JSON nor form data is expected, the
|
||||
:attr:`body <microdot.Request.body>` request attribute returns the entire body
|
||||
of the request as a byte sequence.
|
||||
|
||||
If the expected body is too large to fit in memory, the application can use the
|
||||
:attr:`stream <microdot.Request.stream>` request attribute to read the body
|
||||
contents as a file-like object.
|
||||
|
||||
Cookies
|
||||
^^^^^^^
|
||||
|
||||
Cookies that are sent by the client are made available throught the
|
||||
:attr:`cookies <microdot.Request.cookies>` attribute of the request object in
|
||||
dictionary form.
|
||||
|
||||
The "g" Object
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Sometimes applications need to store data during the lifetime of a request, so
|
||||
that it can be shared between the before or after request handlers and the
|
||||
route function. The request object provides the :attr:`g <microdot.Request.g>`
|
||||
attribute for that purpose.
|
||||
|
||||
In the following example, a before request handler
|
||||
authorizes the client and stores the username so that the route function can
|
||||
use it::
|
||||
|
||||
@app.before_request
|
||||
def authorize(request):
|
||||
username = authenticate_user(request)
|
||||
if not username:
|
||||
return 'Unauthorized', 401
|
||||
request.g.username = username
|
||||
|
||||
@app.get('/')
|
||||
def index(request):
|
||||
return f'Hello, {request.g.username}!'
|
||||
|
||||
Request-Specific After Request Handlers
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Sometimes applications need to perform operations on the response object,
|
||||
before it is sent to the client, for example to set or remove a cookie. A good
|
||||
option to use for this is to define a request-specific after request handler
|
||||
using the :func:`after_request <microdot.Microdot.after_request>` decorator.
|
||||
Request-specific after request handlers are called by Microdot after the route
|
||||
function returns and all the application's after request handlers have been
|
||||
called.
|
||||
|
||||
The next example shows how a cookie can be updated using a request-specific
|
||||
after request handler defined inside a route function::
|
||||
|
||||
@app.post('/logout')
|
||||
def logout(request):
|
||||
@request.after_request
|
||||
def reset_session(request, response):
|
||||
response.set_cookie('session', '', http_only=True)
|
||||
return response
|
||||
|
||||
return 'Logged out'
|
||||
|
||||
Request Limits
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
To help prevent malicious attacks, Microdot provides some configuration options
|
||||
to limit the amount of information that is accepted:
|
||||
|
||||
- :attr:`max_content_length <microdot.Microdot.max_content_length>`: The
|
||||
maximum size accepted for the request body, in bytes. When a client sends a
|
||||
request that is larger than this, the server will respond with a 413 error.
|
||||
The default is 16KB.
|
||||
- :attr:`max_body_length <microdot.Microdot.max_body_length>`: The maximum
|
||||
size that is loaded in the :attr:`body <microdot.Request.body>` attribute, in
|
||||
bytes. Requests that have a body that is larger than this size but smaller
|
||||
than the size set for ``max_content_length`` can only be accessed through the
|
||||
:attr:`stream <microdot.Request.stream>` attribute. The default is also 16KB.
|
||||
- :attr:`max_readline <microdot.Microdot.max_readline>`: The maximum allowed
|
||||
size for a request line, in bytes. The default is 2KB.
|
||||
|
||||
The following example configures the application to accept requests with
|
||||
payloads up to 1MB big, but prevents requests that are larger than 8KB from
|
||||
being loaded into memory::
|
||||
|
||||
Request.max_content_length = 1024 * 1024
|
||||
Request.max_body_length = 8 * 1024
|
||||
|
||||
Responses
|
||||
~~~~~~~~~
|
||||
|
||||
The value or values that are returned from the route function are used by
|
||||
Microdot to build the response that is sent to the client. The following
|
||||
sections describe the different types of responses that are supported.
|
||||
|
||||
The Three Parts of a Response
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Route functions can return one, two or three values. The first or only value is
|
||||
always returned to the client in the response body::
|
||||
|
||||
@app.get('/')
|
||||
def index(request):
|
||||
return 'Hello, World!'
|
||||
|
||||
In the above example, Microdot issues a standard 200 status code response, and
|
||||
inserts the necessary headers.
|
||||
|
||||
The applicaton can provide its own status code as a second value returned from
|
||||
the route. The example below returns a 202 status code::
|
||||
|
||||
@app.get('/')
|
||||
def index(request):
|
||||
return 'Hello, World!', 202
|
||||
|
||||
The application can also return a third value, a dictionary with additional
|
||||
headers that are added to, or replace the default ones provided by Microdot.
|
||||
The next example returns an HTML response, instead of a default text response::
|
||||
|
||||
@app.get('/')
|
||||
def index(request):
|
||||
return '<h1>Hello, World!</h1>', 202, {'Content-Type': 'text/html'}
|
||||
|
||||
If the application needs to return custom headers, but does not need to change
|
||||
the default status code, then it can return two values, omitting the stauts
|
||||
code::
|
||||
|
||||
@app.get('/')
|
||||
def index(request):
|
||||
return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'}
|
||||
|
||||
The application can also return a :class:`Response <microdot.Response>` object
|
||||
containing all the details of the response as a single value.
|
||||
|
||||
JSON Responses
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
If the application needs to return a response with JSON formatted data, it can
|
||||
return a dictionary or a list as the first value, and Microdot will
|
||||
automatically format the response as JSON.
|
||||
|
||||
Example::
|
||||
|
||||
@app.get('/')
|
||||
def index(request):
|
||||
return {'hello': 'world'}
|
||||
|
||||
.. note::
|
||||
JSON responses are sent with the ``Content-Type`` header set to
|
||||
``application/json``.
|
||||
|
||||
Redirects
|
||||
^^^^^^^^^
|
||||
|
||||
The :func:`redirect <microdot.Response.redirect>` function is a helper that
|
||||
creates redirect responses::
|
||||
|
||||
from microdot import redirect
|
||||
|
||||
@app.get('/')
|
||||
def index(request):
|
||||
return redirect('/about')
|
||||
|
||||
File Responses
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The :func:`send_file <microdot.Response.send_file>` function builds a response
|
||||
object for a file::
|
||||
|
||||
from microdot import send_file
|
||||
|
||||
@app.get('/')
|
||||
def index(request):
|
||||
return send_file('/static/index.html')
|
||||
|
||||
.. note::
|
||||
Unlike other web frameworks, Microdot does not automatically configure a
|
||||
route to serve static files. The following is an example route that can be
|
||||
added to the application to serve static files from a *static* directory in
|
||||
the project::
|
||||
|
||||
@app.route('/static/<path:path>')
|
||||
def static(request, path):
|
||||
if '..' in path:
|
||||
# directory traversal is not allowed
|
||||
return 'Not found', 404
|
||||
return send_file('static/' + path)
|
||||
|
||||
Streaming Responses
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Instead of providing a response as a single value, an application can opt to
|
||||
return a response that is generated in chunks by returning a generator. The
|
||||
example below returns all the numbers in the fibonacci sequence below 100::
|
||||
|
||||
@app.get('/fibonacci')
|
||||
def fibonacci(request):
|
||||
def generate_fibonacci():
|
||||
a, b = 0, 1
|
||||
while a < 100:
|
||||
yield str(a) + '\n'
|
||||
a, b = b, a + b
|
||||
|
||||
return generate_fibonacci()
|
||||
|
||||
Changing the Default Response Content Type
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Microdot uses a ``text/plain`` content type by default for responses that do
|
||||
not explicitly include the ``Content-Type`` header. The application can change
|
||||
this default by setting the desired content type in the
|
||||
:attr:`default_content_type <microdot.Response.default_content_type>` attribute
|
||||
of the :class:`Response <microdot.Response>` class.
|
||||
|
||||
The example that follows configures the application to use ``text/html`` as
|
||||
default content type::
|
||||
|
||||
from microdot import Response
|
||||
|
||||
Response.default_content_type = 'text/html'
|
||||
|
||||
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
|
||||
:func:`set_cookie() <microdot.Response.set_cookie>` method in the response
|
||||
object to add a properly formatted cookie header to the response.
|
||||
|
||||
Given that route functions do not normally work directly with the response
|
||||
object, the recommended way to set a cookie is to do it in a
|
||||
:ref:`Request-Specific After Request Handler <Request-Specific After Request Handlers>`.
|
||||
|
||||
Example::
|
||||
|
||||
@app.get('/')
|
||||
def index(request):
|
||||
@request.after_request
|
||||
def set_cookie(request, response):
|
||||
response.set_cookie('name', 'value')
|
||||
return response
|
||||
|
||||
return 'Hello, World!'
|
||||
|
||||
Another option is to create a response object directly in the route function::
|
||||
|
||||
@app.get('/')
|
||||
def index(request):
|
||||
response = Response('Hello, World!'))
|
||||
response.set_cookie('name', 'value')
|
||||
return response
|
||||
|
||||
.. note::
|
||||
Standard cookies do not offer sufficient privacy and security controls, so
|
||||
never store sensitive information in them unless you are adding additional
|
||||
protection mechanisms such as encryption or cryptographic signing. The
|
||||
:ref:`session <Maintaing Secure User Sessions>` extension implements signed cookies that prevent tampering
|
||||
by malicious actors.
|
||||
|
||||
@@ -169,27 +169,7 @@ class MultiDict(dict):
|
||||
|
||||
|
||||
class Request():
|
||||
"""An HTTP request class.
|
||||
|
||||
:var app: The application instance to which this request belongs.
|
||||
:var client_addr: The address of the client, as a tuple (host, port).
|
||||
:var method: The HTTP method of the request.
|
||||
:var path: The path portion of the URL.
|
||||
:var query_string: The query string portion of the URL.
|
||||
:var args: The parsed query string, as a :class:`MultiDict` object.
|
||||
:var headers: A dictionary with the headers included in the request.
|
||||
:var cookies: A dictionary with the cookies included in the request.
|
||||
:var content_length: The parsed ``Content-Length`` header.
|
||||
:var content_type: The parsed ``Content-Type`` header.
|
||||
:var stream: The input stream, containing the request body.
|
||||
:var body: The body of the request, as bytes.
|
||||
:var json: The parsed JSON body, as a dictionary or list, or ``None`` if
|
||||
the request does not have a JSON body.
|
||||
:var form: The parsed form submission body, as a :class:`MultiDict` object,
|
||||
or ``None`` if the request does not have a form submission.
|
||||
:var g: A general purpose container for applications to store data during
|
||||
the life of the request.
|
||||
"""
|
||||
"""An HTTP request."""
|
||||
#: Specify the maximum payload size that is accepted. Requests with larger
|
||||
#: payloads will be rejected with a 413 status code. Applications can
|
||||
#: change this maximum as necessary.
|
||||
@@ -224,21 +204,36 @@ class Request():
|
||||
|
||||
def __init__(self, app, client_addr, method, url, http_version, headers,
|
||||
body=None, stream=None):
|
||||
#: The application instance to which this request belongs.
|
||||
self.app = app
|
||||
#: The address of the client, as a tuple (host, port).
|
||||
self.client_addr = client_addr
|
||||
#: The HTTP method of the request.
|
||||
self.method = method
|
||||
#: The path portion of the URL.
|
||||
self.path = url
|
||||
#: The query string portion of the URL.
|
||||
self.query_string = None
|
||||
#: The parsed query string, as a
|
||||
#: :class:`MultiDict <microdot.MultiDict>` object.
|
||||
self.args = {}
|
||||
#: A dictionary with the headers included in the request.
|
||||
self.headers = headers
|
||||
#: A dictionary with the cookies included in the request.
|
||||
self.cookies = {}
|
||||
#: The parsed ``Content-Length`` header.
|
||||
self.content_length = 0
|
||||
#: The parsed ``Content-Type`` header.
|
||||
self.content_type = None
|
||||
#: A general purpose container for applications to store data during
|
||||
#: the life of the request.
|
||||
self.g = Request.G()
|
||||
|
||||
self.http_version = http_version
|
||||
if '?' in self.path:
|
||||
self.path, self.query_string = self.path.split('?', 1)
|
||||
self.args = self._parse_urlencoded(self.query_string)
|
||||
else:
|
||||
self.query_string = None
|
||||
self.args = {}
|
||||
self.headers = headers
|
||||
self.cookies = {}
|
||||
self.content_length = 0
|
||||
self.content_type = None
|
||||
|
||||
for header, value in self.headers.items():
|
||||
header = header.lower()
|
||||
if header == 'content-length':
|
||||
@@ -249,13 +244,13 @@ class Request():
|
||||
for cookie in value.split(';'):
|
||||
name, value = cookie.strip().split('=', 1)
|
||||
self.cookies[name] = value
|
||||
|
||||
self._body = body
|
||||
self.body_used = False
|
||||
self._stream = stream
|
||||
self.stream_used = False
|
||||
self._json = None
|
||||
self._form = None
|
||||
self.g = Request.G()
|
||||
self.after_request_handlers = []
|
||||
|
||||
@staticmethod
|
||||
@@ -298,6 +293,7 @@ class Request():
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
"""The body of the request, as bytes."""
|
||||
if self.stream_used:
|
||||
raise RuntimeError('Cannot use both stream and body')
|
||||
if self._body is None:
|
||||
@@ -315,6 +311,7 @@ class Request():
|
||||
|
||||
@property
|
||||
def stream(self):
|
||||
"""The input stream, containing the request body."""
|
||||
if self.body_used:
|
||||
raise RuntimeError('Cannot use both stream and body')
|
||||
self.stream_used = True
|
||||
@@ -322,6 +319,8 @@ class Request():
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
"""The parsed JSON body, or ``None`` if the request does not have a
|
||||
JSON body."""
|
||||
if self._json is None:
|
||||
if self.content_type is None:
|
||||
return None
|
||||
@@ -333,6 +332,9 @@ class Request():
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
"""The parsed form submission body, as a
|
||||
:class:`MultiDict <microdot.MultiDict>` object, or ``None`` if the
|
||||
request does not have a form submission."""
|
||||
if self._form is None:
|
||||
if self.content_type is None:
|
||||
return None
|
||||
@@ -398,6 +400,9 @@ class Response():
|
||||
'txt': 'text/plain',
|
||||
}
|
||||
send_file_buffer_size = 1024
|
||||
|
||||
#: The content type to use for responses that do not explicitly define a
|
||||
#: ``Content-Type`` header.
|
||||
default_content_type = 'text/plain'
|
||||
|
||||
def __init__(self, body='', status_code=200, headers=None, reason=None):
|
||||
|
||||
Reference in New Issue
Block a user