This commit is contained in:
Miguel Grinberg
2023-12-22 20:26:07 +00:00
committed by GitHub
parent 7a329d98a8
commit 20ea305fe7
114 changed files with 3868 additions and 6410 deletions

View File

@@ -1,5 +0,0 @@
[run]
omit=
src/microdot_websocket_alt.py
src/microdot_asgi_websocket.py
src/microdot_ssl.py

View File

@@ -1,3 +0,0 @@
[flake8]
select = C,E,F,W,B,B950
per-file-ignores = ./*/__init__.py:F401

View File

@@ -21,7 +21,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ['3.7', '3.8', '3.9', '3.10', '3.11']
python: ['3.8', '3.9', '3.10', '3.11', '3.12']
fail-fast: false
runs-on: ${{ matrix.os }}
steps:

View File

@@ -3,9 +3,9 @@
*“The impossibly small web framework for Python and MicroPython”*
Microdot is a minimalistic Python web framework inspired by Flask, and designed
to run on systems with limited resources such as microcontrollers. It runs on
standard Python and on MicroPython.
Microdot is a minimalistic Python web framework inspired by Flask. Given its
small size, it can run on systems with limited resources such as
microcontrollers. Both standard Python (CPython) and MicroPython are supported.
```python
from microdot import Microdot
@@ -13,13 +13,24 @@ from microdot import Microdot
app = Microdot()
@app.route('/')
def index(request):
async def index(request):
return 'Hello, world!'
app.run()
```
## Migrating to Microdot 2
Version 2 of Microdot incorporates feedback received from users of earlier
releases, and attempts to improve and correct some design decisions that have
proven to be problematic.
For this reason most applications built for earlier versions will need to be
updated to work correctly with Microdot 2. The
[Migration Guide](https://microdot.readthedocs.io/en/stable/migrating.html)
describes the backwards incompatible changes that were made.
## Resources
- [Documentation](https://microdot.readthedocs.io/en/latest/)
- [Documentation](https://microdot.readthedocs.io/en/stable/)
- [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)

View File

@@ -13,103 +13,53 @@ API Reference
.. autoclass:: microdot.Response
:members:
.. autoclass:: microdot.NoCaseDict
:members:
.. autoclass:: microdot.MultiDict
:members:
``microdot_asyncio`` module
---------------------------
.. autoclass:: microdot_asyncio.Microdot
:inherited-members:
:members:
.. autoclass:: microdot_asyncio.Request
:inherited-members:
:members:
.. autoclass:: microdot_asyncio.Response
:inherited-members:
:members:
``microdot_utemplate`` module
-----------------------------
.. automodule:: microdot_utemplate
:members:
``microdot_jinja`` module
-------------------------
.. automodule:: microdot_jinja
:members:
``microdot_session`` module
---------------------------
.. automodule:: microdot_session
:members:
``microdot_cors`` module
------------------------
.. automodule:: microdot_cors
:members:
``microdot_websocket`` module
------------------------------
.. automodule:: microdot_websocket
:members:
``microdot_asyncio_websocket`` module
-------------------------------------
.. automodule:: microdot_asyncio_websocket
:members:
``microdot_asgi_websocket`` module
-------------------------------------
.. automodule:: microdot_asgi_websocket
:members:
``microdot_ssl`` module
``websocket`` extension
-----------------------
.. automodule:: microdot_ssl
.. automodule:: microdot.websocket
:members:
``microdot_test_client`` module
-------------------------------
``utemplate`` templating extension
----------------------------------
.. autoclass:: microdot_test_client.TestClient
.. automodule:: microdot.utemplate
:members:
.. autoclass:: microdot_test_client.TestResponse
``jinja`` templating extension
------------------------------
.. automodule:: microdot.jinja
:members:
``microdot_asyncio_test_client`` module
---------------------------------------
``session`` extension
---------------------
.. autoclass:: microdot_asyncio_test_client.TestClient
.. automodule:: microdot.session
:members:
.. autoclass:: microdot_asyncio_test_client.TestResponse
``cors`` extension
------------------
.. automodule:: microdot.cors
:members:
``microdot_wsgi`` module
------------------------
``test_client`` extension
-------------------------
.. autoclass:: microdot_wsgi.Microdot
.. automodule:: microdot.test_client
:members:
``asgi`` extension
------------------
.. autoclass:: microdot.asgi.Microdot
:members:
:exclude-members: shutdown, run
``microdot_asgi`` module
------------------------
``wsgi`` extension
-------------------
.. autoclass:: microdot_asgi.Microdot
.. autoclass:: microdot.wsgi.Microdot
:members:
:exclude-members: shutdown, run

View File

@@ -2,11 +2,11 @@ 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.
described in this section are maintained as part of the Microdot project in
the same source code repository.
Asynchronous Support with Asyncio
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
WebSocket Support
~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
@@ -15,35 +15,71 @@ Asynchronous Support with Asyncio
- | 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>`_
- | `websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/websocket.py>`_
* - Required external dependencies
- | CPython: None
| MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_
- | None
* - Examples
- | `hello_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_async.py>`_
- | `echo.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo.py>`_
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, and handlers can be defined as coroutines.
The WebSocket extension gives the application the ability to handle WebSocket
requests. The :func:`with_websocket <microdot.websocket.with_websocket>`
decorator is used to mark a route handler as a WebSocket handler. Decorated
routes receive a WebSocket object as a second argument. The WebSocket object
provides ``send()`` and ``receive()`` asynchronous methods to send and receive
messages respectively.
The example that follows uses ``asyncio`` coroutines for concurrency::
Example::
from microdot_asyncio import Microdot
@app.route('/echo')
@with_websocket
async def echo(request, ws):
while True:
message = await ws.receive()
await ws.send(message)
app = Microdot()
Server-Sent Events Support
~~~~~~~~~~~~~~~~~~~~~~~~~~
@app.route('/')
async def hello(request):
return 'Hello, world!'
.. list-table::
:align: left
app.run()
* - Compatibility
- | CPython & MicroPython
Rendering HTML Templates
~~~~~~~~~~~~~~~~~~~~~~~~
* - Required Microdot source files
- | `sse.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/sse.py>`_
* - Required external dependencies
- | None
* - Examples
- | `counter.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/sse/counter.py>`_
The Server-Sent Events (SSE) extension simplifies the creation of a streaming
endpoint that follows the SSE web standard. The :func:`with_sse <microdot.sse.with_sse>`
decorator is used to mark a route as an SSE handler. Decorated routes receive
an SSE object as second argument. The SSE object provides a ``send()``
asynchronous method to send an event to the client.
Example::
@app.route('/events')
@with_sse
async def events(request, sse):
for i in range(10):
await asyncio.sleep(1)
await sse.send({'counter': i}) # unnamed event
await sse.send('end', event='comment') # named event
.. note::
The SSE protocol is unidirectional, so there is no ``receive()`` method in
the SSE object. For bidirectional communication with the client, use the
WebSocket extension.
Rendering Templates
~~~~~~~~~~~~~~~~~~~
Many web applications use HTML templates for rendering content to clients.
Microdot includes extensions to render templates with the
@@ -61,35 +97,41 @@ Using the uTemplate Engine
- | 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>`_
- | `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>`_
* - Examples
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/utemplate/hello.py>`_
| `hello_utemplate_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_utemplate_async.py>`_
The :func:`render_template <microdot_utemplate.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.
The :class:`Template <microdot.utemplate.Template>` class is used to load a
template. The argument is the template filename, relative to the templates
directory, which is *templates* by default.
The ``Template`` object has a :func:`render() <microdot.utemplate.Template.render>`
method that renders the template to a string. This method receives any
arguments that are used by the template.
Example::
from microdot_utemplate import render_template
from microdot.utemplate import Template
@app.get('/')
def index(req):
return render_template('index.html')
async def index(req):
return Template('index.html').render()
The ``Template`` object also has a :func:`generate() <microdot.utemplate.Template.generate>`
method, which returns a generator instead of a string. The
:func:`render_async() <microdot.utemplate.Template.render_async>` and
:func:`generate_async() <microdot.utemplate.Template.generate_async>` methods
are the asynchronous versions of these two methods.
The default location from where templates are loaded is the *templates*
subdirectory. This location can be changed with the
:func:`init_templates <microdot_utemplate.init_templates>` function::
:func:`init_templates <microdot.utemplate.init_templates>` function::
from microdot_utemplate import init_templates
from microdot.utemplate import init_templates
init_templates('my_templates')
@@ -103,8 +145,7 @@ Using the Jinja Engine
- | 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>`_
- | `jinja.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/jinja.py>`_
* - Required external dependencies
- | `Jinja2 <https://jinja.palletsprojects.com/>`_
@@ -112,28 +153,40 @@ Using the Jinja Engine
* - Examples
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/jinja/hello.py>`_
The :func:`render_template <microdot_jinja.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.
The :class:`Template <microdot.jinja.Template>` class is used to load a
template. The argument is the template filename, relative to the templates
directory, which is *templates* by default.
The ``Template`` object has a :func:`render() <microdot.jinja.Template.render>`
method that renders the template to a string. This method receives any
arguments that are used by the template.
Example::
from microdot_jinja import render_template
from microdot.jinja import Template
@app.get('/')
def index(req):
return render_template('index.html')
async def index(req):
return Template('index.html').render()
The ``Template`` object also has a :func:`generate() <microdot.jinja.Template.generate>`
method, which returns a generator instead of a string.
The default location from where templates are loaded is the *templates*
subdirectory. This location can be changed with the
:func:`init_templates <microdot_jinja.init_templates>` function::
:func:`init_templates <microdot.utemplate.init_templates>` function::
from microdot_jinja import init_templates
from microdot.jinja import init_templates
init_templates('my_templates')
The ``init_templates()`` function also accepts ``enable_async`` argument, which
can be set to ``True`` if asynchronous rendering of templates is desired. If
this option is enabled, then the
:func:`render_async() <microdot.utemplate.Template.render_async>` and
:func:`generate_async() <microdot.utemplate.Template.generate_async>` methods
must be used.
.. note::
The Jinja extension is not compatible with MicroPython.
@@ -147,56 +200,48 @@ Maintaining Secure User Sessions
- | 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>`_
- | `session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/session.py>`_
* - Required external dependencies
- | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
| MicroPython: `jwt.py <https://github.com/micropython/micropython-lib/blob/master/python-ecosys/pyjwt/jwt.py>`_,
`hmac <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_
`hmac.py <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_
* - Examples
- | `login.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/sessions/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
user sessions. The session data is stored as a signed cookie in the client's
browser, in `JSON Web Token (JWT) <https://en.wikipedia.org/wiki/JSON_Web_Token>`_
format.
To work with user sessions, the application first must configure the secret key
To work with user sessions, the application first must configure a 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.
key is kept secret, as its name implies. 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 <microdot_session.set_session_secret_key>` function::
To initialize the session extension and configure the secret key, create a
:class:`Session <microdot.session.Session>` object::
from microdot_session import set_session_secret_key
Session(app, secret_key='top-secret')
set_session_secret_key('top-secret!')
The :func:`with_session <microdot.session.with_session>` decorator is the
most convenient way to retrieve the session at the start of a request::
To :func:`get_session <microdot_session.get_session>`,
:func:`update_session <microdot_session.update_session>` and
:func:`delete_session <microdot_session.delete_session>` functions are used
inside route handlers to retrieve, store and delete session data respectively.
The :func:`with_session <microdot_session.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
from microdot import Microdot, redirect
from microdot.session import Session, with_session
app = Microdot()
set_session_secret_key('top-secret')
Session(app, secret_key='top-secret')
@app.route('/', methods=['GET', 'POST'])
@with_session
def index(req, session):
async def index(req, session):
username = session.get('username')
if req.method == 'POST':
username = req.form.get('username')
update_session(req, {'username': username})
session['username'] = username
session.save()
return redirect('/')
if username is None:
return 'Not logged in'
@@ -204,10 +249,15 @@ Example::
return 'Logged in as ' + username
@app.post('/logout')
def logout(req):
delete_session(req)
@with_session
async def logout(req, session):
session.delete()
return redirect('/')
The :func:`save() <microdot.session.SessionDict.save>` and
:func:`delete() <microdot.session.SessionDict.delete>` methods are used to update
and destroy the user session respectively.
Cross-Origin Resource Sharing (CORS)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -218,8 +268,7 @@ Cross-Origin Resource Sharing (CORS)
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_cors.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_cors.py>`_
- | `cors.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/cors.py>`_
* - Required external dependencies
- | None
@@ -234,18 +283,18 @@ resources from each other. For example, a web application running on
``https://example.com`` can access resources from ``https://api.example.com``.
To enable CORS support, create an instance of the
:class:`CORS <microdot_cors.CORS>` class and configure the desired options.
:class:`CORS <microdot.cors.CORS>` class and configure the desired options.
Example::
from microdot import Microdot
from microdot_cors import CORS
from microdot.cors import CORS
app = Microdot()
cors = CORS(app, allowed_origins=['https://example.com'],
allow_credentials=True)
WebSocket Support
~~~~~~~~~~~~~~~~~
Testing with the Test Client
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
@@ -254,96 +303,18 @@ WebSocket Support
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_websocket.py>`_
- | `test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/test_client.py>`_
* - Required external dependencies
- | None
* - Examples
- | `echo.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo.py>`_
| `echo_wsgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_wsgi.py>`_
The WebSocket extension provides a way for the application to handle WebSocket
requests. The :func:`websocket <microdot_websocket.with_websocket>` decorator
is used to mark a route handler as a WebSocket handler. The handler receives
a WebSocket object as a second argument. The WebSocket object provides
``send()`` and ``receive()`` methods to send and receive messages respectively.
Example::
@app.route('/echo')
@with_websocket
def echo(request, ws):
while True:
message = ws.receive()
ws.send(message)
.. note::
An unsupported *microdot_websocket_alt.py* module, with the same
interface, is also provided. This module uses the native WebSocket support
in MicroPython that powers the WebREPL, and may provide slightly better
performance for MicroPython low-end boards. This module is not compatible
with CPython.
Asynchronous WebSocket
~~~~~~~~~~~~~~~~~~~~~~
.. 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>`_
| `microdot_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_websocket.py>`_
| `microdot_asyncio_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio_websocket.py>`_
* - Required external dependencies
- | CPython: None
| MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_
* - Examples
- | `echo_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_async.py>`_
This extension has the same interface as the synchronous WebSocket extension,
but the ``receive()`` and ``send()`` methods are asynchronous.
.. note::
An unsupported *microdot_asgi_websocket.py* module, with the same
interface, is also provided. This module must be used instead of
*microdot_asyncio_websocket.py* when the ASGI support is used. The
`echo_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_asgi.py>`_
example shows how to use this module.
HTTPS Support
~~~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_ssl.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_ssl.py>`_
* - Examples
- | `hello_tls.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/tls/hello_tls.py>`_
| `hello_async_tls.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/tls/hello_async_tls.py>`_
The ``run()`` function accepts an optional ``ssl`` argument, through which an
initialized ``SSLContext`` object can be passed. MicroPython does not currently
have a ``SSLContext`` implementation, so the ``microdot_ssl`` module provides
a basic implementation that can be used to create a context.
The Microdot Test Client is a utility class that can be used in tests to send
requests into the application without having to start a web server.
Example::
from microdot import Microdot
from microdot_ssl import create_ssl_context
from microdot.test_client import TestClient
app = Microdot()
@@ -351,88 +322,13 @@ Example::
def index(req):
return 'Hello, World!'
sslctx = create_ssl_context('cert.der', 'key.der')
app.run(port=4443, debug=True, ssl=sslctx)
.. note::
The ``microdot_ssl`` module is only needed for MicroPython. When used under
CPython, this module creates a standard ``SSLContext`` instance.
.. note::
The ``uasyncio`` library for MicroPython does not currently support TLS, so
this feature is not available for asynchronous applications on that
platform. The ``asyncio`` library for CPython is fully supported.
Test Client
~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_test_client.py>`_
* - 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 <microdot_test_client.TestClient>`
class for more details.
Asynchronous Test Client
~~~~~~~~~~~~~~~~~~~~~~~~
.. 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>`_
| `microdot_test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_test_client.py>`_
| `microdot_asyncio_test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio_test_client.py>`_
* - Required external dependencies
- | None
Similar to the :class:`TestClient <microdot_test_client.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 <microdot_asyncio_test_client.TestClient>`
for details.
See the documentation for the :class:`TestClient <microdot.test_client.TestClient>`
class for more details.
Deploying on a Production Web Server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -440,54 +336,7 @@ 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
^^^^^^^^^^^^^^^^^^^^^^^
.. 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/>`_.
* - Examples
- | `hello_wsgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/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 <https://gunicorn.org/>`_ or
`uWSGI <https://uwsgi-docs.readthedocs.io/en/latest/>`_.
To use a WSGI web server, the application must import the
:class:`Microdot <microdot_wsgi.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
When using this WSGI adapter, the ``environ`` dictionary provided by the web
server is available to request handlers as ``request.environ``.
provides extensions that implement the ASGI and WSGI protocols.
Using an ASGI Web Server
^^^^^^^^^^^^^^^^^^^^^^^^
@@ -499,25 +348,25 @@ Using an ASGI Web Server
- | 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>`_
- | `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/>`_.
* - Examples
- | `hello_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_asgi.py>`_
| `hello_asgi.py (uTemplate) <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/utemplate/hello_asgi.py>`_
| `hello_asgi.py (Jinja) <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/jinja/hello_asgi.py>`_
| `echo_asgi.py (WebSocket) <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_asgi.py>`_
The ``microdot_asgi`` module provides an extended ``Microdot`` class that
The ``asgi`` module provides an extended ``Microdot`` class that
implements the ASGI protocol and can be used with a compliant ASGI server such
as `Uvicorn <https://www.uvicorn.org/>`_.
To use an ASGI web server, the application must import the
:class:`Microdot <microdot_asgi.Microdot>` class from the ``microdot_asgi``
module::
:class:`Microdot <microdot.asgi.Microdot>` class from the ``asgi`` module::
from microdot_asgi import Microdot
from microdot.asgi import Microdot
app = Microdot()
@@ -525,12 +374,67 @@ module::
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::
The ``app`` application instance created from this class can be used as the
ASGI callable with any complaint ASGI web server. If the above example
application was stored in a file called *test.py*, then the following command
runs the web application using the Uvicorn web server::
uvicorn test:app
When using this ASGI adapter, the ``scope`` dictionary provided by the web
When using the ASGI support, the ``scope`` dictionary provided by the web
server is available to request handlers as ``request.asgi_scope``.
Using a WSGI Web Server
^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:align: left
* - Compatibility
- | CPython only
* - Required Microdot source files
- | `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/>`_.
* - Examples
- | `hello_wsgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_wsgi.py>`_
| `hello_wsgi.py (uTemplate) <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/utemplate/hello_wsgi.py>`_
| `hello_wsgi.py (Jinja) <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/jinja/hello_wsgi.py>`_
| `echo_wsgi.py (WebSocket) <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_wsgi.py>`_
The ``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 <https://gunicorn.org/>`_ or
`uWSGI <https://uwsgi-docs.readthedocs.io/en/latest/>`_.
To use a WSGI web server, the application must import the
:class:`Microdot <microdot.wsgi.Microdot>` class from the ``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 can be used as a WSGI
callbable with any complaint WSGI web server. If the above application
was stored in a file called *test.py*, then the following command runs the
web application using the Gunicorn web server::
gunicorn test:app
When using the WSGI support, the ``environ`` dictionary provided by the web
server is available to request handlers as ``request.environ``.
.. note::
In spite of WSGI being a synchronous protocol, the Microdot application
internally runs under an asyncio event loop. For that reason, the
recommendation to prefer ``async def`` handlers over ``def`` still applies
under WSGI. Consult the :ref:`Concurrency` section for a discussion of how
the two types of functions are handled by Microdot.

110
docs/freezing.rst Normal file
View File

@@ -0,0 +1,110 @@
Cross-Compiling and Freezing Microdot (MicroPython Only)
--------------------------------------------------------
Microdot is a fairly small framework, so its size is not something you need to
be concerned about unless you are working with MicroPython on hardware with a
very small amount of disk space and/or RAM. In such cases every byte counts, so
this section provides some recommendations on how to keep Microdot's footprint
as small as possible.
Choosing What Modules to Install
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Microdot has a modular design that allows you to only install the modules that
your application needs.
For minimal web application support based on the core Microdot web server
without extensions, you can just copy `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/microdot.py>`_
to the source directory on your device. The core Microdot web server does not
have any dependencies, so you don't need to install anything else.
If your application uses some of the provided extensions to the core web
server, then instead of installing *microdot.py* you'll need to create a
*microdot* subdirectory and install the following files in it:
- `__init__.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/__init__.py>`_
- `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/microdot.py>`_
- Any extension modules that you need from the `microdot <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot>`_ source directory.
Some of the extensions also have dependencies of their own, so you may need to
install those in your device as well (outside of the ``microdot``
subdirectory). Consult the documentation of each extension to learn if any
third-party dependencies are required.
Cross-Compiling
~~~~~~~~~~~~~~~
An issue that is common with low-end microcontroller boards is that they do not
have enough RAM for the MicroPython compiler to compile the source files, but
once the code is compiled they are able to run it without problems.
To address this, MicroPython allows you to cross-compile source files on your
desktop or laptop computer and then upload their compiled versions to the
device. A good strategy is to cross-compile all the dependencies that are used
by your application, since these are not going to be updated very often. If the
goal is to minimize the use of RAM, you can also opt to cross-compile your
application source files.
The MicroPython cross-compiler is available as a package that you can install
on standard Python. You must determine the version of MicroPython that you will
be running on your device, and install the compiler that matches that version.
For example, if you plan to use MicroPython 1.21.0 on your device, you can
install the cross-compiler for this version with the following command::
pip install mpy-cross==1.21.0
Then run the cross-compiler for each source file that you want to compile.
Since the cross-compilation happens on your computer, you will need to have
copies of all the source files you need to compile locally on your disk. Here
is how you can compile the *microdot.py* file, assuming you have a copy in the
current directory in your computer::
mpy-cross microdot.py
The cross-compiler will create a file with the same name as the source file,
but with the extension changed to *.mpy*.
Once you have all your dependencies compiled, you can replace the *.py* files
in your device with their corresponding *.mpy* versions. MicroPython
automatically recognizes *.mpy* files, so there is no need to make any changes
to any source code to start using compiled files.
Freezing
~~~~~~~~
The ultimate option to reduce the size of a MicroPython application is to
"freeze" it. Freezing is a process that takes MicroPython source code (either
dependencies, application code or both), pre-compiles it and incorporates it
into a custom-built MicroPython firmware that is flashed to the device.
Freezing MicroPython modules to firmware has the advantage that the code is
imported directly from the device's ROM, leaving more RAM available for
application use.
The process to create a custom firmware is unfortunately non-trivial and
different depending on the device, so you will need to consult the MicroPython
documentation that applies to your device to learn how to do this.
The part of the process that is common to all devices is the creation of a
`manifest file <https://docs.micropython.org/en/latest/reference/manifest.html>`_
to tell the MicroPython firmware builder which packages and modules to freeze.
For a minimal installation of Microdot consisting only in its *microdot.py*
source file, the manifest file that you need use to build the firmware must
include the following declaration::
module('microdot')
If instead you are working with a version of Microdot that includes some or all
of its extensions, then the manifest file must reference the ``microdot``
package plus any third-party dependencies that are needed. Below is a manifest
file for a complete Microdot installation that includes all the extensions::
package('microdot')
package('utemplate') # required only if templates are used
module('pyjwt') # required only if user sessions are used
In this example, the *microdot* and *utemplate* packages must be available in
the directory where the manifest file is located so that the MicroPython build
can find them. The `pyjwt` module is part of the MicroPython standard library
and will be downloaded as part of the build.

View File

@@ -9,15 +9,17 @@ 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
Python and on `MicroPython <https://micropython.org>`_.
`Flask <https://flask.palletsprojects.com/>`_. Given its size, it can run on
systems with limited resources such as microcontrollers. Both standard Python
(CPython) and `MicroPython <https://micropython.org>`_ are supported.
.. toctree::
:maxdepth: 3
intro
extensions
migrating
freezing
api
* :ref:`genindex`

View File

@@ -1,26 +1,49 @@
Installation
------------
For standard Python (CPython) projects, Microdot and all of its core extensions
can be installed with ``pip``::
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
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
MicroPython Installation
~~~~~~~~~~~~~~~~~~~~~~~~
For MicroPython, the recommended approach is to manually copy the necessary
source files from the
`GitHub repository <https://github.com/miguelgrinberg/microdot/tree/main/src>`_
into your device, possibly after
into your device, ideally 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.
Use the following guidelines to know what files to copy:
- For a minimal setup with only the base web server functionality, copy
`microdot.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/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 <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/__init__.py>`_
- `microdot.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/microdot.py>`_
- any needed `extensions <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot>`_.
Getting Started
---------------
This section describes the main features of Microdot in an informal manner. For
detailed reference information, consult the :ref:`API Reference`.
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 <Migrating to Microdot 2.x from Older Releases>`.
A Simple Microdot Web Server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -32,7 +55,7 @@ The following is an example of a simple web server::
app = Microdot()
@app.route('/')
def index(request):
async def index(request):
return 'Hello, world!'
app.run()
@@ -46,17 +69,23 @@ 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. 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.
when the client requests the URL.
When the function is called, it 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.
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() <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.
server on port 5000 by default. This method blocks while it waits for
connections from clients.
Running with CPython
~~~~~~~~~~~~~~~~~~~~
^^^^^^^^^^^^^^^^^^^^
.. list-table::
:align: left
@@ -71,17 +100,18 @@ Running with CPython
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello.py>`_
When using CPython, you can start the web server by running the script that
defines and runs the application instance::
has the ``app.run()`` call at the bottom::
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``.
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
@@ -97,11 +127,13 @@ Running with MicroPython
| `gpio.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/gpio/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
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.
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
@@ -109,6 +141,41 @@ device's IP address. As indicated above, the port can be changed by passing the
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() <microdot.Microdot.run>` method supports 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::
The ``ssl`` argument can only be used with CPython at this time, because
MicroPython's asyncio module does not currently support SSL certificates or
TLS encryption. Work on this is
`in progress <https://github.com/micropython/micropython/pull/11897>`_.
Defining Routes
~~~~~~~~~~~~~~~
@@ -119,7 +186,7 @@ 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('/')
def index(request):
async def index(request):
return 'Hello, world!'
When a client requests the root URL (for example, *http://localhost:5000/*),
@@ -127,11 +194,11 @@ 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
Below is another example, this one with a route for a URL with two components
in its path::
@app.route('/users/active')
def active_users(request):
async def active_users(request):
return 'Active users: Susan, Joe, and Bob'
The complete URL that maps to this route is
@@ -144,46 +211,49 @@ 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.
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'])
def invoices(request):
async 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::
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'])
def get_invoices(request):
async def get_invoices(request):
return 'get invoices'
@app.route('/invoices', methods=['POST'])
def create_invoice(request):
async 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::
:func:`delete() <microdot.Microdot.delete>` decorators as shortcuts for the
corresponding HTTP methods. The two example routes above can be written more
concisely with them::
@app.get('/invoices')
def get_invoices(request):
async def get_invoices(request):
return 'get invoices'
@app.post('/invoices')
def create_invoice(request):
async def create_invoice(request):
return 'create an invoice'
Including Dynamic Components in the URL Path
@@ -195,19 +265,19 @@ 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):
async 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.
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/<firstname>/<lastname>')
def get_user(request, firstname, lastname):
async def get_user(request, firstname, lastname):
return 'User: ' + firstname + ' ' + lastname
Dynamic path components are considered to be strings by default. An explicit
@@ -216,7 +286,7 @@ 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):
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
@@ -225,10 +295,12 @@ 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::
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/<path:path>')
def get_test(request, path):
async def get_test(request, path):
return 'Test: ' + path
For the most control, the ``re`` type allows the application to provide a
@@ -237,7 +309,7 @@ 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):
async def get_user(request, username):
return 'User: ' + username
.. note::
@@ -255,54 +327,56 @@ 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
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):
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
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
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.
route.
After request handlers registered with the
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::
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):
async def start_timer(request):
request.g.start_time = time.time()
@app.after_request
def end_timer(request, response):
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.
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.
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
The after-request handlers are only invoked for successful requests. The
:func:`after_error_request() <microdot.Microdot.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.
expected to return an updated response object after performing any necessary
cleanup.
.. note::
The :ref:`request.g <The "g" Object>` object 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.
The :ref:`request.g <The "g" Object>` 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
^^^^^^^^^^^^^^
@@ -312,10 +386,11 @@ 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.
- 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 exception.
- 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
@@ -324,30 +399,31 @@ 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):
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 then invoke the handler when the
exception is an instance of the given class is raised. The next example
provides a custom response for division by zero errors::
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)
def division_by_zero(request, exception):
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 base classes do, Microdot makes an attempt to invoke the
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 an a single source file, but this
is not the best option for applications that past certain size. To make it
Small Microdot applications can be written as a single source file, but this
is not the best option for applications that past 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.
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::
@@ -357,14 +433,14 @@ operations on customers::
customers_app = Microdot()
@customers_app.get('/')
def get_customers(request):
async def get_customers(request):
# return all customers
@customers_app.post('/')
def new_customer(request):
async def new_customer(request):
# create a new customer
In the same way, the *orders.py* sub-application implements operations on
Similar to the above, the *orders.py* sub-application implements operations on
customer orders::
from microdot import Microdot
@@ -372,21 +448,21 @@ customer orders::
orders_app = Microdot()
@orders_app.get('/')
def get_orders(request):
async def get_orders(request):
# return all orders
@orders_app.post('/')
def new_order(request):
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 combined application::
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():
async def create_app():
app = Microdot()
app.mount(customers_app, url_prefix='/customers')
app.mount(orders_app, url_prefix='/orders')
@@ -399,7 +475,7 @@ 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
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
@@ -416,16 +492,20 @@ 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):
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 <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.
to before-request, after-request and error handlers.
Request Attributes
^^^^^^^^^^^^^^^^^^
@@ -448,6 +528,9 @@ The request object provides access to the request attributes, including:
the client, as a tuple (host, port).
- :attr:`app <microdot.Request.app>`: The application instance that created the
request.
- :attr:`g <microdot.Request.g>`: The ``g`` object, where handlers can store
request-specific data to be shared among handlers. See :ref:`The "g" Object`
for details.
JSON Payloads
^^^^^^^^^^^^^
@@ -458,7 +541,7 @@ application can access the parsed JSON data using the
to use this attribute::
@app.post('/customers')
def create_customer(request):
async def create_customer(request):
customer = request.json
# do something with customer
return {'success': True}
@@ -475,7 +558,7 @@ The request object also supports standard HTML form submissions through the
as a :class:`MultiDict <microdot.MultiDict>` object. Example::
@app.route('/', methods=['GET', 'POST'])
def index(req):
async def index(req):
name = 'Unknown'
if req.method == 'POST':
name = req.form.get('name')
@@ -493,9 +576,12 @@ 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.
If the expected body is too large to fit safely in memory, the application can
use the :attr:`stream <microdot.Request.stream>` request attribute to read the
body contents as a file-like object. The
:attr:`max_body_length <microdot.Request.max_body_length>` attribute of the
request object defines the size at which bodies are streamed instead of loaded
into memory.
Cookies
^^^^^^^
@@ -508,41 +594,40 @@ 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.
that it can be shared between the before- and after-request handlers, the
route function and any error handlers. 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::
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):
async def authorize(request):
username = authenticate_user(request)
if not username:
return 'Unauthorized', 401
request.g.username = username
@app.get('/')
def index(request):
async def index(request):
return f'Hello, {request.g.username}!'
Request-Specific After Request Handlers
Request-Specific After-Request Handlers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Sometimes applications need to perform operations on the response object,
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
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
Request-specific after-request handlers are called by Microdot after the route
function returns and all the application-wide 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::
after-request handler defined inside a route function::
@app.post('/logout')
def logout(request):
async def logout(request):
@request.after_request
def reset_session(request, response):
response.set_cookie('session', '', http_only=True)
@@ -556,22 +641,24 @@ 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
- :attr:`max_content_length <microdot.Request.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
- :attr:`max_body_length <microdot.Request.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
- :attr:`max_readline <microdot.Request.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
payloads up to 1MB in size, but prevents requests that are larger than 8KB from
being loaded into memory::
from microdot import Request
Request.max_content_length = 1024 * 1024
Request.max_body_length = 8 * 1024
@@ -589,25 +676,26 @@ 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):
async def index(request):
return 'Hello, World!'
In the above example, Microdot issues a standard 200 status code response, and
inserts the necessary headers.
inserts default headers.
The application can provide its own status code as a second value returned from
the route. The example below returns a 202 status code::
the route to override the 200 default. The example below returns a 202 status
code::
@app.get('/')
def index(request):
async 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.
headers that are added to, or replace the default ones included by Microdot.
The next example returns an HTML response, instead of a default text response::
@app.get('/')
def index(request):
async 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
@@ -615,7 +703,7 @@ the default status code, then it can return two values, omitting the status
code::
@app.get('/')
def index(request):
async def index(request):
return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'}
The application can also return a :class:`Response <microdot.Response>` object
@@ -631,7 +719,7 @@ automatically format the response as JSON.
Example::
@app.get('/')
def index(request):
async def index(request):
return {'hello': 'world'}
.. note::
@@ -647,7 +735,7 @@ creates redirect responses::
from microdot import redirect
@app.get('/')
def index(request):
async def index(request):
return redirect('/about')
File Responses
@@ -659,7 +747,7 @@ object for a file::
from microdot import send_file
@app.get('/')
def index(request):
async def index(request):
return send_file('/static/index.html')
A suggested caching duration can be returned to the client in the ``max_age``
@@ -668,7 +756,7 @@ argument::
from microdot import send_file
@app.get('/')
def image(request):
async def image(request):
return send_file('/static/image.jpg', max_age=3600) # in seconds
.. note::
@@ -678,7 +766,7 @@ argument::
the project::
@app.route('/static/<path:path>')
def static(request, path):
async def static(request, path):
if '..' in path:
# directory traversal is not allowed
return 'Not found', 404
@@ -688,12 +776,12 @@ 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::
return a response that is generated in chunks, by returning a Python generator.
The example below returns all the numbers in the fibonacci sequence below 100::
@app.get('/fibonacci')
def fibonacci(request):
def generate_fibonacci():
async def fibonacci(request):
async def generate_fibonacci():
a, b = 0, 1
while a < 100:
yield str(a) + '\n'
@@ -701,6 +789,14 @@ example below returns all the numbers in the fibonacci sequence below 100::
return generate_fibonacci()
.. note::
Under CPython, the generator function can be a ``def`` or ``async def``
function, as well as a class-based generator.
Under MicroPython, asynchronous generator functions are not supported, so
only ``def`` generator functions can be used. Asynchronous class-based
generators are supported.
Changing the Default Response Content Type
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -728,14 +824,14 @@ 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>`.
:ref:`request-specific after-request handler <Request-Specific After-Request Handlers>`.
Example::
@app.get('/')
def index(request):
async def index(request):
@request.after_request
def set_cookie(request, response):
async def set_cookie(request, response):
response.set_cookie('name', 'value')
return response
@@ -744,7 +840,7 @@ Example::
Another option is to create a response object directly in the route function::
@app.get('/')
def index(request):
async def index(request):
response = Response('Hello, World!')
response.set_cookie('name', 'value')
return response
@@ -759,15 +855,18 @@ Another option is to create a response object directly in the route function::
Concurrency
~~~~~~~~~~~
By default, Microdot runs in synchronous (single-threaded) mode. However, if
the ``threading`` module is available, each request will be started on a
separate thread and requests will be handled concurrently.
Microdot implements concurrency through the ``asyncio`` package. Applications
must ensure their handlers do not block, as this will prevent other concurrent
requests from being handled.
Be aware that most microcontroller boards support a very limited form of
multi-threading that is not appropriate for concurrent request handling. For
that reason, use of the `threading <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/threading/threading.py>`_
module on microcontroller platforms is not recommended.
When running under CPython, ``async def`` handler functions run as native
asyncio tasks, while ``def`` handler functions are executed in a
`thread executor <https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor>`_
to prevent them from blocking the asynchronous loop.
The :ref:`micropython_asyncio <Asynchronous Support with Asyncio>` extension
provides a more robust concurrency option that is supported even on low-end
MicroPython boards.
Under MicroPython the situation is different. Most microcontroller boards
implementing MicroPython do not have threading support or executors, so ``def``
handler functions in this platform can only run in the main and only thread.
These functions will block the asynchronous loop when they take too long to
complete so ``async def`` handlers properly written to allow other handlers to
run in parallel should be preferred.

142
docs/migrating.rst Normal file
View File

@@ -0,0 +1,142 @@
Migrating to Microdot 2.x from Older Releases
---------------------------------------------
Version 2 of Microdot incorporates feedback received from users of earlier
releases, and attempts to improve and correct some design decisions that have
proven to be problematic.
For this reason most applications built for earlier versions will need to be
updated to work correctly with Microdot 2. This section describes the backwards
incompatible changes that were made.
Code reorganization
~~~~~~~~~~~~~~~~~~~
The Microdot source code has been moved into a ``microdot`` package,
eliminating the need for each extension to be named with a *microdot_* prefix.
As a result of this change, all extensions have been renamed to shorter names.
For example, the *microdot_cors.py* module is now called *cors.py*.
This change affects the way extensions are imported. Instead of this::
from microdot_cors import CORS
the import statement should be::
from microdot.cors import CORS
No more synchronous web server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In earlier releases of Microdot the core web server was built on synchronous
Python, and asynchronous support was enabled with the asyncio extension.
Microdot 2 eliminates the synchronous web server, and implements the core
server logic directly with asyncio, eliminating the need for an asyncio
extension.
Any applications built using the asyncio extension will need to update their
imports from this::
from microdot.asyncio import Microdot
to this::
from microdot import Microdot
Applications that were built using the synchronous web server do not need to
change their imports, but will now work asynchronously. Review the
:ref:`Concurrency` section to learn about the potential issues when using
``def`` function handlers, and the benefits of transitioning to ``async def``
handlers.
Removed extensions
~~~~~~~~~~~~~~~~~~
Some extensions became unnecessary and have been removed or merged with other
extensions:
- *microdot_asyncio.py*: this is now the core web server.
- *microdot_asyncio_websocket.py*: this is now the main WebSocket extension.
- *microdot_asyncio_test_client.py*: this is now the main test client
extension.
- *microdot_asgi_websocket.py*: the functionality in this extension is now
available in the ASGI extension.
- *microdot_ssl.py*: this extension was only used with the synchronous web
server, so it is not needed anymore.
- *microdot_websocket_alt.py*: this extension was only used with the
synchronous web server, so it is not needed anymore.
No more ``render_template()`` function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Jinja and uTemplate extensions have been redesigned to work better under
the asynchronous engine, and as a result, the ``render_template()`` function
has been eliminated.
Instead of this::
return render_template('index.html', title='Home')
use this::
return Template('index.html').render(title='Home')
As a result of this change, it is now possible to use asynchronous rendering::
return await Template('index.html').render_async(title='Home')
Also thanks to this redesign, the template can be streamed instead of returned
as a single string::
return Template('index.html').generate(title='Home')
Streamed templates also have an asynchronous version::
return await Template('index.html').generate_async(title='Home')
Class-based user sessions
~~~~~~~~~~~~~~~~~~~~~~~~~
The session extension has been completely redesigned. To initialize session
support for the application, create a ``Session`` object::
app = Microdot()
Session(app, secret_key='top-secret!')
The ``@with_session`` decorator is used to include the session in a request::
@app.get('/')
@with_session
async def index(request, session):
# ...
The ``session`` can be used as a dictionary to retrieve or change the session.
To save the session when it has been modified, call its ``save()`` method::
@app.get('/')
@with_session
async def index(request, session):
# ...
session.save()
return 'OK'
To delete the session, call its ``delete()`` method before returning from the
request.
WSGI extension redesign
~~~~~~~~~~~~~~~~~~~~~~~
Given that the synchronous web server has been removed, the WSGI extension has
been redesigned to work as a synchronous wrapper for the asynchronous web
server.
Applications using the WSGI extension continue to run under an asynchronous
loop and should try to use the recommended ``async def`` handlers, but can be
deployed with standard WSGI servers such as Gunicorn.
WebSocket support when using the WSGI extension is enabled when using a
compatible web server. At this time only Gunicorn is supported for WebSocket.
As before, the WSGI extension is not available under MicroPython.

View File

@@ -4,7 +4,7 @@ app = Microdot()
@app.get('/')
def index(req):
async def index(req):
return {'hello': 'world'}

View File

@@ -1,4 +1,4 @@
from microdot_asgi import Microdot
from microdot.asgi import Microdot
app = Microdot()

View File

@@ -1,11 +0,0 @@
from microdot_asyncio import Microdot
app = Microdot()
@app.get('/')
async def index(req):
return {'hello': 'world'}
app.run()

View File

@@ -4,5 +4,5 @@ app = FastAPI()
@app.get('/')
def index():
async def index():
return {'hello': 'world'}

View File

@@ -4,5 +4,5 @@ app = Quart(__name__)
@app.get('/')
def index():
async def index():
return {'hello': 'world'}

View File

@@ -1,4 +1,4 @@
from microdot_wsgi import Microdot
from microdot.wsgi import Microdot
app = Microdot()

View File

@@ -0,0 +1,9 @@
pip-tools
flask
quart
fastapi
gunicorn
uvicorn
requests
psutil
humanize

View File

@@ -1,33 +1,115 @@
aiofiles==0.8.0
anyio==3.6.1
blinker==1.5
certifi==2023.7.22
charset-normalizer==2.1.0
click==8.1.3
fastapi==0.79.0
Flask==2.3.2
gunicorn==20.1.0
h11==0.13.0
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile requirements.in
#
aiofiles==23.2.1
# via quart
annotated-types==0.6.0
# via pydantic
anyio==3.7.1
# via
# fastapi
# starlette
blinker==1.7.0
# via
# flask
# quart
build==1.0.3
# via pip-tools
certifi==2023.11.17
# via requests
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via
# flask
# pip-tools
# quart
# uvicorn
fastapi==0.104.1
# via -r requirements.in
flask==3.0.0
# via
# -r requirements.in
# quart
gunicorn==21.2.0
# via -r requirements.in
h11==0.14.0
# via
# hypercorn
# uvicorn
# wsproto
h2==4.1.0
# via hypercorn
hpack==4.0.0
humanize==4.3.0
hypercorn==0.13.2
# via h2
humanize==4.9.0
# via -r requirements.in
hypercorn==0.15.0
# via quart
hyperframe==6.0.1
idna==3.3
# via h2
idna==3.6
# via
# anyio
# requests
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.1
microdot
# via
# flask
# quart
jinja2==3.1.2
# via
# flask
# quart
markupsafe==2.1.3
# via
# jinja2
# quart
# werkzeug
packaging==23.2
# via
# build
# gunicorn
pip-tools==7.3.0
# via -r requirements.in
priority==2.0.0
psutil==5.9.1
pydantic==1.9.1
quart==0.18.0
# via hypercorn
psutil==5.9.6
# via -r requirements.in
pydantic==2.5.2
# via fastapi
pydantic-core==2.14.5
# via pydantic
pyproject-hooks==1.0.0
# via build
quart==0.19.4
# via -r requirements.in
requests==2.31.0
sniffio==1.2.0
# via -r requirements.in
sniffio==1.3.0
# via anyio
starlette==0.27.0
toml==0.10.2
typing_extensions==4.3.0
urllib3==1.26.18
uvicorn==0.18.2
Werkzeug==2.2.3
wsproto==1.1.0
# via fastapi
typing-extensions==4.9.0
# via
# fastapi
# pydantic
# pydantic-core
urllib3==2.1.0
# via requests
uvicorn==0.24.0.post1
# via -r requirements.in
werkzeug==3.0.1
# via
# flask
# quart
wheel==0.42.0
# via pip-tools
wsproto==1.2.0
# via hypercorn
# The following packages are considered to be unsafe in a requirements file:
# pip
# setuptools

View File

@@ -14,13 +14,8 @@ apps = [
),
(
'micropython mem.py',
{'MICROPYPATH': '../../src'},
'microdot-micropython-sync'
),
(
'micropython mem_async.py',
{'MICROPYPATH': '../../src:../../libs/micropython'},
'microdot-micropython-async'
'microdot-micropython'
),
(
['python', '-c', 'import time; time.sleep(10)'],
@@ -30,47 +25,42 @@ apps = [
(
'python mem.py',
{'PYTHONPATH': '../../src'},
'microdot-cpython-sync'
),
(
'python mem_async.py',
{'PYTHONPATH': '../../src'},
'microdot-cpython-async'
),
(
'gunicorn --workers 1 --bind :5000 mem_wsgi:app',
{'PYTHONPATH': '../../src'},
'microdot-gunicorn-sync'
'microdot-cpython'
),
(
'uvicorn --workers 1 --port 5000 mem_asgi:app',
{'PYTHONPATH': '../../src'},
'microdot-uvicorn-async'
'microdot-uvicorn'
),
(
'gunicorn --workers 1 --bind :5000 mem_wsgi:app',
{'PYTHONPATH': '../../src'},
'microdot-gunicorn'
),
(
'flask run',
{'FLASK_APP': 'mem_flask.py'},
'flask-run-sync'
'flask-run'
),
(
'quart run',
{'QUART_APP': 'mem_quart.py'},
'quart-run-async'
'quart-run'
),
(
'gunicorn --workers 1 --bind :5000 mem_flask:app',
{},
'flask-gunicorn-sync'
'flask-gunicorn'
),
(
'uvicorn --workers 1 --port 5000 mem_quart:app',
{},
'quart-uvicorn-async'
'quart-uvicorn'
),
(
'uvicorn --workers 1 --port 5000 mem_fastapi:app',
{},
'fastapi-uvicorn-async'
'fastapi-uvicorn'
),
]

View File

@@ -1,5 +1,5 @@
from microdot import Microdot
from microdot_cors import CORS
from microdot.cors import CORS
app = Microdot()
CORS(app, allowed_origins=['https://example.org'], allow_credentials=True)

View File

@@ -2,7 +2,7 @@ from microdot import Microdot
app = Microdot()
htmldoc = '''<!DOCTYPE html>
html = '''<!DOCTYPE html>
<html>
<head>
<title>Microdot Example Page</title>
@@ -20,12 +20,12 @@ htmldoc = '''<!DOCTYPE html>
@app.route('/')
def hello(request):
return htmldoc, 200, {'Content-Type': 'text/html'}
async def hello(request):
return html, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')
def shutdown(request):
async def shutdown(request):
request.app.shutdown()
return 'The server is shutting down...'

View File

@@ -1,8 +1,8 @@
from microdot_asgi import Microdot
from microdot.asgi import Microdot
app = Microdot()
htmldoc = '''<!DOCTYPE html>
html = '''<!DOCTYPE html>
<html>
<head>
<title>Microdot Example Page</title>
@@ -21,7 +21,7 @@ htmldoc = '''<!DOCTYPE html>
@app.route('/')
async def hello(request):
return htmldoc, 200, {'Content-Type': 'text/html'}
return html, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')

View File

@@ -1,33 +0,0 @@
from microdot_asyncio import Microdot
app = Microdot()
htmldoc = '''<!DOCTYPE html>
<html>
<head>
<title>Microdot Example Page</title>
<meta charset="UTF-8">
</head>
<body>
<div>
<h1>Microdot Example Page</h1>
<p>Hello from Microdot!</p>
<p><a href="/shutdown">Click to shutdown the server</a></p>
</div>
</body>
</html>
'''
@app.route('/')
async def hello(request):
return htmldoc, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')
async def shutdown(request):
request.app.shutdown()
return 'The server is shutting down...'
app.run(debug=True)

View File

@@ -1,8 +1,8 @@
from microdot_wsgi import Microdot
from microdot.wsgi import Microdot
app = Microdot()
htmldoc = '''<!DOCTYPE html>
html = '''<!DOCTYPE html>
<html>
<head>
<title>Microdot Example Page</title>
@@ -21,7 +21,7 @@ htmldoc = '''<!DOCTYPE html>
@app.route('/')
def hello(request):
return htmldoc, 200, {'Content-Type': 'text/html'}
return html, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')

View File

@@ -1,6 +1,5 @@
from microdot import Microdot, Response, redirect
from microdot_session import set_session_secret_key, with_session, \
update_session, delete_session
from microdot.session import Session, with_session
BASE_TEMPLATE = '''<!doctype html>
<html>
@@ -29,18 +28,19 @@ LOGGED_IN = '''<p>Hello <b>{username}</b>!</p>
</form>'''
app = Microdot()
set_session_secret_key('top-secret')
Session(app, secret_key='top-secret')
Response.default_content_type = 'text/html'
@app.get('/')
@app.post('/')
@with_session
def index(req, session):
async def index(req, session):
username = session.get('username')
if req.method == 'POST':
username = req.form.get('username')
update_session(req, {'username': username})
session['username'] = username
session.save()
return redirect('/')
if username is None:
return BASE_TEMPLATE.format(content=LOGGED_OUT)
@@ -50,8 +50,9 @@ def index(req, session):
@app.post('/logout')
def logout(req):
delete_session(req)
@with_session
async def logout(req, session):
session.delete()
return redirect('/')

16
examples/sse/counter.py Normal file
View File

@@ -0,0 +1,16 @@
import asyncio
from microdot import Microdot
from microdot.sse import with_sse
app = Microdot()
@app.route('/events')
@with_sse
async def events(request, sse):
for i in range(10):
await asyncio.sleep(1)
await sse.send({'counter': i})
app.run(debug=True)

View File

@@ -1,15 +1,14 @@
from microdot import Microdot, send_file
app = Microdot()
@app.route('/')
def index(request):
async def index(request):
return send_file('static/index.html')
@app.route('/static/<path:path>')
def static(request, path):
async def static(request, path):
if '..' in path:
# directory traversal is not allowed
return 'Not found', 404

View File

@@ -0,0 +1,10 @@
p {
font-family: Arial, Helvetica, sans-serif;
color: #333333;
}
h1 {
font-family: Arial, Helvetica, sans-serif;
color: #3070b3;
text-align: center;
}

View File

@@ -1,18 +0,0 @@
from microdot_asyncio import Microdot, send_file
app = Microdot()
@app.route('/')
async def index(request):
return send_file('static/index.html')
@app.route('/static/<path:path>')
async def static(request, path):
if '..' in path:
# directory traversal is not allowed
return 'Not found', 404
return send_file('static/' + path)
app.run(debug=True)

View File

@@ -1,8 +1,5 @@
try:
import utime as time
except ImportError:
import time
import sys
import asyncio
from microdot import Microdot
app = Microdot()
@@ -14,7 +11,7 @@ for file in ['1.jpg', '2.jpg', '3.jpg']:
@app.route('/')
def index(request):
async def index(request):
return '''<!doctype html>
<html>
<head>
@@ -29,14 +26,38 @@ def index(request):
@app.route('/video_feed')
def video_feed(request):
def stream():
yield b'--frame\r\n'
while True:
for frame in frames:
yield b'Content-Type: image/jpeg\r\n\r\n' + frame + \
b'\r\n--frame\r\n'
time.sleep(1)
async def video_feed(request):
print('Starting video stream.')
if sys.implementation.name != 'micropython':
# CPython supports async generator function
async def stream():
try:
yield b'--frame\r\n'
while True:
for frame in frames:
yield b'Content-Type: image/jpeg\r\n\r\n' + frame + \
b'\r\n--frame\r\n'
await asyncio.sleep(1)
except GeneratorExit:
print('Stopping video stream.')
else:
# MicroPython can only use class-based async generators
class stream():
def __init__(self):
self.i = 0
def __aiter__(self):
return self
async def __anext__(self):
await asyncio.sleep(1)
self.i = (self.i + 1) % len(frames)
return b'Content-Type: image/jpeg\r\n\r\n' + \
frames[self.i] + b'\r\n--frame\r\n'
async def aclose(self):
print('Stopping video stream.')
return stream(), 200, {'Content-Type':
'multipart/x-mixed-replace; boundary=frame'}

View File

@@ -1,65 +0,0 @@
import sys
try:
import uasyncio as asyncio
except ImportError:
import asyncio
from microdot_asyncio import Microdot
app = Microdot()
frames = []
for file in ['1.jpg', '2.jpg', '3.jpg']:
with open(file, 'rb') as f:
frames.append(f.read())
@app.route('/')
def index(request):
return '''<!doctype html>
<html>
<head>
<title>Microdot Video Streaming</title>
<meta charset="UTF-8">
</head>
<body>
<h1>Microdot Video Streaming</h1>
<img src="/video_feed">
</body>
</html>''', 200, {'Content-Type': 'text/html'}
@app.route('/video_feed')
async def video_feed(request):
if sys.implementation.name != 'micropython':
# CPython supports yielding async generators
async def stream():
yield b'--frame\r\n'
while True:
for frame in frames:
yield b'Content-Type: image/jpeg\r\n\r\n' + frame + \
b'\r\n--frame\r\n'
await asyncio.sleep(1)
else:
# MicroPython can only use class-based async generators
class stream():
def __init__(self):
self.i = 0
def __aiter__(self):
return self
async def __anext__(self):
await asyncio.sleep(1)
self.i = (self.i + 1) % len(frames)
return b'Content-Type: image/jpeg\r\n\r\n' + \
frames[self.i] + b'\r\n--frame\r\n'
return stream(), 200, {'Content-Type':
'multipart/x-mixed-replace; boundary=frame'}
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -0,0 +1,18 @@
from microdot import Microdot, Response
from microdot.jinja import template, init_templates
init_templates('templates', enable_async=True)
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST'])
async def index(req):
name = None
if req.method == 'POST':
name = req.form.get('name')
return await template('index.html').render_async(name=name)
if __name__ == '__main__':
app.run()

View File

@@ -1,18 +1,18 @@
from microdot import Microdot, Response
from microdot_jinja import render_template
from microdot.jinja import template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/')
def index(req):
return render_template('page1.html', page='Page 1')
async def index(req):
return template('page1.html').render(page='Page 1')
@app.route('/page2')
def page2(req):
return render_template('page2.html', page='Page 2')
async def page2(req):
return template('page2.html').render(page='Page 2')
if __name__ == '__main__':

View File

@@ -1,16 +1,16 @@
from microdot import Microdot, Response
from microdot_jinja import render_template
from microdot.jinja import template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST'])
def index(req):
async def index(req):
name = None
if req.method == 'POST':
name = req.form.get('name')
return render_template('index.html', name=name)
return template('index.html').render(name=name)
if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot_asyncio import Microdot, Response
from microdot_utemplate import render_template
from microdot.asgi import Microdot, Response
from microdot.jinja import template
app = Microdot()
Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None
if req.method == 'POST':
name = req.form.get('name')
return render_template('index.html', name=name)
return template('index.html').render(name=name)
if __name__ == '__main__':

View File

@@ -0,0 +1,17 @@
from microdot.wsgi import Microdot, Response
from microdot.jinja import template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST'])
async def index(req):
name = None
if req.method == 'POST':
name = req.form.get('name')
return template('index.html').render(name=name)
if __name__ == '__main__':
app.run()

View File

@@ -0,0 +1,17 @@
from microdot import Microdot, Response
from microdot.jinja import template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST'])
async def index(req):
name = None
if req.method == 'POST':
name = req.form.get('name')
return template('index.html').generate(name=name)
if __name__ == '__main__':
app.run()

View File

@@ -0,0 +1,17 @@
from microdot import Microdot, Response
from microdot.utemplate import template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST'])
async def index(req):
name = None
if req.method == 'POST':
name = req.form.get('name')
return await template('index.html').render_async(name=name)
if __name__ == '__main__':
app.run()

View File

@@ -1,18 +1,18 @@
from microdot import Microdot, Response
from microdot_utemplate import render_template
from microdot.utemplate import template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/')
def index(req):
return render_template('page1.html', page='Page 1')
async def index(req):
return template('page1.html').render(page='Page 1')
@app.route('/page2')
def page2(req):
return render_template('page2.html', page='Page 2')
async def page2(req):
return template('page2.html').render(page='Page 2')
if __name__ == '__main__':

View File

@@ -1,16 +1,16 @@
from microdot import Microdot, Response
from microdot_utemplate import render_template
from microdot.utemplate import template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST'])
def index(req):
async def index(req):
name = None
if req.method == 'POST':
name = req.form.get('name')
return render_template('index.html', name=name)
return template('index.html').render(name=name)
if __name__ == '__main__':

View File

@@ -0,0 +1,17 @@
from microdot.asgi import Microdot, Response
from microdot.utemplate import template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST'])
async def index(req):
name = None
if req.method == 'POST':
name = req.form.get('name')
return template('index.html').render(name=name)
if __name__ == '__main__':
app.run()

View File

@@ -0,0 +1,17 @@
from microdot.wsgi import Microdot, Response
from microdot.utemplate import template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST'])
async def index(req):
name = None
if req.method == 'POST':
name = req.form.get('name')
return template('index.html').render(name=name)
if __name__ == '__main__':
app.run()

View File

@@ -0,0 +1,17 @@
from microdot import Microdot, Response
from microdot.utemplate import template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST'])
async def index(req):
name = None
if req.method == 'POST':
name = req.form.get('name')
return template('index.html').generate(name=name)
if __name__ == '__main__':
app.run()

View File

@@ -1,23 +0,0 @@
import ssl
from microdot_asyncio import Microdot, send_file
from microdot_asyncio_websocket import with_websocket
app = Microdot()
@app.route('/')
def index(request):
return send_file('index.html')
@app.route('/echo')
@with_websocket
async def echo(request, ws):
while True:
data = await ws.receive()
await ws.send(data)
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
sslctx.load_cert_chain('cert.pem', 'key.pem')
app.run(port=4443, debug=True, ssl=sslctx)

View File

@@ -1,24 +0,0 @@
import sys
from microdot import Microdot, send_file
from microdot_websocket import with_websocket
from microdot_ssl import create_ssl_context
app = Microdot()
@app.route('/')
def index(request):
return send_file('index.html')
@app.route('/echo')
@with_websocket
def echo(request, ws):
while True:
data = ws.receive()
ws.send(data)
ext = 'der' if sys.implementation.name == 'micropython' else 'pem'
sslctx = create_ssl_context('cert.' + ext, 'key.' + ext)
app.run(port=4443, debug=True, ssl=sslctx)

View File

@@ -1,9 +1,9 @@
import ssl
from microdot_asyncio import Microdot
from microdot import Microdot
app = Microdot()
htmldoc = '''<!DOCTYPE html>
html = '''<!DOCTYPE html>
<html>
<head>
<title>Microdot Example Page</title>
@@ -22,7 +22,7 @@ htmldoc = '''<!DOCTYPE html>
@app.route('/')
async def hello(request):
return htmldoc, 200, {'Content-Type': 'text/html'}
return html, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')

View File

@@ -1,37 +0,0 @@
import sys
from microdot import Microdot
from microdot_ssl import create_ssl_context
app = Microdot()
htmldoc = '''<!DOCTYPE html>
<html>
<head>
<title>Microdot Example Page</title>
<meta charset="UTF-8">
</head>
<body>
<div>
<h1>Microdot Example Page</h1>
<p>Hello from Microdot!</p>
<p><a href="/shutdown">Click to shutdown the server</a></p>
</div>
</body>
</html>
'''
@app.route('/')
def hello(request):
return htmldoc, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')
def shutdown(request):
request.app.shutdown()
return 'The server is shutting down...'
ext = 'der' if sys.implementation.name == 'micropython' else 'pem'
sslctx = create_ssl_context('cert.' + ext, 'key.' + ext)
app.run(port=4443, debug=True, ssl=sslctx)

View File

@@ -1,36 +0,0 @@
<!doctype html>
<html>
<head>
<title>Microdot TLS WebSocket Demo</title>
<meta charset="UTF-8">
</head>
<body>
<h1>Microdot TLS WebSocket Demo</h1>
<div id="log"></div>
<br>
<form id="form">
<label for="text">Input: </label>
<input type="text" id="text" autofocus>
</form>
<script>
const log = (text, color) => {
document.getElementById('log').innerHTML += `<span style="color: ${color}">${text}</span><br>`;
};
const socket = new WebSocket('wss://' + location.host + '/echo');
socket.addEventListener('message', ev => {
log('<<< ' + ev.data, 'blue');
});
socket.addEventListener('close', ev => {
log('<<< closed');
});
document.getElementById('form').onsubmit = ev => {
ev.preventDefault();
const textField = document.getElementById('text');
log('>>> ' + textField.value, 'red');
socket.send(textField.value);
textField.value = '';
};
</script>
</body>
</html>

View File

@@ -5,12 +5,12 @@ Request.max_content_length = 1024 * 1024 # 1MB (change as needed)
@app.get('/')
def index(request):
async def index(request):
return send_file('index.html')
@app.post('/upload')
def upload(request):
async def upload(request):
# obtain the filename and size from request headers
filename = request.headers['Content-Disposition'].split(
'filename=')[1].strip('"')
@@ -22,7 +22,7 @@ def upload(request):
# write the file to the files directory in 1K chunks
with open('files/' + filename, 'wb') as f:
while size > 0:
chunk = request.stream.read(min(size, 1024))
chunk = await request.stream.read(min(size, 1024))
f.write(chunk)
size -= len(chunk)

View File

@@ -1,34 +0,0 @@
from microdot_asyncio import Microdot, send_file, Request
app = Microdot()
Request.max_content_length = 1024 * 1024 # 1MB (change as needed)
@app.get('/')
async def index(request):
return send_file('index.html')
@app.post('/upload')
async def upload(request):
# obtain the filename and size from request headers
filename = request.headers['Content-Disposition'].split(
'filename=')[1].strip('"')
size = int(request.headers['Content-Length'])
# sanitize the filename
filename = filename.replace('/', '_')
# write the file to the files directory in 1K chunks
with open('files/' + filename, 'wb') as f:
while size > 0:
chunk = await request.stream.read(min(size, 1024))
f.write(chunk)
size -= len(chunk)
print('Successfully saved file: ' + filename)
return ''
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -1,20 +1,20 @@
from microdot import Microdot, send_file
from microdot_websocket import with_websocket
from microdot.websocket import with_websocket
app = Microdot()
@app.route('/')
def index(request):
async def index(request):
return send_file('index.html')
@app.route('/echo')
@with_websocket
def echo(request, ws):
async def echo(request, ws):
while True:
data = ws.receive()
ws.send(data)
data = await ws.receive()
await ws.send(data)
app.run()

View File

@@ -1,11 +1,10 @@
from microdot_asgi import Microdot, send_file
from microdot_asgi_websocket import with_websocket
from microdot.asgi import Microdot, send_file, with_websocket
app = Microdot()
@app.route('/')
def index(request):
async def index(request):
return send_file('index.html')
@@ -15,3 +14,7 @@ async def echo(request, ws):
while True:
data = await ws.receive()
await ws.send(data)
if __name__ == '__main__':
app.run()

View File

@@ -1,20 +0,0 @@
from microdot_asyncio import Microdot, send_file
from microdot_asyncio_websocket import with_websocket
app = Microdot()
@app.route('/')
def index(request):
return send_file('index.html')
@app.route('/echo')
@with_websocket
async def echo(request, ws):
while True:
data = await ws.receive()
await ws.send(data)
app.run()

View File

@@ -1,17 +1,20 @@
from microdot_wsgi import Microdot, send_file
from microdot_websocket import with_websocket
from microdot.wsgi import Microdot, send_file, with_websocket
app = Microdot()
@app.route('/')
def index(request):
async def index(request):
return send_file('index.html')
@app.route('/echo')
@with_websocket
def echo(request, ws):
async def echo(request, ws):
while True:
data = ws.receive()
ws.send(data)
data = await ws.receive()
await ws.send(data)
if __name__ == '__main__':
app.run()

View File

@@ -1,8 +1,6 @@
utemplate
=========
*Release: 1.4.1, Source: https://github.com/pfalcon/utemplate*
`utemplate` is a lightweight and memory-efficient template engine for
Python, primarily designed for use with Pycopy, a lightweight Python
implementation (https://github.com/pfalcon/pycopy). It is also fully

View File

@@ -1,4 +1,4 @@
# MicroPython uasyncio module
# MicroPython asyncio module
# MIT license; Copyright (c) 2019 Damien P. George
from .core import *
@@ -18,6 +18,7 @@ _attrs = {
"StreamWriter": "stream",
}
# Lazy loader, effectively does:
# global attr
# from .mod import attr

View File

@@ -1,4 +1,4 @@
# MicroPython uasyncio module
# MicroPython asyncio module
# MIT license; Copyright (c) 2019 Damien P. George
from time import ticks_ms as ticks, ticks_diff, ticks_add
@@ -6,7 +6,7 @@ import sys, select
# Import TaskQueue and Task, preferring built-in C code over Python code
try:
from _uasyncio import TaskQueue, Task
from _asyncio import TaskQueue, Task
except:
from .task import TaskQueue, Task
@@ -30,6 +30,7 @@ _exc_context = {"message": "Task exception wasn't retrieved", "exception": None,
################################################################################
# Sleep functions
# "Yield" once, then raise StopIteration
class SingletonGenerator:
def __init__(self):
@@ -132,6 +133,7 @@ class IOQueue:
################################################################################
# Main run loop
# Ensure the awaitable is a task
def _promote_to_task(aw):
return aw if isinstance(aw, Task) else create_task(aw)
@@ -270,9 +272,9 @@ class Loop:
return Loop._exc_handler
def default_exception_handler(loop, context):
print(context["message"])
print("future:", context["future"], "coro=", context["future"].coro)
sys.print_exception(context["exception"])
print(context["message"], file=sys.stderr)
print("future:", context["future"], "coro=", context["future"].coro, file=sys.stderr)
sys.print_exception(context["exception"], sys.stderr)
def call_exception_handler(context):
(Loop._exc_handler or Loop.default_exception_handler)(Loop, context)

View File

@@ -1,8 +1,9 @@
# MicroPython uasyncio module
# MicroPython asyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
from . import core
# Event class for primitive events that can be waited on, set, and cleared
class Event:
def __init__(self):
@@ -23,7 +24,8 @@ class Event:
def clear(self):
self.state = False
async def wait(self):
# async
def wait(self):
if not self.state:
# Event not set, put the calling task on the event's waiting queue
self.waiting.push(core.cur_task)
@@ -38,16 +40,16 @@ class Event:
# that asyncio will poll until a flag is set.
# Note: Unlike Event, this is self-clearing after a wait().
try:
import uio
import io
class ThreadSafeFlag(uio.IOBase):
class ThreadSafeFlag(io.IOBase):
def __init__(self):
self.state = 0
def ioctl(self, req, flags):
if req == 3: # MP_STREAM_POLL
return self.state * flags
return None
return -1 # Other requests are unsupported
def set(self):
self.state = 1

View File

@@ -1,10 +1,10 @@
# MicroPython uasyncio module
# MicroPython asyncio module
# MIT license; Copyright (c) 2019-2022 Damien P. George
from . import core
def _run(waiter, aw):
async def _run(waiter, aw):
try:
result = await aw
status = True
@@ -61,7 +61,8 @@ class _Remove:
pass
async def gather(*aws, return_exceptions=False):
# async
def gather(*aws, return_exceptions=False):
if not aws:
return []
@@ -122,7 +123,7 @@ async def gather(*aws, return_exceptions=False):
# Either this gather was cancelled, or one of the sub-tasks raised an exception with
# return_exceptions==False, so reraise the exception here.
if state is not 0:
if state:
raise state
# Return the list of return values of each sub-task.

View File

@@ -1,8 +1,9 @@
# MicroPython uasyncio module
# MicroPython asyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
from . import core
# Lock class for primitive mutex capability
class Lock:
def __init__(self):
@@ -28,7 +29,8 @@ class Lock:
# No Task waiting so unlock
self.state = 0
async def acquire(self):
# async
def acquire(self):
if self.state != 0:
# Lock unavailable, put the calling Task on the waiting queue
self.waiting.push(core.cur_task)

View File

@@ -1,7 +1,7 @@
# This list of package files doesn't include task.py because that's provided
# by the C module.
package(
"uasyncio",
"asyncio",
(
"__init__.py",
"core.py",
@@ -13,3 +13,6 @@ package(
base_path="..",
opt=3,
)
# Backwards-compatible uasyncio module.
module("uasyncio.py", opt=3)

View File

@@ -1,4 +1,4 @@
# MicroPython uasyncio module
# MicroPython asyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
from . import core
@@ -26,7 +26,8 @@ class Stream:
# TODO yield?
self.s.close()
async def read(self, n=-1):
# async
def read(self, n=-1):
r = b""
while True:
yield core._io_queue.queue_read(self.s)
@@ -38,11 +39,13 @@ class Stream:
return r
r += r2
async def readinto(self, buf):
# async
def readinto(self, buf):
yield core._io_queue.queue_read(self.s)
return self.s.readinto(buf)
async def readexactly(self, n):
# async
def readexactly(self, n):
r = b""
while n:
yield core._io_queue.queue_read(self.s)
@@ -54,7 +57,8 @@ class Stream:
n -= len(r2)
return r
async def readline(self):
# async
def readline(self):
l = b""
while True:
yield core._io_queue.queue_read(self.s)
@@ -73,10 +77,11 @@ class Stream:
buf = buf[ret:]
self.out_buf += buf
async def drain(self):
# async
def drain(self):
if not self.out_buf:
# Drain must always yield, so a tight loop of write+drain can't block the scheduler.
return await core.sleep_ms(0)
return (yield from core.sleep_ms(0))
mv = memoryview(self.out_buf)
off = 0
while off < len(mv):
@@ -93,9 +98,11 @@ StreamWriter = Stream
# Create a TCP stream connection to a remote host
async def open_connection(host, port):
from uerrno import EINPROGRESS
import usocket as socket
#
# async
def open_connection(host, port):
from errno import EINPROGRESS
import socket
ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0] # TODO this is blocking!
s = socket.socket(ai[0], ai[1], ai[2])
@@ -120,20 +127,30 @@ class Server:
await self.wait_closed()
def close(self):
# Note: the _serve task must have already started by now due to the sleep
# in start_server, so `state` won't be clobbered at the start of _serve.
self.state = True
self.task.cancel()
async def wait_closed(self):
await self.task
async def _serve(self, s, cb):
self.state = False
# Accept incoming connections
while True:
try:
yield core._io_queue.queue_read(s)
except core.CancelledError:
# Shutdown server
except core.CancelledError as er:
# The server task was cancelled, shutdown server and close socket.
s.close()
return
if self.state:
# If the server was explicitly closed, ignore the cancellation.
return
else:
# Otherwise e.g. the parent task was cancelled, propagate
# cancellation.
raise er
try:
s2, addr = s.accept()
except:
@@ -147,7 +164,7 @@ class Server:
# Helper function to start a TCP stream server, running as a new task
# TODO could use an accept-callback on socket read activity instead of creating a task
async def start_server(cb, host, port, backlog=5):
import usocket as socket
import socket
# Create and bind server socket.
host = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
@@ -160,6 +177,16 @@ async def start_server(cb, host, port, backlog=5):
# Create and return server object and task.
srv = Server()
srv.task = core.create_task(srv._serve(s, cb))
try:
# Ensure that the _serve task has been scheduled so that it gets to
# handle cancellation.
await core.sleep_ms(0)
except core.CancelledError as er:
# If the parent task is cancelled during this first sleep, then
# we will leak the task and it will sit waiting for the socket, so
# cancel it.
srv.task.cancel()
raise er
return srv

View File

@@ -1,4 +1,4 @@
# MicroPython uasyncio module
# MicroPython asyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
# This file contains the core TaskQueue based on a pairing heap, and the core Task class.

View File

@@ -0,0 +1,8 @@
# This module just allows `import uasyncio` to work. It lazy-loads from
# `asyncio` without duplicating its globals dict.
def __getattr__(attr):
import asyncio
return getattr(asyncio, attr)

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import sys
try:
import ffi
except ImportError:
@@ -6,6 +7,7 @@ except ImportError:
_cache = {}
def open(name, maxver=10, extra=()):
if not ffi:
return None
@@ -13,16 +15,18 @@ def open(name, maxver=10, extra=()):
return _cache[name]
except KeyError:
pass
def libs():
if sys.platform == "linux":
yield '%s.so' % name
yield "%s.so" % name
for i in range(maxver, -1, -1):
yield '%s.so.%u' % (name, i)
yield "%s.so.%u" % (name, i)
else:
for ext in ('dylib', 'dll'):
yield '%s.%s' % (name, ext)
for ext in ("dylib", "dll"):
yield "%s.%s" % (name, ext)
for n in extra:
yield n
err = None
for n in libs():
try:
@@ -33,9 +37,11 @@ def open(name, maxver=10, extra=()):
err = e
raise err
def libc():
return open("libc", 6)
# Find out bitness of the platform, even if long ints are not supported
# TODO: All bitness differences should be removed from micropython-lib, and
# this snippet too.

View File

@@ -1,76 +1,79 @@
from utime import *
from ucollections import namedtuple
import ustruct
import uctypes
import ffi
import ffilib
import array
from micropython import const
libc = ffilib.libc()
_TS_YEAR = const(0)
_TS_MON = const(1)
_TS_MDAY = const(2)
_TS_HOUR = const(3)
_TS_MIN = const(4)
_TS_SEC = const(5)
_TS_WDAY = const(6)
_TS_YDAY = const(7)
_TS_ISDST = const(8)
# struct tm *gmtime(const time_t *timep);
# struct tm *localtime(const time_t *timep);
# size_t strftime(char *s, size_t max, const char *format,
# const struct tm *tm);
gmtime_ = libc.func("P", "gmtime", "P")
localtime_ = libc.func("P", "localtime", "P")
strftime_ = libc.func("i", "strftime", "sisP")
mktime_ = libc.func("i", "mktime", "P")
_struct_time = namedtuple("struct_time",
["tm_year", "tm_mon", "tm_mday", "tm_hour", "tm_min", "tm_sec", "tm_wday", "tm_yday", "tm_isdst"])
def _tuple_to_c_tm(t):
return ustruct.pack("@iiiiiiiii", t[5], t[4], t[3], t[2], t[1] - 1, t[0] - 1900, (t[6] + 1) % 7, t[7] - 1, t[8])
_WDAY = const(("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"))
_MDAY = const(
(
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
)
)
def _c_tm_to_tuple(tm):
t = ustruct.unpack("@iiiiiiiii", tm)
return _struct_time(t[5] + 1900, t[4] + 1, t[3], t[2], t[1], t[0], (t[6] - 1) % 7, t[7] + 1, t[8])
def strftime(datefmt, ts):
from io import StringIO
def struct_time(tm):
return _struct_time(*tm)
def strftime(format, t=None):
if t is None:
t = localtime()
buf = bytearray(32)
l = strftime_(buf, 32, format, _tuple_to_c_tm(t))
return str(buf[:l], "utf-8")
def localtime(t=None):
if t is None:
t = time()
t = int(t)
a = ustruct.pack('l', t)
tm_p = localtime_(a)
return _c_tm_to_tuple(uctypes.bytearray_at(tm_p, 36))
def gmtime(t=None):
if t is None:
t = time()
t = int(t)
a = ustruct.pack('l', t)
tm_p = gmtime_(a)
return _c_tm_to_tuple(uctypes.bytearray_at(tm_p, 36))
def mktime(tt):
return mktime_(_tuple_to_c_tm(tt))
def perf_counter():
return time()
def process_time():
return clock()
daylight = 0
timezone = 0
fmtsp = False
ftime = StringIO()
for k in datefmt:
if fmtsp:
if k == "a":
ftime.write(_WDAY[ts[_TS_WDAY]][0:3])
elif k == "A":
ftime.write(_WDAY[ts[_TS_WDAY]])
elif k == "b":
ftime.write(_MDAY[ts[_TS_MON] - 1][0:3])
elif k == "B":
ftime.write(_MDAY[ts[_TS_MON] - 1])
elif k == "d":
ftime.write("%02d" % ts[_TS_MDAY])
elif k == "H":
ftime.write("%02d" % ts[_TS_HOUR])
elif k == "I":
ftime.write("%02d" % (ts[_TS_HOUR] % 12))
elif k == "j":
ftime.write("%03d" % ts[_TS_YDAY])
elif k == "m":
ftime.write("%02d" % ts[_TS_MON])
elif k == "M":
ftime.write("%02d" % ts[_TS_MIN])
elif k == "P":
ftime.write("AM" if ts[_TS_HOUR] < 12 else "PM")
elif k == "S":
ftime.write("%02d" % ts[_TS_SEC])
elif k == "w":
ftime.write(str(ts[_TS_WDAY]))
elif k == "y":
ftime.write("%02d" % (ts[_TS_YEAR] % 100))
elif k == "Y":
ftime.write(str(ts[_TS_YEAR]))
else:
ftime.write(k)
fmtsp = False
elif k == "%":
fmtsp = True
else:
ftime.write(k)
val = ftime.getvalue()
ftime.close()
return val

View File

@@ -300,9 +300,10 @@ class TestResult:
return self.errorsNum == 0 and self.failuresNum == 0
def printErrors(self):
print()
self.printErrorList(self.errors)
self.printErrorList(self.failures)
if self.errors or self.failures:
print()
self.printErrorList(self.errors)
self.printErrorList(self.failures)
def printErrorList(self, lst):
sep = "----------------------------------------------------------------------"

14
libs/refresh.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
# this script downloads updated versions of all the MicroPython libraries
cd micropython
curl https://codeload.github.com/micropython/micropython/tar.gz/master | tar -xz --strip=2 micropython-master/extmod/asyncio
curl https://raw.githubusercontent.com/micropython/micropython-lib/master/python-stdlib/datetime/datetime.py > datetime.py
curl https://raw.githubusercontent.com/micropython/micropython-lib/d8e163bb5f3ef45e71e145c27bc4f207beaad70f/unix-ffi/ffilib/ffilib.py > ffilib.py
curl https://raw.githubusercontent.com/micropython/micropython-lib/master/python-stdlib/hmac/hmac.py > hmac.py
curl https://raw.githubusercontent.com/micropython/micropython-lib/master/python-stdlib/time/time.py > time.py
curl https://raw.githubusercontent.com/micropython/micropython-lib/master/python-stdlib/unittest/unittest/__init__.py > unittest.py
cd ../common
curl https://raw.githubusercontent.com/pfalcon/utemplate/master/README.md > utemplate/README.md
curl https://codeload.github.com/pfalcon/utemplate/tar.gz/master | tar -xz --strip=1 utemplate-master/utemplate
cd ..

View File

@@ -13,6 +13,7 @@ classifiers = [
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
requires-python = ">=3.8"
[project.readme]
file = "README.md"
@@ -30,26 +31,16 @@ docs = [
[tool.setuptools]
zip-safe = false
include-package-data = true
py-modules = [
"microdot",
"microdot_asyncio",
"microdot_utemplate",
"microdot_jinja",
"microdot_session",
"microdot_cors",
"microdot_websocket",
"microdot_websocket_alt",
"microdot_asyncio_websocket",
"microdot_test_client",
"microdot_asyncio_test_client",
"microdot_wsgi",
"microdot_asgi",
"microdot_asgi_websocket",
]
[tool.setuptools.package-dir]
"" = "src"
[tool.setuptools.packages.find]
where = [
"src",
]
namespaces = false
[build-system]
requires = [
"setuptools>=61.2",

2
src/microdot/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from microdot.microdot import Microdot, Request, Response, abort, redirect, \
send_file # noqa: F401

View File

@@ -1,10 +1,10 @@
import asyncio
import os
import signal
from microdot_asyncio import * # noqa: F401, F403
from microdot_asyncio import Microdot as BaseMicrodot
from microdot_asyncio import Request
from microdot import NoCaseDict
from microdot import * # noqa: F401, F403
from microdot.microdot import Microdot as BaseMicrodot, Request, Response, \
NoCaseDict, abort
from microdot.websocket import WebSocket as BaseWebSocket, websocket_wrapper
class _BodyStream: # pragma: no cover
@@ -21,7 +21,7 @@ class _BodyStream: # pragma: no cover
async def read(self, n=-1):
while self.more and len(self.data) < n:
self.read_more()
await self.read_more()
if len(self.data) < n:
data = self.data
self.data = b''
@@ -32,14 +32,14 @@ class _BodyStream: # pragma: no cover
return data
async def readline(self):
return self.readuntil()
return await self.readuntil()
async def readexactly(self, n):
return self.read(n)
return await self.read(n)
async def readuntil(self, separator=b'\n'):
if self.more and separator not in self.data:
self.read_more()
await self.read_more()
data, self.data = self.data.split(separator, 1)
return data
@@ -113,11 +113,12 @@ class Microdot(BaseMicrodot):
while True:
event = await receive()
if event['type'] == 'http.disconnect': # pragma: no branch
if event is None or \
event['type'] == 'http.disconnect': # pragma: no cover
cancelled = True
break
asyncio.ensure_future(cancel_monitor())
monitor_task = asyncio.ensure_future(cancel_monitor())
body_iter = res.body_iter().__aiter__()
res_body = b''
@@ -133,6 +134,10 @@ class Microdot(BaseMicrodot):
await send({'type': 'http.response.body',
'body': res_body,
'more_body': False})
if hasattr(body_iter, 'aclose'): # pragma: no branch
await body_iter.aclose()
cancelled = True
await monitor_task
async def __call__(self, scope, receive, send):
return await self.asgi_app(scope, receive, send)
@@ -152,3 +157,79 @@ class Microdot(BaseMicrodot):
"""
self.embedded_server = True
super().run(host=host, port=port, debug=debug, **options)
class WebSocket(BaseWebSocket): # pragma: no cover
async def handshake(self):
connect = await self.request.sock[0]()
if connect['type'] != 'websocket.connect':
abort(400)
await self.request.sock[1]({'type': 'websocket.accept'})
async def receive(self):
message = await self.request.sock[0]()
if message['type'] == 'websocket.disconnect':
raise OSError(32, 'Websocket connection closed')
elif message['type'] != 'websocket.receive':
raise OSError(32, 'Websocket message type not supported')
return message.get('bytes', message.get('text'))
async def send(self, data):
if isinstance(data, str):
await self.request.sock[1](
{'type': 'websocket.send', 'text': data})
else:
await self.request.sock[1](
{'type': 'websocket.send', 'bytes': data})
async def close(self):
if not self.closed:
self.closed = True
try:
await self.request.sock[1]({'type': 'websocket.close'})
except: # noqa E722
pass
async def websocket_upgrade(request): # pragma: no cover
"""Upgrade a request handler to a websocket connection.
This function can be called directly inside a route function to process a
WebSocket upgrade handshake, for example after the user's credentials are
verified. The function returns the websocket object::
@app.route('/echo')
async def echo(request):
if not (await authenticate_user(request)):
abort(401)
ws = await websocket_upgrade(request)
while True:
message = await ws.receive()
await ws.send(message)
"""
ws = WebSocket(request) if not request.app.embedded_server else \
BaseWebSocket(request)
await ws.handshake()
@request.after_request
async def after_request(request, response):
return Response.already_handled
return ws
def with_websocket(f): # pragma: no cover
"""Decorator to make a route a WebSocket endpoint.
This decorator is used to define a route that accepts websocket
connections. The route then receives a websocket object as a second
argument that it can use to send and receive messages::
@app.route('/echo')
@with_websocket
async def echo(request, ws):
while True:
message = await ws.receive()
await ws.send(message)
"""
return websocket_wrapper(f, websocket_upgrade)

58
src/microdot/jinja.py Normal file
View File

@@ -0,0 +1,58 @@
from jinja2 import Environment, FileSystemLoader, select_autoescape
_jinja_env = None
def init_templates(template_dir='templates', enable_async=False, **kwargs):
"""Initialize the templating subsystem.
:param template_dir: the directory where templates are stored. This
argument is optional. The default is to load templates
from a *templates* subdirectory.
:param enable_async: set to ``True`` to enable the async rendering engine
in Jinja, and the ``render_async()`` and
``generate_async()`` methods.
:param kwargs: any additional options to be passed to Jinja's
``Environment`` class.
"""
global _jinja_env
_jinja_env = Environment(
loader=FileSystemLoader(template_dir),
autoescape=select_autoescape(),
enable_async=enable_async,
**kwargs
)
class Template:
"""A template object.
:param template: The filename of the template to render, relative to the
configured template directory.
"""
def __init__(self, template):
if _jinja_env is None: # pragma: no cover
init_templates()
#: The name of the template
self.name = template
self.template = _jinja_env.get_template(template)
def generate(self, *args, **kwargs):
"""Return a generator that renders the template in chunks, with the
given arguments."""
return self.template.generate(*args, **kwargs)
def render(self, *args, **kwargs):
"""Render the template with the given arguments and return it as a
string."""
return self.template.render(*args, **kwargs)
def generate_async(self, *args, **kwargs):
"""Return an asynchronous generator that renders the template in
chunks, using the given arguments."""
return self.template.generate_async(*args, **kwargs)
async def render_async(self, *args, **kwargs):
"""Render the template with the given arguments asynchronously and
return it as a string."""
return await self.template.render_async(*args, **kwargs)

View File

@@ -3,9 +3,43 @@ microdot
--------
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.
servers for MicroPython and standard Python.
"""
import asyncio
import io
import json
import re
import time
try:
from inspect import iscoroutinefunction, iscoroutine
async def invoke_handler(handler, *args, **kwargs):
"""Invoke a handler and return the result.
This method runs sync handlers in a thread pool executor.
"""
if iscoroutinefunction(handler):
ret = await handler(*args, **kwargs)
else:
ret = await asyncio.get_running_loop().run_in_executor(
None, handler, *args, **kwargs)
return ret
except ImportError: # pragma: no cover
def iscoroutine(coro):
return hasattr(coro, 'send') and hasattr(coro, 'throw')
async def invoke_handler(handler, *args, **kwargs):
"""Invoke a handler and return the result.
This method runs sync handlers in the asyncio thread, which can
potentially cause blocking and performance issues.
"""
ret = handler(*args, **kwargs)
if iscoroutine(ret):
ret = await ret
return ret
try:
from sys import print_exception
except ImportError: # pragma: no cover
@@ -13,45 +47,6 @@ except ImportError: # pragma: no cover
def print_exception(exc):
traceback.print_exc()
try:
import uerrno as errno
except ImportError:
import errno
concurrency_mode = 'threaded'
try: # pragma: no cover
import threading
def create_thread(f, *args, **kwargs):
# use the threading module
threading.Thread(target=f, args=args, kwargs=kwargs).start()
except ImportError: # pragma: no cover
def create_thread(f, *args, **kwargs):
# no threads available, call function synchronously
f(*args, **kwargs)
concurrency_mode = 'sync'
try:
import ujson as json
except ImportError:
import json
try:
import ure as re
except ImportError:
import re
socket_timeout_error = OSError
try:
import usocket as socket
except ImportError:
try:
import socket
socket_timeout_error = socket.timeout
except ImportError: # pragma: no cover
socket = None
MUTED_SOCKET_ERRORS = [
32, # Broken pipe
@@ -275,7 +270,31 @@ class MultiDict(dict):
return values
class Request():
class AsyncBytesIO:
"""An async wrapper for BytesIO."""
def __init__(self, data):
self.stream = io.BytesIO(data)
async def read(self, n=-1):
return self.stream.read(n)
async def readline(self): # pragma: no cover
return self.stream.readline()
async def readexactly(self, n): # pragma: no cover
return self.stream.read(n)
async def readuntil(self, separator=b'\n'): # pragma: no cover
return self.stream.readuntil(separator=separator)
async def awrite(self, data): # pragma: no cover
return self.stream.write(data)
async def aclose(self): # pragma: no cover
pass
class 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
@@ -306,12 +325,6 @@ class Request():
#: Request.max_readline = 16 * 1024 # 16KB lines allowed
max_readline = 2 * 1024
#: Specify a suggested read timeout to use when reading the request. Set to
#: 0 to disable the use of a timeout. This timeout should be considered a
#: suggestion only, as some platforms may not support it. The default is
#: 1 second.
socket_read_timeout = 1
class G:
pass
@@ -361,48 +374,62 @@ class Request():
self._body = body
self.body_used = False
self._stream = stream
self.stream_used = False
self.sock = sock
self._json = None
self._form = None
self.after_request_handlers = []
@staticmethod
def create(app, client_stream, client_addr, client_sock=None):
async def create(app, client_reader, client_writer, client_addr):
"""Create a request object.
:param app: The Microdot application instance.
:param client_stream: An input stream from where the request data can
:param client_reader: An input stream from where the request data can
be read.
:param client_writer: An output stream where the response data can be
written.
:param client_addr: The address of the client, as a tuple.
:param client_sock: The low-level socket associated with the request.
This method returns a newly created ``Request`` object.
This method is a coroutine. It returns a newly created ``Request``
object.
"""
# request line
line = Request._safe_readline(client_stream).strip().decode()
if not line:
line = (await Request._safe_readline(client_reader)).strip().decode()
if not line: # pragma: no cover
return None
method, url, http_version = line.split()
http_version = http_version.split('/', 1)[1]
# headers
headers = NoCaseDict()
content_length = 0
while True:
line = Request._safe_readline(client_stream).strip().decode()
line = (await Request._safe_readline(
client_reader)).strip().decode()
if line == '':
break
header, value = line.split(':', 1)
value = value.strip()
headers[header] = value
if header.lower() == 'content-length':
content_length = int(value)
# body
body = b''
if content_length and content_length <= Request.max_body_length:
body = await client_reader.readexactly(content_length)
stream = None
else:
body = b''
stream = client_reader
return Request(app, client_addr, method, url, http_version, headers,
stream=client_stream, sock=client_sock)
body=body, stream=stream,
sock=(client_reader, client_writer))
def _parse_urlencoded(self, urlencoded):
data = MultiDict()
if len(urlencoded) > 0:
if len(urlencoded) > 0: # pragma: no branch
if isinstance(urlencoded, str):
for kv in [pair.split('=', 1)
for pair in urlencoded.split('&') if pair]:
@@ -418,27 +445,13 @@ 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:
self._body = b''
if self.content_length and \
self.content_length <= Request.max_body_length:
while len(self._body) < self.content_length:
data = self._stream.read(
self.content_length - len(self._body))
if len(data) == 0: # pragma: no cover
raise EOFError()
self._body += data
self.body_used = True
return self._body
@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
"""The body of the request, as a bytes stream."""
if self._stream is None:
self._stream = AsyncBytesIO(self._body)
return self._stream
@property
@@ -494,21 +507,21 @@ class Request():
return f
@staticmethod
def _safe_readline(stream):
line = stream.readline(Request.max_readline + 1)
async def _safe_readline(stream):
line = (await stream.readline())
if len(line) > Request.max_readline:
raise ValueError('line too long')
return line
class Response():
class Response:
"""An HTTP response class.
:param body: The body of the response. If a dictionary or list is given,
a JSON formatter is used to generate the body. If a file-like
object or a generator is given, a streaming response is used.
If a string is given, it is encoded from UTF-8. Else, the
body should be a byte sequence.
object or an async generator is given, a streaming response is
used. If a string is given, it is encoded from UTF-8. Else,
the body should be a byte sequence.
:param status_code: The numeric HTTP status code of the response. The
default is 200.
:param headers: A dictionary of headers to include in the response.
@@ -526,6 +539,7 @@ class Response():
'png': 'image/png',
'txt': 'text/plain',
}
send_file_buffer_size = 1024
#: The content type to use for responses that do not explicitly define a
@@ -558,7 +572,8 @@ class Response():
self.is_head = False
def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
max_age=None, secure=False, http_only=False):
max_age=None, secure=False, http_only=False,
partitioned=False):
"""Add a cookie to the response.
:param cookie: The cookie's name.
@@ -570,6 +585,7 @@ class Response():
:param max_age: The cookie's ``Max-Age`` value.
:param secure: The cookie's ``secure`` flag.
:param http_only: The cookie's ``HttpOnly`` flag.
:param partitioned: Whether the cookie is partitioned.
"""
http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value)
if path:
@@ -580,19 +596,31 @@ class Response():
if isinstance(expires, str):
http_cookie += '; Expires=' + expires
else:
http_cookie += '; Expires=' + expires.strftime(
'%a, %d %b %Y %H:%M:%S GMT')
http_cookie += '; Expires=' + time.strftime(
'%a, %d %b %Y %H:%M:%S GMT', expires.timetuple())
if max_age:
http_cookie += '; Max-Age=' + str(max_age)
if secure:
http_cookie += '; Secure'
if http_only:
http_cookie += '; HttpOnly'
if partitioned:
http_cookie += '; Partitioned'
if 'Set-Cookie' in self.headers:
self.headers['Set-Cookie'].append(http_cookie)
else:
self.headers['Set-Cookie'] = [http_cookie]
def delete_cookie(self, cookie, **kwargs):
"""Delete a cookie.
:param cookie: The cookie's name.
:param kwargs: Any cookie opens and flags supported by
``set_cookie()`` except ``expires``.
"""
self.set_cookie(cookie, '', expires='Thu, 01 Jan 1970 00:00:01 GMT',
**kwargs)
def complete(self):
if isinstance(self.body, bytes) and \
'Content-Length' not in self.headers:
@@ -602,54 +630,101 @@ class Response():
if 'charset=' not in self.headers['Content-Type']:
self.headers['Content-Type'] += '; charset=UTF-8'
def write(self, stream):
async def write(self, stream):
self.complete()
# status code
reason = self.reason if self.reason is not None else \
('OK' if self.status_code == 200 else 'N/A')
stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code, reason=reason).encode())
try:
# status code
reason = self.reason if self.reason is not None else \
('OK' if self.status_code == 200 else 'N/A')
await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code, reason=reason).encode())
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
stream.write('{header}: {value}\r\n'.format(
header=header, value=value).encode())
stream.write(b'\r\n')
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
await stream.awrite('{header}: {value}\r\n'.format(
header=header, value=value).encode())
await stream.awrite(b'\r\n')
# body
if not self.is_head:
can_flush = hasattr(stream, 'flush')
try:
for body in self.body_iter():
# body
if not self.is_head:
iter = self.body_iter()
async for body in iter:
if isinstance(body, str): # pragma: no cover
body = body.encode()
stream.write(body)
if can_flush: # pragma: no cover
stream.flush()
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS:
pass
else:
raise
try:
await stream.awrite(body)
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS or \
exc.args[0] == 'Connection lost':
if hasattr(iter, 'aclose'):
await iter.aclose()
raise
if hasattr(iter, 'aclose'): # pragma: no branch
await iter.aclose()
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS or \
exc.args[0] == 'Connection lost':
pass
else:
raise
def body_iter(self):
if self.body:
if hasattr(self.body, 'read'):
while True:
buf = self.body.read(self.send_file_buffer_size)
if len(buf):
yield buf
if len(buf) < self.send_file_buffer_size:
break
if hasattr(self.body, 'close'): # pragma: no cover
self.body.close()
elif hasattr(self.body, '__next__'):
yield from self.body
else:
yield self.body
if hasattr(self.body, '__anext__'):
# response body is an async generator
return self.body
response = self
class iter:
ITER_UNKNOWN = 0
ITER_SYNC_GEN = 1
ITER_FILE_OBJ = 2
ITER_NO_BODY = -1
def __aiter__(self):
if response.body:
self.i = self.ITER_UNKNOWN # need to determine type
else:
self.i = self.ITER_NO_BODY
return self
async def __anext__(self):
if self.i == self.ITER_NO_BODY:
await self.aclose()
raise StopAsyncIteration
if self.i == self.ITER_UNKNOWN:
if hasattr(response.body, 'read'):
self.i = self.ITER_FILE_OBJ
elif hasattr(response.body, '__next__'):
self.i = self.ITER_SYNC_GEN
return next(response.body)
else:
self.i = self.ITER_NO_BODY
return response.body
elif self.i == self.ITER_SYNC_GEN:
try:
return next(response.body)
except StopIteration:
await self.aclose()
raise StopAsyncIteration
buf = response.body.read(response.send_file_buffer_size)
if iscoroutine(buf): # pragma: no cover
buf = await buf
if len(buf) < response.send_file_buffer_size:
self.i = self.ITER_NO_BODY
return buf
async def aclose(self):
if hasattr(response.body, 'close'):
result = response.body.close()
if iscoroutine(result): # pragma: no cover
await result
return iter()
@classmethod
def redirect(cls, location, status_code=302):
@@ -781,7 +856,7 @@ class HTTPException(Exception):
return 'HTTPException: {}'.format(self.status_code)
class Microdot():
class Microdot:
"""An HTTP application class.
This class implements an HTTP application instance and is heavily
@@ -1048,6 +1123,88 @@ class Microdot():
"""
raise HTTPException(status_code, reason)
async def start_server(self, host='0.0.0.0', port=5000, debug=False,
ssl=None):
"""Start the Microdot web server as a coroutine. This coroutine does
not normally return, as the server enters an endless listening loop.
The :func:`shutdown` function provides a method for terminating the
server gracefully.
:param host: The hostname or IP address of the network interface that
will be listening for requests. A value of ``'0.0.0.0'``
(the default) indicates that the server should listen for
requests on all the available interfaces, and a value of
``127.0.0.1`` indicates that the server should listen
for requests only on the internal networking interface of
the host.
:param port: The port number to listen for requests. The default is
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
:param ssl: An ``SSLContext`` instance or ``None`` if the server should
not use TLS. The default is ``None``.
This method is a coroutine.
Example::
import asyncio
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
async def index(request):
return 'Hello, world!'
async def main():
await app.start_server(debug=True)
asyncio.run(main())
"""
self.debug = debug
async def serve(reader, writer):
if not hasattr(writer, 'awrite'): # pragma: no cover
# CPython provides the awrite and aclose methods in 3.8+
async def awrite(self, data):
self.write(data)
await self.drain()
async def aclose(self):
self.close()
await self.wait_closed()
from types import MethodType
writer.awrite = MethodType(awrite, writer)
writer.aclose = MethodType(aclose, writer)
await self.handle_request(reader, writer)
if self.debug: # pragma: no cover
print('Starting async server on {host}:{port}...'.format(
host=host, port=port))
try:
self.server = await asyncio.start_server(serve, host, port,
ssl=ssl)
except TypeError: # pragma: no cover
self.server = await asyncio.start_server(serve, host, port)
while True:
try:
if hasattr(self.server, 'serve_forever'): # pragma: no cover
try:
await self.server.serve_forever()
except asyncio.CancelledError:
pass
await self.server.wait_closed()
break
except AttributeError: # pragma: no cover
# the task hasn't been initialized in the server object yet
# wait a bit and try again
await asyncio.sleep(0.1)
def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None):
"""Start the web server. This function does not normally return, as
the server enters an endless listening loop. The :func:`shutdown`
@@ -1069,45 +1226,18 @@ class Microdot():
Example::
from microdot import Microdot
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
def index(request):
async def index(request):
return 'Hello, world!'
app.run(debug=True)
"""
self.debug = debug
self.shutdown_requested = False
self.server = socket.socket()
ai = socket.getaddrinfo(host, port)
addr = ai[0][-1]
if self.debug: # pragma: no cover
print('Starting {mode} server on {host}:{port}...'.format(
mode=concurrency_mode, host=host, port=port))
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind(addr)
self.server.listen(5)
if ssl:
self.server = ssl.wrap_socket(self.server, server_side=True)
while not self.shutdown_requested:
try:
sock, addr = self.server.accept()
except OSError as exc: # pragma: no cover
if exc.errno == errno.ECONNABORTED:
break
else:
print_exception(exc)
except Exception as exc: # pragma: no cover
print_exception(exc)
else:
create_thread(self.handle_request, sock, addr)
asyncio.run(self.start_server(host=host, port=port, debug=debug,
ssl=ssl)) # pragma: no cover
def shutdown(self):
"""Request a server shutdown. The server will then exit its request
@@ -1122,7 +1252,7 @@ class Microdot():
request.app.shutdown()
return 'The server is shutting down...'
"""
self.shutdown_requested = True
self.server.close()
def find_route(self, req):
method = req.method.upper()
@@ -1151,51 +1281,35 @@ class Microdot():
allow.append('OPTIONS')
return {'Allow': ', '.join(allow)}
def handle_request(self, sock, addr):
if Request.socket_read_timeout and \
hasattr(sock, 'settimeout'): # pragma: no cover
sock.settimeout(Request.socket_read_timeout)
if not hasattr(sock, 'readline'): # pragma: no cover
stream = sock.makefile("rwb")
else:
stream = sock
async def handle_request(self, reader, writer):
req = None
res = None
try:
req = Request.create(self, stream, addr, sock)
res = self.dispatch_request(req)
except socket_timeout_error as exc: # pragma: no cover
if exc.errno and exc.errno != errno.ETIMEDOUT:
print_exception(exc) # not a timeout
req = await Request.create(self, reader, writer,
writer.get_extra_info('peername'))
except Exception as exc: # pragma: no cover
print_exception(exc)
res = await self.dispatch_request(req)
if res != Response.already_handled: # pragma: no branch
await res.write(writer)
try:
if res and res != Response.already_handled: # pragma: no branch
res.write(stream)
stream.close()
await writer.aclose()
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS:
pass
else:
print_exception(exc)
except Exception as exc: # pragma: no cover
print_exception(exc)
if stream != sock: # pragma: no cover
sock.close()
if self.shutdown_requested: # pragma: no cover
self.server.close()
raise
if self.debug and req: # pragma: no cover
print('{method} {path} {status_code}'.format(
method=req.method, path=req.path,
status_code=res.status_code))
def dispatch_request(self, req):
async def dispatch_request(self, req):
after_request_handled = False
if req:
if req.content_length > req.max_content_length:
if 413 in self.error_handlers:
res = self.error_handlers[413](req)
res = await invoke_handler(self.error_handlers[413], req)
else:
res = 'Payload too large', 413
else:
@@ -1204,11 +1318,12 @@ class Microdot():
res = None
if callable(f):
for handler in self.before_request_handlers:
res = handler(req)
res = await invoke_handler(handler, req)
if res:
break
if res is None:
res = f(req, **req.url_args)
res = await invoke_handler(
f, req, **req.url_args)
if isinstance(res, tuple):
body = res[0]
if isinstance(res[1], int):
@@ -1221,14 +1336,16 @@ class Microdot():
elif not isinstance(res, Response):
res = Response(res)
for handler in self.after_request_handlers:
res = handler(req, res) or res
res = await invoke_handler(
handler, req, res) or res
for handler in req.after_request_handlers:
res = handler(req, res) or res
res = await invoke_handler(
handler, req, res) or res
after_request_handled = True
elif isinstance(f, dict):
res = Response(headers=f)
elif f in self.error_handlers:
res = self.error_handlers[f](req)
res = await invoke_handler(self.error_handlers[f], req)
else:
res = 'Not found', f
except HTTPException as exc:
@@ -1249,32 +1366,35 @@ class Microdot():
break
if exc_class:
try:
res = self.error_handlers[exc_class](req, exc)
res = await invoke_handler(
self.error_handlers[exc_class], req, exc)
except Exception as exc2: # pragma: no cover
print_exception(exc2)
if res is None:
if 500 in self.error_handlers:
res = self.error_handlers[500](req)
res = await invoke_handler(
self.error_handlers[500], req)
else:
res = 'Internal server error', 500
else:
if 400 in self.error_handlers:
res = self.error_handlers[400](req)
res = await invoke_handler(self.error_handlers[400], req)
else:
res = 'Bad request', 400
if isinstance(res, tuple):
res = Response(*res)
elif not isinstance(res, Response):
res = Response(res)
if not after_request_handled:
for handler in self.after_error_request_handlers:
res = handler(req, res) or res
res = await invoke_handler(
handler, req, res) or res
res.is_head = (req and req.method == 'HEAD')
return res
abort = Microdot.abort
Response.already_handled = Response()
abort = Microdot.abort
redirect = Response.redirect
send_file = Response.send_file

148
src/microdot/session.py Normal file
View File

@@ -0,0 +1,148 @@
import jwt
from microdot.microdot import invoke_handler
secret_key = None
class SessionDict(dict):
"""A session dictionary.
The session dictionary is a standard Python dictionary that has been
extended with convenience ``save()`` and ``delete()`` methods.
"""
def __init__(self, request, session_dict):
super().__init__(session_dict)
self.request = request
def save(self):
"""Update the session cookie."""
self.request.app._session.update(self.request, self)
def delete(self):
"""Delete the session cookie."""
self.request.app._session.delete(self.request)
class Session:
"""
:param app: The application instance.
:param key: The secret key, as a string or bytes object.
"""
secret_key = None
def __init__(self, app=None, secret_key=None):
self.secret_key = secret_key
if app is not None:
self.initialize(app)
def initialize(self, app, secret_key=None):
if secret_key is not None:
self.secret_key = secret_key
app._session = self
def get(self, request):
"""Retrieve the user session.
:param request: The client request.
The return value is a session dictionary with the data stored in the
user's session, or ``{}`` if the session data is not available or
invalid.
"""
if not self.secret_key:
raise ValueError('The session secret key is not configured')
if hasattr(request.g, '_session'):
return request.g._session
session = request.cookies.get('session')
if session is None:
request.g._session = SessionDict(request, {})
return request.g._session
try:
session = jwt.decode(session, self.secret_key,
algorithms=['HS256'])
except jwt.exceptions.PyJWTError: # pragma: no cover
request.g._session = SessionDict(request, {})
else:
request.g._session = SessionDict(request, session)
return request.g._session
def update(self, request, session):
"""Update the user session.
:param request: The client request.
:param session: A dictionary with the update session data for the user.
Applications would normally not call this method directly, instead they
would use the :meth:`SessionDict.save` method on the session
dictionary, which calls this method. For example::
@app.route('/')
@with_session
def index(request, session):
session['foo'] = 'bar'
session.save()
return 'Hello, World!'
Calling this method adds a cookie with the updated session to the
request currently being processed.
"""
if not self.secret_key:
raise ValueError('The session secret key is not configured')
encoded_session = jwt.encode(session, self.secret_key,
algorithm='HS256')
@request.after_request
def _update_session(request, response):
response.set_cookie('session', encoded_session, http_only=True)
return response
def delete(self, request):
"""Remove the user session.
:param request: The client request.
Applications would normally not call this method directly, instead they
would use the :meth:`SessionDict.delete` method on the session
dictionary, which calls this method. For example::
@app.route('/')
@with_session
def index(request, session):
session.delete()
return 'Hello, World!'
Calling this method 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,
expires='Thu, 01 Jan 1970 00:00:01 GMT')
return 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 <microdot.session.update_session>` function.
"""
async def wrapper(request, *args, **kwargs):
return await invoke_handler(
f, request, request.app._session.get(request), *args, **kwargs)
for attr in ['__name__', '__doc__', '__module__', '__qualname__']:
try:
setattr(wrapper, attr, getattr(f, attr))
except AttributeError: # pragma: no cover
pass
return wrapper

95
src/microdot/sse.py Normal file
View File

@@ -0,0 +1,95 @@
import asyncio
import json
class SSE:
def __init__(self):
self.event = asyncio.Event()
self.queue = []
async def send(self, data, event=None):
if isinstance(data, (dict, list)):
data = json.dumps(data)
elif not isinstance(data, str):
data = str(data)
data = f'data: {data}\n\n'
if event:
data = f'event: {event}\n{data}'
self.queue.append(data)
self.event.set()
def sse_response(request, event_function, *args, **kwargs):
"""Return a response object that initiates an event stream.
:param request: the request object.
:param event_function: an asynchronous function that will send events to
the client. The function is invoked with ``request``
and an ``sse`` object. The function should use
``sse.send()`` to send events to the client.
:param args: additional positional arguments to be passed to the response.
:param kwargs: additional keyword arguments to be passed to the response.
Example::
@app.route('/events')
async def events_route(request):
async def events(request, sse):
# send an unnamed event with string data
await sse.send('hello')
# send an unnamed event with JSON data
await sse.send({'foo': 'bar'})
# send a named event
await sse.send('hello', event='greeting')
return sse_response(request, events)
"""
sse = SSE()
async def sse_task_wrapper():
await event_function(request, sse, *args, **kwargs)
sse.event.set()
task = asyncio.create_task(sse_task_wrapper())
class sse_loop:
def __aiter__(self):
return self
async def __anext__(self):
event = None
while sse.queue or not task.done():
try:
event = sse.queue.pop(0)
break
except IndexError:
await sse.event.wait()
sse.event.clear()
if event is None:
raise StopAsyncIteration
return event
async def aclose(self):
task.cancel()
return sse_loop(), 200, {'Content-Type': 'text/event-stream'}
def with_sse(f):
"""Decorator to make a route a Server-Sent Events endpoint.
This decorator is used to define a route that accepts SSE connections. The
route then receives a sse object as a second argument that it can use to
send events to the client::
@app.route('/events')
@with_sse
async def events(request, sse):
for i in range(10):
await asyncio.sleep(1)
await sse.send(f'{i}')
"""
async def sse_handler(request, *args, **kwargs):
return sse_response(request, f, *args, **kwargs)
return sse_handler

View File

@@ -1,11 +1,13 @@
from io import BytesIO
import json
from microdot import Request, Response, NoCaseDict
from microdot.microdot import Request, Response, AsyncBytesIO
try:
from microdot_websocket import WebSocket
from microdot.websocket import WebSocket
except: # pragma: no cover # noqa: E722
WebSocket = None
__all__ = ['TestClient', 'TestResponse']
class TestResponse:
"""A response object issued by the Microdot test client."""
@@ -32,12 +34,15 @@ class TestResponse:
self.reason = res.reason
self.headers = res.headers
def _initialize_body(self, res):
async def _initialize_body(self, res):
self.body = b''
for body in res.body_iter():
iter = res.body_iter()
async for body in iter: # pragma: no branch
if isinstance(body, str):
body = body.encode()
self.body += body
if hasattr(iter, 'aclose'): # pragma: no branch
await iter.aclose()
def _process_text_body(self):
try:
@@ -52,13 +57,13 @@ class TestResponse:
self.json = json.loads(self.text)
@classmethod
def create(cls, res):
async def create(cls, res):
test_res = cls()
test_res._initialize_response(res)
test_res._initialize_body(res)
test_res._process_text_body()
test_res._process_json_body()
test_res.is_head = res.is_head
if not res.is_head:
await test_res._initialize_body(res)
test_res._process_text_body()
test_res._process_json_body()
return test_res
@@ -72,17 +77,17 @@ class TestClient:
The following example shows how to create a test client for an application
and send a test request::
from microdot import Microdot
from microdot_asyncio import Microdot
app = Microdot()
@app.get('/')
def index():
async def index():
return 'Hello, World!'
def test_hello_world(self):
async def test_hello_world(self):
client = TestClient(app)
res = client.get('/')
res = await client.get('/')
assert res.status_code == 200
assert res.text == 'Hello, World!'
"""
@@ -150,23 +155,30 @@ class TestClient:
else:
self.cookies[cookie_name] = cookie_options[0]
def request(self, method, path, headers=None, body=None, sock=None):
headers = NoCaseDict(headers or {})
async def request(self, method, path, headers=None, body=None, sock=None):
headers = headers or {}
body, headers = self._process_body(body, headers)
cookies, headers = self._process_cookies(headers)
request_bytes = self._render_request(method, path, headers, body)
if sock:
reader = sock[0]
reader.buffer = request_bytes
writer = sock[1]
else:
reader = AsyncBytesIO(request_bytes)
writer = AsyncBytesIO(b'')
req = Request.create(self.app, BytesIO(request_bytes),
('127.0.0.1', 1234), client_sock=sock)
res = self.app.dispatch_request(req)
req = await Request.create(self.app, reader, writer,
('127.0.0.1', 1234))
res = await self.app.dispatch_request(req)
if res == Response.already_handled:
return None
res.complete()
self._update_cookies(res)
return TestResponse.create(res)
return await TestResponse.create(res)
def get(self, path, headers=None):
async def get(self, path, headers=None):
"""Send a GET request to the application.
:param path: The request URL.
@@ -175,9 +187,9 @@ class TestClient:
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return self.request('GET', path, headers=headers)
return await self.request('GET', path, headers=headers)
def post(self, path, headers=None, body=None):
async def post(self, path, headers=None, body=None):
"""Send a POST request to the application.
:param path: The request URL.
@@ -189,9 +201,9 @@ class TestClient:
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return self.request('POST', path, headers=headers, body=body)
return await self.request('POST', path, headers=headers, body=body)
def put(self, path, headers=None, body=None):
async def put(self, path, headers=None, body=None):
"""Send a PUT request to the application.
:param path: The request URL.
@@ -203,9 +215,9 @@ class TestClient:
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return self.request('PUT', path, headers=headers, body=body)
return await self.request('PUT', path, headers=headers, body=body)
def patch(self, path, headers=None, body=None):
async def patch(self, path, headers=None, body=None):
"""Send a PATCH request to the application.
:param path: The request URL.
@@ -217,9 +229,9 @@ class TestClient:
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return self.request('PATCH', path, headers=headers, body=body)
return await self.request('PATCH', path, headers=headers, body=body)
def delete(self, path, headers=None):
async def delete(self, path, headers=None):
"""Send a DELETE request to the application.
:param path: The request URL.
@@ -228,9 +240,9 @@ class TestClient:
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return self.request('DELETE', path, headers=headers)
return await self.request('DELETE', path, headers=headers)
def websocket(self, path, client, headers=None):
async def websocket(self, path, client, headers=None):
"""Send a websocket connection request to the application.
:param path: The request URL.
@@ -245,27 +257,39 @@ class TestClient:
self.closed = False
self.buffer = b''
def _next(self, data=None):
async def _next(self, data=None):
try:
data = gen.send(data)
except StopIteration:
if self.closed: # pragma: no cover
return
self.closed = True
raise OSError(32, 'Websocket connection closed')
data = (await gen.asend(data)) if hasattr(gen, 'asend') \
else gen.send(data)
except (StopIteration, StopAsyncIteration):
if not self.closed:
self.closed = True
raise OSError(32, 'Websocket connection closed')
return # pragma: no cover
opcode = WebSocket.TEXT if isinstance(data, str) \
else WebSocket.BINARY
return WebSocket._encode_websocket_frame(opcode, data)
def recv(self, n):
self.started = True
async def read(self, n):
if not self.buffer:
self.buffer = self._next()
self.started = True
self.buffer = await self._next()
data = self.buffer[:n]
self.buffer = self.buffer[n:]
return data
def send(self, data):
async def readexactly(self, n): # pragma: no cover
return await self.read(n)
async def readline(self):
line = b''
while True:
line += await self.read(1)
if line[-1] in [b'\n', 10]:
break
return line
async def awrite(self, data):
if self.started:
h = WebSocket._parse_frame_header(data[0:2])
if h[3] < 0:
@@ -274,7 +298,7 @@ class TestClient:
data = data[2:]
if h[1] == WebSocket.TEXT:
data = data.decode()
self.buffer = self._next(data)
self.buffer = await self._next(data)
ws_headers = {
'Upgrade': 'websocket',
@@ -283,5 +307,6 @@ class TestClient:
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
}
ws_headers.update(headers or {})
return self.request('GET', path, headers=ws_headers,
sock=FakeWebSocket())
sock = FakeWebSocket()
return await self.request('GET', path, headers=ws_headers,
sock=(sock, sock))

68
src/microdot/utemplate.py Normal file
View File

@@ -0,0 +1,68 @@
from utemplate import recompile
_loader = None
def init_templates(template_dir='templates', loader_class=recompile.Loader):
"""Initialize the templating subsystem.
:param template_dir: the directory where templates are stored. This
argument is optional. The default is to load templates
from a *templates* subdirectory.
:param loader_class: the ``utemplate.Loader`` class to use when loading
templates. This argument is optional. The default is
the ``recompile.Loader`` class, which automatically
recompiles templates when they change.
"""
global _loader
_loader = loader_class(None, template_dir)
class Template:
"""A template object.
:param template: The filename of the template to render, relative to the
configured template directory.
"""
def __init__(self, template):
if _loader is None: # pragma: no cover
init_templates()
#: The name of the template
self.name = template
self.template = _loader.load(template)
def generate(self, *args, **kwargs):
"""Return a generator that renders the template in chunks, with the
given arguments."""
return self.template(*args, **kwargs)
def render(self, *args, **kwargs):
"""Render the template with the given arguments and return it as a
string."""
return ''.join(self.generate(*args, **kwargs))
def generate_async(self, *args, **kwargs):
"""Return an asynchronous generator that renders the template in
chunks, using the given arguments."""
class sync_to_async_iter():
def __init__(self, iter):
self.iter = iter
def __aiter__(self):
return self
async def __anext__(self):
try:
return next(self.iter)
except StopIteration:
raise StopAsyncIteration
return sync_to_async_iter(self.generate(*args, **kwargs))
async def render_async(self, *args, **kwargs):
"""Render the template with the given arguments asynchronously and
return it as a string."""
response = ''
async for chunk in self.generate_async(*args, **kwargs):
response += chunk
return response

View File

@@ -1,6 +1,7 @@
import binascii
import hashlib
from microdot import Response
from microdot.microdot import MUTED_SOCKET_ERRORS
class WebSocket:
@@ -15,33 +16,34 @@ class WebSocket:
self.request = request
self.closed = False
def handshake(self):
async def handshake(self):
response = self._handshake_response()
self.request.sock.send(b'HTTP/1.1 101 Switching Protocols\r\n')
self.request.sock.send(b'Upgrade: websocket\r\n')
self.request.sock.send(b'Connection: Upgrade\r\n')
self.request.sock.send(
await self.request.sock[1].awrite(
b'HTTP/1.1 101 Switching Protocols\r\n')
await self.request.sock[1].awrite(b'Upgrade: websocket\r\n')
await self.request.sock[1].awrite(b'Connection: Upgrade\r\n')
await self.request.sock[1].awrite(
b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
def receive(self):
async def receive(self):
while True:
opcode, payload = self._read_frame()
opcode, payload = await self._read_frame()
send_opcode, data = self._process_websocket_frame(opcode, payload)
if send_opcode: # pragma: no cover
self.send(data, send_opcode)
await self.send(data, send_opcode)
elif data: # pragma: no branch
return data
def send(self, data, opcode=None):
async def send(self, data, opcode=None):
frame = self._encode_websocket_frame(
opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
data)
self.request.sock.send(frame)
await self.request.sock[1].awrite(frame)
def close(self):
async def close(self):
if not self.closed: # pragma: no cover
self.closed = True
self.send(b'', self.CLOSE)
await self.send(b'', self.CLOSE)
def _handshake_response(self):
connection = False
@@ -109,23 +111,26 @@ class WebSocket:
frame.extend(payload)
return frame
def _read_frame(self):
header = self.request.sock.recv(2)
async def _read_frame(self):
header = await self.request.sock[0].read(2)
if len(header) != 2: # pragma: no cover
raise OSError(32, 'Websocket connection closed')
fin, opcode, has_mask, length = self._parse_frame_header(header)
if length < 0:
length = self.request.sock.recv(-length)
if length == -2:
length = await self.request.sock[0].read(2)
length = int.from_bytes(length, 'big')
elif length == -8:
length = await self.request.sock[0].read(8)
length = int.from_bytes(length, 'big')
if has_mask: # pragma: no cover
mask = self.request.sock.recv(4)
payload = self.request.sock.recv(length)
mask = await self.request.sock[0].read(4)
payload = await self.request.sock[0].read(length)
if has_mask: # pragma: no cover
payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
return opcode, payload
def websocket_upgrade(request):
async def websocket_upgrade(request):
"""Upgrade a request handler to a websocket connection.
This function can be called directly inside a route function to process a
@@ -133,24 +138,37 @@ def websocket_upgrade(request):
verified. The function returns the websocket object::
@app.route('/echo')
def echo(request):
async def echo(request):
if not authenticate_user(request):
abort(401)
ws = websocket_upgrade(request)
ws = await websocket_upgrade(request)
while True:
message = ws.receive()
ws.send(message)
message = await ws.receive()
await ws.send(message)
"""
ws = WebSocket(request)
ws.handshake()
await ws.handshake()
@request.after_request
def after_request(request, response):
async def after_request(request, response):
return Response.already_handled
return ws
def websocket_wrapper(f, upgrade_function):
async def wrapper(request, *args, **kwargs):
ws = await upgrade_function(request)
try:
await f(request, ws, *args, **kwargs)
await ws.close() # pragma: no cover
except OSError as exc:
if exc.errno not in MUTED_SOCKET_ERRORS: # pragma: no cover
raise
return ''
return wrapper
def with_websocket(f):
"""Decorator to make a route a WebSocket endpoint.
@@ -160,18 +178,9 @@ def with_websocket(f):
@app.route('/echo')
@with_websocket
def echo(request, ws):
async def echo(request, ws):
while True:
message = ws.receive()
ws.send(message)
message = await ws.receive()
await ws.send(message)
"""
def wrapper(request, *args, **kwargs):
ws = websocket_upgrade(request)
try:
f(request, ws, *args, **kwargs)
ws.close() # pragma: no cover
except OSError as exc:
if exc.errno not in [32, 54, 104]: # pragma: no cover
raise
return ''
return wrapper
return websocket_wrapper(f, websocket_upgrade)

154
src/microdot/wsgi.py Normal file
View File

@@ -0,0 +1,154 @@
import asyncio
import os
import signal
from microdot import * # noqa: F401, F403
from microdot.microdot import Microdot as BaseMicrodot, Request, NoCaseDict, \
MUTED_SOCKET_ERRORS
from microdot.websocket import WebSocket, websocket_upgrade, \
with_websocket # noqa: F401
class Microdot(BaseMicrodot):
def __init__(self):
super().__init__()
self.loop = asyncio.new_event_loop()
self.embedded_server = False
def wsgi_app(self, environ, start_response):
"""A WSGI application callable."""
path = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '')
if 'QUERY_STRING' in environ and environ['QUERY_STRING']:
path += '?' + environ['QUERY_STRING']
headers = NoCaseDict()
content_length = 0
for k, value in environ.items():
if k.startswith('HTTP_'):
key = '-'.join([p.title() for p in k[5:].split('_')])
headers[key] = value
elif k == 'CONTENT_TYPE':
headers['Content-Type'] = value
elif k == 'CONTENT_LENGTH':
headers['Content-Length'] = value
content_length = int(value)
class sync_to_async_body_stream(): # pragma: no cover
def __init__(self, wsgi_input=None):
self.wsgi_input = wsgi_input
async def read(self, n=-1):
return self.wsgi_input.read(n)
async def readline(self):
return self.wsgi_input.readline()
async def readexactly(self, n):
return self.wsgi_input.read(n)
wsgi_input = environ.get('wsgi.input')
if content_length and content_length <= Request.max_body_length:
# the request came with a body that is within the allowed size
body = wsgi_input.read(content_length)
stream = None
sock = (None, None)
else:
body = b''
if content_length:
# the request came with a body that is too large to fit in
# memory, so we stream it
stream = sync_to_async_body_stream(wsgi_input)
sock = (None, None)
else:
# the request did not declare a body size, so we connect the
# raw socket if available
stream = None
if 'gunicorn.socket' in environ: # pragma: no cover
reader, writer = self.loop.run_until_complete(
asyncio.open_connection(
sock=environ['gunicorn.socket'].dup()))
if not hasattr(writer, 'awrite'): # pragma: no cover
async def awrite(self, data):
self.write(data)
await self.drain()
async def aclose(self):
self.close()
await self.wait_closed()
from types import MethodType
writer.awrite = MethodType(awrite, writer)
writer.aclose = MethodType(aclose, writer)
sock = (reader, writer)
else:
sock = (None, None)
req = Request(
self,
(environ['REMOTE_ADDR'], int(environ.get('REMOTE_PORT', '0'))),
environ['REQUEST_METHOD'],
path,
environ['SERVER_PROTOCOL'],
headers,
body=body,
stream=stream,
sock=sock)
req.environ = environ
res = self.loop.run_until_complete(self.dispatch_request(req))
res.complete()
if sock[1]: # pragma: no cover
try:
self.loop.run_until_complete(sock[1].aclose())
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS:
pass
else:
raise
reason = res.reason or ('OK' if res.status_code == 200 else 'N/A')
header_list = []
for name, value in res.headers.items():
if not isinstance(value, list):
header_list.append((name, value))
else:
for v in value:
header_list.append((name, v))
start_response(str(res.status_code) + ' ' + reason, header_list)
class async_to_sync_iter():
def __init__(self, iter, loop):
self.iter = iter.__aiter__()
self.loop = loop
def __iter__(self):
return self
def __next__(self):
try:
return self.loop.run_until_complete(self.iter.__anext__())
except StopAsyncIteration:
raise StopIteration
def close(self): # pragma: no cover
if hasattr(self.iter, 'aclose'):
self.loop.run_until_complete(self.iter.aclose())
return async_to_sync_iter(res.body_iter(), self.loop)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def shutdown(self):
if self.embedded_server: # pragma: no cover
super().shutdown()
else:
pid = os.getpgrp() if hasattr(os, 'getpgrp') else os.getpid()
os.kill(pid, signal.SIGTERM)
def run(self, host='0.0.0.0', port=5000, debug=False,
**options): # pragma: no cover
"""Normally you would not start the server by invoking this method.
Instead, start your chosen WSGI web server and pass the ``Microdot``
instance as the WSGI callable.
"""
self.embedded_server = True
super().run(host=host, port=port, debug=debug, **options)

View File

@@ -1,86 +0,0 @@
from microdot_asyncio import Response, abort
from microdot_websocket import WebSocket as BaseWebSocket
class WebSocket(BaseWebSocket):
async def handshake(self):
connect = await self.request.sock[0]()
if connect['type'] != 'websocket.connect':
abort(400)
await self.request.sock[1]({'type': 'websocket.accept'})
async def receive(self):
message = await self.request.sock[0]()
if message['type'] == 'websocket.disconnect':
raise OSError(32, 'Websocket connection closed')
elif message['type'] != 'websocket.receive':
raise OSError(32, 'Websocket message type not supported')
return message.get('bytes', message.get('text'))
async def send(self, data):
if isinstance(data, str):
await self.request.sock[1](
{'type': 'websocket.send', 'text': data})
else:
await self.request.sock[1](
{'type': 'websocket.send', 'bytes': data})
async def close(self):
if not self.closed:
self.closed = True
try:
await self.request.sock[1]({'type': 'websocket.close'})
except: # noqa E722
pass
async def websocket_upgrade(request):
"""Upgrade a request handler to a websocket connection.
This function can be called directly inside a route function to process a
WebSocket upgrade handshake, for example after the user's credentials are
verified. The function returns the websocket object::
@app.route('/echo')
async def echo(request):
if not (await authenticate_user(request)):
abort(401)
ws = await websocket_upgrade(request)
while True:
message = await ws.receive()
await ws.send(message)
"""
ws = WebSocket(request)
await ws.handshake()
@request.after_request
async def after_request(request, response):
return Response.already_handled
return ws
def with_websocket(f):
"""Decorator to make a route a WebSocket endpoint.
This decorator is used to define a route that accepts websocket
connections. The route then receives a websocket object as a second
argument that it can use to send and receive messages::
@app.route('/echo')
@with_websocket
async def echo(request, ws):
while True:
message = await ws.receive()
await ws.send(message)
"""
async def wrapper(request, *args, **kwargs):
ws = await websocket_upgrade(request)
try:
await f(request, ws, *args, **kwargs)
except OSError as exc:
if exc.errno != 32 and exc.errno != 54:
raise
await ws.close()
return ''
return wrapper

View File

@@ -1,455 +0,0 @@
"""
microdot_asyncio
----------------
The ``microdot_asyncio`` module defines a few classes that help implement
HTTP-based servers for MicroPython and standard Python that use ``asyncio``
and coroutines.
"""
try:
import uasyncio as asyncio
except ImportError:
import asyncio
try:
import uio as io
except ImportError:
import io
from microdot import Microdot as BaseMicrodot
from microdot import mro
from microdot import NoCaseDict
from microdot import Request as BaseRequest
from microdot import Response as BaseResponse
from microdot import print_exception
from microdot import HTTPException
from microdot import MUTED_SOCKET_ERRORS
def _iscoroutine(coro):
return hasattr(coro, 'send') and hasattr(coro, 'throw')
class _AsyncBytesIO:
def __init__(self, data):
self.stream = io.BytesIO(data)
async def read(self, n=-1):
return self.stream.read(n)
async def readline(self): # pragma: no cover
return self.stream.readline()
async def readexactly(self, n): # pragma: no cover
return self.stream.read(n)
async def readuntil(self, separator=b'\n'): # pragma: no cover
return self.stream.readuntil(separator=separator)
async def awrite(self, data): # pragma: no cover
return self.stream.write(data)
async def aclose(self): # pragma: no cover
pass
class Request(BaseRequest):
@staticmethod
async def create(app, client_reader, client_writer, client_addr):
"""Create a request object.
:param app: The Microdot application instance.
:param client_reader: An input stream from where the request data can
be read.
:param client_writer: An output stream where the response data can be
written.
:param client_addr: The address of the client, as a tuple.
This method is a coroutine. It returns a newly created ``Request``
object.
"""
# request line
line = (await Request._safe_readline(client_reader)).strip().decode()
if not line:
return None
method, url, http_version = line.split()
http_version = http_version.split('/', 1)[1]
# headers
headers = NoCaseDict()
content_length = 0
while True:
line = (await Request._safe_readline(
client_reader)).strip().decode()
if line == '':
break
header, value = line.split(':', 1)
value = value.strip()
headers[header] = value
if header.lower() == 'content-length':
content_length = int(value)
# body
body = b''
if content_length and content_length <= Request.max_body_length:
body = await client_reader.readexactly(content_length)
stream = None
else:
body = b''
stream = client_reader
return Request(app, client_addr, method, url, http_version, headers,
body=body, stream=stream,
sock=(client_reader, client_writer))
@property
def stream(self):
if self._stream is None:
self._stream = _AsyncBytesIO(self._body)
return self._stream
@staticmethod
async def _safe_readline(stream):
line = (await stream.readline())
if len(line) > Request.max_readline:
raise ValueError('line too long')
return line
class Response(BaseResponse):
"""An HTTP response class.
:param body: The body of the response. If a dictionary or list is given,
a JSON formatter is used to generate the body. If a file-like
object or an async generator is given, a streaming response is
used. If a string is given, it is encoded from UTF-8. Else,
the body should be a byte sequence.
:param status_code: The numeric HTTP status code of the response. The
default is 200.
:param headers: A dictionary of headers to include in the response.
:param reason: A custom reason phrase to add after the status code. The
default is "OK" for responses with a 200 status code and
"N/A" for any other status codes.
"""
async def write(self, stream):
self.complete()
try:
# status code
reason = self.reason if self.reason is not None else \
('OK' if self.status_code == 200 else 'N/A')
await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code, reason=reason).encode())
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
await stream.awrite('{header}: {value}\r\n'.format(
header=header, value=value).encode())
await stream.awrite(b'\r\n')
# body
if not self.is_head:
async for body in self.body_iter():
if isinstance(body, str): # pragma: no cover
body = body.encode()
await stream.awrite(body)
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS or \
exc.args[0] == 'Connection lost':
pass
else:
raise
def body_iter(self):
if hasattr(self.body, '__anext__'):
# response body is an async generator
return self.body
response = self
class iter:
def __aiter__(self):
if response.body:
self.i = 0 # need to determine type of response.body
else:
self.i = -1 # no response body
return self
async def __anext__(self):
if self.i == -1:
raise StopAsyncIteration
if self.i == 0:
if hasattr(response.body, 'read'):
self.i = 2 # response body is a file-like object
elif hasattr(response.body, '__next__'):
self.i = 1 # response body is a sync generator
return next(response.body)
else:
self.i = -1 # response body is a plain string
return response.body
elif self.i == 1:
try:
return next(response.body)
except StopIteration:
raise StopAsyncIteration
buf = response.body.read(response.send_file_buffer_size)
if _iscoroutine(buf): # pragma: no cover
buf = await buf
if len(buf) < response.send_file_buffer_size:
self.i = -1
if hasattr(response.body, 'close'): # pragma: no cover
result = response.body.close()
if _iscoroutine(result):
await result
return buf
return iter()
class Microdot(BaseMicrodot):
async def start_server(self, host='0.0.0.0', port=5000, debug=False,
ssl=None):
"""Start the Microdot web server as a coroutine. This coroutine does
not normally return, as the server enters an endless listening loop.
The :func:`shutdown` function provides a method for terminating the
server gracefully.
:param host: The hostname or IP address of the network interface that
will be listening for requests. A value of ``'0.0.0.0'``
(the default) indicates that the server should listen for
requests on all the available interfaces, and a value of
``127.0.0.1`` indicates that the server should listen
for requests only on the internal networking interface of
the host.
:param port: The port number to listen for requests. The default is
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
:param ssl: An ``SSLContext`` instance or ``None`` if the server should
not use TLS. The default is ``None``.
This method is a coroutine.
Example::
import asyncio
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
async def index(request):
return 'Hello, world!'
async def main():
await app.start_server(debug=True)
asyncio.run(main())
"""
self.debug = debug
async def serve(reader, writer):
if not hasattr(writer, 'awrite'): # pragma: no cover
# CPython provides the awrite and aclose methods in 3.8+
async def awrite(self, data):
self.write(data)
await self.drain()
async def aclose(self):
self.close()
await self.wait_closed()
from types import MethodType
writer.awrite = MethodType(awrite, writer)
writer.aclose = MethodType(aclose, writer)
await self.handle_request(reader, writer)
if self.debug: # pragma: no cover
print('Starting async server on {host}:{port}...'.format(
host=host, port=port))
try:
self.server = await asyncio.start_server(serve, host, port,
ssl=ssl)
except TypeError:
self.server = await asyncio.start_server(serve, host, port)
while True:
try:
if hasattr(self.server, 'serve_forever'): # pragma: no cover
try:
await self.server.serve_forever()
except asyncio.CancelledError:
pass
await self.server.wait_closed()
break
except AttributeError: # pragma: no cover
# the task hasn't been initialized in the server object yet
# wait a bit and try again
await asyncio.sleep(0.1)
def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None):
"""Start the web server. This function does not normally return, as
the server enters an endless listening loop. The :func:`shutdown`
function provides a method for terminating the server gracefully.
:param host: The hostname or IP address of the network interface that
will be listening for requests. A value of ``'0.0.0.0'``
(the default) indicates that the server should listen for
requests on all the available interfaces, and a value of
``127.0.0.1`` indicates that the server should listen
for requests only on the internal networking interface of
the host.
:param port: The port number to listen for requests. The default is
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
:param ssl: An ``SSLContext`` instance or ``None`` if the server should
not use TLS. The default is ``None``.
Example::
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
async def index(request):
return 'Hello, world!'
app.run(debug=True)
"""
asyncio.run(self.start_server(host=host, port=port, debug=debug,
ssl=ssl))
def shutdown(self):
self.server.close()
async def handle_request(self, reader, writer):
req = None
try:
req = await Request.create(self, reader, writer,
writer.get_extra_info('peername'))
except Exception as exc: # pragma: no cover
print_exception(exc)
res = await self.dispatch_request(req)
if res != Response.already_handled: # pragma: no branch
await res.write(writer)
try:
await writer.aclose()
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS:
pass
else:
raise
if self.debug and req: # pragma: no cover
print('{method} {path} {status_code}'.format(
method=req.method, path=req.path,
status_code=res.status_code))
async def dispatch_request(self, req):
after_request_handled = False
if req:
if req.content_length > req.max_content_length:
if 413 in self.error_handlers:
res = await self._invoke_handler(
self.error_handlers[413], req)
else:
res = 'Payload too large', 413
else:
f = self.find_route(req)
try:
res = None
if callable(f):
for handler in self.before_request_handlers:
res = await self._invoke_handler(handler, req)
if res:
break
if res is None:
res = await self._invoke_handler(
f, req, **req.url_args)
if isinstance(res, tuple):
body = res[0]
if isinstance(res[1], int):
status_code = res[1]
headers = res[2] if len(res) > 2 else {}
else:
status_code = 200
headers = res[1]
res = Response(body, status_code, headers)
elif not isinstance(res, Response):
res = Response(res)
for handler in self.after_request_handlers:
res = await self._invoke_handler(
handler, req, res) or res
for handler in req.after_request_handlers:
res = await self._invoke_handler(
handler, req, res) or res
after_request_handled = True
elif isinstance(f, dict):
res = Response(headers=f)
elif f in self.error_handlers:
res = await self._invoke_handler(
self.error_handlers[f], req)
else:
res = 'Not found', f
except HTTPException as exc:
if exc.status_code in self.error_handlers:
res = self.error_handlers[exc.status_code](req)
else:
res = exc.reason, exc.status_code
except Exception as exc:
print_exception(exc)
exc_class = None
res = None
if exc.__class__ in self.error_handlers:
exc_class = exc.__class__
else:
for c in mro(exc.__class__)[1:]:
if c in self.error_handlers:
exc_class = c
break
if exc_class:
try:
res = await self._invoke_handler(
self.error_handlers[exc_class], req, exc)
except Exception as exc2: # pragma: no cover
print_exception(exc2)
if res is None:
if 500 in self.error_handlers:
res = await self._invoke_handler(
self.error_handlers[500], req)
else:
res = 'Internal server error', 500
else:
if 400 in self.error_handlers:
res = await self._invoke_handler(self.error_handlers[400], req)
else:
res = 'Bad request', 400
if isinstance(res, tuple):
res = Response(*res)
elif not isinstance(res, Response):
res = Response(res)
if not after_request_handled:
for handler in self.after_error_request_handlers:
res = await self._invoke_handler(
handler, req, res) or res
res.is_head = (req and req.method == 'HEAD')
return res
async def _invoke_handler(self, f_or_coro, *args, **kwargs):
ret = f_or_coro(*args, **kwargs)
if _iscoroutine(ret):
ret = await ret
return ret
abort = Microdot.abort
Response.already_handled = Response()
redirect = Response.redirect
send_file = Response.send_file

View File

@@ -1,208 +0,0 @@
from microdot_asyncio import Request, Response, _AsyncBytesIO
from microdot_test_client import TestClient as BaseTestClient, \
TestResponse as BaseTestResponse
try:
from microdot_asyncio_websocket import WebSocket
except: # pragma: no cover # noqa: E722
WebSocket = None
class TestResponse(BaseTestResponse):
"""A response object issued by the Microdot test client."""
@classmethod
async def create(cls, res):
test_res = cls()
test_res._initialize_response(res)
await test_res._initialize_body(res)
test_res._process_text_body()
test_res._process_json_body()
return test_res
async def _initialize_body(self, res):
self.body = b''
async for body in res.body_iter(): # pragma: no branch
if isinstance(body, str):
body = body.encode()
self.body += body
class TestClient(BaseTestClient):
"""A test client for Microdot's Asynchronous web server.
:param app: The Microdot application instance.
:param cookies: A dictionary of cookies to use when sending requests to the
application.
The following example shows how to create a test client for an application
and send a test request::
from microdot_asyncio import Microdot
app = Microdot()
@app.get('/')
async def index():
return 'Hello, World!'
async def test_hello_world(self):
client = TestClient(app)
res = await client.get('/')
assert res.status_code == 200
assert res.text == 'Hello, World!'
"""
async def request(self, method, path, headers=None, body=None, sock=None):
headers = headers or {}
body, headers = self._process_body(body, headers)
cookies, headers = self._process_cookies(headers)
request_bytes = self._render_request(method, path, headers, body)
if sock:
reader = sock[0]
reader.buffer = request_bytes
writer = sock[1]
else:
reader = _AsyncBytesIO(request_bytes)
writer = _AsyncBytesIO(b'')
req = await Request.create(self.app, reader, writer,
('127.0.0.1', 1234))
res = await self.app.dispatch_request(req)
if res == Response.already_handled:
return None
res.complete()
self._update_cookies(res)
return await TestResponse.create(res)
async def get(self, path, headers=None):
"""Send a GET request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return await self.request('GET', path, headers=headers)
async def post(self, path, headers=None, body=None):
"""Send a POST request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
:param body: The request body. If a dictionary or list is provided,
a JSON-encoded body will be sent. A string body is encoded
to bytes as UTF-8. A bytes body is sent as-is.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return await self.request('POST', path, headers=headers, body=body)
async def put(self, path, headers=None, body=None):
"""Send a PUT request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
:param body: The request body. If a dictionary or list is provided,
a JSON-encoded body will be sent. A string body is encoded
to bytes as UTF-8. A bytes body is sent as-is.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return await self.request('PUT', path, headers=headers, body=body)
async def patch(self, path, headers=None, body=None):
"""Send a PATCH request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
:param body: The request body. If a dictionary or list is provided,
a JSON-encoded body will be sent. A string body is encoded
to bytes as UTF-8. A bytes body is sent as-is.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return await self.request('PATCH', path, headers=headers, body=body)
async def delete(self, path, headers=None):
"""Send a DELETE request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return await self.request('DELETE', path, headers=headers)
async def websocket(self, path, client, headers=None):
"""Send a websocket connection request to the application.
:param path: The request URL.
:param client: A generator function that yields client messages.
:param headers: A dictionary of headers to send with the request.
"""
gen = client()
class FakeWebSocket:
def __init__(self):
self.started = False
self.closed = False
self.buffer = b''
async def _next(self, data=None):
try:
data = (await gen.asend(data)) if hasattr(gen, 'asend') \
else gen.send(data)
except (StopIteration, StopAsyncIteration):
if not self.closed:
self.closed = True
raise OSError(32, 'Websocket connection closed')
return # pragma: no cover
opcode = WebSocket.TEXT if isinstance(data, str) \
else WebSocket.BINARY
return WebSocket._encode_websocket_frame(opcode, data)
async def read(self, n):
if not self.buffer:
self.started = True
self.buffer = await self._next()
data = self.buffer[:n]
self.buffer = self.buffer[n:]
return data
async def readexactly(self, n): # pragma: no cover
return await self.read(n)
async def readline(self):
line = b''
while True:
line += await self.read(1)
if line[-1] in [b'\n', 10]:
break
return line
async def awrite(self, data):
if self.started:
h = WebSocket._parse_frame_header(data[0:2])
if h[3] < 0:
data = data[2 - h[3]:]
else:
data = data[2:]
if h[1] == WebSocket.TEXT:
data = data.decode()
self.buffer = await self._next(data)
ws_headers = {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
}
ws_headers.update(headers or {})
sock = FakeWebSocket()
return await self.request('GET', path, headers=ws_headers,
sock=(sock, sock))

View File

@@ -1,103 +0,0 @@
from microdot_asyncio import Response
from microdot_websocket import WebSocket as BaseWebSocket
class WebSocket(BaseWebSocket):
async def handshake(self):
response = self._handshake_response()
await self.request.sock[1].awrite(
b'HTTP/1.1 101 Switching Protocols\r\n')
await self.request.sock[1].awrite(b'Upgrade: websocket\r\n')
await self.request.sock[1].awrite(b'Connection: Upgrade\r\n')
await self.request.sock[1].awrite(
b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
async def receive(self):
while True:
opcode, payload = await self._read_frame()
send_opcode, data = self._process_websocket_frame(opcode, payload)
if send_opcode: # pragma: no cover
await self.send(data, send_opcode)
elif data: # pragma: no branch
return data
async def send(self, data, opcode=None):
frame = self._encode_websocket_frame(
opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
data)
await self.request.sock[1].awrite(frame)
async def close(self):
if not self.closed: # pragma: no cover
self.closed = True
await self.send(b'', self.CLOSE)
async def _read_frame(self):
header = await self.request.sock[0].read(2)
if len(header) != 2: # pragma: no cover
raise OSError(32, 'Websocket connection closed')
fin, opcode, has_mask, length = self._parse_frame_header(header)
if length == -2:
length = await self.request.sock[0].read(2)
length = int.from_bytes(length, 'big')
elif length == -8:
length = await self.request.sock[0].read(8)
length = int.from_bytes(length, 'big')
if has_mask: # pragma: no cover
mask = await self.request.sock[0].read(4)
payload = await self.request.sock[0].read(length)
if has_mask: # pragma: no cover
payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
return opcode, payload
async def websocket_upgrade(request):
"""Upgrade a request handler to a websocket connection.
This function can be called directly inside a route function to process a
WebSocket upgrade handshake, for example after the user's credentials are
verified. The function returns the websocket object::
@app.route('/echo')
async def echo(request):
if not authenticate_user(request):
abort(401)
ws = await websocket_upgrade(request)
while True:
message = await ws.receive()
await ws.send(message)
"""
ws = WebSocket(request)
await ws.handshake()
@request.after_request
async def after_request(request, response):
return Response.already_handled
return ws
def with_websocket(f):
"""Decorator to make a route a WebSocket endpoint.
This decorator is used to define a route that accepts websocket
connections. The route then receives a websocket object as a second
argument that it can use to send and receive messages::
@app.route('/echo')
@with_websocket
async def echo(request, ws):
while True:
message = await ws.receive()
await ws.send(message)
"""
async def wrapper(request, *args, **kwargs):
ws = await websocket_upgrade(request)
try:
await f(request, ws, *args, **kwargs)
await ws.close() # pragma: no cover
except OSError as exc:
if exc.errno not in [32, 54, 104]: # pragma: no cover
raise
return ''
return wrapper

View File

@@ -1,33 +0,0 @@
from jinja2 import Environment, FileSystemLoader, select_autoescape
_jinja_env = None
def init_templates(template_dir='templates'):
"""Initialize the templating subsystem.
:param template_dir: the directory where templates are stored. This
argument is optional. The default is to load templates
from a *templates* subdirectory.
"""
global _jinja_env
_jinja_env = Environment(
loader=FileSystemLoader(template_dir),
autoescape=select_autoescape()
)
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)
return template.render(*args, **kwargs)

View File

@@ -1,98 +0,0 @@
import jwt
secret_key = None
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')
if hasattr(request.g, '_session'):
return request.g._session
session = request.cookies.get('session')
if session is None:
request.g._session = {}
return request.g._session
try:
session = jwt.decode(session, secret_key, algorithms=['HS256'])
except jwt.exceptions.PyJWTError: # pragma: no cover
request.g._session = {}
else:
request.g._session = session
return request.g._session
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')
encoded_session = jwt.encode(session, secret_key, algorithm='HS256')
@request.after_request
def _update_session(request, response):
response.set_cookie('session', encoded_session, http_only=True)
return 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,
expires='Thu, 01 Jan 1970 00:00:01 GMT')
return 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 <microdot_session.update_session>` function.
"""
def wrapper(request, *args, **kwargs):
return f(request, get_session(request), *args, **kwargs)
for attr in ['__name__', '__doc__', '__module__', '__qualname__']:
try:
setattr(wrapper, attr, getattr(f, attr))
except AttributeError: # pragma: no cover
pass
return wrapper

View File

@@ -1,61 +0,0 @@
import ssl
def create_ssl_context(cert, key, **kwargs):
"""Create an SSL context to wrap sockets with.
:param cert: The certificate to use. If it is given as a string, it is
assumed to be a filename. If it is given as a bytes object, it
is assumed to be the certificate data. In both cases the data
is expected to be in PEM format for CPython and in DER format
for MicroPython.
:param key: The private key to use. If it is given as a string, it is
assumed to be a filename. If it is given as a bytes object, it
is assumed to be the private key data. in both cases the data
is expected to be in PEM format for CPython and in DER format
for MicroPython.
:param kwargs: Additional arguments to pass to the ``ssl.wrap_socket``
function.
Note: This function creates a fairly limited SSL context object to enable
the use of certificates under MicroPython. It is not intended to be used in
any other context, and in particular, it is not needed when using CPython
or any other Python implementation that has native support for
``SSLContext`` objects. Once MicroPython implements ``SSLContext``
natively, this function will be deprecated.
"""
if hasattr(ssl, 'SSLContext'):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER, **kwargs)
ctx.load_cert_chain(cert, key)
return ctx
if isinstance(cert, str):
with open(cert, 'rb') as f:
cert = f.read()
if isinstance(key, str):
with open(key, 'rb') as f:
key = f.read()
class FakeSSLSocket:
def __init__(self, sock, **kwargs):
self.sock = sock
self.kwargs = kwargs
def accept(self):
client, addr = self.sock.accept()
return (ssl.wrap_socket(client, cert=cert, key=key, **self.kwargs),
addr)
def close(self):
self.sock.close()
class FakeSSLContext:
def __init__(self, **kwargs):
self.kwargs = kwargs
def wrap_socket(self, sock, **kwargs):
all_kwargs = self.kwargs.copy()
all_kwargs.update(kwargs)
return FakeSSLSocket(sock, **all_kwargs)
return FakeSSLContext(**kwargs)

View File

@@ -1,34 +0,0 @@
from utemplate import recompile
_loader = None
def init_templates(template_dir='templates', loader_class=recompile.Loader):
"""Initialize the templating subsystem.
:param template_dir: the directory where templates are stored. This
argument is optional. The default is to load templates
from a *templates* subdirectory.
:param loader_class: the ``utemplate.Loader`` class to use when loading
templates. This argument is optional. The default is
the ``recompile.Loader`` class, which automatically
recompiles templates when they change.
"""
global _loader
_loader = loader_class(None, template_dir)
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)
return render(*args, **kwargs)

View File

@@ -1,114 +0,0 @@
import binascii
import hashlib
import select
import websocket as _websocket
from microdot import Response
class WebSocket:
CONT = 0
TEXT = 1
BINARY = 2
CLOSE = 8
PING = 9
PONG = 10
def __init__(self, request):
self.request = request
self.poll = select.poll()
self.poll.register(self.request.sock, select.POLLIN)
self.ws = _websocket.websocket(self.request.sock, True)
self.request.sock.setblocking(False)
def handshake(self):
response = self._handshake_response()
self.request.sock.write(b'HTTP/1.1 101 Switching Protocols\r\n')
self.request.sock.write(b'Upgrade: websocket\r\n')
self.request.sock.write(b'Connection: Upgrade\r\n')
self.request.sock.write(
b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
def receive(self):
while True:
self.poll.poll()
data = self.ws.read()
if data:
try:
data = data.decode()
except ValueError:
pass
return data
def send(self, data):
self.ws.write(data)
def close(self):
self.poll.unregister(self.request.sock)
self.ws.close()
def _handshake_response(self):
for header, value in self.request.headers.items():
h = header.lower()
if h == 'connection' and not value.lower().startswith('upgrade'):
return self.request.app.abort(400)
elif h == 'upgrade' and not value.lower() == 'websocket':
return self.request.app.abort(400)
elif h == 'sec-websocket-key':
websocket_key = value
if not websocket_key:
return self.request.app.abort(400)
d = hashlib.sha1(websocket_key.encode())
d.update(b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
return binascii.b2a_base64(d.digest())[:-1]
def websocket_upgrade(request):
"""Upgrade a request handler to a websocket connection.
This function can be called directly inside a route function to process a
WebSocket upgrade handshake, for example after the user's credentials are
verified. The function returns the websocket object::
@app.route('/echo')
def echo(request):
if not authenticate_user(request):
abort(401)
ws = websocket_upgrade(request)
while True:
message = ws.receive()
ws.send(message)
"""
ws = WebSocket(request)
ws.handshake()
@request.after_request
def after_request(request, response):
return Response.already_handled
return ws
def with_websocket(f):
"""Decorator to make a route a WebSocket endpoint.
This decorator is used to define a route that accepts websocket
connections. The route then receives a websocket object as a second
argument that it can use to send and receive messages::
@app.route('/echo')
@with_websocket
def echo(request, ws):
while True:
message = ws.receive()
ws.send(message)
"""
def wrapper(request, *args, **kwargs):
ws = websocket_upgrade(request)
try:
f(request, ws, *args, **kwargs)
except OSError as exc:
if exc.errno != 32 and exc.errno != 54:
raise
ws.close()
return ''
return wrapper

View File

@@ -1,64 +0,0 @@
import os
import signal
from microdot import * # noqa: F401, F403
from microdot import Microdot as BaseMicrodot, Request, NoCaseDict
class Microdot(BaseMicrodot):
def __init__(self):
super().__init__()
self.embedded_server = False
def wsgi_app(self, environ, start_response):
"""A WSGI application callable."""
path = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '')
if 'QUERY_STRING' in environ and environ['QUERY_STRING']:
path += '?' + environ['QUERY_STRING']
headers = NoCaseDict()
for k, v in environ.items():
if k.startswith('HTTP_'):
h = '-'.join([p.title() for p in k[5:].split('_')])
headers[h] = v
req = Request(
self,
(environ['REMOTE_ADDR'], int(environ.get('REMOTE_PORT', '0'))),
environ['REQUEST_METHOD'],
path,
environ['SERVER_PROTOCOL'],
headers,
stream=environ['wsgi.input'],
sock=environ.get('gunicorn.socket'))
req.environ = environ
res = self.dispatch_request(req)
res.complete()
reason = res.reason or ('OK' if res.status_code == 200 else 'N/A')
header_list = []
for name, value in res.headers.items():
if not isinstance(value, list):
header_list.append((name, value))
else:
for v in value:
header_list.append((name, v))
start_response(str(res.status_code) + ' ' + reason, header_list)
return res.body_iter()
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def shutdown(self):
if self.embedded_server: # pragma: no cover
super().shutdown()
else:
pid = os.getpgrp() if hasattr(os, 'getpgrp') else os.getpid()
os.kill(pid, signal.SIGTERM)
def run(self, host='0.0.0.0', port=5000, debug=False,
**options): # pragma: no cover
"""Normally you would not start the server by invoking this method.
Instead, start your chosen WSGI web server and pass the ``Microdot``
instance as the WSGI callable.
"""
self.embedded_server = True
super().run(host=host, port=port, debug=debug, **options)

View File

@@ -1,14 +1,11 @@
from .test_multidict import TestMultiDict
from .test_request import TestRequest
from .test_response import TestResponse
from .test_url_pattern import TestURLPattern
from .test_microdot import TestMicrodot
from .test_microdot_websocket import TestMicrodotWebSocket
from .test_request_asyncio import TestRequestAsync
from .test_response_asyncio import TestResponseAsync
from .test_microdot_asyncio import TestMicrodotAsync
from .test_microdot_asyncio_websocket import TestMicrodotAsyncWebSocket
from .test_utemplate import TestUTemplate
from .test_session import TestSession
from tests.test_microdot import * # noqa: F401, F403
from tests.test_multidict import * # noqa: F401, F403
from tests.test_request import * # noqa: F401, F403
from tests.test_response import * # noqa: F401, F403
from tests.test_urlencode import * # noqa: F401, F403
from tests.test_url_pattern import * # noqa: F401, F403
from tests.test_websocket import * # noqa: F401, F403
from tests.test_sse import * # noqa: F401, F403
from tests.test_cors import * # noqa: F401, F403
from tests.test_utemplate import * # noqa: F401, F403
from tests.test_session import * # noqa: F401, F403

View File

@@ -1,39 +0,0 @@
try:
import uasyncio as asyncio
except ImportError:
import asyncio
from tests import mock_socket
def get_event_loop():
return asyncio.get_event_loop()
async def start_server(cb, host, port):
class MockServer:
def __init__(self):
self.closed = False
async def run(self):
s = mock_socket.socket()
while not self.closed:
fd, addr = s.accept()
fd = mock_socket.FakeStreamAsync(fd)
await cb(fd, fd)
def close(self):
self.closed = True
async def wait_closed(self):
while not self.closed:
await asyncio.sleep(0.01)
server = MockServer()
asyncio.get_event_loop().create_task(server.run())
return server
def run(coro):
loop = asyncio.get_event_loop()
return loop.run_until_complete(coro)

View File

@@ -1,23 +1,23 @@
import unittest
import asyncio
import sys
import unittest
from unittest import mock
try:
import asyncio
except ImportError:
pass
try:
from unittest import mock
except ImportError:
mock = None
from microdot_asgi import Microdot, Response
from tests import mock_asyncio
from microdot.asgi import Microdot, Response
@unittest.skipIf(sys.implementation.name == 'micropython',
'not supported under MicroPython')
class TestMicrodotASGI(unittest.TestCase):
class TestASGI(unittest.TestCase):
@classmethod
def setUpClass(cls):
if hasattr(asyncio, 'set_event_loop'):
asyncio.set_event_loop(asyncio.new_event_loop())
cls.loop = asyncio.get_event_loop()
def _run(self, coro):
return self.loop.run_until_complete(coro)
def test_asgi_request_with_query_string(self):
app = Microdot()
@@ -98,7 +98,7 @@ class TestMicrodotASGI(unittest.TestCase):
original_buffer_size = Response.send_file_buffer_size
Response.send_file_buffer_size = 2
mock_asyncio.run(app(scope, receive, send))
self._run(app(scope, receive, send))
Response.send_file_buffer_size = original_buffer_size
@@ -143,7 +143,7 @@ class TestMicrodotASGI(unittest.TestCase):
async def send(packet):
pass
mock_asyncio.run(app(scope, receive, send))
self._run(app(scope, receive, send))
def test_shutdown(self):
app = Microdot()
@@ -166,7 +166,7 @@ class TestMicrodotASGI(unittest.TestCase):
async def send(packet):
pass
with mock.patch('microdot_asgi.os.kill') as kill:
mock_asyncio.run(app(scope, receive, send))
with mock.patch('microdot.asgi.os.kill') as kill:
self._run(app(scope, receive, send))
kill.assert_called()

View File

@@ -1,10 +1,18 @@
import asyncio
import unittest
from microdot import Microdot
from microdot_test_client import TestClient
from microdot_cors import CORS
from microdot.test_client import TestClient
from microdot.cors import CORS
class TestCORS(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.loop = asyncio.new_event_loop()
def _run(self, coro):
return self.loop.run_until_complete(coro)
def test_origin(self):
app = Microdot()
cors = CORS(allowed_origins=['https://example.com'],
@@ -16,13 +24,14 @@ class TestCORS(unittest.TestCase):
return 'foo'
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertFalse('Access-Control-Allow-Origin' in res.headers)
self.assertFalse('Access-Control-Allow-Credentials' in res.headers)
self.assertFalse('Vary' in res.headers)
res = client.get('/', headers={'Origin': 'https://example.com'})
res = self._run(client.get(
'/', headers={'Origin': 'https://example.com'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
@@ -32,14 +41,15 @@ class TestCORS(unittest.TestCase):
cors.allow_credentials = False
res = client.get('/foo', headers={'Origin': 'https://example.com'})
res = self._run(client.get(
'/foo', headers={'Origin': 'https://example.com'}))
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
self.assertFalse('Access-Control-Allow-Credentials' in res.headers)
self.assertEqual(res.headers['Vary'], 'Origin')
res = client.get('/', headers={'Origin': 'https://bad.com'})
res = self._run(client.get('/', headers={'Origin': 'https://bad.com'}))
self.assertEqual(res.status_code, 200)
self.assertFalse('Access-Control-Allow-Origin' in res.headers)
self.assertFalse('Access-Control-Allow-Credentials' in res.headers)
@@ -58,14 +68,15 @@ class TestCORS(unittest.TestCase):
return 'foo', {'Vary': 'X-Foo, X-Bar'}
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'], '*')
self.assertFalse('Vary' in res.headers)
self.assertEqual(res.headers['Access-Control-Expose-Headers'],
'X-Test, X-Test2')
res = client.get('/', headers={'Origin': 'https://example.com'})
res = self._run(client.get(
'/', headers={'Origin': 'https://example.com'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
@@ -73,7 +84,8 @@ class TestCORS(unittest.TestCase):
self.assertEqual(res.headers['Access-Control-Expose-Headers'],
'X-Test, X-Test2')
res = client.get('/bad', headers={'Origin': 'https://example.com'})
res = self._run(client.get(
'/bad', headers={'Origin': 'https://example.com'}))
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
@@ -81,7 +93,8 @@ class TestCORS(unittest.TestCase):
self.assertEqual(res.headers['Access-Control-Expose-Headers'],
'X-Test, X-Test2')
res = client.get('/foo', headers={'Origin': 'https://example.com'})
res = self._run(client.get(
'/foo', headers={'Origin': 'https://example.com'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Vary'], 'X-Foo, X-Bar, Origin')
@@ -94,10 +107,10 @@ class TestCORS(unittest.TestCase):
return 'foo'
client = TestClient(app)
res = client.request('OPTIONS', '/', headers={
res = self._run(client.request('OPTIONS', '/', headers={
'Origin': 'https://example.com',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'X-Test, X-Test2'})
'Access-Control-Request-Headers': 'X-Test, X-Test2'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
@@ -106,8 +119,8 @@ class TestCORS(unittest.TestCase):
self.assertEqual(res.headers['Access-Control-Allow-Headers'],
'X-Test, X-Test2')
res = client.request('OPTIONS', '/', headers={
'Origin': 'https://example.com'})
res = self._run(client.request('OPTIONS', '/', headers={
'Origin': 'https://example.com'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
@@ -125,10 +138,10 @@ class TestCORS(unittest.TestCase):
return 'foo'
client = TestClient(app)
res = client.request('OPTIONS', '/', headers={
res = self._run(client.request('OPTIONS', '/', headers={
'Origin': 'https://example.com',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'X-Test, X-Test2'})
'Access-Control-Request-Headers': 'X-Test, X-Test2'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
@@ -136,9 +149,9 @@ class TestCORS(unittest.TestCase):
self.assertEqual(res.headers['Access-Control-Allow-Methods'], 'POST')
self.assertEqual(res.headers['Access-Control-Allow-Headers'], 'X-Test')
res = client.request('OPTIONS', '/', headers={
res = self._run(client.request('OPTIONS', '/', headers={
'Origin': 'https://example.com',
'Access-Control-Request-Method': 'GET'})
'Access-Control-Request-Method': 'GET'}))
self.assertEqual(res.status_code, 200)
self.assertFalse('Access-Control-Allow-Methods' in res.headers)
self.assertFalse('Access-Control-Allow-Headers' in res.headers)
@@ -152,7 +165,7 @@ class TestCORS(unittest.TestCase):
return 'foo'
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertFalse('Access-Control-Allow-Origin' in res.headers)
self.assertFalse('Vary' in res.headers)

104
tests/test_end2end.py Normal file
View File

@@ -0,0 +1,104 @@
import asyncio
import sys
import time
import unittest
from microdot import Microdot
class TestEnd2End(unittest.TestCase):
async def request(self, url, method='GET'):
while True:
reader, writer = await asyncio.open_connection('localhost', 5678)
try:
writer.write(f'{method} {url} HTTP/1.0\r\n\r\n'.encode())
break
except OSError:
# micropython's server sometimes needs a moment
writer.close()
await writer.wait_closed()
await asyncio.sleep(0.1)
await writer.drain()
response = await reader.read()
writer.close()
await writer.wait_closed()
return response.decode().splitlines()
def test_get(self):
app = Microdot()
@app.route('/')
def index(request):
return 'Hello, World!'
@app.route('/shutdown')
def shutdown(request):
app.shutdown()
return ''
async def run():
server = asyncio.create_task(app.start_server(host='127.0.0.1',
port=5678))
await asyncio.sleep(0.1)
response = await self.request('/')
self.assertEqual(response[0], 'HTTP/1.0 200 OK')
self.assertEqual(response[-1], 'Hello, World!')
await self.request('/shutdown')
await server
asyncio.run(run())
@unittest.skipIf(sys.implementation.name == 'micropython',
'not supported under MicroPython')
def test_concurrent_requests(self):
app = Microdot()
counter = 0
@app.route('/async1')
async def async1(request):
nonlocal counter
counter += 1
while counter < 4:
await asyncio.sleep(0.01)
return 'OK'
@app.route('/async2')
async def async2(request):
nonlocal counter
counter += 1
while counter < 4:
await asyncio.sleep(0.01)
return 'OK'
@app.route('/sync1')
def sync1(request):
nonlocal counter
counter += 1
while counter < 4:
time.sleep(0.01)
return 'OK'
@app.route('/sync2')
def sync2(request):
nonlocal counter
counter += 1
while counter < 4:
time.sleep(0.01)
return 'OK'
@app.route('/shutdown')
def shutdown(request):
app.shutdown()
return ''
async def run():
server = asyncio.create_task(app.start_server(port=5678))
await asyncio.sleep(0.1)
await asyncio.gather(self.request('/async1'),
self.request('/async2'),
self.request('/sync1'),
self.request('/sync2'))
await self.request('/shutdown')
await server
asyncio.run(run())

View File

@@ -1,58 +1,81 @@
try:
import uasyncio as asyncio
except ImportError:
import asyncio
import asyncio
import sys
import unittest
from microdot import Microdot, Request
from microdot_asyncio import Microdot as MicrodotAsync, Request as RequestAsync
from microdot_jinja import render_template, init_templates
from tests.mock_socket import get_request_fd, get_async_request_fd
from microdot import Microdot
from microdot.jinja import Template, init_templates
from microdot.test_client import TestClient
init_templates('tests/templates')
def _run(coro):
return asyncio.get_event_loop().run_until_complete(coro)
@unittest.skipIf(sys.implementation.name == 'micropython',
'not supported under MicroPython')
class TestJinja(unittest.TestCase):
@classmethod
def setUpClass(cls):
if hasattr(asyncio, 'set_event_loop'):
asyncio.set_event_loop(asyncio.new_event_loop())
cls.loop = asyncio.get_event_loop()
def _run(self, coro):
return self.loop.run_until_complete(coro)
def test_render_template(self):
s = render_template('hello.jinja.txt', name='foo')
s = Template('hello.jinja.txt').render(name='foo')
self.assertEqual(s, 'Hello, foo!')
def test_render_template_in_app(self):
app = Microdot()
@app.route('/')
def index(req):
return render_template('hello.jinja.txt', name='foo')
async def index(req):
return Template('hello.jinja.txt').render(name='foo')
req = Request.create(app, get_request_fd('GET', '/'), 'addr')
res = app.dispatch_request(req)
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(list(res.body_iter()), [b'Hello, foo!'])
self.assertEqual(res.body, b'Hello, foo!')
def test_render_template_in_app_async(self):
app = MicrodotAsync()
def test_generate_template_in_app(self):
app = Microdot()
@app.route('/')
async def index(req):
return render_template('hello.jinja.txt', name='foo')
return Template('hello.jinja.txt').generate(name='foo')
req = _run(RequestAsync.create(
app, get_async_request_fd('GET', '/'), 'writer', 'addr'))
res = _run(app.dispatch_request(req))
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.body, b'Hello, foo!')
async def get_result():
result = []
async for chunk in res.body_iter():
result.append(chunk)
return result
def test_render_async_template_in_app(self):
init_templates('tests/templates', enable_async=True)
result = _run(get_result())
self.assertEqual(result, [b'Hello, foo!'])
app = Microdot()
@app.route('/')
async def index(req):
return await Template('hello.jinja.txt').render_async(name='foo')
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.body, b'Hello, foo!')
init_templates('tests/templates')
def test_generate_async_template_in_app(self):
init_templates('tests/templates', enable_async=True)
app = Microdot()
@app.route('/')
async def index(req):
return Template('hello.jinja.txt').generate_async(name='foo')
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.body, b'Hello, foo!')
init_templates('tests/templates')

View File

@@ -1,31 +1,18 @@
import sys
import asyncio
import unittest
from microdot import Microdot, Response, abort
from microdot_test_client import TestClient
from tests import mock_socket
from microdot.test_client import TestClient
class TestMicrodot(unittest.TestCase):
def _mock(self):
def mock_create_thread(f, *args, **kwargs):
f(*args, **kwargs)
@classmethod
def setUpClass(cls):
if hasattr(asyncio, 'set_event_loop'):
asyncio.set_event_loop(asyncio.new_event_loop())
cls.loop = asyncio.get_event_loop()
self.original_socket = sys.modules['microdot'].socket
self.original_create_thread = sys.modules['microdot'].create_thread
sys.modules['microdot'].socket = mock_socket
sys.modules['microdot'].create_thread = mock_create_thread
def _unmock(self):
sys.modules['microdot'].socket = self.original_socket
sys.modules['microdot'].create_thread = self.original_create_thread
def _add_shutdown(self, app):
@app.route('/shutdown')
def shutdown(req):
app.shutdown()
return ''
mock_socket.add_request('GET', '/shutdown')
def _run(self, coro):
return self.loop.run_until_complete(coro)
def test_get_request(self):
app = Microdot()
@@ -34,8 +21,13 @@ class TestMicrodot(unittest.TestCase):
def index(req):
return 'foo'
@app.route('/async')
async def index2(req):
return 'foo-async'
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -44,6 +36,15 @@ class TestMicrodot(unittest.TestCase):
self.assertEqual(res.body, b'foo')
self.assertEqual(res.json, None)
res = self._run(client.get('/async'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '9')
self.assertEqual(res.text, 'foo-async')
self.assertEqual(res.body, b'foo-async')
self.assertEqual(res.json, None)
def test_post_request(self):
app = Microdot()
@@ -55,78 +56,71 @@ class TestMicrodot(unittest.TestCase):
def index_post(req):
return Response('bar')
@app.route('/async', methods=['POST'])
async def index_post2(req):
return Response('bar-async')
client = TestClient(app)
res = client.post('/')
res = self._run(client.post('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'bar')
self.assertEqual(res.body, b'bar')
self.assertEqual(res.json, None)
res = self._run(client.post('/async'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '9')
self.assertEqual(res.text, 'bar-async')
self.assertEqual(res.body, b'bar-async')
self.assertEqual(res.json, None)
def test_head_request(self):
self._mock()
app = Microdot()
@app.route('/foo')
def index(req):
return 'foo'
mock_socket.clear_requests()
fd = mock_socket.add_request('HEAD', '/foo')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n'))
self._unmock()
client = TestClient(app)
res = self._run(client.request('HEAD', '/foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertIsNone(res.body)
self.assertIsNone(res.text)
self.assertIsNone(res.json)
def test_options_request(self):
app = Microdot()
@app.route('/', methods=['GET', 'DELETE'])
def index(req):
async def index(req):
return 'foo'
@app.post('/')
def index_post(req):
async def index_post(req):
return 'bar'
@app.route('/foo', methods=['POST', 'PUT'])
def foo(req):
async def foo(req):
return 'baz'
client = TestClient(app)
res = client.request('OPTIONS', '/')
res = self._run(client.request('OPTIONS', '/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Allow'],
'GET, DELETE, POST, HEAD, OPTIONS')
res = client.request('OPTIONS', '/foo')
res = self._run(client.request('OPTIONS', '/foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Allow'], 'POST, PUT, OPTIONS')
def test_empty_request(self):
self._mock()
app = Microdot()
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 11\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
self._unmock()
def test_method_decorators(self):
app = Microdot()
@@ -135,7 +129,7 @@ class TestMicrodot(unittest.TestCase):
return 'GET'
@app.post('/post')
def post(req):
async def post(req):
return 'POST'
@app.put('/put')
@@ -143,7 +137,7 @@ class TestMicrodot(unittest.TestCase):
return 'PUT'
@app.patch('/patch')
def patch(req):
async def patch(req):
return 'PATCH'
@app.delete('/delete')
@@ -153,7 +147,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
for method in methods:
res = getattr(client, method.lower())('/' + method.lower())
res = self._run(getattr(
client, method.lower())('/' + method.lower()))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -167,7 +162,7 @@ class TestMicrodot(unittest.TestCase):
return req.headers.get('X-Foo')
client = TestClient(app)
res = client.get('/', headers={'X-Foo': 'bar'})
res = self._run(client.get('/', headers={'X-Foo': 'bar'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -178,15 +173,19 @@ class TestMicrodot(unittest.TestCase):
@app.route('/')
def index(req):
return req.cookies['one'] + req.cookies['two'] + \
req.cookies['three']
res = Response(
req.cookies['one'] + req.cookies['two'] + req.cookies['three'])
res.set_cookie('four', '4')
res.delete_cookie('two', path='/')
return res
client = TestClient(app, cookies={'one': '1', 'two': '2'})
res = client.get('/', headers={'Cookie': 'three=3'})
res = self._run(client.get('/', headers={'Cookie': 'three=3'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '123')
self.assertEqual(client.cookies, {'one': '1', 'four': '4'})
def test_binary_payload(self):
app = Microdot()
@@ -196,7 +195,7 @@ class TestMicrodot(unittest.TestCase):
return req.body
client = TestClient(app)
res = client.post('/', body=b'foo')
res = self._run(client.post('/', body=b'foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -216,13 +215,13 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.post('/dict', body={'foo': 'bar'})
res = self._run(client.post('/dict', body={'foo': 'bar'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'bar')
res = client.post('/list', body=['foo', 'bar'])
res = self._run(client.post('/list', body=['foo', 'bar']))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -250,24 +249,24 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.get('/body')
res = self._run(client.get('/body'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'one')
res = client.get('/body-status')
res = self._run(client.get('/body-status'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'two')
res = client.get('/body-headers')
res = self._run(client.get('/body-headers'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/html')
self.assertEqual(res.text, '<p>three</p>')
res = client.get('/body-status-headers')
res = self._run(client.get('/body-status-headers'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'],
'text/html; charset=UTF-8')
@@ -280,7 +279,7 @@ class TestMicrodot(unittest.TestCase):
def before_request(req):
if req.path == '/bar':
@req.after_request
def after_request(req, res):
async def after_request(req, res):
res.headers['X-Two'] = '2'
return res
return 'bar', 202
@@ -291,7 +290,7 @@ class TestMicrodot(unittest.TestCase):
res.headers['X-One'] = '1'
@app.after_request
def after_request_two(req, res):
async def after_request_two(req, res):
res.set_cookie('foo', 'bar')
return res
@@ -305,7 +304,7 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.get('/bar')
res = self._run(client.get('/bar'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -315,7 +314,7 @@ class TestMicrodot(unittest.TestCase):
self.assertEqual(res.text, 'bar')
self.assertEqual(client.cookies['foo'], 'bar')
res = client.get('/baz')
res = self._run(client.get('/baz'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -343,14 +342,14 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.get('/foo')
res = self._run(client.get('/foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertFalse('X-One' in res.headers)
self.assertFalse('Set-Cookie' in res.headers)
res = client.get('/bar')
res = self._run(client.get('/bar'))
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -359,44 +358,22 @@ class TestMicrodot(unittest.TestCase):
self.assertEqual(client.cookies['foo'], 'bar')
def test_400(self):
self._mock()
app = Microdot()
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 11\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
self._unmock()
res = self._run(app.dispatch_request(None))
self.assertEqual(res.status_code, 400)
self.assertEqual(res.body, b'Bad request')
def test_400_handler(self):
self._mock()
app = Microdot()
@app.errorhandler(400)
def handle_400(req):
async def handle_400(req):
return '400'
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n400'))
self._unmock()
res = self._run(app.dispatch_request(None))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.body, b'400')
def test_404(self):
app = Microdot()
@@ -406,7 +383,7 @@ class TestMicrodot(unittest.TestCase):
return 'foo'
client = TestClient(app)
res = client.post('/foo')
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -420,11 +397,11 @@ class TestMicrodot(unittest.TestCase):
return 'foo'
@app.errorhandler(404)
def handle_404(req):
async def handle_404(req):
return '404'
client = TestClient(app)
res = client.post('/foo')
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -438,7 +415,7 @@ class TestMicrodot(unittest.TestCase):
return 'foo'
client = TestClient(app)
res = client.post('/foo')
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -452,11 +429,11 @@ class TestMicrodot(unittest.TestCase):
return 'foo'
@app.errorhandler(405)
def handle_405(req):
async def handle_405(req):
return '405', 405
client = TestClient(app)
res = client.patch('/foo')
res = self._run(client.patch('/foo'))
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -465,12 +442,12 @@ class TestMicrodot(unittest.TestCase):
def test_413(self):
app = Microdot()
@app.post('/')
@app.route('/')
def index(req):
return 'foo'
client = TestClient(app)
res = client.post('/foo', body='x' * 17000)
res = self._run(client.post('/foo', body='x' * 17000))
self.assertEqual(res.status_code, 413)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -484,11 +461,11 @@ class TestMicrodot(unittest.TestCase):
return 'foo'
@app.errorhandler(413)
def handle_413(req):
async def handle_413(req):
return '413', 400
client = TestClient(app)
res = client.post('/foo', body='x' * 17000)
res = self._run(client.post('/foo', body='x' * 17000))
self.assertEqual(res.status_code, 400)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -502,7 +479,7 @@ class TestMicrodot(unittest.TestCase):
return 1 / 0
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 500)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -520,7 +497,7 @@ class TestMicrodot(unittest.TestCase):
return '501', 501
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -534,11 +511,11 @@ class TestMicrodot(unittest.TestCase):
return 1 / 0
@app.errorhandler(ZeroDivisionError)
def handle_div_zero(req, exc):
async def handle_div_zero(req, exc):
return '501', 501
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -553,11 +530,11 @@ class TestMicrodot(unittest.TestCase):
return foo[1]
@app.errorhandler(LookupError)
def handle_lookup_error(req, exc):
async def handle_lookup_error(req, exc):
return exc.__class__.__name__, 501
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -572,15 +549,15 @@ class TestMicrodot(unittest.TestCase):
return foo[1]
@app.errorhandler(LookupError)
def handle_lookup_error(req, exc):
async def handle_lookup_error(req, exc):
return 'LookupError', 501
@app.errorhandler(IndexError)
def handle_index_error(req, exc):
async def handle_index_error(req, exc):
return 'IndexError', 501
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -595,15 +572,15 @@ class TestMicrodot(unittest.TestCase):
return foo[1]
@app.errorhandler(Exception)
def handle_generic_exception(req, exc):
async def handle_generic_exception(req, exc):
return 'Exception', 501
@app.errorhandler(LookupError)
def handle_lookup_error(req, exc):
async def handle_lookup_error(req, exc):
return 'LookupError', 501
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -618,11 +595,11 @@ class TestMicrodot(unittest.TestCase):
return foo[1]
@app.errorhandler(RuntimeError)
def handle_runtime_error(req, exc):
async def handle_runtime_error(req, exc):
return 'RuntimeError', 501
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 500)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -637,7 +614,7 @@ class TestMicrodot(unittest.TestCase):
return 'foo'
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 406)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -652,11 +629,11 @@ class TestMicrodot(unittest.TestCase):
return 'foo'
@app.errorhandler(406)
def handle_406(req):
def handle_500(req):
return '406', 406
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 406)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
@@ -666,7 +643,7 @@ class TestMicrodot(unittest.TestCase):
app = Microdot()
@app.route('/dict')
def json_dict(req):
async def json_dict(req):
return {'foo': 'bar'}
@app.route('/list')
@@ -675,13 +652,13 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.get('/dict')
res = self._run(client.get('/dict'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'application/json; charset=UTF-8')
self.assertEqual(res.json, {'foo': 'bar'})
res = client.get('/list')
res = self._run(client.get('/list'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'application/json; charset=UTF-8')
@@ -695,7 +672,7 @@ class TestMicrodot(unittest.TestCase):
return b'\xff\xfe', {'Content-Type': 'application/octet-stream'}
client = TestClient(app)
res = client.get('/bin')
res = self._run(client.get('/bin'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'application/octet-stream')
@@ -705,20 +682,38 @@ class TestMicrodot(unittest.TestCase):
def test_streaming(self):
app = Microdot()
done = False
@app.route('/')
def index(req):
def stream():
yield 'foo'
yield b'bar'
class stream():
def __init__(self):
self.i = 0
self.data = ['foo', b'bar']
def __aiter__(self):
return self
async def __anext__(self):
if self.i >= len(self.data):
raise StopAsyncIteration
data = self.data[self.i]
self.i += 1
return data
async def aclose(self):
nonlocal done
done = True
return stream()
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'foobar')
self.assertEqual(done, True)
def test_already_handled_response(self):
app = Microdot()
@@ -728,7 +723,7 @@ class TestMicrodot(unittest.TestCase):
return Response.already_handled
client = TestClient(app)
res = client.get('/')
res = self._run(client.get('/'))
self.assertEqual(res, None)
def test_mount(self):
@@ -759,39 +754,14 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.get('/app')
res = self._run(client.get('/app'))
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '404:errorafter')
res = client.get('/sub/app')
res = self._run(client.get('/sub/app'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'before:foo:after')
def test_ssl(self):
self._mock()
app = Microdot()
@app.route('/foo')
def foo(req):
return 'bar'
class FakeSSL:
def wrap_socket(self, sock, **kwargs):
return sock
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/foo')
self._add_shutdown(app)
app.run(ssl=FakeSSL())
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nbar'))
self._unmock()

View File

@@ -1,775 +0,0 @@
try:
import uasyncio as asyncio
except ImportError:
import asyncio
import sys
import unittest
from microdot_asyncio import Microdot, Response, abort
from microdot_asyncio_test_client import TestClient
from tests import mock_asyncio, mock_socket
class TestMicrodotAsync(unittest.TestCase):
def setUp(self):
self._mock()
def tearDown(self):
self._unmock()
def _run(self, coro):
loop = asyncio.get_event_loop()
return loop.run_until_complete(coro)
def _mock(self):
self.original_asyncio = sys.modules['microdot_asyncio'].asyncio
sys.modules['microdot_asyncio'].asyncio = mock_asyncio
def _unmock(self):
sys.modules['microdot_asyncio'].asyncio = self.original_asyncio
def _add_shutdown(self, app):
@app.route('/shutdown')
def shutdown(req):
app.shutdown()
return ''
mock_socket.add_request('GET', '/shutdown')
def test_get_request(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.route('/async')
async def index2(req):
return 'foo-async'
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'foo')
self.assertEqual(res.body, b'foo')
self.assertEqual(res.json, None)
res = self._run(client.get('/async'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '9')
self.assertEqual(res.text, 'foo-async')
self.assertEqual(res.body, b'foo-async')
self.assertEqual(res.json, None)
def test_post_request(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.route('/', methods=['POST'])
def index_post(req):
return Response('bar')
@app.route('/async', methods=['POST'])
async def index_post2(req):
return Response('bar-async')
client = TestClient(app)
res = self._run(client.post('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'bar')
self.assertEqual(res.body, b'bar')
self.assertEqual(res.json, None)
res = self._run(client.post('/async'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '9')
self.assertEqual(res.text, 'bar-async')
self.assertEqual(res.body, b'bar-async')
self.assertEqual(res.json, None)
def test_head_request(self):
app = Microdot()
@app.route('/foo')
def index(req):
return 'foo'
mock_socket.clear_requests()
fd = mock_socket.add_request('HEAD', '/foo')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n'))
def test_options_request(self):
app = Microdot()
@app.route('/', methods=['GET', 'DELETE'])
async def index(req):
return 'foo'
@app.post('/')
async def index_post(req):
return 'bar'
@app.route('/foo', methods=['POST', 'PUT'])
async def foo(req):
return 'baz'
client = TestClient(app)
res = self._run(client.request('OPTIONS', '/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Allow'],
'GET, DELETE, POST, HEAD, OPTIONS')
res = self._run(client.request('OPTIONS', '/foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Allow'], 'POST, PUT, OPTIONS')
def test_empty_request(self):
app = Microdot()
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 11\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
def test_method_decorators(self):
app = Microdot()
@app.get('/get')
def get(req):
return 'GET'
@app.post('/post')
async def post(req):
return 'POST'
@app.put('/put')
def put(req):
return 'PUT'
@app.patch('/patch')
async def patch(req):
return 'PATCH'
@app.delete('/delete')
def delete(req):
return 'DELETE'
client = TestClient(app)
methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
for method in methods:
res = self._run(getattr(
client, method.lower())('/' + method.lower()))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, method)
def test_headers(self):
app = Microdot()
@app.route('/')
def index(req):
return req.headers.get('X-Foo')
client = TestClient(app)
res = self._run(client.get('/', headers={'X-Foo': 'bar'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'bar')
def test_cookies(self):
app = Microdot()
@app.route('/')
def index(req):
return req.cookies['one'] + req.cookies['two'] + \
req.cookies['three']
client = TestClient(app, cookies={'one': '1', 'two': '2'})
res = self._run(client.get('/', headers={'Cookie': 'three=3'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '123')
def test_binary_payload(self):
app = Microdot()
@app.post('/')
def index(req):
return req.body
client = TestClient(app)
res = self._run(client.post('/', body=b'foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'foo')
def test_json_payload(self):
app = Microdot()
@app.post('/dict')
def json_dict(req):
print(req.headers)
return req.json.get('foo')
@app.post('/list')
def json_list(req):
return req.json[0]
client = TestClient(app)
res = self._run(client.post('/dict', body={'foo': 'bar'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'bar')
res = self._run(client.post('/list', body=['foo', 'bar']))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'foo')
def test_tuple_responses(self):
app = Microdot()
@app.route('/body')
def one(req):
return 'one'
@app.route('/body-status')
def two(req):
return 'two', 202
@app.route('/body-headers')
def three(req):
return '<p>three</p>', {'Content-Type': 'text/html'}
@app.route('/body-status-headers')
def four(req):
return '<p>four</p>', 202, \
{'Content-Type': 'text/html; charset=UTF-8'}
client = TestClient(app)
res = self._run(client.get('/body'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'one')
res = self._run(client.get('/body-status'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'two')
res = self._run(client.get('/body-headers'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/html')
self.assertEqual(res.text, '<p>three</p>')
res = self._run(client.get('/body-status-headers'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'],
'text/html; charset=UTF-8')
self.assertEqual(res.text, '<p>four</p>')
def test_before_after_request(self):
app = Microdot()
@app.before_request
def before_request(req):
if req.path == '/bar':
@req.after_request
async def after_request(req, res):
res.headers['X-Two'] = '2'
return res
return 'bar', 202
req.g.message = 'baz'
@app.after_request
def after_request_one(req, res):
res.headers['X-One'] = '1'
@app.after_request
async def after_request_two(req, res):
res.set_cookie('foo', 'bar')
return res
@app.route('/bar')
def bar(req):
return 'foo'
@app.route('/baz')
def baz(req):
return req.g.message
client = TestClient(app)
res = self._run(client.get('/bar'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertEqual(res.headers['X-Two'], '2')
self.assertEqual(res.text, 'bar')
self.assertEqual(client.cookies['foo'], 'bar')
res = self._run(client.get('/baz'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertFalse('X-Two' in res.headers)
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'baz')
def test_after_error_request(self):
app = Microdot()
@app.after_error_request
def after_error_request_one(req, res):
res.headers['X-One'] = '1'
@app.after_error_request
def after_error_request_two(req, res):
res.set_cookie('foo', 'bar')
return res
@app.route('/foo')
def foo(req):
return 'foo'
client = TestClient(app)
res = self._run(client.get('/foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertFalse('X-One' in res.headers)
self.assertFalse('Set-Cookie' in res.headers)
res = self._run(client.get('/bar'))
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertEqual(client.cookies['foo'], 'bar')
def test_400(self):
self._mock()
app = Microdot()
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 11\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
self._unmock()
def test_400_handler(self):
self._mock()
app = Microdot()
@app.errorhandler(400)
async def handle_404(req):
return '400'
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n400'))
self._unmock()
def test_404(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
client = TestClient(app)
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Not found')
def test_404_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.errorhandler(404)
async def handle_404(req):
return '404'
client = TestClient(app)
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '404')
def test_405(self):
app = Microdot()
@app.route('/foo')
def index(req):
return 'foo'
client = TestClient(app)
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Not found')
def test_405_handler(self):
app = Microdot()
@app.route('/foo')
def index(req):
return 'foo'
@app.errorhandler(405)
async def handle_405(req):
return '405', 405
client = TestClient(app)
res = self._run(client.patch('/foo'))
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '405')
def test_413(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
client = TestClient(app)
res = self._run(client.post('/foo', body='x' * 17000))
self.assertEqual(res.status_code, 413)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Payload too large')
def test_413_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.errorhandler(413)
async def handle_413(req):
return '413', 400
client = TestClient(app)
res = self._run(client.post('/foo', body='x' * 17000))
self.assertEqual(res.status_code, 400)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '413')
def test_500(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 500)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Internal server error')
def test_500_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
@app.errorhandler(500)
def handle_500(req):
return '501', 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '501')
def test_exception_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
@app.errorhandler(ZeroDivisionError)
async def handle_div_zero(req, exc):
return '501', 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '501')
def test_exception_handler_parent(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(LookupError)
async def handle_lookup_error(req, exc):
return exc.__class__.__name__, 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'IndexError')
def test_exception_handler_redundant_parent(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(LookupError)
async def handle_lookup_error(req, exc):
return 'LookupError', 501
@app.errorhandler(IndexError)
async def handle_index_error(req, exc):
return 'IndexError', 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'IndexError')
def test_exception_handler_multiple_parents(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(Exception)
async def handle_generic_exception(req, exc):
return 'Exception', 501
@app.errorhandler(LookupError)
async def handle_lookup_error(req, exc):
return 'LookupError', 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'LookupError')
def test_exception_handler_no_viable_parents(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(RuntimeError)
async def handle_runtime_error(req, exc):
return 'RuntimeError', 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 500)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Internal server error')
def test_abort(self):
app = Microdot()
@app.route('/')
def index(req):
abort(406, 'Not acceptable')
return 'foo'
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 406)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Not acceptable')
def test_abort_handler(self):
app = Microdot()
@app.route('/')
def index(req):
abort(406)
return 'foo'
@app.errorhandler(406)
def handle_500(req):
return '406', 406
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 406)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '406')
def test_json_response(self):
app = Microdot()
@app.route('/dict')
async def json_dict(req):
return {'foo': 'bar'}
@app.route('/list')
def json_list(req):
return ['foo', 'bar']
client = TestClient(app)
res = self._run(client.get('/dict'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'application/json; charset=UTF-8')
self.assertEqual(res.json, {'foo': 'bar'})
res = self._run(client.get('/list'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'application/json; charset=UTF-8')
self.assertEqual(res.json, ['foo', 'bar'])
def test_binary_response(self):
app = Microdot()
@app.route('/bin')
def index(req):
return b'\xff\xfe', {'Content-Type': 'application/octet-stream'}
client = TestClient(app)
res = self._run(client.get('/bin'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'application/octet-stream')
self.assertEqual(res.text, None)
self.assertEqual(res.json, None)
self.assertEqual(res.body, b'\xff\xfe')
def test_streaming(self):
app = Microdot()
@app.route('/')
def index(req):
class stream():
def __init__(self):
self.i = 0
self.data = ['foo', b'bar']
def __aiter__(self):
return self
async def __anext__(self):
if self.i >= len(self.data):
raise StopAsyncIteration
data = self.data[self.i]
self.i += 1
return data
return stream()
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'foobar')
def test_already_handled_response(self):
app = Microdot()
@app.route('/')
def index(req):
return Response.already_handled
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res, None)

View File

@@ -1,71 +0,0 @@
import sys
try:
import uasyncio as asyncio
except ImportError:
import asyncio
import unittest
from microdot_asyncio import Microdot
from microdot_asyncio_websocket import with_websocket
from microdot_asyncio_test_client import TestClient
class TestMicrodotAsyncWebSocket(unittest.TestCase):
def _run(self, coro):
loop = asyncio.get_event_loop()
return loop.run_until_complete(coro)
def test_websocket_echo(self):
app = Microdot()
@app.route('/echo')
@with_websocket
async def index(req, ws):
while True:
data = await ws.receive()
await ws.send(data)
results = []
def ws():
data = yield 'hello'
results.append(data)
data = yield b'bye'
results.append(data)
data = yield b'*' * 300
results.append(data)
data = yield b'+' * 65537
results.append(data)
client = TestClient(app)
res = self._run(client.websocket('/echo', ws))
self.assertIsNone(res)
self.assertEqual(results, ['hello', b'bye', b'*' * 300, b'+' * 65537])
@unittest.skipIf(sys.implementation.name == 'micropython',
'no support for async generators in MicroPython')
def test_websocket_echo_async_client(self):
app = Microdot()
@app.route('/echo')
@with_websocket
async def index(req, ws):
while True:
data = await ws.receive()
await ws.send(data)
results = []
async def ws():
data = yield 'hello'
results.append(data)
data = yield b'bye'
results.append(data)
data = yield b'*' * 300
results.append(data)
data = yield b'+' * 65537
results.append(data)
client = TestClient(app)
res = self._run(client.websocket('/echo', ws))
self.assertIsNone(res)
self.assertEqual(results, ['hello', b'bye', b'*' * 300, b'+' * 65537])

Some files were not shown because too many files have changed in this diff Show More