48 Commits

Author SHA1 Message Date
Miguel Grinberg
92e571ee32 Release 2.0.0 2023-12-22 20:41:15 +00:00
Miguel Grinberg
655f23ee7e Documentation links update #nolog 2023-12-22 20:34:48 +00:00
Miguel Grinberg
20ea305fe7 v2 (#186) 2023-12-22 20:26:07 +00:00
Miguel Grinberg
7a329d98a8 Version 1.3.5.dev0 2023-11-08 00:15:07 +00:00
Miguel Grinberg
93411c6a9f Release 1.3.4 2023-11-08 00:14:21 +00:00
Miguel Grinberg
5550b20cdd Handle change in wait_closed() behavior in python 3.12 (Fixes #177) 2023-11-08 00:11:14 +00:00
dependabot[bot]
d8d2667053 Bump urllib3 from 1.26.17 to 1.26.18 in /examples/benchmark (#173) #nolog
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.17 to 1.26.18.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.17...1.26.18)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-18 09:38:45 +01:00
Miguel Grinberg
3943a69374 Migrate Python package metadata to pyproject.toml 2023-10-15 12:51:27 +01:00
dependabot[bot]
a2f6985d01 Bump urllib3 from 1.26.11 to 1.26.17 in /examples/benchmark (#172) #nolog
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.11 to 1.26.17.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.11...1.26.17)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 12:10:54 +01:00
dependabot[bot]
4238aa4cd4 Bump flask from 2.2.1 to 2.3.2 in /examples/benchmark (#131) #nolog
Bumps [flask](https://github.com/pallets/flask) from 2.2.1 to 2.3.2.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/2.2.1...2.3.2)

---
updated-dependencies:
- dependency-name: flask
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-14 10:20:02 +01:00
Miguel Grinberg
744548f8dc Added missing request argument in some documentation examples (Fixes #163) 2023-09-01 10:33:58 +01:00
dependabot[bot]
d46d2950c8 Bump certifi from 2022.12.7 to 2023.7.22 in /examples/benchmark (#158) #nolog
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-20 11:41:24 +01:00
Andy Piper
2e4911d108 Docs: fix minor typos (#161) 2023-08-03 10:40:55 +01:00
Miguel Grinberg
3eb57d0fcf Version 1.3.4.dev0 2023-07-16 11:42:54 +01:00
Miguel Grinberg
42406cef42 Release 1.3.3 2023-07-16 11:41:02 +01:00
Miguel Grinberg
e09e9830f4 Support empty responses with ASGI adapter 2023-07-16 11:36:48 +01:00
Miguel Grinberg
304ca2ef68 Added CORS extension to Python package 2023-06-29 00:36:06 +01:00
Miguel Grinberg
d99df2c401 Document access to WSGI and ASGI attributes (Fixes #153) 2023-06-24 10:34:55 +01:00
Miguel Grinberg
3554bc91cb Handle query string arguments without value (Fixes #149) 2023-06-21 20:20:53 +01:00
Miguel Grinberg
51f910087a Add readthedocs config file 2023-06-20 12:34:59 +01:00
Miguel Grinberg
e0f0565551 Upgrade micropython tests to use v1.20 2023-06-16 16:53:03 +01:00
Miguel Grinberg
2a6e76c685 Version 1.3.3.dev0 2023-06-13 14:45:20 +01:00
Miguel Grinberg
42c88b6b20 Release 1.3.2 2023-06-13 14:45:10 +01:00
Miguel Grinberg
c07a539435 Incorrect import in static_async.py 2023-06-08 00:33:58 +01:00
Miguel Grinberg
e92310fa55 In ASGI, return headers as strings and not binary (Fixes #144) 2023-06-07 23:50:44 +01:00
dependabot[bot]
9b9b7aa76d Bump requests from 2.28.1 to 2.31.0 in /examples/benchmark (#138) #nolog
Bumps [requests](https://github.com/psf/requests) from 2.28.1 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.28.1...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:40:38 +01:00
Miguel Grinberg
696f2e3e18 Version 1.3.2.dev0 2023-05-21 23:37:55 +01:00
Miguel Grinberg
87c47ccefc Release 1.3.1 2023-05-21 23:37:40 +01:00
Miguel Grinberg
a0dd7c8ab6 Support negative numbers for int path components (Fixes #137) 2023-05-21 23:22:03 +01:00
dependabot[bot]
a80841f464 Bump starlette from 0.25.0 to 0.27.0 in /examples/benchmark (#136) #nolog
Bumps [starlette](https://github.com/encode/starlette) from 0.25.0 to 0.27.0.
- [Release notes](https://github.com/encode/starlette/releases)
- [Changelog](https://github.com/encode/starlette/blob/master/docs/release-notes.md)
- [Commits](https://github.com/encode/starlette/compare/0.25.0...0.27.0)

---
updated-dependencies:
- dependency-name: starlette
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 11:19:39 +01:00
Miguel Grinberg
f81de6d958 Explicitly set UTF-8 encoding for HTML in examples (Fixes #132) 2023-05-12 09:17:54 +01:00
Miguel Grinberg
efec9f14be More robust check for socket timeout error code (Fixes #106) 2023-04-24 18:24:16 +01:00
Miguel Grinberg
239cf4ff37 Use a more conservative default for socket timeout (Fixes #130) 2023-04-24 18:19:41 +01:00
Miguel Grinberg
87cd098f66 WebSocket error when handling PING packet (Fixes #129) 2023-04-14 15:29:26 +01:00
Miguel Grinberg
bb75e15b2d Upgrade GitHub actions #nolog 2023-04-14 12:56:33 +01:00
Miguel Grinberg
b7ad02eaf1 Version 1.3.1.dev0 2023-04-08 17:23:33 +01:00
Miguel Grinberg
79e11262d1 Release 1.3.0 2023-04-08 17:21:30 +01:00
Miguel Grinberg
a1b061656f Tolerate slightly invalid formats in query strings (Fixes #126) 2023-04-08 17:15:54 +01:00
Miguel Grinberg
67798f7dbf Cross-Origin Resource Sharing (CORS) support (Fixes #45) 2023-03-23 00:03:21 +00:00
Miguel Grinberg
ea6766cea9 Add update() method to NoCaseDict class 2023-03-22 20:22:29 +00:00
Miguel Grinberg
6a31f89673 Respond to HEAD and OPTIONS requests 2023-03-22 12:25:21 +00:00
Miguel Grinberg
eaf2ef62d1 Documentation typo #nolog 2023-03-21 00:32:22 +00:00
Miguel Grinberg
a350e8fd1e Set exit code to 1 for failed MicroPython test runs 2023-03-21 00:28:23 +00:00
Miguel Grinberg
daf1001ec5 Support compressed files in send_file() (Fixes #93) 2023-03-21 00:24:57 +00:00
Miguel Grinberg
e684ee32d9 Add max_age argument to send_file() 2023-03-20 12:11:01 +00:00
Miguel Grinberg
573e303a98 Issue templates #nolog 2023-03-03 14:49:10 +00:00
Miguel Grinberg
3592f53999 Update gitignore #nolog 2023-03-03 11:06:50 +00:00
Miguel Grinberg
ea3722ca5c Version 1.2.5.dev0 2023-03-03 08:46:27 +00:00
136 changed files with 4537 additions and 6358 deletions

View File

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

View File

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

26
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

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

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

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

View File

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

5
.gitignore vendored
View File

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

16
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,16 @@
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,5 +1,46 @@
# Microdot change log
**Release 2.0.0** - 2023-12-22
- Major redesign switching to asyncio as the base implementation (See the [Migration Guide](https://microdot.readthedocs.io/en/stable/migrating.html) in the docs for details) [#186](https://github.com/miguelgrinberg/microdot/issues/186) ([commit](https://github.com/miguelgrinberg/microdot/commit/20ea305fe793eb206b52af9eb5c5f3c1e9f57dbb))
**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))

5
MANIFEST.in Normal file
View File

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

Binary file not shown.

View File

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

View File

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

110
docs/freezing.rst Normal file
View File

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

View File

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

View File

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

142
docs/migrating.rst Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

1
examples/cors/README.md Normal file
View File

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

14
examples/cors/app.py Normal file
View File

@@ -0,0 +1,14 @@
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,6 +2,7 @@
<html>
<head>
<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">
<script>
function getCookie(name) {

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
<!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,6 +2,7 @@
<html>
<head>
<title>Microdot Upload Example</title>
<meta charset="UTF-8">
</head>
<body>
<h1>Microdot Upload Example</h1>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -300,6 +300,7 @@ class TestResult:
return self.errorsNum == 0 and self.failuresNum == 0
def printErrors(self):
if self.errors or self.failures:
print()
self.printErrorList(self.errors)
self.printErrorList(self.failures)

14
libs/refresh.sh Executable file
View File

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

View File

@@ -1,6 +1,48 @@
[project]
name = "microdot"
version = "2.0.0"
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]
requires = [
"setuptools>=42",
"wheel"
"setuptools>=61.2",
]
build-backend = "setuptools.build_meta"

View File

@@ -6,4 +6,5 @@ sys.path.insert(3, 'libs/micropython')
import unittest
unittest.main('tests')
if not unittest.main('tests').wasSuccessful():
sys.exit(1)

View File

@@ -1,38 +0,0 @@
[metadata]
name = microdot
version = 1.2.4
author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com
description = The impossibly small web framework for MicroPython
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/miguelgrinberg/microdot
project_urls =
Bug Tracker = https://github.com/miguelgrinberg/microdot/issues
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
[options]
zip_safe = False
include_package_data = True
package_dir =
= src
py_modules =
microdot
microdot_asyncio
microdot_utemplate
microdot_jinja
microdot_session
microdot_websocket
microdot_websocket_alt
microdot_asyncio_websocket
microdot_test_client
microdot_asyncio_test_client
microdot_wsgi
microdot_asgi
microdot_asgi_websocket

View File

@@ -1,3 +0,0 @@
import setuptools
setuptools.setup()

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

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

View File

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

110
src/microdot/cors.py Normal file
View File

@@ -0,0 +1,110 @@
class CORS:
"""Add CORS headers to HTTP responses.
:param app: The application to add CORS headers to.
:param allowed_origins: A list of origins that are allowed to make
cross-site requests. If set to '*', all origins are
allowed.
:param allow_credentials: If set to True, the
``Access-Control-Allow-Credentials`` header will
be set to ``true`` to indicate to the browser
that it can expose cookies and authentication
headers.
:param allowed_methods: A list of methods that are allowed to be used when
making cross-site requests. If not set, all methods
are allowed.
:param expose_headers: A list of headers that the browser is allowed to
exposed.
:param allowed_headers: A list of headers that are allowed to be used when
making cross-site requests. If not set, all headers
are allowed.
:param max_age: The maximum amount of time in seconds that the browser
should cache the results of a preflight request.
:param handle_cors: If set to False, CORS headers will not be added to
responses. This can be useful if you want to add CORS
headers manually.
"""
def __init__(self, app=None, allowed_origins=None, allow_credentials=False,
allowed_methods=None, expose_headers=None,
allowed_headers=None, max_age=None, handle_cors=True):
self.allowed_origins = allowed_origins
self.allow_credentials = allow_credentials
self.allowed_methods = allowed_methods
self.expose_headers = expose_headers
self.allowed_headers = None if allowed_headers is None \
else [h.lower() for h in allowed_headers]
self.max_age = max_age
if app is not None:
self.initialize(app, handle_cors=handle_cors)
def initialize(self, app, handle_cors=True):
"""Initialize the CORS object for the given application.
:param app: The application to add CORS headers to.
:param handle_cors: If set to False, CORS headers will not be added to
responses. This can be useful if you want to add
CORS headers manually.
"""
self.default_options_handler = app.options_handler
if handle_cors:
app.options_handler = self.options_handler
app.after_request(self.after_request)
app.after_error_request(self.after_request)
def options_handler(self, request):
headers = self.default_options_handler(request)
headers.update(self.get_cors_headers(request))
return headers
def get_cors_headers(self, request):
"""Return a dictionary of CORS headers to add to a given request.
:param request: The request to add CORS headers to.
"""
cors_headers = {}
origin = request.headers.get('Origin')
if self.allowed_origins == '*':
cors_headers['Access-Control-Allow-Origin'] = origin or '*'
if origin:
cors_headers['Vary'] = 'Origin'
elif origin in (self.allowed_origins or []):
cors_headers['Access-Control-Allow-Origin'] = origin
cors_headers['Vary'] = 'Origin'
if self.allow_credentials and \
'Access-Control-Allow-Origin' in cors_headers:
cors_headers['Access-Control-Allow-Credentials'] = 'true'
if self.expose_headers:
cors_headers['Access-Control-Expose-Headers'] = \
', '.join(self.expose_headers)
if request.method == 'OPTIONS':
# handle preflight request
if self.max_age:
cors_headers['Access-Control-Max-Age'] = str(self.max_age)
method = request.headers.get('Access-Control-Request-Method')
if method:
method = method.upper()
if self.allowed_methods is None or \
method in self.allowed_methods:
cors_headers['Access-Control-Allow-Methods'] = method
headers = request.headers.get('Access-Control-Request-Headers')
if headers:
if self.allowed_headers is None:
cors_headers['Access-Control-Allow-Headers'] = headers
else:
headers = [h.strip() for h in headers.split(',')]
headers = [h for h in headers
if h.lower() in self.allowed_headers]
cors_headers['Access-Control-Allow-Headers'] = \
', '.join(headers)
return cors_headers
def after_request(self, request, response):
saved_vary = response.headers.get('Vary')
response.headers.update(self.get_cors_headers(request))
if saved_vary and saved_vary != response.headers.get('Vary'):
response.headers['Vary'] = (
saved_vary + ', ' + response.headers['Vary'])

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

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

View File

@@ -3,9 +3,43 @@ microdot
--------
The ``microdot`` module defines a few classes that help implement HTTP-based
servers for MicroPython and standard Python, with multithreading support for
Python interpreters that support it.
servers for MicroPython and standard Python.
"""
import asyncio
import io
import json
import re
import time
try:
from inspect import iscoroutinefunction, iscoroutine
async def invoke_handler(handler, *args, **kwargs):
"""Invoke a handler and return the result.
This method runs sync handlers in a thread pool executor.
"""
if iscoroutinefunction(handler):
ret = await handler(*args, **kwargs)
else:
ret = await asyncio.get_running_loop().run_in_executor(
None, handler, *args, **kwargs)
return ret
except ImportError: # pragma: no cover
def iscoroutine(coro):
return hasattr(coro, 'send') and hasattr(coro, 'throw')
async def invoke_handler(handler, *args, **kwargs):
"""Invoke a handler and return the result.
This method runs sync handlers in the asyncio thread, which can
potentially cause blocking and performance issues.
"""
ret = handler(*args, **kwargs)
if iscoroutine(ret):
ret = await ret
return ret
try:
from sys import print_exception
except ImportError: # pragma: no cover
@@ -13,45 +47,6 @@ except ImportError: # pragma: no cover
def print_exception(exc):
traceback.print_exc()
try:
import uerrno as errno
except ImportError:
import errno
concurrency_mode = 'threaded'
try: # pragma: no cover
import threading
def create_thread(f, *args, **kwargs):
# use the threading module
threading.Thread(target=f, args=args, kwargs=kwargs).start()
except ImportError: # pragma: no cover
def create_thread(f, *args, **kwargs):
# no threads available, call function synchronously
f(*args, **kwargs)
concurrency_mode = 'sync'
try:
import ujson as json
except ImportError:
import json
try:
import ure as re
except ImportError:
import re
socket_timeout_error = OSError
try:
import usocket as socket
except ImportError:
try:
import socket
socket_timeout_error = socket.timeout
except ImportError: # pragma: no cover
socket = None
MUTED_SOCKET_ERRORS = [
32, # Broken pipe
@@ -146,6 +141,10 @@ class NoCaseDict(dict):
kl = key.lower()
return super().get(self.keymap.get(kl, kl), default)
def update(self, other_dict):
for key, value in other_dict.items():
self[key] = value
def mro(cls): # pragma: no cover
"""Return the method resolution order of a class.
@@ -271,7 +270,31 @@ class MultiDict(dict):
return values
class Request():
class AsyncBytesIO:
"""An async wrapper for BytesIO."""
def __init__(self, data):
self.stream = io.BytesIO(data)
async def read(self, n=-1):
return self.stream.read(n)
async def readline(self): # pragma: no cover
return self.stream.readline()
async def readexactly(self, n): # pragma: no cover
return self.stream.read(n)
async def readuntil(self, separator=b'\n'): # pragma: no cover
return self.stream.readuntil(separator=separator)
async def awrite(self, data): # pragma: no cover
return self.stream.write(data)
async def aclose(self): # pragma: no cover
pass
class Request:
"""An HTTP request."""
#: Specify the maximum payload size that is accepted. Requests with larger
#: payloads will be rejected with a 413 status code. Applications can
@@ -302,11 +325,6 @@ class Request():
#: Request.max_readline = 16 * 1024 # 16KB lines allowed
max_readline = 2 * 1024
#: Specify a suggested read timeout to use when reading the request. Set to
#: 0 to disable the use of a timeout. This timeout should be considered a
#: suggestion only, as some platforms may not support it.
socket_read_timeout = 0.1
class G:
pass
@@ -356,82 +374,84 @@ class Request():
self._body = body
self.body_used = False
self._stream = stream
self.stream_used = False
self.sock = sock
self._json = None
self._form = None
self.after_request_handlers = []
@staticmethod
def create(app, client_stream, client_addr, client_sock=None):
async def create(app, client_reader, client_writer, client_addr):
"""Create a request object.
:param app: The Microdot application instance.
:param client_stream: An input stream from where the request data can
:param client_reader: An input stream from where the request data can
be read.
:param client_writer: An output stream where the response data can be
written.
:param client_addr: The address of the client, as a tuple.
:param client_sock: The low-level socket associated with the request.
This method returns a newly created ``Request`` object.
This method is a coroutine. It returns a newly created ``Request``
object.
"""
# request line
line = Request._safe_readline(client_stream).strip().decode()
if not line:
line = (await Request._safe_readline(client_reader)).strip().decode()
if not line: # pragma: no cover
return None
method, url, http_version = line.split()
http_version = http_version.split('/', 1)[1]
# headers
headers = NoCaseDict()
content_length = 0
while True:
line = Request._safe_readline(client_stream).strip().decode()
line = (await Request._safe_readline(
client_reader)).strip().decode()
if line == '':
break
header, value = line.split(':', 1)
value = value.strip()
headers[header] = value
if header.lower() == 'content-length':
content_length = int(value)
# body
body = b''
if content_length and content_length <= Request.max_body_length:
body = await client_reader.readexactly(content_length)
stream = None
else:
body = b''
stream = client_reader
return Request(app, client_addr, method, url, http_version, headers,
stream=client_stream, sock=client_sock)
body=body, stream=stream,
sock=(client_reader, client_writer))
def _parse_urlencoded(self, urlencoded):
data = MultiDict()
if len(urlencoded) > 0:
if len(urlencoded) > 0: # pragma: no branch
if isinstance(urlencoded, str):
for k, v in [pair.split('=', 1)
for pair in urlencoded.split('&')]:
data[urldecode_str(k)] = urldecode_str(v)
for kv in [pair.split('=', 1)
for pair in urlencoded.split('&') if pair]:
data[urldecode_str(kv[0])] = urldecode_str(kv[1]) \
if len(kv) > 1 else ''
elif isinstance(urlencoded, bytes): # pragma: no branch
for k, v in [pair.split(b'=', 1)
for pair in urlencoded.split(b'&')]:
data[urldecode_bytes(k)] = urldecode_bytes(v)
for kv in [pair.split(b'=', 1)
for pair in urlencoded.split(b'&') if pair]:
data[urldecode_bytes(kv[0])] = urldecode_bytes(kv[1]) \
if len(kv) > 1 else b''
return data
@property
def body(self):
"""The body of the request, as bytes."""
if self.stream_used:
raise RuntimeError('Cannot use both stream and body')
if self._body is None:
self._body = b''
if self.content_length and \
self.content_length <= Request.max_body_length:
while len(self._body) < self.content_length:
data = self._stream.read(
self.content_length - len(self._body))
if len(data) == 0: # pragma: no cover
raise EOFError()
self._body += data
self.body_used = True
return self._body
@property
def stream(self):
"""The input stream, containing the request body."""
if self.body_used:
raise RuntimeError('Cannot use both stream and body')
self.stream_used = True
"""The body of the request, as a bytes stream."""
if self._stream is None:
self._stream = AsyncBytesIO(self._body)
return self._stream
@property
@@ -487,21 +507,21 @@ class Request():
return f
@staticmethod
def _safe_readline(stream):
line = stream.readline(Request.max_readline + 1)
async def _safe_readline(stream):
line = (await stream.readline())
if len(line) > Request.max_readline:
raise ValueError('line too long')
return line
class Response():
class Response:
"""An HTTP response class.
:param body: The body of the response. If a dictionary or list is given,
a JSON formatter is used to generate the body. If a file-like
object or a generator is given, a streaming response is used.
If a string is given, it is encoded from UTF-8. Else, the
body should be a byte sequence.
object or an async generator is given, a streaming response is
used. If a string is given, it is encoded from UTF-8. Else,
the body should be a byte sequence.
:param status_code: The numeric HTTP status code of the response. The
default is 200.
:param headers: A dictionary of headers to include in the response.
@@ -519,12 +539,17 @@ class Response():
'png': 'image/png',
'txt': 'text/plain',
}
send_file_buffer_size = 1024
#: The content type to use for responses that do not explicitly define a
#: ``Content-Type`` header.
default_content_type = 'text/plain'
#: The default cache control max age used by :meth:`send_file`. A value
#: of ``None`` means that no ``Cache-Control`` header is added.
default_send_file_max_age = None
#: Special response used to signal that a response does not need to be
#: written to the client. Used to exit WebSocket connections cleanly.
already_handled = None
@@ -544,9 +569,11 @@ class Response():
else:
# this applies to bytes, file-like objects or generators
self.body = body
self.is_head = False
def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
max_age=None, secure=False, http_only=False):
max_age=None, secure=False, http_only=False,
partitioned=False):
"""Add a cookie to the response.
:param cookie: The cookie's name.
@@ -558,6 +585,7 @@ class Response():
:param max_age: The cookie's ``Max-Age`` value.
:param secure: The cookie's ``secure`` flag.
:param http_only: The cookie's ``HttpOnly`` flag.
:param partitioned: Whether the cookie is partitioned.
"""
http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value)
if path:
@@ -568,19 +596,31 @@ class Response():
if isinstance(expires, str):
http_cookie += '; Expires=' + expires
else:
http_cookie += '; Expires=' + expires.strftime(
'%a, %d %b %Y %H:%M:%S GMT')
http_cookie += '; Expires=' + time.strftime(
'%a, %d %b %Y %H:%M:%S GMT', expires.timetuple())
if max_age:
http_cookie += '; Max-Age=' + str(max_age)
if secure:
http_cookie += '; Secure'
if http_only:
http_cookie += '; HttpOnly'
if partitioned:
http_cookie += '; Partitioned'
if 'Set-Cookie' in self.headers:
self.headers['Set-Cookie'].append(http_cookie)
else:
self.headers['Set-Cookie'] = [http_cookie]
def delete_cookie(self, cookie, **kwargs):
"""Delete a cookie.
:param cookie: The cookie's name.
:param kwargs: Any cookie opens and flags supported by
``set_cookie()`` except ``expires``.
"""
self.set_cookie(cookie, '', expires='Thu, 01 Jan 1970 00:00:01 GMT',
**kwargs)
def complete(self):
if isinstance(self.body, bytes) and \
'Content-Length' not in self.headers:
@@ -590,53 +630,101 @@ class Response():
if 'charset=' not in self.headers['Content-Type']:
self.headers['Content-Type'] += '; charset=UTF-8'
def write(self, stream):
async def write(self, stream):
self.complete()
try:
# status code
reason = self.reason if self.reason is not None else \
('OK' if self.status_code == 200 else 'N/A')
stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format(
await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code, reason=reason).encode())
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
stream.write('{header}: {value}\r\n'.format(
await stream.awrite('{header}: {value}\r\n'.format(
header=header, value=value).encode())
stream.write(b'\r\n')
await stream.awrite(b'\r\n')
# body
can_flush = hasattr(stream, 'flush')
try:
for body in self.body_iter():
if not self.is_head:
iter = self.body_iter()
async for body in iter:
if isinstance(body, str): # pragma: no cover
body = body.encode()
stream.write(body)
if can_flush: # pragma: no cover
stream.flush()
try:
await stream.awrite(body)
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS:
if exc.errno in MUTED_SOCKET_ERRORS or \
exc.args[0] == 'Connection lost':
if hasattr(iter, 'aclose'):
await iter.aclose()
raise
if hasattr(iter, 'aclose'): # pragma: no branch
await iter.aclose()
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS or \
exc.args[0] == 'Connection lost':
pass
else:
raise
def body_iter(self):
if self.body:
if hasattr(self.body, 'read'):
while True:
buf = self.body.read(self.send_file_buffer_size)
if len(buf):
yield buf
if len(buf) < self.send_file_buffer_size:
break
if hasattr(self.body, 'close'): # pragma: no cover
self.body.close()
elif hasattr(self.body, '__next__'):
yield from self.body
if hasattr(self.body, '__anext__'):
# response body is an async generator
return self.body
response = self
class iter:
ITER_UNKNOWN = 0
ITER_SYNC_GEN = 1
ITER_FILE_OBJ = 2
ITER_NO_BODY = -1
def __aiter__(self):
if response.body:
self.i = self.ITER_UNKNOWN # need to determine type
else:
yield self.body
self.i = self.ITER_NO_BODY
return self
async def __anext__(self):
if self.i == self.ITER_NO_BODY:
await self.aclose()
raise StopAsyncIteration
if self.i == self.ITER_UNKNOWN:
if hasattr(response.body, 'read'):
self.i = self.ITER_FILE_OBJ
elif hasattr(response.body, '__next__'):
self.i = self.ITER_SYNC_GEN
return next(response.body)
else:
self.i = self.ITER_NO_BODY
return response.body
elif self.i == self.ITER_SYNC_GEN:
try:
return next(response.body)
except StopIteration:
await self.aclose()
raise StopAsyncIteration
buf = response.body.read(response.send_file_buffer_size)
if iscoroutine(buf): # pragma: no cover
buf = await buf
if len(buf) < response.send_file_buffer_size:
self.i = self.ITER_NO_BODY
return buf
async def aclose(self):
if hasattr(response.body, 'close'):
result = response.body.close()
if iscoroutine(result): # pragma: no cover
await result
return iter()
@classmethod
def redirect(cls, location, status_code=302):
@@ -651,7 +739,9 @@ class Response():
return cls(status_code=status_code, headers={'Location': location})
@classmethod
def send_file(cls, filename, status_code=200, content_type=None):
def send_file(cls, filename, status_code=200, content_type=None,
stream=None, max_age=None, compressed=False,
file_extension=''):
"""Send file contents in a response.
:param filename: The filename of the file.
@@ -659,7 +749,25 @@ class Response():
default is 302.
:param content_type: The ``Content-Type`` header to use in the
response. If omitted, it is generated
automatically from the file extension.
automatically from the file extension of the
``filename`` parameter.
:param stream: A file-like object to read the file contents from. If
a stream is given, the ``filename`` parameter is only
used when generating the ``Content-Type`` header.
:param max_age: The ``Cache-Control`` header's ``max-age`` value in
seconds. If omitted, the value of the
:attr:`Response.default_send_file_max_age` attribute is
used.
:param compressed: Whether the file is compressed. If ``True``, the
``Content-Encoding`` header is set to ``gzip``. A
string with the header value can also be passed.
Note that when using this option the file must have
been compressed beforehand. This option only sets
the header.
:param file_extension: A file extension to append to the ``filename``
parameter when opening the file, including the
dot. The extension given here is not considered
when generating the ``Content-Type`` header.
Security note: The filename is assumed to be trusted. Never pass
filenames provided by the user without validating and sanitizing them
@@ -671,9 +779,19 @@ class Response():
content_type = Response.types_map[ext]
else:
content_type = 'application/octet-stream'
f = open(filename, 'rb')
return cls(body=f, status_code=status_code,
headers={'Content-Type': content_type})
headers = {'Content-Type': content_type}
if max_age is None:
max_age = cls.default_send_file_max_age
if max_age is not None:
headers['Cache-Control'] = 'max-age={}'.format(max_age)
if compressed:
headers['Content-Encoding'] = compressed \
if isinstance(compressed, str) else 'gzip'
f = stream or open(filename + file_extension, 'rb')
return cls(body=f, status_code=status_code, headers=headers)
class URLPattern():
@@ -695,7 +813,7 @@ class URLPattern():
if type_ == 'string':
pattern = '[^/]+'
elif type_ == 'int':
pattern = '\\d+'
pattern = '-?\\d+'
elif type_ == 'path':
pattern = '.+'
elif type_.startswith('re:'):
@@ -738,7 +856,7 @@ class HTTPException(Exception):
return 'HTTPException: {}'.format(self.status_code)
class Microdot():
class Microdot:
"""An HTTP application class.
This class implements an HTTP application instance and is heavily
@@ -759,6 +877,7 @@ class Microdot():
self.after_error_request_handlers = []
self.error_handlers = {}
self.shutdown_requested = False
self.options_handler = self.default_options_handler
self.debug = False
self.server = None
@@ -794,7 +913,8 @@ class Microdot():
"""
def decorated(f):
self.url_map.append(
(methods or ['GET'], URLPattern(url_pattern), f))
([m.upper() for m in (methods or ['GET'])],
URLPattern(url_pattern), f))
return f
return decorated
@@ -1003,6 +1123,88 @@ class Microdot():
"""
raise HTTPException(status_code, reason)
async def start_server(self, host='0.0.0.0', port=5000, debug=False,
ssl=None):
"""Start the Microdot web server as a coroutine. This coroutine does
not normally return, as the server enters an endless listening loop.
The :func:`shutdown` function provides a method for terminating the
server gracefully.
:param host: The hostname or IP address of the network interface that
will be listening for requests. A value of ``'0.0.0.0'``
(the default) indicates that the server should listen for
requests on all the available interfaces, and a value of
``127.0.0.1`` indicates that the server should listen
for requests only on the internal networking interface of
the host.
:param port: The port number to listen for requests. The default is
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
:param ssl: An ``SSLContext`` instance or ``None`` if the server should
not use TLS. The default is ``None``.
This method is a coroutine.
Example::
import asyncio
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
async def index(request):
return 'Hello, world!'
async def main():
await app.start_server(debug=True)
asyncio.run(main())
"""
self.debug = debug
async def serve(reader, writer):
if not hasattr(writer, 'awrite'): # pragma: no cover
# CPython provides the awrite and aclose methods in 3.8+
async def awrite(self, data):
self.write(data)
await self.drain()
async def aclose(self):
self.close()
await self.wait_closed()
from types import MethodType
writer.awrite = MethodType(awrite, writer)
writer.aclose = MethodType(aclose, writer)
await self.handle_request(reader, writer)
if self.debug: # pragma: no cover
print('Starting async server on {host}:{port}...'.format(
host=host, port=port))
try:
self.server = await asyncio.start_server(serve, host, port,
ssl=ssl)
except TypeError: # pragma: no cover
self.server = await asyncio.start_server(serve, host, port)
while True:
try:
if hasattr(self.server, 'serve_forever'): # pragma: no cover
try:
await self.server.serve_forever()
except asyncio.CancelledError:
pass
await self.server.wait_closed()
break
except AttributeError: # pragma: no cover
# the task hasn't been initialized in the server object yet
# wait a bit and try again
await asyncio.sleep(0.1)
def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None):
"""Start the web server. This function does not normally return, as
the server enters an endless listening loop. The :func:`shutdown`
@@ -1024,45 +1226,18 @@ class Microdot():
Example::
from microdot import Microdot
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
def index():
async def index(request):
return 'Hello, world!'
app.run(debug=True)
"""
self.debug = debug
self.shutdown_requested = False
self.server = socket.socket()
ai = socket.getaddrinfo(host, port)
addr = ai[0][-1]
if self.debug: # pragma: no cover
print('Starting {mode} server on {host}:{port}...'.format(
mode=concurrency_mode, host=host, port=port))
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind(addr)
self.server.listen(5)
if ssl:
self.server = ssl.wrap_socket(self.server, server_side=True)
while not self.shutdown_requested:
try:
sock, addr = self.server.accept()
except OSError as exc: # pragma: no cover
if exc.errno == errno.ECONNABORTED:
break
else:
print_exception(exc)
except Exception as exc: # pragma: no cover
print_exception(exc)
else:
create_thread(self.handle_request, sock, addr)
asyncio.run(self.start_server(host=host, port=port, debug=debug,
ssl=ssl)) # pragma: no cover
def shutdown(self):
"""Request a server shutdown. The server will then exit its request
@@ -1077,65 +1252,64 @@ class Microdot():
request.app.shutdown()
return 'The server is shutting down...'
"""
self.shutdown_requested = True
self.server.close()
def find_route(self, req):
method = req.method.upper()
if method == 'OPTIONS' and self.options_handler:
return self.options_handler(req)
if method == 'HEAD':
method = 'GET'
f = 404
for route_methods, route_pattern, route_handler in self.url_map:
req.url_args = route_pattern.match(req.path)
if req.url_args is not None:
if req.method in route_methods:
if method in route_methods:
f = route_handler
break
else:
f = 405
return f
def handle_request(self, sock, addr):
if Request.socket_read_timeout and \
hasattr(sock, 'settimeout'): # pragma: no cover
sock.settimeout(Request.socket_read_timeout)
if not hasattr(sock, 'readline'): # pragma: no cover
stream = sock.makefile("rwb")
else:
stream = sock
def default_options_handler(self, req):
allow = []
for route_methods, route_pattern, route_handler in self.url_map:
if route_pattern.match(req.path) is not None:
allow.extend(route_methods)
if 'GET' in allow:
allow.append('HEAD')
allow.append('OPTIONS')
return {'Allow': ', '.join(allow)}
async def handle_request(self, reader, writer):
req = None
res = None
try:
req = Request.create(self, stream, addr, sock)
res = self.dispatch_request(req)
except socket_timeout_error as exc: # pragma: no cover
if exc.errno and exc.errno not in [60, 110]:
print_exception(exc) # not a timeout
req = await Request.create(self, reader, writer,
writer.get_extra_info('peername'))
except Exception as exc: # pragma: no cover
print_exception(exc)
res = await self.dispatch_request(req)
if res != Response.already_handled: # pragma: no branch
await res.write(writer)
try:
if res and res != Response.already_handled: # pragma: no branch
res.write(stream)
stream.close()
await writer.aclose()
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS:
pass
else:
print_exception(exc)
except Exception as exc: # pragma: no cover
print_exception(exc)
if stream != sock: # pragma: no cover
sock.close()
if self.shutdown_requested: # pragma: no cover
self.server.close()
raise
if self.debug and req: # pragma: no cover
print('{method} {path} {status_code}'.format(
method=req.method, path=req.path,
status_code=res.status_code))
def dispatch_request(self, req):
async def dispatch_request(self, req):
after_request_handled = False
if req:
if req.content_length > req.max_content_length:
if 413 in self.error_handlers:
res = self.error_handlers[413](req)
res = await invoke_handler(self.error_handlers[413], req)
else:
res = 'Payload too large', 413
else:
@@ -1144,11 +1318,12 @@ class Microdot():
res = None
if callable(f):
for handler in self.before_request_handlers:
res = handler(req)
res = await invoke_handler(handler, req)
if res:
break
if res is None:
res = f(req, **req.url_args)
res = await invoke_handler(
f, req, **req.url_args)
if isinstance(res, tuple):
body = res[0]
if isinstance(res[1], int):
@@ -1161,12 +1336,16 @@ class Microdot():
elif not isinstance(res, Response):
res = Response(res)
for handler in self.after_request_handlers:
res = handler(req, res) or res
res = await invoke_handler(
handler, req, res) or res
for handler in req.after_request_handlers:
res = handler(req, res) or res
res = await invoke_handler(
handler, req, res) or res
after_request_handled = True
elif isinstance(f, dict):
res = Response(headers=f)
elif f in self.error_handlers:
res = self.error_handlers[f](req)
res = await invoke_handler(self.error_handlers[f], req)
else:
res = 'Not found', f
except HTTPException as exc:
@@ -1187,31 +1366,35 @@ class Microdot():
break
if exc_class:
try:
res = self.error_handlers[exc_class](req, exc)
res = await invoke_handler(
self.error_handlers[exc_class], req, exc)
except Exception as exc2: # pragma: no cover
print_exception(exc2)
if res is None:
if 500 in self.error_handlers:
res = self.error_handlers[500](req)
res = await invoke_handler(
self.error_handlers[500], req)
else:
res = 'Internal server error', 500
else:
if 400 in self.error_handlers:
res = self.error_handlers[400](req)
res = await invoke_handler(self.error_handlers[400], req)
else:
res = 'Bad request', 400
if isinstance(res, tuple):
res = Response(*res)
elif not isinstance(res, Response):
res = Response(res)
if not after_request_handled:
for handler in self.after_error_request_handlers:
res = handler(req, res) or res
res = await invoke_handler(
handler, req, res) or res
res.is_head = (req and req.method == 'HEAD')
return res
abort = Microdot.abort
Response.already_handled = Response()
abort = Microdot.abort
redirect = Response.redirect
send_file = Response.send_file

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

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

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

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

View File

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

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

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

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