1 Commits

Author SHA1 Message Date
Miguel Grinberg
7ee1c7eef9 Authentication support 2022-09-24 19:54:26 +01:00
156 changed files with 6892 additions and 5600 deletions

5
.coveragerc Normal file
View File

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

3
.flake8 Normal file
View File

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

View File

@@ -1,26 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**IMPORTANT**: If you have a question, or you are not sure if you have found a bug in this package, then you are in the wrong place. Hit back in your web browser, and then open a GitHub Discussion instead. Likewise, if you are unable to provide the information requested below, open a discussion to troubleshoot your issue.
**Describe the bug**
A clear and concise description of what the bug is. If you are getting errors, please include the complete error message, including the stack trace.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional context**
Add any other context about the problem here.

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: GitHub Discussions
url: https://github.com/miguelgrinberg/microdot/discussions
about: Ask questions here.

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -11,8 +11,8 @@ jobs:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-python@v3 - uses: actions/setup-python@v2
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions - run: pip install tox tox-gh-actions
- run: tox -eflake8 - run: tox -eflake8
@@ -21,12 +21,12 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python: ['3.8', '3.9', '3.10', '3.11', '3.12'] python: ['3.6', '3.7', '3.8', '3.9', '3.10']
fail-fast: false fail-fast: false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-python@v3 - uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
@@ -36,8 +36,8 @@ jobs:
name: tests-micropython name: tests-micropython
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-python@v3 - uses: actions/setup-python@v2
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions - run: pip install tox tox-gh-actions
- run: tox -eupy - run: tox -eupy
@@ -45,21 +45,18 @@ jobs:
name: coverage name: coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-python@v3 - uses: actions/setup-python@v2
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions - run: pip install tox tox-gh-actions codecov
- run: tox - run: tox
- uses: codecov/codecov-action@v3 - run: codecov
with:
files: ./coverage.xml
fail_ci_if_error: true
benchmark: benchmark:
name: benchmark name: benchmark
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-python@v3 - uses: actions/setup-python@v2
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions - run: pip install tox tox-gh-actions
- run: tox -ebenchmark - run: tox -ebenchmark

5
.gitignore vendored
View File

@@ -103,8 +103,3 @@ venv.bak/
# mypy # mypy
.mypy_cache/ .mypy_cache/
# other
*.der
*.pem
*_txt.py

View File

@@ -1,16 +0,0 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: docs/conf.py
python:
install:
- method: pip
path: .
extra_requirements:
- docs

View File

@@ -1,107 +1,5 @@
# Microdot change log # Microdot change log
**Release 2.0.2** - 2023-12-28
- Support binary data in the SSE extension ([commit](https://github.com/miguelgrinberg/microdot/commit/1fc11193da0d298f5539e2ad218836910a13efb2))
- Upgrade micropython tests to use v1.22 + initial CircuitPython testing work ([commit](https://github.com/miguelgrinberg/microdot/commit/79452a46992351ccad2c0317c20bf50be0d76641))
- Improvements to migration guide ([commit](https://github.com/miguelgrinberg/microdot/commit/84842e39c360a8b3ddf36feac8af201fb19bbb0b))
- Remove spurious async in documentation example [#187](https://github.com/miguelgrinberg/microdot/issues/187) ([commit](https://github.com/miguelgrinberg/microdot/commit/ad368be993e2e3007579f1d3880e36d60c71da92)) (thanks **Tak Tran**!)
**Release 2.0.1** - 2023-12-23
- Addressed some inadvertent mistakes in the template extensions ([commit](https://github.com/miguelgrinberg/microdot/commit/bd18ceb4424e9dfb52b1e6d498edd260aa24fc53))
**Release 2.0.0** - 2023-12-22
- Major redesign [#186](https://github.com/miguelgrinberg/microdot/issues/186) ([commit](https://github.com/miguelgrinberg/microdot/commit/20ea305fe793eb206b52af9eb5c5f3c1e9f57dbb))
- Code reorganization as a `microdot` package
- Asyncio is now the core implementation
- New support for Server-Sent Events (SSE)
- Several extensions redesigned
- Support for "partitioned" cookies
- [Cross-compiling and freezing](https://microdot.readthedocs.io/en/stable/freezing.html) guidance
- A [Migration Guide](https://microdot.readthedocs.io/en/stable/migrating.html) to help transition to version 2 from older releases
**Release 1.3.4** - 2023-11-08
- Handle change in `wait_closed()` behavior in Python 3.12 [#177](https://github.com/miguelgrinberg/microdot/issues/177) ([commit](https://github.com/miguelgrinberg/microdot/commit/5550b20cdd347d59e2aa68f6ebf9e9abffaff9fc))
- Added missing request argument in some documentation examples [#163](https://github.com/miguelgrinberg/microdot/issues/163) ([commit](https://github.com/miguelgrinberg/microdot/commit/744548f8dc33a72512b34c4001ee9c6c1edd22ee))
- Fix minor documentation typos [#161](https://github.com/miguelgrinberg/microdot/issues/161) ([commit](https://github.com/miguelgrinberg/microdot/commit/2e4911d10826cbb3914de4a45e495c3be36543fa)) (thanks **Andy Piper**!)
**Release 1.3.3** - 2023-07-16
- Handle query string arguments without value [#149](https://github.com/miguelgrinberg/microdot/issues/149) ([commit](https://github.com/miguelgrinberg/microdot/commit/3554bc91cb1523efa5b66fe3ef173f8e86e8c2a0))
- Support empty responses with ASGI adapter ([commit](https://github.com/miguelgrinberg/microdot/commit/e09e9830f43af41d38775547637558494151a385))
- Added CORS extension to Python package ([commit](https://github.com/miguelgrinberg/microdot/commit/304ca2ef6881fe718126b3e308211e760109d519))
- Document access to WSGI and ASGI attributes [#153](https://github.com/miguelgrinberg/microdot/issues/153) ([commit](https://github.com/miguelgrinberg/microdot/commit/d99df2c4010ab70c60b86ab334d656903e04eb26))
- Upgrade micropython tests to use v1.20 ([commit](https://github.com/miguelgrinberg/microdot/commit/e0f0565551966ee0238a5a1819c78a13639ad704))
**Release 1.3.2** - 2023-06-13
- In ASGI, return headers as strings and not binary [#144](https://github.com/miguelgrinberg/microdot/issues/144) ([commit](https://github.com/miguelgrinberg/microdot/commit/e92310fa55bbffcdcbb33f560e27c3579d7ac451))
- Incorrect import in `static_async.py` example ([commit](https://github.com/miguelgrinberg/microdot/commit/c07a53943508e64baea160748e67efc92e75b036))
**Release 1.3.1** - 2023-05-21
- Support negative numbers for int path components [#137](https://github.com/miguelgrinberg/microdot/issues/137) ([commit](https://github.com/miguelgrinberg/microdot/commit/a0dd7c8ab6d681932324e56ed101aba861a105a0))
- Use a more conservative default for socket timeout [#130](https://github.com/miguelgrinberg/microdot/issues/130) ([commit](https://github.com/miguelgrinberg/microdot/commit/239cf4ff37268a7e2467b93be44fe9f91cee8aee))
- More robust check for socket timeout error code [#106](https://github.com/miguelgrinberg/microdot/issues/106) ([commit](https://github.com/miguelgrinberg/microdot/commit/efec9f14be7b6f3451e4d1d0fe7e528ce6ca74dc))
- WebSocket error when handling PING packet [#129](https://github.com/miguelgrinberg/microdot/issues/129) ([commit](https://github.com/miguelgrinberg/microdot/commit/87cd098f66e24bed6bbad29b1490a129e355bbb3))
- Explicitly set UTF-8 encoding for HTML files in examples [#132](https://github.com/miguelgrinberg/microdot/issues/132) ([commit](https://github.com/miguelgrinberg/microdot/commit/f81de6d9582f4905b9c2735d3c639b92d7e77994))
**Release 1.3.0** - 2023-04-08
- Cross-Origin Resource Sharing (CORS) extension [#45](https://github.com/miguelgrinberg/microdot/issues/45) ([commit](https://github.com/miguelgrinberg/microdot/commit/67798f7dbffb30018ab4b62a9aaa297f63bc9e64))
- Respond to `HEAD` and `OPTIONS` requests ([commit](https://github.com/miguelgrinberg/microdot/commit/6a31f89673518e79fef5659c04e609b7976a5e34))
- Tolerate slightly invalid formats in query strings [#126](https://github.com/miguelgrinberg/microdot/issues/126) ([commit](https://github.com/miguelgrinberg/microdot/commit/a1b061656fa19dae583951596b0f1f0603652a56))
- Support compressed files in `send_file()` [#93](https://github.com/miguelgrinberg/microdot/issues/93) ([commit](https://github.com/miguelgrinberg/microdot/commit/daf1001ec55ab38e6cdfee4931729a3b7506858b))
- Add `max_age` argument to `send_file()` ([commit](https://github.com/miguelgrinberg/microdot/commit/e684ee32d91d3e2ab9569bb5fd342986c010ffeb))
- Add `update()` method to `NoCaseDict` class ([commit](https://github.com/miguelgrinberg/microdot/commit/ea6766cea96b756b36ed777f9c1b6a6680db09ba))
- Set exit code to 1 for failed MicroPython test runs ([commit](https://github.com/miguelgrinberg/microdot/commit/a350e8fd1e55fac12c9e5b909cfa82d880b177ef))
**Release 1.2.4** - 2023-03-03
- One more attempt to correct build issues ([commit](https://github.com/miguelgrinberg/microdot/commit/cb39898829f4edc233ab4e7ba3f7ef3c5c50f196))
**Release 1.2.3** - 2023-03-03
- Corrected a problem with previous build.
**Release 1.2.2** - 2023-03-03
- Add a socket read timeout to abort incomplete requests [#99](https://github.com/miguelgrinberg/microdot/issues/99) ([commit](https://github.com/miguelgrinberg/microdot/commit/d0d358f94a63f8565d6406feff0c6e7418cc7f81))
- More robust timeout handling [#106](https://github.com/miguelgrinberg/microdot/issues/106) ([commit](https://github.com/miguelgrinberg/microdot/commit/4d432a7d6cd88b874a8b825fb62891ed22881f74))
- Add @after_error_handler decorator [#97](https://github.com/miguelgrinberg/microdot/issues/97) ([commit](https://github.com/miguelgrinberg/microdot/commit/fcaeee69052b5681706f65b022e667baeee30d4d))
- Return headers as lowercase byte sequences as required by ASGI ([commit](https://github.com/miguelgrinberg/microdot/commit/ddb3b8f442d3683df04554104edaf8acd9c68148))
- Async example of static file serving ([commit](https://github.com/miguelgrinberg/microdot/commit/680cd9c023352f0ff03d67f1041ea174b7b7385b))
- Fixing broken links to examples in documentation [#101](https://github.com/miguelgrinberg/microdot/issues/101) ([commit](https://github.com/miguelgrinberg/microdot/commit/c00b24c9436e1b8f3d4c9bb6f2adfca988902e91)) (thanks **Eric Welch**!)
- Add scrollbar to documentation's left sidebar ([commit](https://github.com/miguelgrinberg/microdot/commit/2aa90d42451dc64c84efcc4f40a1b6c8d1ef1e8d))
- Documentation typo [#90](https://github.com/miguelgrinberg/microdot/issues/90) ([commit](https://github.com/miguelgrinberg/microdot/commit/81394980234f24aac834faf8e2e8225231e9014b)) (thanks **William Wheeler**!)
- Add CPU timing to benchmark ([commit](https://github.com/miguelgrinberg/microdot/commit/9398c960752f87bc32d7c4349cbf594e5d678e99))
- Upgrade uasyncio release used in tests ([commit](https://github.com/miguelgrinberg/microdot/commit/3d6815119ca1ec989f704f626530f938c857a8e5))
- Update unittest library for MicroPython ([commit](https://github.com/miguelgrinberg/microdot/commit/ecd84ecb7bd3c29d5af96739442b908badeab804))
- New build of micropython for unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/818f98d9a4e531e01c0f913813425ab2b40c289d))
- Remove 3.6, add 3.11 to builds ([commit](https://github.com/miguelgrinberg/microdot/commit/dd15d90239b73b5fd413515c9cd4ac23f6d42f67))
**Release 1.2.1** - 2022-12-06
- Error handling invokes parent exceptions [#74](https://github.com/miguelgrinberg/microdot/issues/74) ([commit](https://github.com/miguelgrinberg/microdot/commit/24d74fb8483b04e8abe6e303e06f0a310f32700b)) (thanks **Diego Pomares**!)
- Addressed error when deleting a user session in async app [#86](https://github.com/miguelgrinberg/microdot/issues/86) ([commit](https://github.com/miguelgrinberg/microdot/commit/5a589afd5e519e94e84fc1ee69033f2dad51c3ea))
- Add asyncio file upload example ([commit](https://github.com/miguelgrinberg/microdot/commit/c841cbedda40f59a9d87f6895fdf9fd954f854a2))
- New Jinja and uTemplate examples with Bootstrap ([commit](https://github.com/miguelgrinberg/microdot/commit/211ad953aeedb4c7f73fe210424aa173b4dc7fee))
- Fix typos in documentation [#77](https://github.com/miguelgrinberg/microdot/issues/77) ([commit](https://github.com/miguelgrinberg/microdot/commit/4a9b92b800d3fd87110f7bc9f546c10185ee13bc)) (thanks **Diego Pomares**!)
- Add missing exception argument to error handler example in documentation [#73](https://github.com/miguelgrinberg/microdot/issues/73) ([commit](https://github.com/miguelgrinberg/microdot/commit/c443599089f2127d1cb052dfba8a05c1969d65e3)) (thanks **Diego Pomares**!)
**Release 1.2.0** - 2022-09-25
- Use a case insensitive dict for headers ([commit #1](https://github.com/miguelgrinberg/microdot/commit/b0fd6c432371ca5cb10d07ff84c4deed7aa0ce2e) [commit #2](https://github.com/miguelgrinberg/microdot/commit/a8515c97b030f942fa6ca85cbe1772291468fb0d))
- urlencode() helper function ([commit #1](https://github.com/miguelgrinberg/microdot/commit/672512e086384e808489305502e6ebebcc5a888f) [commit #2](https://github.com/miguelgrinberg/microdot/commit/b133dcc34368853ee685396a1bcb50360e807813))
- Added `request.url` attribute with the complete URL of the request ([commit](https://github.com/miguelgrinberg/microdot/commit/1547e861ee28d43d10fe4c4ed1871345d4b81086))
- Do not log HTTPException occurrences ([commit](https://github.com/miguelgrinberg/microdot/commit/cbefb6bf3a3fdcff8b7a8bacad3449be18e46e3b))
- Cache user session for performance ([commit](https://github.com/miguelgrinberg/microdot/commit/01947b101ebe198312c88d73872e3248024918f0))
- File upload example ([commit](https://github.com/miguelgrinberg/microdot/commit/8ebe81c09b604ddc1123e78ad6bc87ceda5f8597))
- Minor documentation styling fixes ([commit](https://github.com/miguelgrinberg/microdot/commit/4f263c63ab7bb1ce0dd48d8e00f3c6891e1bf07e))
**Release 1.1.1** - 2022-09-18 **Release 1.1.1** - 2022-09-18
- Make WebSocket internals consistent between TLS and non-TLS [#61](https://github.com/miguelgrinberg/microdot/issues/61) ([commit](https://github.com/miguelgrinberg/microdot/commit/5693b812ceb2c0d51ec3c991adf6894a87e6fcc7)) - Make WebSocket internals consistent between TLS and non-TLS [#61](https://github.com/miguelgrinberg/microdot/issues/61) ([commit](https://github.com/miguelgrinberg/microdot/commit/5693b812ceb2c0d51ec3c991adf6894a87e6fcc7))

View File

@@ -1,5 +0,0 @@
include README.md LICENSE tox.ini
recursive-include docs *
recursive-exclude docs/_build *
recursive-include tests *
exclude **/*.pyc

View File

@@ -3,9 +3,9 @@
*“The impossibly small web framework for Python and MicroPython”* *“The impossibly small web framework for Python and MicroPython”*
Microdot is a minimalistic Python web framework inspired by Flask. Given its Microdot is a minimalistic Python web framework inspired by Flask, and designed
small size, it can run on systems with limited resources such as to run on systems with limited resources such as microcontrollers. It runs on
microcontrollers. Both standard Python (CPython) and MicroPython are supported. standard Python and on MicroPython.
```python ```python
from microdot import Microdot from microdot import Microdot
@@ -13,29 +13,13 @@ from microdot import Microdot
app = Microdot() app = Microdot()
@app.route('/') @app.route('/')
async def index(request): def index(request):
return 'Hello, world!' return 'Hello, world!'
app.run() 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 ## Resources
- Documentation - [Documentation](https://microdot.readthedocs.io/en/latest/)
- [Stable](https://microdot.readthedocs.io/en/stable/)
- [Latest](https://microdot.readthedocs.io/en/latest/)
- Still using version 1?
- [Code](https://github.com/miguelgrinberg/microdot/tree/v1)
- [Documentation](https://microdot.readthedocs.io/en/v1/)
- [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md) - [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)

Binary file not shown.

View File

@@ -1,8 +1,3 @@
.py.class, .py.function, .py.method, .py.property { .py.class, .py.function, .py.method, .py.property {
margin-top: 20px; margin-top: 20px;
} }
div.sphinxsidebar {
max-height: 100%;
overflow-y: auto;
}

View File

@@ -13,53 +13,97 @@ API Reference
.. autoclass:: microdot.Response .. autoclass:: microdot.Response
:members: :members:
.. autoclass:: microdot.NoCaseDict
``websocket`` extension
-----------------------
.. automodule:: microdot.websocket
:members: :members:
``utemplate`` templating extension .. autoclass:: microdot.MultiDict
----------------------------------
.. automodule:: microdot.utemplate
:members: :members:
``jinja`` templating extension ``microdot_asyncio`` module
------------------------------ ---------------------------
.. automodule:: microdot.jinja .. autoclass:: microdot_asyncio.Microdot
:inherited-members:
:members: :members:
``session`` extension .. autoclass:: microdot_asyncio.Request
--------------------- :inherited-members:
.. automodule:: microdot.session
:members: :members:
``cors`` extension .. autoclass:: microdot_asyncio.Response
------------------ :inherited-members:
.. automodule:: microdot.cors
:members: :members:
``test_client`` extension ``microdot_utemplate`` module
-----------------------------
.. automodule:: microdot_utemplate
:members:
``microdot_jinja`` module
------------------------- -------------------------
.. automodule:: microdot.test_client .. automodule:: microdot_jinja
:members: :members:
``asgi`` extension ``microdot_session`` module
------------------ ---------------------------
.. autoclass:: microdot.asgi.Microdot .. automodule:: microdot_session
: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
-----------------------
.. automodule:: microdot_ssl
:members:
``microdot_test_client`` module
-------------------------------
.. autoclass:: microdot_test_client.TestClient
:members:
.. autoclass:: microdot_test_client.TestResponse
:members:
``microdot_asyncio_test_client`` module
---------------------------------------
.. autoclass:: microdot_asyncio_test_client.TestClient
:members:
.. autoclass:: microdot_asyncio_test_client.TestResponse
:members:
``microdot_wsgi`` module
------------------------
.. autoclass:: microdot_wsgi.Microdot
:members: :members:
:exclude-members: shutdown, run :exclude-members: shutdown, run
``wsgi`` extension ``microdot_asgi`` module
------------------- ------------------------
.. autoclass:: microdot.wsgi.Microdot .. autoclass:: microdot_asgi.Microdot
:members: :members:
:exclude-members: shutdown, run :exclude-members: shutdown, run

View File

@@ -2,11 +2,11 @@ Core Extensions
--------------- ---------------
Microdot is a highly extensible web application framework. The extensions Microdot is a highly extensible web application framework. The extensions
described in this section are maintained as part of the Microdot project in described in this section are maintained as part of the Microdot project and
the same source code repository. can be obtained from the same source code repository.
WebSocket Support Asynchronous Support with Asyncio
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: .. list-table::
:align: left :align: left
@@ -15,71 +15,35 @@ WebSocket Support
- | CPython & MicroPython - | CPython & MicroPython
* - Required Microdot source files * - Required Microdot source files
- | `websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/websocket.py>`_ - | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
* - Required external dependencies * - Required external dependencies
- | None - | CPython: None
| MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_
* - Examples * - Examples
- | `echo.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo.py>`_ - | `hello_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_async.py>`_
The WebSocket extension gives the application the ability to handle WebSocket Microdot can be extended to use an asynchronous programming model based on the
requests. The :func:`with_websocket <microdot.websocket.with_websocket>` ``asyncio`` package. When the :class:`Microdot <microdot_asyncio.Microdot>`
decorator is used to mark a route handler as a WebSocket handler. Decorated class is imported from the ``microdot_asyncio`` package, an asynchronous server
routes receive a WebSocket object as a second argument. The WebSocket object is used, and handlers can be defined as coroutines.
provides ``send()`` and ``receive()`` asynchronous methods to send and receive
messages respectively.
Example:: The example that follows uses ``asyncio`` coroutines for concurrency::
@app.route('/echo') from microdot_asyncio import Microdot
@with_websocket
async def echo(request, ws):
while True:
message = await ws.receive()
await ws.send(message)
Server-Sent Events Support app = Microdot()
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: @app.route('/')
:align: left async def hello(request):
return 'Hello, world!'
* - Compatibility app.run()
- | CPython & MicroPython
* - Required Microdot source files Rendering HTML Templates
- | `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. Many web applications use HTML templates for rendering content to clients.
Microdot includes extensions to render templates with the Microdot includes extensions to render templates with the
@@ -97,42 +61,37 @@ Using the uTemplate Engine
- | CPython & MicroPython - | CPython & MicroPython
* - Required Microdot source files * - Required Microdot source files
- | `utemplate.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/utemplate.py>`_ - | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_utemplate.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_utemplate.py>`_
* - Required external dependencies * - Required external dependencies
- | `utemplate <https://github.com/pfalcon/utemplate/tree/master/utemplate>`_ - | `utemplate <https://github.com/pfalcon/utemplate/tree/master/utemplate>`_
* - Examples * - Examples
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/utemplate/hello.py>`_ - | `hello_utemplate.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_utemplate.py>`_
| `hello_utemplate_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_utemplate_async.py>`_
The :class:`Template <microdot.utemplate.Template>` class is used to load a The :func:`render_template <microdot_utemplate.render_template>` function is
template. The argument is the template filename, relative to the templates used to render HTML templates with the uTemplate engine. The first argument is
directory, which is *templates* by default. the template filename, relative to the templates directory, which is
*templates* by default. Any additional arguments are passed to the template
The ``Template`` object has a :func:`render() <microdot.utemplate.Template.render>` engine to be used as arguments.
method that renders the template to a string. This method receives any
arguments that are used by the template.
Example:: Example::
from microdot.utemplate import Template from microdot_utemplate import render_template
@app.get('/') @app.get('/')
async def index(req): def index(req):
return Template('index.html').render() return render_template('index.html')
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* The default location from where templates are loaded is the *templates*
subdirectory. This location can be changed with the subdirectory. This location can be changed with the
:func:`Template.initialize <microdot.utemplate.Template.initialize>` class :func:`init_templates <microdot_utemplate.init_templates>` function::
method::
Template.initialize('my_templates') from microdot_utemplate import init_templates
init_templates('my_templates')
Using the Jinja Engine Using the Jinja Engine
^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
@@ -144,51 +103,42 @@ Using the Jinja Engine
- | CPython only - | CPython only
* - Required Microdot source files * - Required Microdot source files
- | `jinja.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/jinja.py>`_ - | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_jinja.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_jinja.py>`_
* - Required external dependencies * - Required external dependencies
- | `Jinja2 <https://jinja.palletsprojects.com/>`_ - | `Jinja2 <https://jinja.palletsprojects.com/>`_
* - Examples * - Examples
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/jinja/hello.py>`_ - | `hello_jinja.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_jinja.py>`_
The :class:`Template <microdot.jinja.Template>` class is used to load a The :func:`render_template <microdot_jinja.render_template>` function is used
template. The argument is the template filename, relative to the templates to render HTML templates with the Jinja engine. The first argument is the
directory, which is *templates* by default. template filename, relative to the templates directory, which is *templates* by
default. Any additional arguments are passed to the template engine to be used
The ``Template`` object has a :func:`render() <microdot.jinja.Template.render>` as arguments.
method that renders the template to a string. This method receives any
arguments that are used by the template.
Example:: Example::
from microdot.jinja import Template from microdot_jinja import render_template
@app.get('/') @app.get('/')
async def index(req): def index(req):
return Template('index.html').render() return render_template('index.html')
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* The default location from where templates are loaded is the *templates*
subdirectory. This location can be changed with the subdirectory. This location can be changed with the
:func:`Template.initialize <microdot.jinja.Template.initialize>` class method:: :func:`init_templates <microdot_jinja.init_templates>` function::
Template.initialize('my_templates') from microdot_jinja import init_templates
The ``initialize()`` method also accepts ``enable_async`` argument, which init_templates('my_templates')
can be set to ``True`` if asynchronous rendering of templates is desired. If
this option is enabled, then the
:func:`render_async() <microdot.jinja.Template.render_async>` and
:func:`generate_async() <microdot.jinja.Template.generate_async>` methods
must be used.
.. note:: .. note::
The Jinja extension is not compatible with MicroPython. The Jinja extension is not compatible with MicroPython.
Maintaining Secure User Sessions Maintaing Secure User Sessions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: .. list-table::
:align: left :align: left
@@ -197,48 +147,56 @@ Maintaining Secure User Sessions
- | CPython & MicroPython - | CPython & MicroPython
* - Required Microdot source files * - Required Microdot source files
- | `session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/session.py>`_ - | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_session.py>`_
* - Required external dependencies * - Required external dependencies
- | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_ - | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
| MicroPython: `jwt.py <https://github.com/micropython/micropython-lib/blob/master/python-ecosys/pyjwt/jwt.py>`_, | MicroPython: `jwt.py <https://github.com/micropython/micropython-lib/blob/master/python-ecosys/pyjwt/jwt.py>`_,
`hmac.py <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_ `hmac <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_
* - Examples * - Examples
- | `login.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/sessions/login.py>`_ - | `login.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/login.py>`_
The session extension provides a secure way for the application to maintain The session extension provides a secure way for the application to maintain
user sessions. The session data is stored as a signed cookie in the client's user sessions. The session is stored as a signed cookie in the client's
browser, in `JSON Web Token (JWT) <https://en.wikipedia.org/wiki/JSON_Web_Token>`_ browser, in `JSON Web Token (JWT) <https://en.wikipedia.org/wiki/JSON_Web_Token>`_
format. format.
To work with user sessions, the application first must configure a secret key To work with user sessions, the application first must configure the secret key
that will be used to sign the session cookies. It is very important that this that will be used to sign the session cookies. It is very important that this
key is kept secret, as its name implies. An attacker who is in possession of key is kept secret. An attacker who is in possession of this key can generate
this key can generate valid user session cookies with any contents. valid user session cookies with any contents.
To initialize the session extension and configure the secret key, create a To set the secret key, use the :func:`set_session_secret_key <microdot_session.set_session_secret_key>` function::
:class:`Session <microdot.session.Session>` object::
Session(app, secret_key='top-secret') from microdot_session import set_session_secret_key
The :func:`with_session <microdot.session.with_session>` decorator is the set_session_secret_key('top-secret!')
most convenient way to retrieve the session at the start of a request::
from microdot import Microdot, redirect To :func:`get_session <microdot_session.get_session>`,
from microdot.session import Session, with_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
app = Microdot() app = Microdot()
Session(app, secret_key='top-secret') set_session_secret_key('top-secret')
@app.route('/', methods=['GET', 'POST']) @app.route('/', methods=['GET', 'POST'])
@with_session @with_session
async def index(req, session): def index(req, session):
username = session.get('username') username = session.get('username')
if req.method == 'POST': if req.method == 'POST':
username = req.form.get('username') username = req.form.get('username')
session['username'] = username update_session(req, {'username': username})
session.save()
return redirect('/') return redirect('/')
if username is None: if username is None:
return 'Not logged in' return 'Not logged in'
@@ -246,17 +204,12 @@ most convenient way to retrieve the session at the start of a request::
return 'Logged in as ' + username return 'Logged in as ' + username
@app.post('/logout') @app.post('/logout')
@with_session def logout(req):
async def logout(req, session): delete_session(req)
session.delete()
return redirect('/') return redirect('/')
The :func:`save() <microdot.session.SessionDict.save>` and WebSocket Support
:func:`delete() <microdot.session.SessionDict.delete>` methods are used to update ~~~~~~~~~~~~~~~~~
and destroy the user session respectively.
Cross-Origin Resource Sharing (CORS)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: .. list-table::
:align: left :align: left
@@ -265,33 +218,40 @@ Cross-Origin Resource Sharing (CORS)
- | CPython & MicroPython - | CPython & MicroPython
* - Required Microdot source files * - Required Microdot source files
- | `cors.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/cors.py>`_ - | `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>`_
* - Required external dependencies * - Required external dependencies
- | None - | None
* - Examples * - Examples
- | `cors.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/cors/cors.py>`_ - | `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 CORS extension provides support for `Cross-Origin Resource Sharing The WebSocket extension provides a way for the application to handle WebSocket
(CORS) <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_. CORS is a requests. The :func:`websocket <microdot_websocket.with_websocket>` decorator
mechanism that allows web applications running on different origins to access is used to mark a route handler as a WebSocket handler. The handler receives
resources from each other. For example, a web application running on a WebSocket object as a second argument. The WebSocket object provides
``https://example.com`` can access resources from ``https://api.example.com``. ``send()`` and ``receive()`` methods to send and receive messages respectively.
To enable CORS support, create an instance of the
:class:`CORS <microdot.cors.CORS>` class and configure the desired options.
Example:: Example::
from microdot import Microdot @app.route('/echo')
from microdot.cors import CORS @with_websocket
def echo(request, ws):
while True:
message = ws.receive()
ws.send(message)
app = Microdot() .. note::
cors = CORS(app, allowed_origins=['https://example.com'], An unsupported *microsoft_websocket_alt.py* module, with the same
allow_credentials=True) 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.
Testing with the Test Client Asynchronous WebSocket
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: .. list-table::
:align: left :align: left
@@ -300,18 +260,54 @@ Testing with the Test Client
- | CPython & MicroPython - | CPython & MicroPython
* - Required Microdot source files * - Required Microdot source files
- | `test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/test_client.py>`_ - | `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 * - Required external dependencies
- | None - | CPython: None
| MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_
The Microdot Test Client is a utility class that can be used in tests to send * - Examples
requests into the application without having to start a web server. - | `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 *microsoft_asgi_websocket.py* module, with the same
interface, is also provided. This module must be used instead of
*microsoft_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_asyncio_tls.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/tls/hello_asyncio_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.
Example:: Example::
from microdot import Microdot from microdot import Microdot
from microdot.test_client import TestClient from microdot_ssl import create_ssl_context
app = Microdot() app = Microdot()
@@ -319,13 +315,88 @@ Example::
def index(req): def index(req):
return 'Hello, World!' 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(): async def test_app():
client = TestClient(app) client = TestClient(app)
response = await client.get('/') response = await client.get('/')
assert response.text == 'Hello, World!' assert response.text == 'Hello, World!'
See the documentation for the :class:`TestClient <microdot.test_client.TestClient>` See the :class:`reference documentation <microdot_asyncio_test_client.TestClient>`
class for more details. for details.
Deploying on a Production Web Server Deploying on a Production Web Server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -333,53 +404,7 @@ Deploying on a Production Web Server
The ``Microdot`` class creates its own simple web server. This is enough for an 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 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 to use a separate, battle-tested web server. To address this need, Microdot
provides extensions that implement the ASGI and WSGI protocols. provides extensions that implement the WSGI and ASGI protocols.
Using an ASGI Web Server
^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:align: left
* - Compatibility
- | CPython only
* - Required Microdot source files
- | `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 ``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 ``asgi`` module::
from microdot.asgi import Microdot
app = Microdot()
@app.route('/')
async def index(req):
return 'Hello, World!'
The ``app`` application instance created from this class 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 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 Using a WSGI Web Server
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
@@ -391,27 +416,26 @@ Using a WSGI Web Server
- | CPython only - | CPython only
* - Required Microdot source files * - Required Microdot source files
- | `wsgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/wsgi.py>`_ - | `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 * - Required external dependencies
- | A WSGI web server, such as `Gunicorn <https://gunicorn.org/>`_. - | A WSGI web server, such as `Gunicorn <https://gunicorn.org/>`_.
* - Examples * - Examples
- | `hello_wsgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_wsgi.py>`_ - | `hello_wsgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/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 The ``microdot_wsgi`` module provides an extended ``Microdot`` class that
WSGI protocol and can be used with a compliant WSGI web server such as implements the WSGI protocol and can be used with a compliant WSGI web server
`Gunicorn <https://gunicorn.org/>`_ or such as `Gunicorn <https://gunicorn.org/>`_ or
`uWSGI <https://uwsgi-docs.readthedocs.io/en/latest/>`_. `uWSGI <https://uwsgi-docs.readthedocs.io/en/latest/>`_.
To use a WSGI web server, the application must import the To use a WSGI web server, the application must import the
:class:`Microdot <microdot.wsgi.Microdot>` class from the ``wsgi`` module:: :class:`Microdot <microdot_wsgi.Microdot>` class from the ``microdot_wsgi``
module::
from microdot.wsgi import Microdot from microdot_wsgi import Microdot
app = Microdot() app = Microdot()
@@ -419,19 +443,53 @@ To use a WSGI web server, the application must import the
def index(req): def index(req):
return 'Hello, World!' return 'Hello, World!'
The ``app`` application instance created from this class can be used as a WSGI The ``app`` application instance created from this class is a WSGI application
callbable with any complaint WSGI web server. If the above application that can be used with any complaint WSGI web server. If the above application
was stored in a file called *test.py*, then the following command runs the is stored in a file called *test.py*, then the following command runs the
web application using the Gunicorn web server:: web application using the Gunicorn web server::
gunicorn test:app gunicorn test:app
When using the WSGI support, the ``environ`` dictionary provided by the web Using an ASGI Web Server
server is available to request handlers as ``request.environ``. ^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:align: left
* - Compatibility
- | CPython only
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
| `microdot_asgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asgi.py>`_
* - Required external dependencies
- | An ASGI web server, such as `Uvicorn <https://uvicorn.org/>`_.
* - Examples
- | `hello_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_asgi.py>`_
The ``microdot_asgi`` module provides an extended ``Microdot`` class that
implements the ASGI protocol and can be used with a compliant ASGI server such
as `Uvicorn <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::
from microdot_asgi import Microdot
app = Microdot()
@app.route('/')
async def index(req):
return 'Hello, World!'
The ``app`` application instance created from this class is an ASGI application
that can be used with any complaint ASGI web server. If the above application
is stored in a file called *test.py*, then the following command runs the
web application using the Uvicorn web server::
uvicorn test:app
.. 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.

View File

@@ -1,110 +0,0 @@
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,17 +9,15 @@ Microdot
*"The impossibly small web framework for Python and MicroPython"* *"The impossibly small web framework for Python and MicroPython"*
Microdot is a minimalistic Python web framework inspired by Microdot is a minimalistic Python web framework inspired by
`Flask <https://flask.palletsprojects.com/>`_. Given its size, it can run on `Flask <https://flask.palletsprojects.com/>`_, and designed to run on
systems with limited resources such as microcontrollers. Both standard Python systems with limited resources such as microcontrollers. It runs on standard
(CPython) and `MicroPython <https://micropython.org>`_ are supported. Python and on `MicroPython <https://micropython.org>`_.
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 3
intro intro
extensions extensions
migrating
freezing
api api
* :ref:`genindex` * :ref:`genindex`

View File

@@ -1,49 +1,26 @@
Installation Installation
------------ ------------
The installation method is different depending on the version of Python. For standard Python (CPython) projects, Microdot and all of its core extensions
can be installed with ``pip``::
CPython Installation
~~~~~~~~~~~~~~~~~~~~
For use with standard Python (CPython) projects, Microdot and all of its core
extensions are installed with ``pip``::
pip install microdot pip install microdot
MicroPython Installation 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
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>`_ `GitHub repository <https://github.com/miguelgrinberg/microdot/tree/main/src>`_
into your device, ideally after into your device, possibly after
`compiling <https://docs.micropython.org/en/latest/reference/mpyfiles.html>`_ `compiling <https://docs.micropython.org/en/latest/reference/mpyfiles.html>`_
them to *.mpy* files. These source files can also be them to *.mpy* files. These source files can also be
`frozen <https://docs.micropython.org/en/latest/develop/optimizations.html?highlight=frozen#frozen-bytecode>`_ `frozen <https://docs.micropython.org/en/latest/develop/optimizations.html?highlight=frozen#frozen-bytecode>`_
and incorporated into a custom MicroPython firmware. 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 Getting Started
--------------- ---------------
This section describes the main features of Microdot in an informal manner. This section describes the main features of Microdot in an informal manner. For
detailed reference information, consult the :ref:`API Reference`.
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 A Simple Microdot Web Server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -55,7 +32,7 @@ The following is an example of a simple web server::
app = Microdot() app = Microdot()
@app.route('/') @app.route('/')
async def index(request): def index(request):
return 'Hello, world!' return 'Hello, world!'
app.run() app.run()
@@ -69,23 +46,17 @@ application.
The ``route()`` decorator takes the path portion of the URL as an 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 argument, and maps it to the decorated function, so that the function is called
when the client requests the URL. when the client requests the URL. The function is passed a
:class:`Request <microdot.Request>` object as an argument, which provides
When the function is called, it is passed a :class:`Request <microdot.Request>` access to the information passed by the client. The value returned by the
object as an argument, which provides access to the information passed by the function is sent back to the client as the response.
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 The :func:`run() <microdot.Microdot.run>` method starts the application's web
server on port 5000 by default. This method blocks while it waits for server on port 5000 (or the port number passed in the ``port`` argument). This
connections from clients. method blocks while it waits for connections from clients.
Running with CPython Running with CPython
^^^^^^^^^^^^^^^^^^^^ ~~~~~~~~~~~~~~~~~~~~
.. list-table:: .. list-table::
:align: left :align: left
@@ -100,18 +71,17 @@ Running with CPython
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello.py>`_ - | `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 When using CPython, you can start the web server by running the script that
has the ``app.run()`` call at the bottom:: defines and runs the application instance::
python main.py python main.py
After starting the script, open a web browser and navigate to While the script is running, you can open a web browser and navigate to
*http://localhost:5000/* to access the application at the default address for *http://localhost:5000/*, which is the default address for the Microdot web
the Microdot web server. From other computers in the same network, use the IP server. From other computers in the same network, use the IP address or
address or hostname of the computer running the script instead of hostname of the computer running the script instead of ``localhost``.
``localhost``.
Running with MicroPython Running with MicroPython
^^^^^^^^^^^^^^^^^^^^^^^^ ~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: .. list-table::
:align: left :align: left
@@ -127,13 +97,11 @@ Running with MicroPython
| `gpio.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/gpio/gpio.py>`_ | `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 When using MicroPython, you can upload a *main.py* file containing the web
server code to your device, along with the required Microdot files, as defined server code to your device along with *microdot.py*. MicroPython will
in the :ref:`MicroPython Installation` section. 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
MicroPython will automatically run *main.py* when the device is powered on, so device's IP address. As indicated above, the port can be changed by passing the
the web server will automatically start. The application can be accessed on ``port`` argument to the ``run()`` method.
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:: .. note::
Microdot does not configure the network interface of the device in which it Microdot does not configure the network interface of the device in which it
@@ -141,41 +109,6 @@ changed by passing the ``port`` argument to the ``run()`` method.
advance, for example to a Wi-Fi access point, this must be configured before advance, for example to a Wi-Fi access point, this must be configured before
the ``run()`` method is invoked. 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 Defining Routes
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
@@ -186,7 +119,7 @@ to the decorator is the path portion of the URL.
The following example creates a route for the root URL of the application:: The following example creates a route for the root URL of the application::
@app.route('/') @app.route('/')
async def index(request): def index(request):
return 'Hello, world!' return 'Hello, world!'
When a client requests the root URL (for example, *http://localhost:5000/*), When a client requests the root URL (for example, *http://localhost:5000/*),
@@ -194,11 +127,11 @@ Microdot will call the ``index()`` function, passing it a
:class:`Request <microdot.Request>` object. The return value of the function :class:`Request <microdot.Request>` object. The return value of the function
is the response that is sent to the client. is the response that is sent to the client.
Below is another example, this one with a route for a URL with two components Below is a another example, this one with a route for a URL with two components
in its path:: in its path::
@app.route('/users/active') @app.route('/users/active')
async def active_users(request): def active_users(request):
return 'Active users: Susan, Joe, and Bob' return 'Active users: Susan, Joe, and Bob'
The complete URL that maps to this route is The complete URL that maps to this route is
@@ -211,49 +144,46 @@ request.
Choosing the HTTP Method Choosing the HTTP Method
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
All the example routes shown above are associated with ``GET`` requests, which All the example routes shown above are associated with ``GET`` requests. But
are the default. Applications often need to define routes for other HTTP applications often need to define routes for other HTTP methods, such as
methods, such as ``POST``, ``PUT``, ``PATCH`` and ``DELETE``. The ``route()`` ``POST``, ``PUT``, ``PATCH`` and ``DELETE``. The ``route()`` decorator takes a
decorator takes a ``methods`` optional argument, in which the application can ``methods`` optional argument, in which the application can provide a list of
provide a list of HTTP methods that the route should be associated with on the HTTP methods that the route should be associated with on the given path.
given path.
The following example defines a route that handles ``GET`` and ``POST`` The following example defines a route that handles ``GET`` and ``POST``
requests within the same function:: requests within the same function::
@app.route('/invoices', methods=['GET', 'POST']) @app.route('/invoices', methods=['GET', 'POST'])
async def invoices(request): def invoices(request):
if request.method == 'GET': if request.method == 'GET':
return 'get invoices' return 'get invoices'
elif request.method == 'POST': elif request.method == 'POST':
return 'create an invoice' return 'create an invoice'
As an alternative to the example above, in which a single function is used to In cases like the above, where a single URL is used to handle multiple HTTP
handle multiple HTTP methods, sometimes it may be desirable to write a separate methods, it may be desirable to write a separate function for each HTTP method.
function for each HTTP method. The above example can be implemented with two The above example can be implemented with two routes as follows::
routes as follows::
@app.route('/invoices', methods=['GET']) @app.route('/invoices', methods=['GET'])
async def get_invoices(request): def get_invoices(request):
return 'get invoices' return 'get invoices'
@app.route('/invoices', methods=['POST']) @app.route('/invoices', methods=['POST'])
async def create_invoice(request): def create_invoice(request):
return 'create an invoice' return 'create an invoice'
Microdot provides the :func:`get() <microdot.Microdot.get>`, Microdot provides the :func:`get() <microdot.Microdot.get>`,
:func:`post() <microdot.Microdot.post>`, :func:`put() <microdot.Microdot.put>`, :func:`post() <microdot.Microdot.post>`, :func:`put() <microdot.Microdot.put>`,
:func:`patch() <microdot.Microdot.patch>`, and :func:`patch() <microdot.Microdot.patch>`, and
:func:`delete() <microdot.Microdot.delete>` decorators as shortcuts for the :func:`delete() <microdot.Microdot.delete>` decorator shortcuts as well. The
corresponding HTTP methods. The two example routes above can be written more two example routes above can be written more concisely with them::
concisely with them::
@app.get('/invoices') @app.get('/invoices')
async def get_invoices(request): def get_invoices(request):
return 'get invoices' return 'get invoices'
@app.post('/invoices') @app.post('/invoices')
async def create_invoice(request): def create_invoice(request):
return 'create an invoice' return 'create an invoice'
Including Dynamic Components in the URL Path Including Dynamic Components in the URL Path
@@ -265,19 +195,19 @@ the following route associates all URLs that have a path following the pattern
*http://localhost:5000/users/<username>* with the ``get_user()`` function:: *http://localhost:5000/users/<username>* with the ``get_user()`` function::
@app.get('/users/<username>') @app.get('/users/<username>')
async def get_user(request, username): def get_user(request, username):
return 'User: ' + username return 'User: ' + username
As shown in the example, a path component that is enclosed in angle brackets As shown in the example, a path components that is enclosed in angle brackets
is considered a placeholder. Microdot accepts any values for that portion of is considered dynamic. Microdot accepts any values for that section of the URL
the URL path, and passes the value received to the function as an argument path, and passes the value received to the function as an argument after
after the request object. the request object.
Routes are not limited to a single dynamic component. The following route shows Routes are not limited to a single dynamic component. The following route shows
how multiple dynamic components can be included in the path:: how multiple dynamic components can be included in the path::
@app.get('/users/<firstname>/<lastname>') @app.get('/users/<firstname>/<lastname>')
async def get_user(request, firstname, lastname): def get_user(request, firstname, lastname):
return 'User: ' + firstname + ' ' + lastname return 'User: ' + firstname + ' ' + lastname
Dynamic path components are considered to be strings by default. An explicit Dynamic path components are considered to be strings by default. An explicit
@@ -286,7 +216,7 @@ a colon. The following route has two dynamic components declared as an integer
and a string respectively:: and a string respectively::
@app.get('/users/<int:id>/<string:username>') @app.get('/users/<int:id>/<string:username>')
async def get_user(request, id, username): def get_user(request, id, username):
return 'User: ' + username + ' (' + str(id) + ')' return 'User: ' + username + ' (' + str(id) + ')'
If a dynamic path component is defined as an integer, the value passed to the If a dynamic path component is defined as an integer, the value passed to the
@@ -295,12 +225,10 @@ integer in the corresponding section of the URL path, then the URL will not
match and the route will not be called. match and the route will not be called.
A special type ``path`` can be used to capture the remainder of the path as a A special type ``path`` can be used to capture the remainder of the path as a
single argument. The difference between an argument of type ``path`` and one of single argument::
type ``string`` is that the latter stops capturing when a ``/`` appears in the
URL::
@app.get('/tests/<path:path>') @app.get('/tests/<path:path>')
async def get_test(request, path): def get_test(request, path):
return 'Test: ' + path return 'Test: ' + path
For the most control, the ``re`` type allows the application to provide a For the most control, the ``re`` type allows the application to provide a
@@ -309,7 +237,7 @@ a route that only matches usernames that begin with an upper or lower case
letter, followed by a sequence of letters or numbers:: letter, followed by a sequence of letters or numbers::
@app.get('/users/<re:[a-zA-Z][a-zA-Z0-9]*:username>') @app.get('/users/<re:[a-zA-Z][a-zA-Z0-9]*:username>')
async def get_user(request, username): def get_user(request, username):
return 'User: ' + username return 'User: ' + username
.. note:: .. note::
@@ -327,56 +255,48 @@ resource can be obtained from a cache. The
:func:`before_request() <microdot.Microdot.before_request>` decorator registers :func:`before_request() <microdot.Microdot.before_request>` decorator registers
a function to be called before the request is dispatched to the route function. 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:: client is authenticated before the request is handled::
@app.before_request @app.before_request
async def authenticate(request): def authenticate(request):
user = authorize(request) user = authorize(request)
if not user: if not user:
return 'Unauthorized', 401 return 'Unauthorized', 401
request.g.user = user 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 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 power to intercept a request if necessary. The example above uses this
technique to prevent an unauthorized user from accessing the requested technique to prevent an unauthorized user from accessing the requested
route. resource.
After-request handlers registered with the After request handlers registered with the
:func:`after_request() <microdot.Microdot.after_request>` decorator are called :func:`after_request() <microdot.Microdot.after_request>` decorator are called
after the route function returns a response. Their purpose is to perform any 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 common closing or cleanup tasks. The next example shows a combination of before
before- and after-request handlers that print the time it takes for a request and after request handlers that print the time it takes for a request to be
to be handled:: handled::
@app.before_request @app.before_request
async def start_timer(request): def start_timer(request):
request.g.start_time = time.time() request.g.start_time = time.time()
@app.after_request @ap.after_request
async def end_timer(request, response): def end_timer(request, response):
duration = time.time() - request.g.start_time duration = time.time() - request.g.start_time
print(f'Request took {duration:0.2f} seconds') print(f'Request took {duration:0.2f} seconds')
After-request handlers receive the request and response objects as arguments, After request handlers receive the request and response objects as arguments.
and they can return a modified response object to replace the original. If The function can return a modified response object to replace the original. If
no value is returned from an after-request handler, then the original response the function does not return a value, then the original response object is
object is used. used.
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 after performing any necessary
cleanup.
.. note:: .. note::
The :ref:`request.g <The "g" Object>` object used in many of the above The :ref:`request.g <The "g" Object>` object is a special object that allows
examples is a special object that allows the before- and after-request the before and after request handlers, as well sa the route function to
handlers, as well as the route function to share data during the life of the share data during the life of the request.
request.
Error Handlers Error Handlers
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
@@ -386,44 +306,38 @@ the client receives an appropriate error response. Some of the common errors
automatically handled by Microdot are: automatically handled by Microdot are:
- 400 for malformed requests. - 400 for malformed requests.
- 404 for URLs that are unknown. - 404 for URLs that are not defined.
- 405 for URLs that are known, but not implemented for the requested HTTP - 405 for URLs that are defined, but not for the requested HTTP method.
method.
- 413 for requests that are larger than the allowed size. - 413 for requests that are larger than the allowed size.
- 500 when the application raises an unhandled exception. - 500 when the application raises an exception.
While the above errors are fully complaint with the HTTP specification, the While the above errors are fully complaint with the HTTP specification, the
application might want to provide custom responses for them. The application might want to provide custom responses for them. The
:func:`errorhandler() <microdot.Microdot.errorhandler>` decorator registers :func:`errorhandler() <microdot.Microdot.errorhandler>` decorator registers
functions to respond to specific error codes. The following example shows a a functions to respond to specific error codes. The following example shows a
custom error handler for 404 errors:: custom error handler for 404 errors::
@app.errorhandler(404) @app.errorhandler(404)
async def not_found(request): def not_found(request):
return {'error': 'resource not found'}, 404 return {'error': 'resource not found'}, 404
The ``errorhandler()`` decorator has a second form, in which it takes an The ``errorhandler()`` decorator has a second form, in which it takes an
exception class as an argument. Microdot will invoke the handler when an exception class as an argument. Microdot will then invoke the handler when an
unhandled exception that is an instance of the given class is raised. The next exception of that class is raised. The next example provides a custom response
example provides a custom response for division by zero errors:: for division by zero errors::
@app.errorhandler(ZeroDivisionError) @app.errorhandler(ZeroDivisionError)
async def division_by_zero(request, exception): def division_by_zero(request):
return {'error': 'division by zero'}, 500 return {'error': 'division by zero'}, 500
When the raised exception class does not have an error handler defined, but
one or more of its parent classes do, Microdot makes an attempt to invoke the
most specific handler.
Mounting a Sub-Application Mounting a Sub-Application
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
Small Microdot applications can be written as a single source file, but this Small Microdot applications can be written an a single source file, but this
is not the best option for applications that past a certain size. To make it is not the best option for applications that past certain size. To make it
simpler to write large applications, Microdot supports the concept of simpler to write large applications, Microdot supports the concept of
sub-applications that can be "mounted" on a larger application, possibly with sub-applications that can be "mounted" on a larger application, possibly with
a common URL prefix applied to all of its routes. For developers familiar with a common URL prefix applied to all of its routes.
the Flask framework, this is a similar concept to Flask's blueprints.
Consider, for example, a *customers.py* sub-application that implements Consider, for example, a *customers.py* sub-application that implements
operations on customers:: operations on customers::
@@ -433,14 +347,14 @@ operations on customers::
customers_app = Microdot() customers_app = Microdot()
@customers_app.get('/') @customers_app.get('/')
async def get_customers(request): def get_customers(request):
# return all customers # return all customers
@customers_app.post('/') @customers_app.post('/')
async def new_customer(request): def new_customer(request):
# create a new customer # create a new customer
Similar to the above, the *orders.py* sub-application implements operations on In the same way, the *orders.py* sub-application implements operations on
customer orders:: customer orders::
from microdot import Microdot from microdot import Microdot
@@ -448,15 +362,15 @@ customer orders::
orders_app = Microdot() orders_app = Microdot()
@orders_app.get('/') @orders_app.get('/')
async def get_orders(request): def get_orders(request):
# return all orders # return all orders
@orders_app.post('/') @orders_app.post('/')
async def new_order(request): def new_order(request):
# create a new order # create a new order
Now the main application, which is stored in *main.py*, can import and mount Now the main application, which is stored in *main.py*, can import and mount
the sub-applications to build the larger combined application:: the sub-applications to build the combined application::
from microdot import Microdot from microdot import Microdot
from customers import customers_app from customers import customers_app
@@ -475,7 +389,7 @@ The resulting application will have the customer endpoints available at
*/customers/* and the order endpoints available at */orders/*. */customers/* and the order endpoints available at */orders/*.
.. note:: .. 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. 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 Once installed in the main application, these handlers will apply to the
whole application and not just the sub-application in which they were whole application and not just the sub-application in which they were
@@ -492,20 +406,16 @@ 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:: request completes. The next example shows how to use this feature::
@app.get('/shutdown') @app.get('/shutdown')
async def shutdown(request): def shutdown(request):
request.app.shutdown() request.app.shutdown()
return 'The server is shutting down...' 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 Request Object
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
The :class:`Request <microdot.Request>` object encapsulates all the information 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 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 Request Attributes
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
@@ -528,9 +438,6 @@ The request object provides access to the request attributes, including:
the client, as a tuple (host, port). the client, as a tuple (host, port).
- :attr:`app <microdot.Request.app>`: The application instance that created the - :attr:`app <microdot.Request.app>`: The application instance that created the
request. 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 JSON Payloads
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
@@ -541,7 +448,7 @@ application can access the parsed JSON data using the
to use this attribute:: to use this attribute::
@app.post('/customers') @app.post('/customers')
async def create_customer(request): def create_customer(request):
customer = request.json customer = request.json
# do something with customer # do something with customer
return {'success': True} return {'success': True}
@@ -558,7 +465,7 @@ The request object also supports standard HTML form submissions through the
as a :class:`MultiDict <microdot.MultiDict>` object. Example:: as a :class:`MultiDict <microdot.MultiDict>` object. Example::
@app.route('/', methods=['GET', 'POST']) @app.route('/', methods=['GET', 'POST'])
async def index(req): def index(req):
name = 'Unknown' name = 'Unknown'
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
@@ -574,19 +481,16 @@ Accessing the Raw Request Body
For cases in which neither JSON nor form data is expected, the For cases in which neither JSON nor form data is expected, the
:attr:`body <microdot.Request.body>` request attribute returns the entire body :attr:`body <microdot.Request.body>` request attribute returns the entire body
of the request as a byte sequence. of the request as a byte sequence.
If the expected body is too large to fit safely in memory, the application can If the expected body is too large to fit in memory, the application can use the
use the :attr:`stream <microdot.Request.stream>` request attribute to read the :attr:`stream <microdot.Request.stream>` request attribute to read the body
body contents as a file-like object. The contents as a file-like object.
: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 Cookies
^^^^^^^ ^^^^^^^
Cookies that are sent by the client are made available through the Cookies that are sent by the client are made available throught the
:attr:`cookies <microdot.Request.cookies>` attribute of the request object in :attr:`cookies <microdot.Request.cookies>` attribute of the request object in
dictionary form. dictionary form.
@@ -594,40 +498,41 @@ The "g" Object
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
Sometimes applications need to store data during the lifetime of a request, so Sometimes applications need to store data during the lifetime of a request, so
that it can be shared between the before- and after-request handlers, the that it can be shared between the before or after request handlers and the
route function and any error handlers. The request object provides the route function. The request object provides the :attr:`g <microdot.Request.g>`
:attr:`g <microdot.Request.g>` attribute for that purpose. attribute for that purpose.
In the following example, a before request handler authorizes the client and In the following example, a before request handler
stores the username so that the route function can use it:: authorizes the client and stores the username so that the route function can
use it::
@app.before_request @app.before_request
async def authorize(request): def authorize(request):
username = authenticate_user(request) username = authenticate_user(request)
if not username: if not username:
return 'Unauthorized', 401 return 'Unauthorized', 401
request.g.username = username request.g.username = username
@app.get('/') @app.get('/')
async def index(request): def index(request):
return f'Hello, {request.g.username}!' 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 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. using the :func:`after_request <microdot.Microdot.after_request>` decorator.
Request-specific after-request handlers are called by Microdot after the route Request-specific after request handlers are called by Microdot after the route
function returns and all the application-wide after-request handlers have been function returns and all the application's after request handlers have been
called. called.
The next example shows how a cookie can be updated using a request-specific 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') @app.post('/logout')
async def logout(request): def logout(request):
@request.after_request @request.after_request
def reset_session(request, response): def reset_session(request, response):
response.set_cookie('session', '', http_only=True) response.set_cookie('session', '', http_only=True)
@@ -641,24 +546,22 @@ Request Limits
To help prevent malicious attacks, Microdot provides some configuration options To help prevent malicious attacks, Microdot provides some configuration options
to limit the amount of information that is accepted: to limit the amount of information that is accepted:
- :attr:`max_content_length <microdot.Request.max_content_length>`: The - :attr:`max_content_length <microdot.Microdot.max_content_length>`: The
maximum size accepted for the request body, in bytes. When a client sends a 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. request that is larger than this, the server will respond with a 413 error.
The default is 16KB. The default is 16KB.
- :attr:`max_body_length <microdot.Request.max_body_length>`: The maximum - :attr:`max_body_length <microdot.Microdot.max_body_length>`: The maximum
size that is loaded in the :attr:`body <microdot.Request.body>` attribute, in 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 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 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:`stream <microdot.Request.stream>` attribute. The default is also 16KB.
- :attr:`max_readline <microdot.Request.max_readline>`: The maximum allowed - :attr:`max_readline <microdot.Microdot.max_readline>`: The maximum allowed
size for a request line, in bytes. The default is 2KB. size for a request line, in bytes. The default is 2KB.
The following example configures the application to accept requests with The following example configures the application to accept requests with
payloads up to 1MB in size, but prevents requests that are larger than 8KB from payloads up to 1MB big, but prevents requests that are larger than 8KB from
being loaded into memory:: being loaded into memory::
from microdot import Request
Request.max_content_length = 1024 * 1024 Request.max_content_length = 1024 * 1024
Request.max_body_length = 8 * 1024 Request.max_body_length = 8 * 1024
@@ -676,34 +579,33 @@ Route functions can return one, two or three values. The first or only value is
always returned to the client in the response body:: always returned to the client in the response body::
@app.get('/') @app.get('/')
async def index(request): def index(request):
return 'Hello, World!' return 'Hello, World!'
In the above example, Microdot issues a standard 200 status code response, and In the above example, Microdot issues a standard 200 status code response, and
inserts default headers. inserts the necessary headers.
The application can provide its own status code as a second value returned from The applicaton can provide its own status code as a second value returned from
the route to override the 200 default. The example below returns a 202 status the route. The example below returns a 202 status code::
code::
@app.get('/') @app.get('/')
async def index(request): def index(request):
return 'Hello, World!', 202 return 'Hello, World!', 202
The application can also return a third value, a dictionary with additional The application can also return a third value, a dictionary with additional
headers that are added to, or replace the default ones included by Microdot. headers that are added to, or replace the default ones provided by Microdot.
The next example returns an HTML response, instead of a default text response:: The next example returns an HTML response, instead of a default text response::
@app.get('/') @app.get('/')
async def index(request): def index(request):
return '<h1>Hello, World!</h1>', 202, {'Content-Type': 'text/html'} return '<h1>Hello, World!</h1>', 202, {'Content-Type': 'text/html'}
If the application needs to return custom headers, but does not need to change If the application needs to return custom headers, but does not need to change
the default status code, then it can return two values, omitting the status the default status code, then it can return two values, omitting the stauts
code:: code::
@app.get('/') @app.get('/')
async def index(request): def index(request):
return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'} return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'}
The application can also return a :class:`Response <microdot.Response>` object The application can also return a :class:`Response <microdot.Response>` object
@@ -719,7 +621,7 @@ automatically format the response as JSON.
Example:: Example::
@app.get('/') @app.get('/')
async def index(request): def index(request):
return {'hello': 'world'} return {'hello': 'world'}
.. note:: .. note::
@@ -735,7 +637,7 @@ creates redirect responses::
from microdot import redirect from microdot import redirect
@app.get('/') @app.get('/')
async def index(request): def index(request):
return redirect('/about') return redirect('/about')
File Responses File Responses
@@ -743,22 +645,13 @@ File Responses
The :func:`send_file <microdot.Response.send_file>` function builds a response The :func:`send_file <microdot.Response.send_file>` function builds a response
object for a file:: object for a file::
from microdot import send_file from microdot import send_file
@app.get('/') @app.get('/')
async def index(request): def index(request):
return send_file('/static/index.html') return send_file('/static/index.html')
A suggested caching duration can be returned to the client in the ``max_age``
argument::
from microdot import send_file
@app.get('/')
async def image(request):
return send_file('/static/image.jpg', max_age=3600) # in seconds
.. note:: .. note::
Unlike other web frameworks, Microdot does not automatically configure a Unlike other web frameworks, Microdot does not automatically configure a
route to serve static files. The following is an example route that can be route to serve static files. The following is an example route that can be
@@ -766,22 +659,22 @@ argument::
the project:: the project::
@app.route('/static/<path:path>') @app.route('/static/<path:path>')
async def static(request, path): def static(request, path):
if '..' in path: if '..' in path:
# directory traversal is not allowed # directory traversal is not allowed
return 'Not found', 404 return 'Not found', 404
return send_file('static/' + path, max_age=86400) return send_file('static/' + path)
Streaming Responses Streaming Responses
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
Instead of providing a response as a single value, an application can opt to 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 Python generator. return a response that is generated in chunks by returning a generator. The
The example below returns all the numbers in the fibonacci sequence below 100:: example below returns all the numbers in the fibonacci sequence below 100::
@app.get('/fibonacci') @app.get('/fibonacci')
async def fibonacci(request): def fibonacci(request):
async def generate_fibonacci(): def generate_fibonacci():
a, b = 0, 1 a, b = 0, 1
while a < 100: while a < 100:
yield str(a) + '\n' yield str(a) + '\n'
@@ -789,14 +682,6 @@ The example below returns all the numbers in the fibonacci sequence below 100::
return generate_fibonacci() 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 Changing the Default Response Content Type
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -824,14 +709,14 @@ object to add a properly formatted cookie header to the response.
Given that route functions do not normally work directly with 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 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:: Example::
@app.get('/') @app.get('/')
async def index(request): def index(request):
@request.after_request @request.after_request
async def set_cookie(request, response): def set_cookie(request, response):
response.set_cookie('name', 'value') response.set_cookie('name', 'value')
return response return response
@@ -840,7 +725,7 @@ Example::
Another option is to create a response object directly in the route function:: Another option is to create a response object directly in the route function::
@app.get('/') @app.get('/')
async def index(request): def index(request):
response = Response('Hello, World!') response = Response('Hello, World!')
response.set_cookie('name', 'value') response.set_cookie('name', 'value')
return response return response
@@ -849,24 +734,21 @@ Another option is to create a response object directly in the route function::
Standard cookies do not offer sufficient privacy and security controls, so Standard cookies do not offer sufficient privacy and security controls, so
never store sensitive information in them unless you are adding additional never store sensitive information in them unless you are adding additional
protection mechanisms such as encryption or cryptographic signing. The protection mechanisms such as encryption or cryptographic signing. The
:ref:`session <Maintaining Secure User Sessions>` extension implements signed :ref:`session <Maintaing Secure User Sessions>` extension implements signed
cookies that prevent tampering by malicious actors. cookies that prevent tampering by malicious actors.
Concurrency Concurrency
~~~~~~~~~~~ ~~~~~~~~~~~
Microdot implements concurrency through the ``asyncio`` package. Applications By default, Microdot runs in synchronous (single-threaded) mode. However, if
must ensure their handlers do not block, as this will prevent other concurrent the ``threading`` module is available, each request will be started on a
requests from being handled. separate thread and requests will be handled concurrently.
When running under CPython, ``async def`` handler functions run as native Be aware that most microcontroller boards support a very limited form of
asyncio tasks, while ``def`` handler functions are executed in a multi-threading that is not appropriate for concurrent request handling. For
`thread executor <https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor>`_ that reason, use of the `threading <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/threading/threading.py>`_
to prevent them from blocking the asynchronous loop. module on microcontroller platforms is not recommended.
Under MicroPython the situation is different. Most microcontroller boards The :ref:`micropython_asyncio <Asynchronous Support with Asyncio>` extension
implementing MicroPython do not have threading support or executors, so ``def`` provides a more robust concurrency option that is supported even on low-end
handler functions in this platform can only run in the main and only thread. MicroPython boards.
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.

View File

@@ -1,145 +0,0 @@
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 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.
Given that WebSocket support is asynchronous, it would be better to switch to
the ASGI extension, which has full support for WebSocket as defined in the ASGI
specification.
As before, the WSGI extension is not available under MicroPython.

View File

@@ -0,0 +1,27 @@
from microdot import Microdot
from microdot_auth import BasicAuth
app = Microdot()
basic_auth = BasicAuth()
USERS = {
'susan': 'hello',
'david': 'bye',
}
@basic_auth.callback
def verify_password(request, username, password):
if username in USERS and USERS[username] == password:
request.g.user = username
return True
@app.route('/')
@basic_auth
def index(request):
return f'Hello, {request.g.user}!'
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -0,0 +1,60 @@
from microdot import Microdot, redirect
from microdot_session import set_session_secret_key
from microdot_login import LoginAuth
app = Microdot()
set_session_secret_key('top-secret')
login_auth = LoginAuth()
USERS = {
'susan': 'hello',
'david': 'bye',
}
@login_auth.callback
def check_user(request, user_id):
request.g.user = user_id
return True
@app.route('/')
@login_auth
def index(request):
return f'''
<h1>Login Auth Example</h1>
<p>Hello, {request.g.user}!</p>
<form method="POST" action="/logout">
<button type="submit">Logout</button>
</form>
''', {'Content-Type': 'text/html'}
@app.route('/login', methods=['GET', 'POST'])
def login(request):
if request.method == 'GET':
return '''
<h1>Login Auth Example</h1>
<form method="POST">
<input name="username" placeholder="username">
<input name="password" type="password" placeholder="password">
<button type="submit">Login</button>
</form>
''', {'Content-Type': 'text/html'}
username = request.form['username']
password = request.form['password']
if USERS.get(username) == password:
login_auth.login_user(request, username)
return login_auth.redirect_to_next(request)
else:
return redirect('/login')
@app.post('/logout')
def logout(request):
login_auth.logout_user(request)
return redirect('/')
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -0,0 +1,27 @@
from microdot import Microdot
from microdot_auth import TokenAuth
app = Microdot()
token_auth = TokenAuth()
TOKENS = {
'hello': 'susan',
'bye': 'david',
}
@token_auth.callback
def verify_token(request, token):
if token in TOKENS:
request.g.user = TOKENS[token]
return True
@app.route('/')
@token_auth
def index(request):
return f'Hello, {request.g.user}!'
if __name__ == '__main__':
app.run(debug=True)

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
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('/') @app.get('/')
async def index(): def index():
return {'hello': 'world'} return {'hello': 'world'}

View File

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

View File

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

View File

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

View File

@@ -1,115 +1,33 @@
# aiofiles==0.8.0
# This file is autogenerated by pip-compile with Python 3.12 anyio==3.6.1
# by the following command: blinker==1.5
# certifi==2022.6.15
# pip-compile requirements.in charset-normalizer==2.1.0
# click==8.1.3
aiofiles==23.2.1 fastapi==0.79.0
# via quart Flask==2.2.1
annotated-types==0.6.0 gunicorn==20.1.0
# via pydantic h11==0.13.0
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 h2==4.1.0
# via hypercorn
hpack==4.0.0 hpack==4.0.0
# via h2 humanize==4.3.0
humanize==4.9.0 hypercorn==0.13.2
# via -r requirements.in
hypercorn==0.15.0
# via quart
hyperframe==6.0.1 hyperframe==6.0.1
# via h2 idna==3.3
idna==3.6
# via
# anyio
# requests
itsdangerous==2.1.2 itsdangerous==2.1.2
# via Jinja2==3.1.2
# flask MarkupSafe==2.1.1
# quart microdot
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 priority==2.0.0
# via hypercorn psutil==5.9.1
psutil==5.9.6 pydantic==1.9.1
# via -r requirements.in quart==0.18.0
pydantic==2.5.2 requests==2.28.1
# via fastapi sniffio==1.2.0
pydantic-core==2.14.5 starlette==0.19.1
# via pydantic toml==0.10.2
pyproject-hooks==1.0.0 typing_extensions==4.3.0
# via build urllib3==1.26.11
quart==0.19.4 uvicorn==0.18.2
# via -r requirements.in Werkzeug==2.2.1
requests==2.31.0 wsproto==1.1.0
# via -r requirements.in
sniffio==1.3.0
# via anyio
starlette==0.27.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

@@ -0,0 +1,14 @@
curl -X GET http://localhost:5000/ <-- microdot
{"ram": 8429568}%
curl -X GET http://localhost:5000/ <-- microdot_asyncio
{"ram": 12410880}%
curl -X GET http://localhost:8000/ <-- microdot_wsgi
{"ram": 9101312}%
curl -X GET http://localhost:8000/ <-- microdot_asgi
{"ram": 18620416}%
curl -X GET http://localhost:5000/ <-- flask app.run
{"ram":25460736}
curl -X GET http://localhost:5000/ <-- flask run
{"ram":26210304}
curl -X GET http://localhost:5000/ <-- quart run
{"ram":31748096}%

View File

@@ -1,7 +1,6 @@
import os import os
import subprocess import subprocess
import time import time
from timeit import timeit
import requests import requests
import psutil import psutil
import humanize import humanize
@@ -14,8 +13,13 @@ apps = [
), ),
( (
'micropython mem.py', 'micropython mem.py',
{'MICROPYPATH': '../../src'},
'microdot-micropython-sync'
),
(
'micropython mem_async.py',
{'MICROPYPATH': '../../src:../../libs/micropython'}, {'MICROPYPATH': '../../src:../../libs/micropython'},
'microdot-micropython' 'microdot-micropython-async'
), ),
( (
['python', '-c', 'import time; time.sleep(10)'], ['python', '-c', 'import time; time.sleep(10)'],
@@ -25,65 +29,66 @@ apps = [
( (
'python mem.py', 'python mem.py',
{'PYTHONPATH': '../../src'}, {'PYTHONPATH': '../../src'},
'microdot-cpython' 'microdot-cpython-sync'
), ),
( (
'uvicorn --workers 1 --port 5000 mem_asgi:app', 'python mem_async.py',
{'PYTHONPATH': '../../src'}, {'PYTHONPATH': '../../src'},
'microdot-uvicorn' 'microdot-cpython-async'
), ),
( (
'gunicorn --workers 1 --bind :5000 mem_wsgi:app', 'gunicorn --workers 1 --bind :5000 mem_wsgi:app',
{'PYTHONPATH': '../../src'}, {'PYTHONPATH': '../../src'},
'microdot-gunicorn' 'microdot-gunicorn-sync'
),
(
'uvicorn --workers 1 --port 5000 mem_asgi:app',
{'PYTHONPATH': '../../src'},
'microdot-uvicorn-async'
), ),
( (
'flask run', 'flask run',
{'FLASK_APP': 'mem_flask.py'}, {'FLASK_APP': 'mem_flask.py'},
'flask-run' 'flask-run-sync'
), ),
( (
'quart run', 'quart run',
{'QUART_APP': 'mem_quart.py'}, {'QUART_APP': 'mem_quart.py'},
'quart-run' 'quart-run-async'
), ),
( (
'gunicorn --workers 1 --bind :5000 mem_flask:app', 'gunicorn --workers 1 --bind :5000 mem_flask:app',
{}, {},
'flask-gunicorn' 'flask-gunicorn-sync'
), ),
( (
'uvicorn --workers 1 --port 5000 mem_quart:app', 'uvicorn --workers 1 --port 5000 mem_quart:app',
{}, {},
'quart-uvicorn' 'quart-uvicorn-async'
), ),
( (
'uvicorn --workers 1 --port 5000 mem_fastapi:app', 'uvicorn --workers 1 --port 5000 mem_fastapi:app',
{}, {},
'fastapi-uvicorn' 'fastapi-uvicorn-async'
), ),
] ]
for app, env, name in apps: for app, env, name in apps:
p = subprocess.Popen( p = subprocess.Popen(
app.split() if isinstance(app, str) else app, app.split() if isinstance(app, str) else app,
env={'PATH': os.environ['PATH'] + ':../../bin', **env}, env={'PATH': os.environ['PATH'], **env},
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL stderr=subprocess.DEVNULL
) )
time.sleep(1) time.sleep(1)
tm = 0
if not name.startswith('baseline'): if not name.startswith('baseline'):
def req(): r = requests.get('http://localhost:5000')
r = requests.get('http://localhost:5000') r.raise_for_status()
r.raise_for_status()
tm = timeit(req, number=1000)
proc = psutil.Process(p.pid) proc = psutil.Process(p.pid)
mem = proc.memory_info().rss mem = proc.memory_info().rss
for child in proc.children(recursive=True): for child in proc.children(recursive=True):
mem += child.memory_info().rss mem += child.memory_info().rss
bar = '*' * (mem // (1024 * 1024)) bar = '*' * (mem // (1024 * 1024))
print(f'{name:<28}{tm:10.2f}s {humanize.naturalsize(mem):>10} {bar}') print(f'{name:<28}{humanize.naturalsize(mem):>10} {bar}')
p.terminate() p.terminate()
time.sleep(1) time.sleep(1)

View File

@@ -1 +0,0 @@
This directory contains Cross-Origin Resource Sharing (CORS) examples.

View File

@@ -1,14 +0,0 @@
from microdot import Microdot
from microdot.cors import CORS
app = Microdot()
CORS(app, allowed_origins=['https://example.org'], allow_credentials=True)
@app.route('/')
def index(request):
return 'Hello World!'
if __name__ == '__main__':
app.run()

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Microdot GPIO Example</title> <title>Microdot GPIO Example</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script> <script>
function getCookie(name) { function getCookie(name) {

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
from microdot_asyncio import Microdot
app = Microdot()
htmldoc = '''<!DOCTYPE html>
<html>
<head>
<title>Microdot Example Page</title>
</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,16 +1,16 @@
from microdot import Microdot, Response from microdot import Microdot, Response
from microdot.jinja import Template from microdot_jinja import render_template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST']) @app.route('/', methods=['GET', 'POST'])
async def index(req): def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return Template('index.html').generate(name=name) return render_template('index_jinja.html', name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Microdot + Jinja example</title> <title>Microdot + Jinja example</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot + Jinja example</h1> <h1>Microdot + Jinja example</h1>

View File

@@ -3,7 +3,6 @@
<html> <html>
<head> <head>
<title>Microdot + uTemplate example</title> <title>Microdot + uTemplate example</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot + uTemplate example</h1> <h1>Microdot + uTemplate example</h1>

View File

@@ -1,11 +1,11 @@
from microdot import Microdot, Response, redirect from microdot import Microdot, Response, redirect
from microdot.session import Session, with_session from microdot_session import set_session_secret_key, with_session, \
update_session, delete_session
BASE_TEMPLATE = '''<!doctype html> BASE_TEMPLATE = '''<!doctype html>
<html> <html>
<head> <head>
<title>Microdot login example</title> <title>Microdot login example</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot login example</h1> <h1>Microdot login example</h1>
@@ -17,7 +17,7 @@ LOGGED_OUT = '''<p>You are not logged in.</p>
<form method="POST"> <form method="POST">
<p> <p>
Username: Username:
<input name="username" autofocus /> <input type="text" name="username" autofocus />
</p> </p>
<input type="submit" value="Submit" /> <input type="submit" value="Submit" />
</form>''' </form>'''
@@ -28,19 +28,18 @@ LOGGED_IN = '''<p>Hello <b>{username}</b>!</p>
</form>''' </form>'''
app = Microdot() app = Microdot()
Session(app, secret_key='top-secret') set_session_secret_key('top-secret')
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@app.get('/') @app.get('/')
@app.post('/') @app.post('/')
@with_session @with_session
async def index(req, session): def index(req, session):
username = session.get('username') username = session.get('username')
if req.method == 'POST': if req.method == 'POST':
username = req.form.get('username') username = req.form.get('username')
session['username'] = username update_session(req, {'username': username})
session.save()
return redirect('/') return redirect('/')
if username is None: if username is None:
return BASE_TEMPLATE.format(content=LOGGED_OUT) return BASE_TEMPLATE.format(content=LOGGED_OUT)
@@ -50,9 +49,8 @@ async def index(req, session):
@app.post('/logout') @app.post('/logout')
@with_session def logout(req):
async def logout(req, session): delete_session(req)
session.delete()
return redirect('/') return redirect('/')

View File

@@ -1,16 +0,0 @@
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,20 +0,0 @@
from microdot import Microdot, send_file
app = Microdot()
@app.route('/')
def index(request):
return send_file('gzstatic/index.html', compressed=True,
file_extension='.gz')
@app.route('/static/<path:path>')
def static(request, path):
if '..' in path:
# directory traversal is not allowed
return 'Not found', 404
return send_file('gzstatic/' + path, compressed=True, file_extension='.gz')
app.run(debug=True)

Binary file not shown.

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Static File Serving Demo</title> <title>Static File Serving Demo</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Static File Serving Demo</h1> <h1>Static File Serving Demo</h1>

View File

@@ -1,5 +1,8 @@
import sys try:
import asyncio import utime as time
except ImportError:
import time
from microdot import Microdot from microdot import Microdot
app = Microdot() app = Microdot()
@@ -11,12 +14,11 @@ for file in ['1.jpg', '2.jpg', '3.jpg']:
@app.route('/') @app.route('/')
async def index(request): def index(request):
return '''<!doctype html> return '''<!doctype html>
<html> <html>
<head> <head>
<title>Microdot Video Streaming</title> <title>Microdot Video Streaming</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot Video Streaming</h1> <h1>Microdot Video Streaming</h1>
@@ -26,38 +28,14 @@ async def index(request):
@app.route('/video_feed') @app.route('/video_feed')
async def video_feed(request): def video_feed(request):
print('Starting video stream.') def stream():
yield b'--frame\r\n'
if sys.implementation.name != 'micropython': while True:
# CPython supports async generator function for frame in frames:
async def stream(): yield b'Content-Type: image/jpeg\r\n\r\n' + frame + \
try: b'\r\n--frame\r\n'
yield b'--frame\r\n' time.sleep(1)
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': return stream(), 200, {'Content-Type':
'multipart/x-mixed-replace; boundary=frame'} 'multipart/x-mixed-replace; boundary=frame'}

View File

@@ -0,0 +1,64 @@
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>
</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

@@ -1,18 +0,0 @@
from microdot import Microdot, Response
from microdot.jinja import Template
Template.initialize('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,19 +0,0 @@
from microdot import Microdot, Response
from microdot.jinja import Template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/')
async def index(req):
return Template('page1.html').render(page='Page 1')
@app.route('/page2')
async def page2(req):
return Template('page2.html').render(page='Page 2')
if __name__ == '__main__':
app.run()

View File

@@ -1,17 +0,0 @@
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

@@ -1,48 +0,0 @@
<!--
This is based on the Bootstrap 5 starter template from the documentation:
https://getbootstrap.com/docs/5.0/getting-started/introduction/#starter-template
-->
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>Microdot + Jinja + Bootstrap</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="/">Microdot + Jinja + Bootstrap</a>
</div>
</nav>
<br>
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="info-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</symbol>
</svg>
<div class="alert alert-primary d-flex align-items-center" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>
<div>This example demonstrates how to create an application that uses <a href="https://getbootstrap.com" class="alert-link">Bootstrap</a> styling. The page layout is defined in a base template that is inherited by several pages.</div>
</div>
{% block content %}{% endblock %}
</div>
<!-- Optional JavaScript; choose one of the two! -->
<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<!-- Option 2: Separate Popper and Bootstrap JS -->
<!--
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
-->
</body>
</html>

View File

@@ -1,6 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h2>This is {{ page }}</h2>
<p>Go to <a href="/page2">Page 2</a>.</p>
{% endblock %}

View File

@@ -1,6 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h2>This is {{ page }}</h2>
<p>Go back <a href="/">Page 1</a>.</p>
{% endblock %}

View File

@@ -1,17 +0,0 @@
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,19 +0,0 @@
from microdot import Microdot, Response
from microdot.utemplate import Template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/')
async def index(req):
return Template('page1.html').render(page='Page 1')
@app.route('/page2')
async def page2(req):
return Template('page2.html').render(page='Page 2')
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -1,17 +0,0 @@
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').render(name=name)
if __name__ == '__main__':
app.run()

View File

@@ -1,17 +0,0 @@
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

@@ -1,17 +0,0 @@
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

@@ -1,17 +0,0 @@
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,14 +0,0 @@
</div>
<!-- Optional JavaScript; choose one of the two! -->
<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<!-- Option 2: Separate Popper and Bootstrap JS -->
<!--
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
-->
</body>
</html>

View File

@@ -1,33 +0,0 @@
<!--
This is based on the Bootstrap 5 starter template from the documentation:
https://getbootstrap.com/docs/5.0/getting-started/introduction/#starter-template
-->
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>Microdot + uTemplate + Bootstrap</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="/">Microdot + uTemplate + Bootstrap</a>
</div>
</nav>
<br>
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="info-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</symbol>
</svg>
<div class="alert alert-primary d-flex align-items-center" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>
<div>This example demonstrates how to create an application that uses <a href="https://getbootstrap.com" class="alert-link">Bootstrap</a> styling. The page layout is defined in a base template that is inherited by several pages.</div>
</div>

View File

@@ -1,7 +0,0 @@
{% args page %}
{% include 'base_header.html' %}
<h2>This is {{ page }}</h2>
<p>Go to <a href="/page2">Page 2</a>.</p>
{% include 'base_footer.html' %}

View File

@@ -1,7 +0,0 @@
{% args page %}
{% include 'base_header.html' %}
<h2>This is {{ page }}</h2>
<p>Go back <a href="/">Page 1</a>.</p>
{% include 'base_footer.html' %}

View File

@@ -0,0 +1,23 @@
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)

24
examples/tls/echo_tls.py Normal file
View File

@@ -0,0 +1,24 @@
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,13 +1,12 @@
import ssl import ssl
from microdot import Microdot from microdot_asyncio import Microdot
app = Microdot() app = Microdot()
html = '''<!DOCTYPE html> htmldoc = '''<!DOCTYPE html>
<html> <html>
<head> <head>
<title>Microdot Example Page</title> <title>Microdot Example Page</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<div> <div>
@@ -22,7 +21,7 @@ html = '''<!DOCTYPE html>
@app.route('/') @app.route('/')
async def hello(request): async def hello(request):
return html, 200, {'Content-Type': 'text/html'} return htmldoc, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown') @app.route('/shutdown')

36
examples/tls/hello_tls.py Normal file
View File

@@ -0,0 +1,36 @@
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>
</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)

35
examples/tls/index.html Normal file
View File

@@ -0,0 +1,35 @@
<!doctype html>
<html>
<head>
<title>Microdot TLS WebSocket Demo</title>
</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

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Microdot Upload Example</title> <title>Microdot Upload Example</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot Upload Example</h1> <h1>Microdot Upload Example</h1>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Microdot WebSocket Demo</title> <title>Microdot WebSocket Demo</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot WebSocket Demo</h1> <h1>Microdot WebSocket Demo</h1>

View File

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

View File

@@ -1,130 +0,0 @@
# MicroPython asyncio module
# MIT license; Copyright (c) 2019-2022 Damien P. George
from . import core
async def _run(waiter, aw):
try:
result = await aw
status = True
except BaseException as er:
result = None
status = er
if waiter.data is None:
# The waiter is still waiting, cancel it.
if waiter.cancel():
# Waiter was cancelled by us, change its CancelledError to an instance of
# CancelledError that contains the status and result of waiting on aw.
# If the wait_for task subsequently gets cancelled externally then this
# instance will be reset to a CancelledError instance without arguments.
waiter.data = core.CancelledError(status, result)
async def wait_for(aw, timeout, sleep=core.sleep):
aw = core._promote_to_task(aw)
if timeout is None:
return await aw
# Run aw in a separate runner task that manages its exceptions.
runner_task = core.create_task(_run(core.cur_task, aw))
try:
# Wait for the timeout to elapse.
await sleep(timeout)
except core.CancelledError as er:
status = er.value
if status is None:
# This wait_for was cancelled externally, so cancel aw and re-raise.
runner_task.cancel()
raise er
elif status is True:
# aw completed successfully and cancelled the sleep, so return aw's result.
return er.args[1]
else:
# aw raised an exception, propagate it out to the caller.
raise status
# The sleep finished before aw, so cancel aw and raise TimeoutError.
runner_task.cancel()
await runner_task
raise core.TimeoutError
def wait_for_ms(aw, timeout):
return wait_for(aw, timeout, core.sleep_ms)
class _Remove:
@staticmethod
def remove(t):
pass
# async
def gather(*aws, return_exceptions=False):
if not aws:
return []
def done(t, er):
# Sub-task "t" has finished, with exception "er".
nonlocal state
if gather_task.data is not _Remove:
# The main gather task has already been scheduled, so do nothing.
# This happens if another sub-task already raised an exception and
# woke the main gather task (via this done function), or if the main
# gather task was cancelled externally.
return
elif not return_exceptions and not isinstance(er, StopIteration):
# A sub-task raised an exception, indicate that to the gather task.
state = er
else:
state -= 1
if state:
# Still some sub-tasks running.
return
# Gather waiting is done, schedule the main gather task.
core._task_queue.push(gather_task)
ts = [core._promote_to_task(aw) for aw in aws]
for i in range(len(ts)):
if ts[i].state is not True:
# Task is not running, gather not currently supported for this case.
raise RuntimeError("can't gather")
# Register the callback to call when the task is done.
ts[i].state = done
# Set the state for execution of the gather.
gather_task = core.cur_task
state = len(ts)
cancel_all = False
# Wait for the a sub-task to need attention.
gather_task.data = _Remove
try:
yield
except core.CancelledError as er:
cancel_all = True
state = er
# Clean up tasks.
for i in range(len(ts)):
if ts[i].state is done:
# Sub-task is still running, deregister the callback and cancel if needed.
ts[i].state = True
if cancel_all:
ts[i].cancel()
elif isinstance(ts[i].data, StopIteration):
# Sub-task ran to completion, get its return value.
ts[i] = ts[i].data.value
else:
# Sub-task had an exception with return_exceptions==True, so get its exception.
ts[i] = ts[i].data
# 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:
raise state
# Return the list of return values of each sub-task.
return ts

View File

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

View File

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

76
libs/micropython/time.py Normal file
View File

@@ -0,0 +1,76 @@
from utime import *
from ucollections import namedtuple
import ustruct
import uctypes
import ffi
import ffilib
import array
libc = ffilib.libc()
# 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])
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 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

View File

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

View File

@@ -1,4 +1,4 @@
# MicroPython asyncio module # MicroPython uasyncio module
# MIT license; Copyright (c) 2019 Damien P. George # MIT license; Copyright (c) 2019 Damien P. George
from time import ticks_ms as ticks, ticks_diff, ticks_add 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 # Import TaskQueue and Task, preferring built-in C code over Python code
try: try:
from _asyncio import TaskQueue, Task from _uasyncio import TaskQueue, Task
except: except:
from .task import TaskQueue, Task from .task import TaskQueue, Task
@@ -30,7 +30,6 @@ _exc_context = {"message": "Task exception wasn't retrieved", "exception": None,
################################################################################ ################################################################################
# Sleep functions # Sleep functions
# "Yield" once, then raise StopIteration # "Yield" once, then raise StopIteration
class SingletonGenerator: class SingletonGenerator:
def __init__(self): def __init__(self):
@@ -42,7 +41,7 @@ class SingletonGenerator:
def __next__(self): def __next__(self):
if self.state is not None: if self.state is not None:
_task_queue.push(cur_task, self.state) _task_queue.push_sorted(cur_task, self.state)
self.state = None self.state = None
return None return None
else: else:
@@ -116,11 +115,11 @@ class IOQueue:
# print('poll', s, sm, ev) # print('poll', s, sm, ev)
if ev & ~select.POLLOUT and sm[0] is not None: if ev & ~select.POLLOUT and sm[0] is not None:
# POLLIN or error # POLLIN or error
_task_queue.push(sm[0]) _task_queue.push_head(sm[0])
sm[0] = None sm[0] = None
if ev & ~select.POLLIN and sm[1] is not None: if ev & ~select.POLLIN and sm[1] is not None:
# POLLOUT or error # POLLOUT or error
_task_queue.push(sm[1]) _task_queue.push_head(sm[1])
sm[1] = None sm[1] = None
if sm[0] is None and sm[1] is None: if sm[0] is None and sm[1] is None:
self._dequeue(s) self._dequeue(s)
@@ -133,7 +132,6 @@ class IOQueue:
################################################################################ ################################################################################
# Main run loop # Main run loop
# Ensure the awaitable is a task # Ensure the awaitable is a task
def _promote_to_task(aw): def _promote_to_task(aw):
return aw if isinstance(aw, Task) else create_task(aw) return aw if isinstance(aw, Task) else create_task(aw)
@@ -144,7 +142,7 @@ def create_task(coro):
if not hasattr(coro, "send"): if not hasattr(coro, "send"):
raise TypeError("coroutine expected") raise TypeError("coroutine expected")
t = Task(coro, globals()) t = Task(coro, globals())
_task_queue.push(t) _task_queue.push_head(t)
return t return t
@@ -169,7 +167,7 @@ def run_until_complete(main_task=None):
_io_queue.wait_io_event(dt) _io_queue.wait_io_event(dt)
# Get next task to run and continue it # Get next task to run and continue it
t = _task_queue.pop() t = _task_queue.pop_head()
cur_task = t cur_task = t
try: try:
# Continue running the coroutine, it's responsible for rescheduling itself # Continue running the coroutine, it's responsible for rescheduling itself
@@ -177,10 +175,6 @@ def run_until_complete(main_task=None):
if not exc: if not exc:
t.coro.send(None) t.coro.send(None)
else: else:
# If the task is finished and on the run queue and gets here, then it
# had an exception and was not await'ed on. Throwing into it now will
# raise StopIteration and the code below will catch this and run the
# call_exception_handler function.
t.data = None t.data = None
t.coro.throw(exc) t.coro.throw(exc)
except excs_all as er: except excs_all as er:
@@ -191,37 +185,22 @@ def run_until_complete(main_task=None):
if isinstance(er, StopIteration): if isinstance(er, StopIteration):
return er.value return er.value
raise er raise er
if t.state: # Schedule any other tasks waiting on the completion of this task
# Task was running but is now finished. waiting = False
waiting = False if hasattr(t, "waiting"):
if t.state is True: while t.waiting.peek():
# "None" indicates that the task is complete and not await'ed on (yet). _task_queue.push_head(t.waiting.pop_head())
t.state = None
elif callable(t.state):
# The task has a callback registered to be called on completion.
t.state(t, er)
t.state = False
waiting = True waiting = True
else: t.waiting = None # Free waiting queue head
# Schedule any other tasks waiting on the completion of this task. if not waiting and not isinstance(er, excs_stop):
while t.state.peek(): # An exception ended this detached task, so queue it for later
_task_queue.push(t.state.pop()) # execution to handle the uncaught exception if no other task retrieves
waiting = True # the exception in the meantime (this is handled by Task.throw).
# "False" indicates that the task is complete and has been await'ed on. _task_queue.push_head(t)
t.state = False # Indicate task is done by setting coro to the task object itself
if not waiting and not isinstance(er, excs_stop): t.coro = t
# An exception ended this detached task, so queue it for later # Save return value of coro to pass up to caller
# execution to handle the uncaught exception if no other task retrieves t.data = er
# the exception in the meantime (this is handled by Task.throw).
_task_queue.push(t)
# Save return value of coro to pass up to caller.
t.data = er
elif t.state is None:
# Task is already finished and nothing await'ed on the task,
# so call the exception handler.
_exc_context["exception"] = exc
_exc_context["future"] = t
Loop.call_exception_handler(_exc_context)
# Create a new task from a coroutine and run it until it finishes # Create a new task from a coroutine and run it until it finishes
@@ -258,7 +237,7 @@ class Loop:
def stop(): def stop():
global _stop_task global _stop_task
if _stop_task is not None: if _stop_task is not None:
_task_queue.push(_stop_task) _task_queue.push_head(_stop_task)
# If stop() is called again, do nothing # If stop() is called again, do nothing
_stop_task = None _stop_task = None
@@ -272,9 +251,9 @@ class Loop:
return Loop._exc_handler return Loop._exc_handler
def default_exception_handler(loop, context): def default_exception_handler(loop, context):
print(context["message"], file=sys.stderr) print(context["message"])
print("future:", context["future"], "coro=", context["future"].coro, file=sys.stderr) print("future:", context["future"], "coro=", context["future"].coro)
sys.print_exception(context["exception"], sys.stderr) sys.print_exception(context["exception"])
def call_exception_handler(context): def call_exception_handler(context):
(Loop._exc_handler or Loop.default_exception_handler)(Loop, context) (Loop._exc_handler or Loop.default_exception_handler)(Loop, context)

View File

@@ -1,9 +1,8 @@
# MicroPython asyncio module # MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George # MIT license; Copyright (c) 2019-2020 Damien P. George
from . import core from . import core
# Event class for primitive events that can be waited on, set, and cleared # Event class for primitive events that can be waited on, set, and cleared
class Event: class Event:
def __init__(self): def __init__(self):
@@ -18,17 +17,16 @@ class Event:
# Note: This must not be called from anything except the thread running # Note: This must not be called from anything except the thread running
# the asyncio loop (i.e. neither hard or soft IRQ, or a different thread). # the asyncio loop (i.e. neither hard or soft IRQ, or a different thread).
while self.waiting.peek(): while self.waiting.peek():
core._task_queue.push(self.waiting.pop()) core._task_queue.push_head(self.waiting.pop_head())
self.state = True self.state = True
def clear(self): def clear(self):
self.state = False self.state = False
# async async def wait(self):
def wait(self):
if not self.state: if not self.state:
# Event not set, put the calling task on the event's waiting queue # Event not set, put the calling task on the event's waiting queue
self.waiting.push(core.cur_task) self.waiting.push_head(core.cur_task)
# Set calling task's data to the event's queue so it can be removed if needed # Set calling task's data to the event's queue so it can be removed if needed
core.cur_task.data = self.waiting core.cur_task.data = self.waiting
yield yield
@@ -38,29 +36,27 @@ class Event:
# MicroPython-extension: This can be set from outside the asyncio event loop, # MicroPython-extension: This can be set from outside the asyncio event loop,
# such as other threads, IRQs or scheduler context. Implementation is a stream # such as other threads, IRQs or scheduler context. Implementation is a stream
# that asyncio will poll until a flag is set. # that asyncio will poll until a flag is set.
# Note: Unlike Event, this is self-clearing after a wait(). # Note: Unlike Event, this is self-clearing.
try: try:
import io import uio
class ThreadSafeFlag(io.IOBase): class ThreadSafeFlag(uio.IOBase):
def __init__(self): def __init__(self):
self.state = 0 self._flag = 0
def ioctl(self, req, flags): def ioctl(self, req, flags):
if req == 3: # MP_STREAM_POLL if req == 3: # MP_STREAM_POLL
return self.state * flags return self._flag * flags
return -1 # Other requests are unsupported return None
def set(self): def set(self):
self.state = 1 self._flag = 1
def clear(self):
self.state = 0
async def wait(self): async def wait(self):
if not self.state: if not self._flag:
yield core._io_queue.queue_read(self) yield core._io_queue.queue_read(self)
self.state = 0 self._flag = 0
except ImportError: except ImportError:
pass pass

View File

@@ -0,0 +1,74 @@
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
from . import core
async def wait_for(aw, timeout, sleep=core.sleep):
aw = core._promote_to_task(aw)
if timeout is None:
return await aw
def runner(waiter, aw):
nonlocal status, result
try:
result = await aw
s = True
except BaseException as er:
s = er
if status is None:
# The waiter is still waiting, set status for it and cancel it.
status = s
waiter.cancel()
# Run aw in a separate runner task that manages its exceptions.
status = None
result = None
runner_task = core.create_task(runner(core.cur_task, aw))
try:
# Wait for the timeout to elapse.
await sleep(timeout)
except core.CancelledError as er:
if status is True:
# aw completed successfully and cancelled the sleep, so return aw's result.
return result
elif status is None:
# This wait_for was cancelled externally, so cancel aw and re-raise.
status = True
runner_task.cancel()
raise er
else:
# aw raised an exception, propagate it out to the caller.
raise status
# The sleep finished before aw, so cancel aw and raise TimeoutError.
status = True
runner_task.cancel()
await runner_task
raise core.TimeoutError
def wait_for_ms(aw, timeout):
return wait_for(aw, timeout, core.sleep_ms)
async def gather(*aws, return_exceptions=False):
ts = [core._promote_to_task(aw) for aw in aws]
for i in range(len(ts)):
try:
# TODO handle cancel of gather itself
# if ts[i].coro:
# iter(ts[i]).waiting.push_head(cur_task)
# try:
# yield
# except CancelledError as er:
# # cancel all waiting tasks
# raise er
ts[i] = await ts[i]
except Exception as er:
if return_exceptions:
ts[i] = er
else:
raise er
return ts

View File

@@ -1,9 +1,8 @@
# MicroPython asyncio module # MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George # MIT license; Copyright (c) 2019-2020 Damien P. George
from . import core from . import core
# Lock class for primitive mutex capability # Lock class for primitive mutex capability
class Lock: class Lock:
def __init__(self): def __init__(self):
@@ -23,17 +22,16 @@ class Lock:
raise RuntimeError("Lock not acquired") raise RuntimeError("Lock not acquired")
if self.waiting.peek(): if self.waiting.peek():
# Task(s) waiting on lock, schedule next Task # Task(s) waiting on lock, schedule next Task
self.state = self.waiting.pop() self.state = self.waiting.pop_head()
core._task_queue.push(self.state) core._task_queue.push_head(self.state)
else: else:
# No Task waiting so unlock # No Task waiting so unlock
self.state = 0 self.state = 0
# async async def acquire(self):
def acquire(self):
if self.state != 0: if self.state != 0:
# Lock unavailable, put the calling Task on the waiting queue # Lock unavailable, put the calling Task on the waiting queue
self.waiting.push(core.cur_task) self.waiting.push_head(core.cur_task)
# Set calling task's data to the lock's queue so it can be removed if needed # Set calling task's data to the lock's queue so it can be removed if needed
core.cur_task.data = self.waiting core.cur_task.data = self.waiting
try: try:

View File

@@ -0,0 +1,13 @@
# This list of frozen files doesn't include task.py because that's provided by the C module.
freeze(
"..",
(
"uasyncio/__init__.py",
"uasyncio/core.py",
"uasyncio/event.py",
"uasyncio/funcs.py",
"uasyncio/lock.py",
"uasyncio/stream.py",
),
opt=3,
)

View File

@@ -1,4 +1,4 @@
# MicroPython asyncio module # MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George # MIT license; Copyright (c) 2019-2020 Damien P. George
from . import core from . import core
@@ -26,26 +26,11 @@ class Stream:
# TODO yield? # TODO yield?
self.s.close() self.s.close()
# async async def read(self, n):
def read(self, n=-1):
r = b""
while True:
yield core._io_queue.queue_read(self.s)
r2 = self.s.read(n)
if r2 is not None:
if n >= 0:
return r2
if not len(r2):
return r
r += r2
# async
def readinto(self, buf):
yield core._io_queue.queue_read(self.s) yield core._io_queue.queue_read(self.s)
return self.s.readinto(buf) return self.s.read(n)
# async async def readexactly(self, n):
def readexactly(self, n):
r = b"" r = b""
while n: while n:
yield core._io_queue.queue_read(self.s) yield core._io_queue.queue_read(self.s)
@@ -57,8 +42,7 @@ class Stream:
n -= len(r2) n -= len(r2)
return r return r
# async async def readline(self):
def readline(self):
l = b"" l = b""
while True: while True:
yield core._io_queue.queue_read(self.s) yield core._io_queue.queue_read(self.s)
@@ -68,20 +52,9 @@ class Stream:
return l return l
def write(self, buf): def write(self, buf):
if not self.out_buf:
# Try to write immediately to the underlying stream.
ret = self.s.write(buf)
if ret == len(buf):
return
if ret is not None:
buf = buf[ret:]
self.out_buf += buf self.out_buf += buf
# async async def drain(self):
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 (yield from core.sleep_ms(0))
mv = memoryview(self.out_buf) mv = memoryview(self.out_buf)
off = 0 off = 0
while off < len(mv): while off < len(mv):
@@ -98,14 +71,12 @@ StreamWriter = Stream
# Create a TCP stream connection to a remote host # Create a TCP stream connection to a remote host
# async def open_connection(host, port):
# async from uerrno import EINPROGRESS
def open_connection(host, port): import usocket as socket
from errno import EINPROGRESS
import socket
ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0] # TODO this is blocking! ai = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
s = socket.socket(ai[0], ai[1], ai[2]) s = socket.socket()
s.setblocking(False) s.setblocking(False)
ss = Stream(s) ss = Stream(s)
try: try:
@@ -127,30 +98,29 @@ class Server:
await self.wait_closed() await self.wait_closed()
def close(self): 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() self.task.cancel()
async def wait_closed(self): async def wait_closed(self):
await self.task await self.task
async def _serve(self, s, cb): async def _serve(self, cb, host, port, backlog):
self.state = False import usocket as socket
ai = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
s = socket.socket()
s.setblocking(False)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(ai[-1])
s.listen(backlog)
self.task = core.cur_task
# Accept incoming connections # Accept incoming connections
while True: while True:
try: try:
yield core._io_queue.queue_read(s) yield core._io_queue.queue_read(s)
except core.CancelledError as er: except core.CancelledError:
# The server task was cancelled, shutdown server and close socket. # Shutdown server
s.close() s.close()
if self.state: return
# If the server was explicitly closed, ignore the cancellation.
return
else:
# Otherwise e.g. the parent task was cancelled, propagate
# cancellation.
raise er
try: try:
s2, addr = s.accept() s2, addr = s.accept()
except: except:
@@ -164,30 +134,9 @@ class Server:
# Helper function to start a TCP stream server, running as a new task # 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 # TODO could use an accept-callback on socket read activity instead of creating a task
async def start_server(cb, host, port, backlog=5): async def start_server(cb, host, port, backlog=5):
import socket s = Server()
core.create_task(s._serve(cb, host, port, backlog))
# Create and bind server socket. return s
host = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
s = socket.socket()
s.setblocking(False)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(host[-1])
s.listen(backlog)
# 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 asyncio module # MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George # 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. # This file contains the core TaskQueue based on a pairing heap, and the core Task class.
@@ -99,18 +99,19 @@ class TaskQueue:
def peek(self): def peek(self):
return self.heap return self.heap
def push(self, v, key=None): def push_sorted(self, v, key):
assert v.ph_child is None
assert v.ph_next is None
v.data = None v.data = None
v.ph_key = key if key is not None else core.ticks() v.ph_key = key
v.ph_child = None
v.ph_next = None
self.heap = ph_meld(v, self.heap) self.heap = ph_meld(v, self.heap)
def pop(self): def push_head(self, v):
self.push_sorted(v, core.ticks())
def pop_head(self):
v = self.heap v = self.heap
assert v.ph_next is None self.heap = ph_pairing(self.heap.ph_child)
self.heap = ph_pairing(v.ph_child)
v.ph_child = None
return v return v
def remove(self, v): def remove(self, v):
@@ -122,7 +123,6 @@ class Task:
def __init__(self, coro, globals=None): def __init__(self, coro, globals=None):
self.coro = coro # Coroutine of this Task self.coro = coro # Coroutine of this Task
self.data = None # General data for queue it is waiting on self.data = None # General data for queue it is waiting on
self.state = True # None, False, True, a callable, or a TaskQueue instance
self.ph_key = 0 # Pairing heap self.ph_key = 0 # Pairing heap
self.ph_child = None # Paring heap self.ph_child = None # Paring heap
self.ph_child_last = None # Paring heap self.ph_child_last = None # Paring heap
@@ -130,33 +130,30 @@ class Task:
self.ph_rightmost_parent = None # Paring heap self.ph_rightmost_parent = None # Paring heap
def __iter__(self): def __iter__(self):
if not self.state: if self.coro is self:
# Task finished, signal that is has been await'ed on. # Signal that the completed-task has been await'ed on.
self.state = False self.waiting = None
elif self.state is True: elif not hasattr(self, "waiting"):
# Allocated head of linked list of Tasks waiting on completion of this task. # Lazily allocated head of linked list of Tasks waiting on completion of this task.
self.state = TaskQueue() self.waiting = TaskQueue()
elif type(self.state) is not TaskQueue:
# Task has state used for another purpose, so can't also wait on it.
raise RuntimeError("can't wait")
return self return self
def __next__(self): def __next__(self):
if not self.state: if self.coro is self:
# Task finished, raise return value to caller so it can continue. # Task finished, raise return value to caller so it can continue.
raise self.data raise self.data
else: else:
# Put calling task on waiting queue. # Put calling task on waiting queue.
self.state.push(core.cur_task) self.waiting.push_head(core.cur_task)
# Set calling task's data to this task that it waits on, to double-link it. # Set calling task's data to this task that it waits on, to double-link it.
core.cur_task.data = self core.cur_task.data = self
def done(self): def done(self):
return not self.state return self.coro is self
def cancel(self): def cancel(self):
# Check if task is already finished. # Check if task is already finished.
if not self.state: if self.coro is self:
return False return False
# Can't cancel self (not supported yet). # Can't cancel self (not supported yet).
if self is core.cur_task: if self is core.cur_task:
@@ -168,10 +165,20 @@ class Task:
if hasattr(self.data, "remove"): if hasattr(self.data, "remove"):
# Not on the main running queue, remove the task from the queue it's on. # Not on the main running queue, remove the task from the queue it's on.
self.data.remove(self) self.data.remove(self)
core._task_queue.push(self) core._task_queue.push_head(self)
elif core.ticks_diff(self.ph_key, core.ticks()) > 0: elif core.ticks_diff(self.ph_key, core.ticks()) > 0:
# On the main running queue but scheduled in the future, so bring it forward to now. # On the main running queue but scheduled in the future, so bring it forward to now.
core._task_queue.remove(self) core._task_queue.remove(self)
core._task_queue.push(self) core._task_queue.push_head(self)
self.data = core.CancelledError self.data = core.CancelledError
return True return True
def throw(self, value):
# This task raised an exception which was uncaught; handle that now.
# Set the data because it was cleared by the main scheduling loop.
self.data = value
if not hasattr(self, "waiting"):
# Nothing await'ed on the task so call the exception handler.
core._exc_context["exception"] = value
core._exc_context["future"] = self
core.Loop.call_exception_handler(core._exc_context)

View File

@@ -1,18 +1,12 @@
import io
import os
import sys import sys
try:
import traceback
except ImportError:
traceback = None
class SkipTest(Exception): class SkipTest(Exception):
pass pass
class AssertRaisesContext: class AssertRaisesContext:
def __init__(self, exc): def __init__(self, exc):
self.expected = exc self.expected = exc
@@ -20,98 +14,29 @@ class AssertRaisesContext:
return self return self
def __exit__(self, exc_type, exc_value, tb): def __exit__(self, exc_type, exc_value, tb):
self.exception = exc_value
if exc_type is None: if exc_type is None:
assert False, "%r not raised" % self.expected assert False, "%r not raised" % self.expected
if issubclass(exc_type, self.expected): if issubclass(exc_type, self.expected):
# store exception for later retrieval
self.exception = exc_value
return True return True
return False return False
# These are used to provide required context to things like subTest
__current_test__ = None
__test_result__ = None
class SubtestContext:
def __init__(self, msg=None, params=None):
self.msg = msg
self.params = params
def __enter__(self):
pass
def __exit__(self, *exc_info):
if exc_info[0] is not None:
# Exception raised
global __test_result__, __current_test__
test_details = __current_test__
if self.msg:
test_details += (f" [{self.msg}]",)
if self.params:
detail = ", ".join(f"{k}={v}" for k, v in self.params.items())
test_details += (f" ({detail})",)
_handle_test_exception(test_details, __test_result__, exc_info, False)
# Suppress the exception as we've captured it above
return True
class NullContext:
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, traceback):
pass
class TestCase: class TestCase:
def __init__(self):
pass
def addCleanup(self, func, *args, **kwargs): def fail(self, msg=''):
if not hasattr(self, "_cleanups"):
self._cleanups = []
self._cleanups.append((func, args, kwargs))
def doCleanups(self):
if hasattr(self, "_cleanups"):
while self._cleanups:
func, args, kwargs = self._cleanups.pop()
func(*args, **kwargs)
def subTest(self, msg=None, **params):
return SubtestContext(msg=msg, params=params)
def skipTest(self, reason):
raise SkipTest(reason)
def fail(self, msg=""):
assert False, msg assert False, msg
def assertEqual(self, x, y, msg=""): def assertEqual(self, x, y, msg=''):
if not msg: if not msg:
msg = "%r vs (expected) %r" % (x, y) msg = "%r vs (expected) %r" % (x, y)
assert x == y, msg assert x == y, msg
def assertNotEqual(self, x, y, msg=""): def assertNotEqual(self, x, y, msg=''):
if not msg: if not msg:
msg = "%r not expected to be equal %r" % (x, y) msg = "%r not expected to be equal %r" % (x, y)
assert x != y, msg assert x != y, msg
def assertLessEqual(self, x, y, msg=None): def assertAlmostEqual(self, x, y, places=None, msg='', delta=None):
if msg is None:
msg = "%r is expected to be <= %r" % (x, y)
assert x <= y, msg
def assertGreaterEqual(self, x, y, msg=None):
if msg is None:
msg = "%r is expected to be >= %r" % (x, y)
assert x >= y, msg
def assertAlmostEqual(self, x, y, places=None, msg="", delta=None):
if x == y: if x == y:
return return
if delta is not None and places is not None: if delta is not None and places is not None:
@@ -121,18 +46,18 @@ class TestCase:
if abs(x - y) <= delta: if abs(x - y) <= delta:
return return
if not msg: if not msg:
msg = "%r != %r within %r delta" % (x, y, delta) msg = '%r != %r within %r delta' % (x, y, delta)
else: else:
if places is None: if places is None:
places = 7 places = 7
if round(abs(y - x), places) == 0: if round(abs(y-x), places) == 0:
return return
if not msg: if not msg:
msg = "%r != %r within %r places" % (x, y, places) msg = '%r != %r within %r places' % (x, y, places)
assert False, msg assert False, msg
def assertNotAlmostEqual(self, x, y, places=None, msg="", delta=None): def assertNotAlmostEqual(self, x, y, places=None, msg='', delta=None):
if delta is not None and places is not None: if delta is not None and places is not None:
raise TypeError("specify delta or places not both") raise TypeError("specify delta or places not both")
@@ -140,53 +65,53 @@ class TestCase:
if not (x == y) and abs(x - y) > delta: if not (x == y) and abs(x - y) > delta:
return return
if not msg: if not msg:
msg = "%r == %r within %r delta" % (x, y, delta) msg = '%r == %r within %r delta' % (x, y, delta)
else: else:
if places is None: if places is None:
places = 7 places = 7
if not (x == y) and round(abs(y - x), places) != 0: if not (x == y) and round(abs(y-x), places) != 0:
return return
if not msg: if not msg:
msg = "%r == %r within %r places" % (x, y, places) msg = '%r == %r within %r places' % (x, y, places)
assert False, msg assert False, msg
def assertIs(self, x, y, msg=""): def assertIs(self, x, y, msg=''):
if not msg: if not msg:
msg = "%r is not %r" % (x, y) msg = "%r is not %r" % (x, y)
assert x is y, msg assert x is y, msg
def assertIsNot(self, x, y, msg=""): def assertIsNot(self, x, y, msg=''):
if not msg: if not msg:
msg = "%r is %r" % (x, y) msg = "%r is %r" % (x, y)
assert x is not y, msg assert x is not y, msg
def assertIsNone(self, x, msg=""): def assertIsNone(self, x, msg=''):
if not msg: if not msg:
msg = "%r is not None" % x msg = "%r is not None" % x
assert x is None, msg assert x is None, msg
def assertIsNotNone(self, x, msg=""): def assertIsNotNone(self, x, msg=''):
if not msg: if not msg:
msg = "%r is None" % x msg = "%r is None" % x
assert x is not None, msg assert x is not None, msg
def assertTrue(self, x, msg=""): def assertTrue(self, x, msg=''):
if not msg: if not msg:
msg = "Expected %r to be True" % x msg = "Expected %r to be True" % x
assert x, msg assert x, msg
def assertFalse(self, x, msg=""): def assertFalse(self, x, msg=''):
if not msg: if not msg:
msg = "Expected %r to be False" % x msg = "Expected %r to be False" % x
assert not x, msg assert not x, msg
def assertIn(self, x, y, msg=""): def assertIn(self, x, y, msg=''):
if not msg: if not msg:
msg = "Expected %r to be in %r" % (x, y) msg = "Expected %r to be in %r" % (x, y)
assert x in y, msg assert x in y, msg
def assertIsInstance(self, x, y, msg=""): def assertIsInstance(self, x, y, msg=''):
assert isinstance(x, y), msg assert isinstance(x, y), msg
def assertRaises(self, exc, func=None, *args, **kwargs): def assertRaises(self, exc, func=None, *args, **kwargs):
@@ -195,15 +120,12 @@ class TestCase:
try: try:
func(*args, **kwargs) func(*args, **kwargs)
assert False, "%r not raised" % exc
except Exception as e: except Exception as e:
if isinstance(e, exc): if isinstance(e, exc):
return return
raise raise
assert False, "%r not raised" % exc
def assertWarns(self, warn):
return NullContext()
def skip(msg): def skip(msg):
@@ -211,253 +133,92 @@ def skip(msg):
# We just replace original fun with _inner # We just replace original fun with _inner
def _inner(self): def _inner(self):
raise SkipTest(msg) raise SkipTest(msg)
return _inner return _inner
return _decor return _decor
def skipIf(cond, msg): def skipIf(cond, msg):
if not cond: if not cond:
return lambda x: x return lambda x: x
return skip(msg) return skip(msg)
def skipUnless(cond, msg): def skipUnless(cond, msg):
if cond: if cond:
return lambda x: x return lambda x: x
return skip(msg) return skip(msg)
def expectedFailure(test):
def test_exp_fail(*args, **kwargs):
try:
test(*args, **kwargs)
except:
pass
else:
assert False, "unexpected success"
return test_exp_fail
class TestSuite: class TestSuite:
def __init__(self, name=""): def __init__(self):
self._tests = [] self.tests = []
self.name = name
def addTest(self, cls): def addTest(self, cls):
self._tests.append(cls) self.tests.append(cls)
def run(self, result):
for c in self._tests:
_run_suite(c, result, self.name)
return result
def _load_module(self, mod):
for tn in dir(mod):
c = getattr(mod, tn)
if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase):
self.addTest(c)
elif tn.startswith("test") and callable(c):
self.addTest(c)
class TestRunner: class TestRunner:
def run(self, suite: TestSuite): def run(self, suite):
res = TestResult() res = TestResult()
suite.run(res) for c in suite.tests:
run_class(c, res)
res.printErrors()
print("----------------------------------------------------------------------")
print("Ran %d tests\n" % res.testsRun) print("Ran %d tests\n" % res.testsRun)
if res.failuresNum > 0 or res.errorsNum > 0: if res.failuresNum > 0 or res.errorsNum > 0:
print("FAILED (failures=%d, errors=%d)" % (res.failuresNum, res.errorsNum)) print("FAILED (failures=%d, errors=%d)" % (res.failuresNum, res.errorsNum))
else: else:
msg = "OK" msg = "OK"
if res.skippedNum > 0: if res.skippedNum > 0:
msg += " (skipped=%d)" % res.skippedNum msg += " (%d skipped)" % res.skippedNum
print(msg) print(msg)
return res return res
TextTestRunner = TestRunner
class TestResult: class TestResult:
def __init__(self): def __init__(self):
self.errorsNum = 0 self.errorsNum = 0
self.failuresNum = 0 self.failuresNum = 0
self.skippedNum = 0 self.skippedNum = 0
self.testsRun = 0 self.testsRun = 0
self.errors = []
self.failures = []
self.skipped = []
self._newFailures = 0
def wasSuccessful(self): def wasSuccessful(self):
return self.errorsNum == 0 and self.failuresNum == 0 return self.errorsNum == 0 and self.failuresNum == 0
def printErrors(self): # TODO: Uncompliant
if self.errors or self.failures: def run_class(c, test_result):
print() o = c()
self.printErrorList(self.errors)
self.printErrorList(self.failures)
def printErrorList(self, lst):
sep = "----------------------------------------------------------------------"
for c, e in lst:
detail = " ".join((str(i) for i in c))
print("======================================================================")
print(f"FAIL: {detail}")
print(sep)
print(e)
def __repr__(self):
# Format is compatible with CPython.
return "<unittest.result.TestResult run=%d errors=%d failures=%d>" % (
self.testsRun,
self.errorsNum,
self.failuresNum,
)
def __add__(self, other):
self.errorsNum += other.errorsNum
self.failuresNum += other.failuresNum
self.skippedNum += other.skippedNum
self.testsRun += other.testsRun
self.errors.extend(other.errors)
self.failures.extend(other.failures)
self.skipped.extend(other.skipped)
return self
def _capture_exc(exc, exc_traceback):
buf = io.StringIO()
if hasattr(sys, "print_exception"):
sys.print_exception(exc, buf)
elif traceback is not None:
traceback.print_exception(None, exc, exc_traceback, file=buf)
return buf.getvalue()
def _handle_test_exception(
current_test: tuple, test_result: TestResult, exc_info: tuple, verbose=True
):
exc = exc_info[1]
traceback = exc_info[2]
ex_str = _capture_exc(exc, traceback)
if isinstance(exc, AssertionError):
test_result.failuresNum += 1
test_result.failures.append((current_test, ex_str))
if verbose:
print(" FAIL")
else:
test_result.errorsNum += 1
test_result.errors.append((current_test, ex_str))
if verbose:
print(" ERROR")
test_result._newFailures += 1
def _run_suite(c, test_result: TestResult, suite_name=""):
if isinstance(c, TestSuite):
c.run(test_result)
return
if isinstance(c, type):
o = c()
else:
o = c
set_up_class = getattr(o, "setUpClass", lambda: None)
tear_down_class = getattr(o, "tearDownClass", lambda: None)
set_up = getattr(o, "setUp", lambda: None) set_up = getattr(o, "setUp", lambda: None)
tear_down = getattr(o, "tearDown", lambda: None) tear_down = getattr(o, "tearDown", lambda: None)
exceptions = [] for name in dir(o):
try: if name.startswith("test"):
suite_name += "." + c.__qualname__ print("%s (%s) ..." % (name, c.__qualname__), end="")
except AttributeError: m = getattr(o, name)
pass set_up()
def run_one(test_function):
global __test_result__, __current_test__
print("%s (%s) ..." % (name, suite_name), end="")
set_up()
__test_result__ = test_result
test_container = f"({suite_name})"
__current_test__ = (name, test_container)
try:
test_result._newFailures = 0
test_result.testsRun += 1
test_function()
# No exception occurred, test passed
if test_result._newFailures:
print(" FAIL")
else:
print(" ok")
except SkipTest as e:
reason = e.args[0]
print(" skipped:", reason)
test_result.skippedNum += 1
test_result.skipped.append((name, c, reason))
except Exception as ex:
_handle_test_exception(
current_test=(name, c), test_result=test_result, exc_info=(type(ex), ex, None)
)
# Uncomment to investigate failure in detail
# raise
finally:
__test_result__ = None
__current_test__ = None
tear_down()
try: try:
o.doCleanups() test_result.testsRun += 1
except AttributeError: m()
pass print(" ok")
except SkipTest as e:
set_up_class() print(" skipped:", e.args[0])
try: test_result.skippedNum += 1
if hasattr(o, "runTest"): except:
name = str(o) print(" FAIL")
run_one(o.runTest) test_result.failuresNum += 1
return # Uncomment to investigate failure in detail
#raise
for name in dir(o): continue
if name.startswith("test"): finally:
m = getattr(o, name) tear_down()
if not callable(m):
continue
run_one(m)
if callable(o):
name = o.__name__
run_one(o)
finally:
tear_down_class()
return exceptions
# This supports either: def main(module="__main__"):
# def test_cases(m):
# >>> import mytest for tn in dir(m):
# >>> unitttest.main(mytest) c = getattr(m, tn)
# if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase):
# >>> unittest.main("mytest") yield c
#
# Or, a script that ends with:
# if __name__ == "__main__":
# unittest.main()
# e.g. run via `mpremote run mytest.py`
def main(module="__main__", testRunner=None):
if testRunner is None:
testRunner = TestRunner()
elif isinstance(testRunner, type):
testRunner = testRunner()
if isinstance(module, str): m = __import__(module)
module = __import__(module) suite = TestSuite()
suite = TestSuite(module.__name__) for c in test_cases(m):
suite._load_module(module) suite.addTest(c)
return testRunner.run(suite) runner = TestRunner()
result = runner.run(suite)
# Terminate with non zero return code in case of failures
sys.exit(result.failuresNum > 0)

View File

@@ -1,14 +0,0 @@
#!/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

@@ -1,48 +1,6 @@
[project]
name = "microdot"
version = "2.0.2"
authors = [
{ name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
]
description = "The impossibly small web framework for MicroPython"
classifiers = [
"Environment :: Web Environment",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: Implementation :: MicroPython",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
requires-python = ">=3.8"
[project.readme]
file = "README.md"
content-type = "text/markdown"
[project.urls]
Homepage = "https://github.com/miguelgrinberg/microdot"
"Bug Tracker" = "https://github.com/miguelgrinberg/microdot/issues"
[project.optional-dependencies]
docs = [
"sphinx",
]
[tool.setuptools]
zip-safe = false
include-package-data = true
[tool.setuptools.package-dir]
"" = "src"
[tool.setuptools.packages.find]
where = [
"src",
]
namespaces = false
[build-system] [build-system]
requires = [ requires = [
"setuptools>=61.2", "setuptools>=42",
"wheel"
] ]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"

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