51 Commits

Author SHA1 Message Date
Miguel Grinberg
a8ae47799c Release v2.0.0 2023-12-22 20:39:09 +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
Miguel Grinberg
358fe6d2cc Release 1.2.4 2023-03-03 08:40:22 +00:00
Miguel Grinberg
cb39898829 One more attempt to correct build issues 2023-03-03 08:39:21 +00:00
Miguel Grinberg
db908fe7c3 Version 1.2.4.dev0 2023-03-03 08:20:53 +00:00
136 changed files with 4541 additions and 6363 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 name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-python@v2 - uses: actions/setup-python@v3
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions - run: pip install tox tox-gh-actions
- run: tox -eflake8 - run: tox -eflake8
@@ -21,12 +21,12 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python: ['3.7', '3.8', '3.9', '3.10', '3.11'] python: ['3.8', '3.9', '3.10', '3.11', '3.12']
fail-fast: false fail-fast: false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-python@v2 - uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
@@ -36,8 +36,8 @@ jobs:
name: tests-micropython name: tests-micropython
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-python@v2 - uses: actions/setup-python@v3
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions - run: pip install tox tox-gh-actions
- run: tox -eupy - run: tox -eupy
@@ -45,18 +45,21 @@ jobs:
name: coverage name: coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-python@v2 - uses: actions/setup-python@v3
- run: python -m pip install --upgrade pip wheel - 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: tox
- run: codecov - uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
fail_ci_if_error: true
benchmark: benchmark:
name: benchmark name: benchmark
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-python@v2 - uses: actions/setup-python@v3
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions - run: pip install tox tox-gh-actions
- run: tox -ebenchmark - run: tox -ebenchmark

5
.gitignore vendored
View File

@@ -103,3 +103,8 @@ venv.bak/
# mypy # mypy
.mypy_cache/ .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,50 @@
# Microdot change log # Microdot change log
**Release v2.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))
**Release 1.2.3** - 2023-03-03 **Release 1.2.3** - 2023-03-03
- Corrected a problem with previous build. - Corrected a problem with previous build.

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”* *“The impossibly small web framework for Python and MicroPython”*
Microdot is a minimalistic Python web framework inspired by Flask, and designed Microdot is a minimalistic Python web framework inspired by Flask. Given its
to run on systems with limited resources such as microcontrollers. It runs on small size, it can run on systems with limited resources such as
standard Python and on MicroPython. microcontrollers. Both standard Python (CPython) and MicroPython are supported.
```python ```python
from microdot import Microdot from microdot import Microdot
@@ -13,13 +13,24 @@ from microdot import Microdot
app = Microdot() app = Microdot()
@app.route('/') @app.route('/')
def index(request): async def index(request):
return 'Hello, world!' return 'Hello, world!'
app.run() app.run()
``` ```
## Migrating to Microdot 2
Version 2 of Microdot incorporates feedback received from users of earlier
releases, and attempts to improve and correct some design decisions that have
proven to be problematic.
For this reason most applications built for earlier versions will need to be
updated to work correctly with Microdot 2. The
[Migration Guide](https://microdot.readthedocs.io/en/stable/migrating.html)
describes the backwards incompatible changes that were made.
## Resources ## Resources
- [Documentation](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) - [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 .. autoclass:: microdot.Response
:members: :members:
.. autoclass:: microdot.NoCaseDict
:members:
.. autoclass:: microdot.MultiDict ``websocket`` extension
: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
----------------------- -----------------------
.. automodule:: microdot_ssl .. automodule:: microdot.websocket
:members: :members:
``microdot_test_client`` module ``utemplate`` templating extension
------------------------------- ----------------------------------
.. autoclass:: microdot_test_client.TestClient .. automodule:: microdot.utemplate
:members: :members:
.. autoclass:: microdot_test_client.TestResponse ``jinja`` templating extension
------------------------------
.. automodule:: microdot.jinja
:members: :members:
``microdot_asyncio_test_client`` module ``session`` extension
--------------------------------------- ---------------------
.. autoclass:: microdot_asyncio_test_client.TestClient .. automodule:: microdot.session
:members: :members:
.. autoclass:: microdot_asyncio_test_client.TestResponse ``cors`` extension
------------------
.. automodule:: microdot.cors
:members: :members:
``microdot_wsgi`` module ``test_client`` extension
------------------------ -------------------------
.. autoclass:: microdot_wsgi.Microdot .. automodule:: microdot.test_client
:members:
``asgi`` extension
------------------
.. autoclass:: microdot.asgi.Microdot
:members: :members:
:exclude-members: shutdown, run :exclude-members: shutdown, run
``microdot_asgi`` module ``wsgi`` extension
------------------------ -------------------
.. autoclass:: microdot_asgi.Microdot .. autoclass:: microdot.wsgi.Microdot
:members: :members:
:exclude-members: shutdown, run :exclude-members: shutdown, run

View File

@@ -2,11 +2,11 @@ Core Extensions
--------------- ---------------
Microdot is a highly extensible web application framework. The extensions Microdot is a highly extensible web application framework. The extensions
described in this section are maintained as part of the Microdot project and described in this section are maintained as part of the Microdot project in
can be obtained from the same source code repository. the same source code repository.
Asynchronous Support with Asyncio WebSocket Support
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
.. list-table:: .. list-table::
:align: left :align: left
@@ -15,35 +15,71 @@ Asynchronous Support with Asyncio
- | CPython & MicroPython - | CPython & MicroPython
* - Required Microdot source files * - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_ - | `websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/websocket.py>`_
| `microdot_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
* - Required external dependencies * - Required external dependencies
- | CPython: None - | None
| MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_
* - Examples * - 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 The WebSocket extension gives the application the ability to handle WebSocket
``asyncio`` package. When the :class:`Microdot <microdot_asyncio.Microdot>` requests. The :func:`with_websocket <microdot.websocket.with_websocket>`
class is imported from the ``microdot_asyncio`` package, an asynchronous server decorator is used to mark a route handler as a WebSocket handler. Decorated
is used, and handlers can be defined as coroutines. 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('/') .. list-table::
async def hello(request): :align: left
return 'Hello, world!'
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. Many web applications use HTML templates for rendering content to clients.
Microdot includes extensions to render templates with the Microdot includes extensions to render templates with the
@@ -61,35 +97,41 @@ Using the uTemplate Engine
- | CPython & MicroPython - | CPython & MicroPython
* - Required Microdot source files * - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_ - | `utemplate.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/utemplate.py>`_
| `microdot_utemplate.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_utemplate.py>`_
* - Required external dependencies * - Required external dependencies
- | `utemplate <https://github.com/pfalcon/utemplate/tree/master/utemplate>`_ - | `utemplate <https://github.com/pfalcon/utemplate/tree/master/utemplate>`_
* - Examples * - Examples
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/utemplate/hello.py>`_ - | `hello.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 The :class:`Template <microdot.utemplate.Template>` class is used to load a
used to render HTML templates with the uTemplate engine. The first argument is template. The argument is the template filename, relative to the templates
the template filename, relative to the templates directory, which is directory, which is *templates* by default.
*templates* by default. Any additional arguments are passed to the template
engine to be used as arguments. 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:: Example::
from microdot_utemplate import render_template from microdot.utemplate import Template
@app.get('/') @app.get('/')
def index(req): async def index(req):
return render_template('index.html') 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* The default location from where templates are loaded is the *templates*
subdirectory. This location can be changed with the subdirectory. This location can be changed with the
:func:`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') init_templates('my_templates')
@@ -103,8 +145,7 @@ Using the Jinja Engine
- | CPython only - | CPython only
* - Required Microdot source files * - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_ - | `jinja.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/jinja.py>`_
| `microdot_jinja.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_jinja.py>`_
* - Required external dependencies * - Required external dependencies
- | `Jinja2 <https://jinja.palletsprojects.com/>`_ - | `Jinja2 <https://jinja.palletsprojects.com/>`_
@@ -112,33 +153,45 @@ Using the Jinja Engine
* - Examples * - Examples
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/jinja/hello.py>`_ - | `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 The :class:`Template <microdot.jinja.Template>` class is used to load a
to render HTML templates with the Jinja engine. The first argument is the template. The argument is the template filename, relative to the templates
template filename, relative to the templates directory, which is *templates* by directory, which is *templates* by default.
default. Any additional arguments are passed to the template engine to be used
as arguments. 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:: Example::
from microdot_jinja import render_template from microdot.jinja import Template
@app.get('/') @app.get('/')
def index(req): async def index(req):
return render_template('index.html') 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* The default location from where templates are loaded is the *templates*
subdirectory. This location can be changed with the subdirectory. This location can be changed with the
:func:`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') 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:: .. note::
The Jinja extension is not compatible with MicroPython. The Jinja extension is not compatible with MicroPython.
Maintaing Secure User Sessions Maintaining Secure User Sessions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: .. list-table::
:align: left :align: left
@@ -147,56 +200,48 @@ Maintaing Secure User Sessions
- | CPython & MicroPython - | CPython & MicroPython
* - Required Microdot source files * - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_ - | `session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/session.py>`_
| `microdot_session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_session.py>`_
* - Required external dependencies * - Required external dependencies
- | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_ - | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
| MicroPython: `jwt.py <https://github.com/micropython/micropython-lib/blob/master/python-ecosys/pyjwt/jwt.py>`_, | MicroPython: `jwt.py <https://github.com/micropython/micropython-lib/blob/master/python-ecosys/pyjwt/jwt.py>`_,
`hmac <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 * - Examples
- | `login.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/sessions/login.py>`_ - | `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 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>`_ browser, in `JSON Web Token (JWT) <https://en.wikipedia.org/wiki/JSON_Web_Token>`_
format. format.
To work with user sessions, the application first must configure 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 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 key is kept secret, as its name implies. An attacker who is in possession of
valid user session cookies with any contents. 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>`, from microdot import Microdot, redirect
:func:`update_session <microdot_session.update_session>` and from microdot.session import Session, with_session
:func:`delete_session <microdot_session.delete_session>` functions are used
inside route handlers to retrieve, store and delete session data respectively.
The :func:`with_session <microdot_session.with_session>` decorator is provided
as a convenient way to retrieve the session at the start of a route handler.
Example::
from microdot import Microdot
from microdot_session import set_session_secret_key, with_session, \
update_session, delete_session
app = Microdot() app = Microdot()
set_session_secret_key('top-secret') Session(app, secret_key='top-secret')
@app.route('/', methods=['GET', 'POST']) @app.route('/', methods=['GET', 'POST'])
@with_session @with_session
def index(req, session): async def index(req, session):
username = session.get('username') username = session.get('username')
if req.method == 'POST': if req.method == 'POST':
username = req.form.get('username') username = req.form.get('username')
update_session(req, {'username': username}) session['username'] = username
session.save()
return redirect('/') return redirect('/')
if username is None: if username is None:
return 'Not logged in' return 'Not logged in'
@@ -204,12 +249,17 @@ Example::
return 'Logged in as ' + username return 'Logged in as ' + username
@app.post('/logout') @app.post('/logout')
def logout(req): @with_session
delete_session(req) async def logout(req, session):
session.delete()
return redirect('/') 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:: .. list-table::
:align: left :align: left
@@ -218,40 +268,33 @@ WebSocket Support
- | CPython & MicroPython - | CPython & MicroPython
* - Required Microdot source files * - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_ - | `cors.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/cors.py>`_
| `microdot_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_websocket.py>`_
* - Required external dependencies * - Required external dependencies
- | None - | None
* - Examples * - Examples
- | `echo.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo.py>`_ - | `cors.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/cors/cors.py>`_
| `echo_wsgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_wsgi.py>`_
The WebSocket extension provides a way for the application to handle WebSocket The CORS extension provides support for `Cross-Origin Resource Sharing
requests. The :func:`websocket <microdot_websocket.with_websocket>` decorator (CORS) <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_. CORS is a
is used to mark a route handler as a WebSocket handler. The handler receives mechanism that allows web applications running on different origins to access
a WebSocket object as a second argument. The WebSocket object provides resources from each other. For example, a web application running on
``send()`` and ``receive()`` methods to send and receive messages respectively. ``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:: Example::
@app.route('/echo') from microdot import Microdot
@with_websocket from microdot.cors import CORS
def echo(request, ws):
while True:
message = ws.receive()
ws.send(message)
.. note:: app = Microdot()
An unsupported *microdot_websocket_alt.py* module, with the same cors = CORS(app, allowed_origins=['https://example.com'],
interface, is also provided. This module uses the native WebSocket support allow_credentials=True)
in MicroPython that powers the WebREPL, and may provide slightly better
performance for MicroPython low-end boards. This module is not compatible
with CPython.
Asynchronous WebSocket Testing with the Test Client
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: .. list-table::
:align: left :align: left
@@ -260,54 +303,18 @@ Asynchronous WebSocket
- | CPython & MicroPython - | CPython & MicroPython
* - Required Microdot source files * - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_ - | `test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/test_client.py>`_
| `microdot_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
| `microdot_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_websocket.py>`_
| `microdot_asyncio_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio_websocket.py>`_
* - Required external dependencies * - Required external dependencies
- | CPython: None - | None
| MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_
* - Examples The Microdot Test Client is a utility class that can be used in tests to send
- | `echo_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_async.py>`_ requests into the application without having to start a web server.
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.
Example:: Example::
from microdot import Microdot from microdot import Microdot
from microdot_ssl import create_ssl_context from microdot.test_client import TestClient
app = Microdot() app = Microdot()
@@ -315,88 +322,13 @@ Example::
def index(req): def index(req):
return 'Hello, World!' return 'Hello, World!'
sslctx = create_ssl_context('cert.der', 'key.der')
app.run(port=4443, debug=True, ssl=sslctx)
.. note::
The ``microdot_ssl`` module is only needed for MicroPython. When used under
CPython, this module creates a standard ``SSLContext`` instance.
.. note::
The ``uasyncio`` library for MicroPython does not currently support TLS, so
this feature is not available for asynchronous applications on that
platform. The ``asyncio`` library for CPython is fully supported.
Test Client
~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_test_client.py>`_
* - Required external dependencies
- | None
The Microdot Test Client is a utility class that can be used during testing to
send requests into the application.
Example::
from microdot import Microdot
from microdot_test_client import TestClient
app = Microdot()
@app.route('/')
def index(req):
return 'Hello, World!'
def test_app():
client = TestClient(app)
response = client.get('/')
assert response.text == 'Hello, World!'
See the documentation for the :class:`TestClient <microdot_test_client.TestClient>`
class for more details.
Asynchronous Test Client
~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
| `microdot_test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_test_client.py>`_
| `microdot_asyncio_test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio_test_client.py>`_
* - Required external dependencies
- | None
Similar to the :class:`TestClient <microdot_test_client.TestClient>` class
above, but for asynchronous applications.
Example usage::
from microdot_asyncio_test_client import TestClient
async def test_app(): async def test_app():
client = TestClient(app) client = TestClient(app)
response = await client.get('/') response = await client.get('/')
assert response.text == 'Hello, World!' assert response.text == 'Hello, World!'
See the :class:`reference documentation <microdot_asyncio_test_client.TestClient>` See the documentation for the :class:`TestClient <microdot.test_client.TestClient>`
for details. class for more details.
Deploying on a Production Web Server 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 The ``Microdot`` class creates its own simple web server. This is enough for an
application deployed with MicroPython, but when using CPython it may be useful application deployed with MicroPython, but when using CPython it may be useful
to use a separate, battle-tested web server. To address this need, Microdot to use a separate, battle-tested web server. To address this need, Microdot
provides extensions that implement the WSGI and ASGI protocols. provides extensions that implement the ASGI and WSGI 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
Using an ASGI Web Server Using an ASGI Web Server
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
@@ -460,25 +348,25 @@ Using an ASGI Web Server
- | CPython only - | CPython only
* - Required Microdot source files * - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_ - | `asgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/asgi.py>`_
| `microdot_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
| `microdot_asgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asgi.py>`_
* - Required external dependencies * - Required external dependencies
- | An ASGI web server, such as `Uvicorn <https://uvicorn.org/>`_. - | An ASGI web server, such as `Uvicorn <https://uvicorn.org/>`_.
* - Examples * - Examples
- | `hello_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_asgi.py>`_ - | `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 implements the ASGI protocol and can be used with a compliant ASGI server such
as `Uvicorn <https://www.uvicorn.org/>`_. as `Uvicorn <https://www.uvicorn.org/>`_.
To use an ASGI web server, the application must import the To use an ASGI web server, the application must import the
:class:`Microdot <microdot_asgi.Microdot>` class from the ``microdot_asgi`` :class:`Microdot <microdot.asgi.Microdot>` class from the ``asgi`` module::
module::
from microdot_asgi import Microdot from microdot.asgi import Microdot
app = Microdot() app = Microdot()
@@ -486,10 +374,67 @@ module::
async def index(req): async def index(req):
return 'Hello, World!' return 'Hello, World!'
The ``app`` application instance created from this class is an ASGI application The ``app`` application instance created from this class can be used as the
that can be used with any complaint ASGI web server. If the above application ASGI callable with any complaint ASGI web server. If the above example
is stored in a file called *test.py*, then the following command runs the application was stored in a file called *test.py*, then the following command
web application using the Uvicorn web server:: runs the web application using the Uvicorn web server::
uvicorn test:app 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"* *"The impossibly small web framework for Python and MicroPython"*
Microdot is a minimalistic Python web framework inspired by Microdot is a minimalistic Python web framework inspired by
`Flask <https://flask.palletsprojects.com/>`_, and designed to run on `Flask <https://flask.palletsprojects.com/>`_. Given its size, it can run on
systems with limited resources such as microcontrollers. It runs on standard systems with limited resources such as microcontrollers. Both standard Python
Python and on `MicroPython <https://micropython.org>`_. (CPython) and `MicroPython <https://micropython.org>`_ are supported.
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 3
intro intro
extensions extensions
migrating
freezing
api api
* :ref:`genindex` * :ref:`genindex`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 # This file is autogenerated by pip-compile with Python 3.12
blinker==1.5 # by the following command:
certifi==2022.12.7 #
charset-normalizer==2.1.0 # pip-compile requirements.in
click==8.1.3 #
fastapi==0.79.0 aiofiles==23.2.1
Flask==2.2.1 # via quart
gunicorn==20.1.0 annotated-types==0.6.0
h11==0.13.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 h2==4.1.0
# via hypercorn
hpack==4.0.0 hpack==4.0.0
humanize==4.3.0 # via h2
hypercorn==0.13.2 humanize==4.9.0
# via -r requirements.in
hypercorn==0.15.0
# via quart
hyperframe==6.0.1 hyperframe==6.0.1
idna==3.3 # via h2
idna==3.6
# via
# anyio
# requests
itsdangerous==2.1.2 itsdangerous==2.1.2
Jinja2==3.1.2 # via
MarkupSafe==2.1.1 # flask
microdot # 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 priority==2.0.0
psutil==5.9.1 # via hypercorn
pydantic==1.9.1 psutil==5.9.6
quart==0.18.0 # via -r requirements.in
requests==2.28.1 pydantic==2.5.2
sniffio==1.2.0 # via fastapi
starlette==0.25.0 pydantic-core==2.14.5
toml==0.10.2 # via pydantic
typing_extensions==4.3.0 pyproject-hooks==1.0.0
urllib3==1.26.11 # via build
uvicorn==0.18.2 quart==0.19.4
Werkzeug==2.2.3 # via -r requirements.in
wsproto==1.1.0 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', 'micropython mem.py',
{'MICROPYPATH': '../../src'},
'microdot-micropython-sync'
),
(
'micropython mem_async.py',
{'MICROPYPATH': '../../src:../../libs/micropython'}, {'MICROPYPATH': '../../src:../../libs/micropython'},
'microdot-micropython-async' 'microdot-micropython'
), ),
( (
['python', '-c', 'import time; time.sleep(10)'], ['python', '-c', 'import time; time.sleep(10)'],
@@ -30,47 +25,42 @@ apps = [
( (
'python mem.py', 'python mem.py',
{'PYTHONPATH': '../../src'}, {'PYTHONPATH': '../../src'},
'microdot-cpython-sync' 'microdot-cpython'
),
(
'python mem_async.py',
{'PYTHONPATH': '../../src'},
'microdot-cpython-async'
),
(
'gunicorn --workers 1 --bind :5000 mem_wsgi:app',
{'PYTHONPATH': '../../src'},
'microdot-gunicorn-sync'
), ),
( (
'uvicorn --workers 1 --port 5000 mem_asgi:app', 'uvicorn --workers 1 --port 5000 mem_asgi:app',
{'PYTHONPATH': '../../src'}, {'PYTHONPATH': '../../src'},
'microdot-uvicorn-async' 'microdot-uvicorn'
),
(
'gunicorn --workers 1 --bind :5000 mem_wsgi:app',
{'PYTHONPATH': '../../src'},
'microdot-gunicorn'
), ),
( (
'flask run', 'flask run',
{'FLASK_APP': 'mem_flask.py'}, {'FLASK_APP': 'mem_flask.py'},
'flask-run-sync' 'flask-run'
), ),
( (
'quart run', 'quart run',
{'QUART_APP': 'mem_quart.py'}, {'QUART_APP': 'mem_quart.py'},
'quart-run-async' 'quart-run'
), ),
( (
'gunicorn --workers 1 --bind :5000 mem_flask:app', 'gunicorn --workers 1 --bind :5000 mem_flask:app',
{}, {},
'flask-gunicorn-sync' 'flask-gunicorn'
), ),
( (
'uvicorn --workers 1 --port 5000 mem_quart:app', 'uvicorn --workers 1 --port 5000 mem_quart:app',
{}, {},
'quart-uvicorn-async' 'quart-uvicorn'
), ),
( (
'uvicorn --workers 1 --port 5000 mem_fastapi:app', '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> <html>
<head> <head>
<title>Microdot GPIO Example</title> <title>Microdot GPIO Example</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script> <script>
function getCookie(name) { function getCookie(name) {

View File

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

View File

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

View File

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

View File

@@ -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> <html>
<head> <head>
<title>Static File Serving Demo</title> <title>Static File Serving Demo</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Static File Serving Demo</h1> <h1>Static File Serving Demo</h1>

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
from microdot_asyncio import Microdot, Response from microdot.asgi import Microdot, Response
from microdot_utemplate import render_template from microdot.jinja import template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return render_template('index.html', name=name) return template('index.html').render(name=name)
if __name__ == '__main__': 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> <html>
<head> <head>
<title>Microdot + Jinja example</title> <title>Microdot + Jinja example</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot + Jinja example</h1> <h1>Microdot + Jinja example</h1>

View File

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

View File

@@ -1,16 +1,16 @@
from microdot import Microdot, Response from microdot import Microdot, Response
from microdot_utemplate import render_template from microdot.utemplate import template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST']) @app.route('/', methods=['GET', 'POST'])
def index(req): async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return render_template('index.html', name=name) return template('index.html').render(name=name)
if __name__ == '__main__': 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> <html>
<head> <head>
<title>Microdot + uTemplate example</title> <title>Microdot + uTemplate example</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot + uTemplate example</h1> <h1>Microdot + uTemplate example</h1>

View File

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

View File

@@ -5,12 +5,12 @@ Request.max_content_length = 1024 * 1024 # 1MB (change as needed)
@app.get('/') @app.get('/')
def index(request): async def index(request):
return send_file('index.html') return send_file('index.html')
@app.post('/upload') @app.post('/upload')
def upload(request): async def upload(request):
# obtain the filename and size from request headers # obtain the filename and size from request headers
filename = request.headers['Content-Disposition'].split( filename = request.headers['Content-Disposition'].split(
'filename=')[1].strip('"') 'filename=')[1].strip('"')
@@ -22,7 +22,7 @@ def upload(request):
# write the file to the files directory in 1K chunks # write the file to the files directory in 1K chunks
with open('files/' + filename, 'wb') as f: with open('files/' + filename, 'wb') as f:
while size > 0: while size > 0:
chunk = request.stream.read(min(size, 1024)) chunk = await request.stream.read(min(size, 1024))
f.write(chunk) f.write(chunk)
size -= len(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 import Microdot, send_file
from microdot_websocket import with_websocket from microdot.websocket import with_websocket
app = Microdot() app = Microdot()
@app.route('/') @app.route('/')
def index(request): async def index(request):
return send_file('index.html') return send_file('index.html')
@app.route('/echo') @app.route('/echo')
@with_websocket @with_websocket
def echo(request, ws): async def echo(request, ws):
while True: while True:
data = ws.receive() data = await ws.receive()
ws.send(data) await ws.send(data)
app.run() app.run()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
# MicroPython uasyncio module # MicroPython asyncio module
# MIT license; Copyright (c) 2019-2022 Damien P. George # MIT license; Copyright (c) 2019-2022 Damien P. George
from . import core from . import core
def _run(waiter, aw): async def _run(waiter, aw):
try: try:
result = await aw result = await aw
status = True status = True
@@ -61,7 +61,8 @@ class _Remove:
pass pass
async def gather(*aws, return_exceptions=False): # async
def gather(*aws, return_exceptions=False):
if not aws: if not aws:
return [] 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 # Either this gather was cancelled, or one of the sub-tasks raised an exception with
# return_exceptions==False, so reraise the exception here. # return_exceptions==False, so reraise the exception here.
if state is not 0: if state:
raise state raise state
# Return the list of return values of each sub-task. # 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 # MIT license; Copyright (c) 2019-2020 Damien P. George
from . import core from . import core
# Lock class for primitive mutex capability # Lock class for primitive mutex capability
class Lock: class Lock:
def __init__(self): def __init__(self):
@@ -28,7 +29,8 @@ class Lock:
# No Task waiting so unlock # No Task waiting so unlock
self.state = 0 self.state = 0
async def acquire(self): # async
def acquire(self):
if self.state != 0: if self.state != 0:
# Lock unavailable, put the calling Task on the waiting queue # Lock unavailable, put the calling Task on the waiting queue
self.waiting.push(core.cur_task) self.waiting.push(core.cur_task)

View File

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

View File

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

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

View File

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

View File

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

14
libs/refresh.sh Executable file
View File

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

View File

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

View File

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

View File

@@ -1,43 +0,0 @@
[metadata]
name = microdot
version = 1.2.3
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
packages = find:
[options.packages.find]
where = 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 asyncio
import os import os
import signal import signal
from microdot_asyncio import * # noqa: F401, F403 from microdot import * # noqa: F401, F403
from microdot_asyncio import Microdot as BaseMicrodot from microdot.microdot import Microdot as BaseMicrodot, Request, Response, \
from microdot_asyncio import Request NoCaseDict, abort
from microdot import NoCaseDict from microdot.websocket import WebSocket as BaseWebSocket, websocket_wrapper
class _BodyStream: # pragma: no cover class _BodyStream: # pragma: no cover
@@ -21,7 +21,7 @@ class _BodyStream: # pragma: no cover
async def read(self, n=-1): async def read(self, n=-1):
while self.more and len(self.data) < n: while self.more and len(self.data) < n:
self.read_more() await self.read_more()
if len(self.data) < n: if len(self.data) < n:
data = self.data data = self.data
self.data = b'' self.data = b''
@@ -32,14 +32,14 @@ class _BodyStream: # pragma: no cover
return data return data
async def readline(self): async def readline(self):
return self.readuntil() return await self.readuntil()
async def readexactly(self, n): async def readexactly(self, n):
return self.read(n) return await self.read(n)
async def readuntil(self, separator=b'\n'): async def readuntil(self, separator=b'\n'):
if self.more and separator not in self.data: if self.more and separator not in self.data:
self.read_more() await self.read_more()
data, self.data = self.data.split(separator, 1) data, self.data = self.data.split(separator, 1)
return data return data
@@ -59,8 +59,9 @@ class Microdot(BaseMicrodot):
headers = NoCaseDict() headers = NoCaseDict()
content_length = 0 content_length = 0
for key, value in scope.get('headers', []): for key, value in scope.get('headers', []):
headers[key] = value key = key.decode().title()
if key.lower() == 'content-length': headers[key] = value.decode()
if key == 'Content-Length':
content_length = int(value) content_length = int(value)
if content_length and content_length <= Request.max_body_length: if content_length and content_length <= Request.max_body_length:
@@ -112,25 +113,31 @@ class Microdot(BaseMicrodot):
while True: while True:
event = await receive() 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 cancelled = True
break break
asyncio.ensure_future(cancel_monitor()) monitor_task = asyncio.ensure_future(cancel_monitor())
body_iter = res.body_iter().__aiter__() body_iter = res.body_iter().__aiter__()
res_body = b''
try: try:
body = await body_iter.__anext__() res_body = await body_iter.__anext__()
while not cancelled: # pragma: no branch while not cancelled: # pragma: no branch
next_body = await body_iter.__anext__() next_body = await body_iter.__anext__()
await send({'type': 'http.response.body', await send({'type': 'http.response.body',
'body': body, 'body': res_body,
'more_body': True}) 'more_body': True})
body = next_body res_body = next_body
except StopAsyncIteration: except StopAsyncIteration:
await send({'type': 'http.response.body', await send({'type': 'http.response.body',
'body': body, 'body': res_body,
'more_body': False}) '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): async def __call__(self, scope, receive, send):
return await self.asgi_app(scope, receive, send) return await self.asgi_app(scope, receive, send)
@@ -150,3 +157,79 @@ class Microdot(BaseMicrodot):
""" """
self.embedded_server = True self.embedded_server = True
super().run(host=host, port=port, debug=debug, **options) 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 The ``microdot`` module defines a few classes that help implement HTTP-based
servers for MicroPython and standard Python, with multithreading support for servers for MicroPython and standard Python.
Python interpreters that support it.
""" """
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: try:
from sys import print_exception from sys import print_exception
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
@@ -13,45 +47,6 @@ except ImportError: # pragma: no cover
def print_exception(exc): def print_exception(exc):
traceback.print_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 = [ MUTED_SOCKET_ERRORS = [
32, # Broken pipe 32, # Broken pipe
@@ -146,6 +141,10 @@ class NoCaseDict(dict):
kl = key.lower() kl = key.lower()
return super().get(self.keymap.get(kl, kl), default) 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 def mro(cls): # pragma: no cover
"""Return the method resolution order of a class. """Return the method resolution order of a class.
@@ -271,7 +270,31 @@ class MultiDict(dict):
return values 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.""" """An HTTP request."""
#: Specify the maximum payload size that is accepted. Requests with larger #: Specify the maximum payload size that is accepted. Requests with larger
#: payloads will be rejected with a 413 status code. Applications can #: 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 #: Request.max_readline = 16 * 1024 # 16KB lines allowed
max_readline = 2 * 1024 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: class G:
pass pass
@@ -356,82 +374,84 @@ class Request():
self._body = body self._body = body
self.body_used = False self.body_used = False
self._stream = stream self._stream = stream
self.stream_used = False
self.sock = sock self.sock = sock
self._json = None self._json = None
self._form = None self._form = None
self.after_request_handlers = [] self.after_request_handlers = []
@staticmethod @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. """Create a request object.
:param app: The Microdot application instance. :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. 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_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 # request line
line = Request._safe_readline(client_stream).strip().decode() line = (await Request._safe_readline(client_reader)).strip().decode()
if not line: if not line: # pragma: no cover
return None return None
method, url, http_version = line.split() method, url, http_version = line.split()
http_version = http_version.split('/', 1)[1] http_version = http_version.split('/', 1)[1]
# headers # headers
headers = NoCaseDict() headers = NoCaseDict()
content_length = 0
while True: while True:
line = Request._safe_readline(client_stream).strip().decode() line = (await Request._safe_readline(
client_reader)).strip().decode()
if line == '': if line == '':
break break
header, value = line.split(':', 1) header, value = line.split(':', 1)
value = value.strip() value = value.strip()
headers[header] = value 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, 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): def _parse_urlencoded(self, urlencoded):
data = MultiDict() data = MultiDict()
if len(urlencoded) > 0: if len(urlencoded) > 0: # pragma: no branch
if isinstance(urlencoded, str): if isinstance(urlencoded, str):
for k, v in [pair.split('=', 1) for kv in [pair.split('=', 1)
for pair in urlencoded.split('&')]: for pair in urlencoded.split('&') if pair]:
data[urldecode_str(k)] = urldecode_str(v) data[urldecode_str(kv[0])] = urldecode_str(kv[1]) \
if len(kv) > 1 else ''
elif isinstance(urlencoded, bytes): # pragma: no branch elif isinstance(urlencoded, bytes): # pragma: no branch
for k, v in [pair.split(b'=', 1) for kv in [pair.split(b'=', 1)
for pair in urlencoded.split(b'&')]: for pair in urlencoded.split(b'&') if pair]:
data[urldecode_bytes(k)] = urldecode_bytes(v) data[urldecode_bytes(kv[0])] = urldecode_bytes(kv[1]) \
if len(kv) > 1 else b''
return data return data
@property @property
def body(self): def body(self):
"""The body of the request, as bytes.""" """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 return self._body
@property @property
def stream(self): def stream(self):
"""The input stream, containing the request body.""" """The body of the request, as a bytes stream."""
if self.body_used: if self._stream is None:
raise RuntimeError('Cannot use both stream and body') self._stream = AsyncBytesIO(self._body)
self.stream_used = True
return self._stream return self._stream
@property @property
@@ -487,21 +507,21 @@ class Request():
return f return f
@staticmethod @staticmethod
def _safe_readline(stream): async def _safe_readline(stream):
line = stream.readline(Request.max_readline + 1) line = (await stream.readline())
if len(line) > Request.max_readline: if len(line) > Request.max_readline:
raise ValueError('line too long') raise ValueError('line too long')
return line return line
class Response(): class Response:
"""An HTTP response class. """An HTTP response class.
:param body: The body of the response. If a dictionary or list is given, :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 a JSON formatter is used to generate the body. If a file-like
object or a generator is given, a streaming response is used. object or an async generator is given, a streaming response is
If a string is given, it is encoded from UTF-8. Else, the used. If a string is given, it is encoded from UTF-8. Else,
body should be a byte sequence. the body should be a byte sequence.
:param status_code: The numeric HTTP status code of the response. The :param status_code: The numeric HTTP status code of the response. The
default is 200. default is 200.
:param headers: A dictionary of headers to include in the response. :param headers: A dictionary of headers to include in the response.
@@ -519,12 +539,17 @@ class Response():
'png': 'image/png', 'png': 'image/png',
'txt': 'text/plain', 'txt': 'text/plain',
} }
send_file_buffer_size = 1024 send_file_buffer_size = 1024
#: The content type to use for responses that do not explicitly define a #: The content type to use for responses that do not explicitly define a
#: ``Content-Type`` header. #: ``Content-Type`` header.
default_content_type = 'text/plain' 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 #: Special response used to signal that a response does not need to be
#: written to the client. Used to exit WebSocket connections cleanly. #: written to the client. Used to exit WebSocket connections cleanly.
already_handled = None already_handled = None
@@ -544,9 +569,11 @@ class Response():
else: else:
# this applies to bytes, file-like objects or generators # this applies to bytes, file-like objects or generators
self.body = body self.body = body
self.is_head = False
def set_cookie(self, cookie, value, path=None, domain=None, expires=None, 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. """Add a cookie to the response.
:param cookie: The cookie's name. :param cookie: The cookie's name.
@@ -558,6 +585,7 @@ class Response():
:param max_age: The cookie's ``Max-Age`` value. :param max_age: The cookie's ``Max-Age`` value.
:param secure: The cookie's ``secure`` flag. :param secure: The cookie's ``secure`` flag.
:param http_only: The cookie's ``HttpOnly`` 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) http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value)
if path: if path:
@@ -568,19 +596,31 @@ class Response():
if isinstance(expires, str): if isinstance(expires, str):
http_cookie += '; Expires=' + expires http_cookie += '; Expires=' + expires
else: else:
http_cookie += '; Expires=' + expires.strftime( http_cookie += '; Expires=' + time.strftime(
'%a, %d %b %Y %H:%M:%S GMT') '%a, %d %b %Y %H:%M:%S GMT', expires.timetuple())
if max_age: if max_age:
http_cookie += '; Max-Age=' + str(max_age) http_cookie += '; Max-Age=' + str(max_age)
if secure: if secure:
http_cookie += '; Secure' http_cookie += '; Secure'
if http_only: if http_only:
http_cookie += '; HttpOnly' http_cookie += '; HttpOnly'
if partitioned:
http_cookie += '; Partitioned'
if 'Set-Cookie' in self.headers: if 'Set-Cookie' in self.headers:
self.headers['Set-Cookie'].append(http_cookie) self.headers['Set-Cookie'].append(http_cookie)
else: else:
self.headers['Set-Cookie'] = [http_cookie] 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): def complete(self):
if isinstance(self.body, bytes) and \ if isinstance(self.body, bytes) and \
'Content-Length' not in self.headers: 'Content-Length' not in self.headers:
@@ -590,53 +630,101 @@ class Response():
if 'charset=' not in self.headers['Content-Type']: if 'charset=' not in self.headers['Content-Type']:
self.headers['Content-Type'] += '; charset=UTF-8' self.headers['Content-Type'] += '; charset=UTF-8'
def write(self, stream): async def write(self, stream):
self.complete() self.complete()
# status code
reason = self.reason if self.reason is not None else \
('OK' if self.status_code == 200 else 'N/A')
stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code, reason=reason).encode())
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
stream.write('{header}: {value}\r\n'.format(
header=header, value=value).encode())
stream.write(b'\r\n')
# body
can_flush = hasattr(stream, 'flush')
try: try:
for body in self.body_iter(): # status code
if isinstance(body, str): # pragma: no cover reason = self.reason if self.reason is not None else \
body = body.encode() ('OK' if self.status_code == 200 else 'N/A')
stream.write(body) await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
if can_flush: # pragma: no cover status_code=self.status_code, reason=reason).encode())
stream.flush()
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
await stream.awrite('{header}: {value}\r\n'.format(
header=header, value=value).encode())
await stream.awrite(b'\r\n')
# body
if not self.is_head:
iter = self.body_iter()
async for body in iter:
if isinstance(body, str): # pragma: no cover
body = body.encode()
try:
await stream.awrite(body)
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS or \
exc.args[0] == 'Connection lost':
if hasattr(iter, 'aclose'):
await iter.aclose()
raise
if hasattr(iter, 'aclose'): # pragma: no branch
await iter.aclose()
except OSError as exc: # pragma: no cover 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':
pass pass
else: else:
raise raise
def body_iter(self): def body_iter(self):
if self.body: if hasattr(self.body, '__anext__'):
if hasattr(self.body, 'read'): # response body is an async generator
while True: return self.body
buf = self.body.read(self.send_file_buffer_size)
if len(buf): response = self
yield buf
if len(buf) < self.send_file_buffer_size: class iter:
break ITER_UNKNOWN = 0
if hasattr(self.body, 'close'): # pragma: no cover ITER_SYNC_GEN = 1
self.body.close() ITER_FILE_OBJ = 2
elif hasattr(self.body, '__next__'): ITER_NO_BODY = -1
yield from self.body
else: def __aiter__(self):
yield self.body if response.body:
self.i = self.ITER_UNKNOWN # need to determine type
else:
self.i = self.ITER_NO_BODY
return self
async def __anext__(self):
if self.i == self.ITER_NO_BODY:
await self.aclose()
raise StopAsyncIteration
if self.i == self.ITER_UNKNOWN:
if hasattr(response.body, 'read'):
self.i = self.ITER_FILE_OBJ
elif hasattr(response.body, '__next__'):
self.i = self.ITER_SYNC_GEN
return next(response.body)
else:
self.i = self.ITER_NO_BODY
return response.body
elif self.i == self.ITER_SYNC_GEN:
try:
return next(response.body)
except StopIteration:
await self.aclose()
raise StopAsyncIteration
buf = response.body.read(response.send_file_buffer_size)
if iscoroutine(buf): # pragma: no cover
buf = await buf
if len(buf) < response.send_file_buffer_size:
self.i = self.ITER_NO_BODY
return buf
async def aclose(self):
if hasattr(response.body, 'close'):
result = response.body.close()
if iscoroutine(result): # pragma: no cover
await result
return iter()
@classmethod @classmethod
def redirect(cls, location, status_code=302): def redirect(cls, location, status_code=302):
@@ -651,7 +739,9 @@ class Response():
return cls(status_code=status_code, headers={'Location': location}) return cls(status_code=status_code, headers={'Location': location})
@classmethod @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. """Send file contents in a response.
:param filename: The filename of the file. :param filename: The filename of the file.
@@ -659,7 +749,25 @@ class Response():
default is 302. default is 302.
:param content_type: The ``Content-Type`` header to use in the :param content_type: The ``Content-Type`` header to use in the
response. If omitted, it is generated 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 Security note: The filename is assumed to be trusted. Never pass
filenames provided by the user without validating and sanitizing them filenames provided by the user without validating and sanitizing them
@@ -671,9 +779,19 @@ class Response():
content_type = Response.types_map[ext] content_type = Response.types_map[ext]
else: else:
content_type = 'application/octet-stream' content_type = 'application/octet-stream'
f = open(filename, 'rb') headers = {'Content-Type': content_type}
return cls(body=f, status_code=status_code,
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(): class URLPattern():
@@ -695,7 +813,7 @@ class URLPattern():
if type_ == 'string': if type_ == 'string':
pattern = '[^/]+' pattern = '[^/]+'
elif type_ == 'int': elif type_ == 'int':
pattern = '\\d+' pattern = '-?\\d+'
elif type_ == 'path': elif type_ == 'path':
pattern = '.+' pattern = '.+'
elif type_.startswith('re:'): elif type_.startswith('re:'):
@@ -738,7 +856,7 @@ class HTTPException(Exception):
return 'HTTPException: {}'.format(self.status_code) return 'HTTPException: {}'.format(self.status_code)
class Microdot(): class Microdot:
"""An HTTP application class. """An HTTP application class.
This class implements an HTTP application instance and is heavily This class implements an HTTP application instance and is heavily
@@ -759,6 +877,7 @@ class Microdot():
self.after_error_request_handlers = [] self.after_error_request_handlers = []
self.error_handlers = {} self.error_handlers = {}
self.shutdown_requested = False self.shutdown_requested = False
self.options_handler = self.default_options_handler
self.debug = False self.debug = False
self.server = None self.server = None
@@ -794,7 +913,8 @@ class Microdot():
""" """
def decorated(f): def decorated(f):
self.url_map.append( 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 f
return decorated return decorated
@@ -1003,6 +1123,88 @@ class Microdot():
""" """
raise HTTPException(status_code, reason) 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): 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 """Start the web server. This function does not normally return, as
the server enters an endless listening loop. The :func:`shutdown` the server enters an endless listening loop. The :func:`shutdown`
@@ -1024,45 +1226,18 @@ class Microdot():
Example:: Example::
from microdot import Microdot from microdot_asyncio import Microdot
app = Microdot() app = Microdot()
@app.route('/') @app.route('/')
def index(): async def index(request):
return 'Hello, world!' return 'Hello, world!'
app.run(debug=True) app.run(debug=True)
""" """
self.debug = debug asyncio.run(self.start_server(host=host, port=port, debug=debug,
self.shutdown_requested = False ssl=ssl)) # pragma: no cover
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)
def shutdown(self): def shutdown(self):
"""Request a server shutdown. The server will then exit its request """Request a server shutdown. The server will then exit its request
@@ -1077,65 +1252,64 @@ class Microdot():
request.app.shutdown() request.app.shutdown()
return 'The server is shutting down...' return 'The server is shutting down...'
""" """
self.shutdown_requested = True self.server.close()
def find_route(self, req): 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 f = 404
for route_methods, route_pattern, route_handler in self.url_map: for route_methods, route_pattern, route_handler in self.url_map:
req.url_args = route_pattern.match(req.path) req.url_args = route_pattern.match(req.path)
if req.url_args is not None: if req.url_args is not None:
if req.method in route_methods: if method in route_methods:
f = route_handler f = route_handler
break break
else: else:
f = 405 f = 405
return f return f
def handle_request(self, sock, addr): def default_options_handler(self, req):
if Request.socket_read_timeout and \ allow = []
hasattr(sock, 'settimeout'): # pragma: no cover for route_methods, route_pattern, route_handler in self.url_map:
sock.settimeout(Request.socket_read_timeout) if route_pattern.match(req.path) is not None:
if not hasattr(sock, 'readline'): # pragma: no cover allow.extend(route_methods)
stream = sock.makefile("rwb") if 'GET' in allow:
else: allow.append('HEAD')
stream = sock allow.append('OPTIONS')
return {'Allow': ', '.join(allow)}
async def handle_request(self, reader, writer):
req = None req = None
res = None
try: try:
req = Request.create(self, stream, addr, sock) req = await Request.create(self, reader, writer,
res = self.dispatch_request(req) writer.get_extra_info('peername'))
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
except Exception as exc: # pragma: no cover except Exception as exc: # pragma: no cover
print_exception(exc) print_exception(exc)
res = await self.dispatch_request(req)
if res != Response.already_handled: # pragma: no branch
await res.write(writer)
try: try:
if res and res != Response.already_handled: # pragma: no branch await writer.aclose()
res.write(stream)
stream.close()
except OSError as exc: # pragma: no cover except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS: if exc.errno in MUTED_SOCKET_ERRORS:
pass pass
else: else:
print_exception(exc) raise
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()
if self.debug and req: # pragma: no cover if self.debug and req: # pragma: no cover
print('{method} {path} {status_code}'.format( print('{method} {path} {status_code}'.format(
method=req.method, path=req.path, method=req.method, path=req.path,
status_code=res.status_code)) status_code=res.status_code))
def dispatch_request(self, req): async def dispatch_request(self, req):
after_request_handled = False after_request_handled = False
if req: if req:
if req.content_length > req.max_content_length: if req.content_length > req.max_content_length:
if 413 in self.error_handlers: if 413 in self.error_handlers:
res = self.error_handlers[413](req) res = await invoke_handler(self.error_handlers[413], req)
else: else:
res = 'Payload too large', 413 res = 'Payload too large', 413
else: else:
@@ -1144,11 +1318,12 @@ class Microdot():
res = None res = None
if callable(f): if callable(f):
for handler in self.before_request_handlers: for handler in self.before_request_handlers:
res = handler(req) res = await invoke_handler(handler, req)
if res: if res:
break break
if res is None: if res is None:
res = f(req, **req.url_args) res = await invoke_handler(
f, req, **req.url_args)
if isinstance(res, tuple): if isinstance(res, tuple):
body = res[0] body = res[0]
if isinstance(res[1], int): if isinstance(res[1], int):
@@ -1161,12 +1336,16 @@ class Microdot():
elif not isinstance(res, Response): elif not isinstance(res, Response):
res = Response(res) res = Response(res)
for handler in self.after_request_handlers: 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: 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 after_request_handled = True
elif isinstance(f, dict):
res = Response(headers=f)
elif f in self.error_handlers: elif f in self.error_handlers:
res = self.error_handlers[f](req) res = await invoke_handler(self.error_handlers[f], req)
else: else:
res = 'Not found', f res = 'Not found', f
except HTTPException as exc: except HTTPException as exc:
@@ -1187,31 +1366,35 @@ class Microdot():
break break
if exc_class: if exc_class:
try: 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 except Exception as exc2: # pragma: no cover
print_exception(exc2) print_exception(exc2)
if res is None: if res is None:
if 500 in self.error_handlers: if 500 in self.error_handlers:
res = self.error_handlers[500](req) res = await invoke_handler(
self.error_handlers[500], req)
else: else:
res = 'Internal server error', 500 res = 'Internal server error', 500
else: else:
if 400 in self.error_handlers: if 400 in self.error_handlers:
res = self.error_handlers[400](req) res = await invoke_handler(self.error_handlers[400], req)
else: else:
res = 'Bad request', 400 res = 'Bad request', 400
if isinstance(res, tuple): if isinstance(res, tuple):
res = Response(*res) res = Response(*res)
elif not isinstance(res, Response): elif not isinstance(res, Response):
res = Response(res) res = Response(res)
if not after_request_handled: if not after_request_handled:
for handler in self.after_error_request_handlers: 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 return res
abort = Microdot.abort
Response.already_handled = Response() Response.already_handled = Response()
abort = Microdot.abort
redirect = Response.redirect redirect = Response.redirect
send_file = Response.send_file 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 import json
from microdot import Request, Response, NoCaseDict from microdot.microdot import Request, Response, AsyncBytesIO
try: try:
from microdot_websocket import WebSocket from microdot.websocket import WebSocket
except: # pragma: no cover # noqa: E722 except: # pragma: no cover # noqa: E722
WebSocket = None WebSocket = None
__all__ = ['TestClient', 'TestResponse']
class TestResponse: class TestResponse:
"""A response object issued by the Microdot test client.""" """A response object issued by the Microdot test client."""
@@ -32,12 +34,15 @@ class TestResponse:
self.reason = res.reason self.reason = res.reason
self.headers = res.headers self.headers = res.headers
def _initialize_body(self, res): async def _initialize_body(self, res):
self.body = b'' 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): if isinstance(body, str):
body = body.encode() body = body.encode()
self.body += body self.body += body
if hasattr(iter, 'aclose'): # pragma: no branch
await iter.aclose()
def _process_text_body(self): def _process_text_body(self):
try: try:
@@ -52,12 +57,13 @@ class TestResponse:
self.json = json.loads(self.text) self.json = json.loads(self.text)
@classmethod @classmethod
def create(cls, res): async def create(cls, res):
test_res = cls() test_res = cls()
test_res._initialize_response(res) test_res._initialize_response(res)
test_res._initialize_body(res) if not res.is_head:
test_res._process_text_body() await test_res._initialize_body(res)
test_res._process_json_body() test_res._process_text_body()
test_res._process_json_body()
return test_res return test_res
@@ -71,17 +77,17 @@ class TestClient:
The following example shows how to create a test client for an application The following example shows how to create a test client for an application
and send a test request:: and send a test request::
from microdot import Microdot from microdot_asyncio import Microdot
app = Microdot() app = Microdot()
@app.get('/') @app.get('/')
def index(): async def index():
return 'Hello, World!' return 'Hello, World!'
def test_hello_world(self): async def test_hello_world(self):
client = TestClient(app) client = TestClient(app)
res = client.get('/') res = await client.get('/')
assert res.status_code == 200 assert res.status_code == 200
assert res.text == 'Hello, World!' assert res.text == 'Hello, World!'
""" """
@@ -149,23 +155,30 @@ class TestClient:
else: else:
self.cookies[cookie_name] = cookie_options[0] self.cookies[cookie_name] = cookie_options[0]
def request(self, method, path, headers=None, body=None, sock=None): async def request(self, method, path, headers=None, body=None, sock=None):
headers = NoCaseDict(headers or {}) headers = headers or {}
body, headers = self._process_body(body, headers) body, headers = self._process_body(body, headers)
cookies, headers = self._process_cookies(headers) cookies, headers = self._process_cookies(headers)
request_bytes = self._render_request(method, path, headers, body) 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), req = await Request.create(self.app, reader, writer,
('127.0.0.1', 1234), client_sock=sock) ('127.0.0.1', 1234))
res = self.app.dispatch_request(req) res = await self.app.dispatch_request(req)
if res == Response.already_handled: if res == Response.already_handled:
return None return None
res.complete() res.complete()
self._update_cookies(res) 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. """Send a GET request to the application.
:param path: The request URL. :param path: The request URL.
@@ -174,9 +187,9 @@ class TestClient:
This method returns a This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object. :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. """Send a POST request to the application.
:param path: The request URL. :param path: The request URL.
@@ -188,9 +201,9 @@ class TestClient:
This method returns a This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object. :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. """Send a PUT request to the application.
:param path: The request URL. :param path: The request URL.
@@ -202,9 +215,9 @@ class TestClient:
This method returns a This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object. :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. """Send a PATCH request to the application.
:param path: The request URL. :param path: The request URL.
@@ -216,9 +229,9 @@ class TestClient:
This method returns a This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object. :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. """Send a DELETE request to the application.
:param path: The request URL. :param path: The request URL.
@@ -227,9 +240,9 @@ class TestClient:
This method returns a This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object. :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. """Send a websocket connection request to the application.
:param path: The request URL. :param path: The request URL.
@@ -244,27 +257,39 @@ class TestClient:
self.closed = False self.closed = False
self.buffer = b'' self.buffer = b''
def _next(self, data=None): async def _next(self, data=None):
try: try:
data = gen.send(data) data = (await gen.asend(data)) if hasattr(gen, 'asend') \
except StopIteration: else gen.send(data)
if self.closed: # pragma: no cover except (StopIteration, StopAsyncIteration):
return if not self.closed:
self.closed = True self.closed = True
raise OSError(32, 'Websocket connection closed') raise OSError(32, 'Websocket connection closed')
return # pragma: no cover
opcode = WebSocket.TEXT if isinstance(data, str) \ opcode = WebSocket.TEXT if isinstance(data, str) \
else WebSocket.BINARY else WebSocket.BINARY
return WebSocket._encode_websocket_frame(opcode, data) return WebSocket._encode_websocket_frame(opcode, data)
def recv(self, n): async def read(self, n):
self.started = True
if not self.buffer: if not self.buffer:
self.buffer = self._next() self.started = True
self.buffer = await self._next()
data = self.buffer[:n] data = self.buffer[:n]
self.buffer = self.buffer[n:] self.buffer = self.buffer[n:]
return data 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: if self.started:
h = WebSocket._parse_frame_header(data[0:2]) h = WebSocket._parse_frame_header(data[0:2])
if h[3] < 0: if h[3] < 0:
@@ -273,7 +298,7 @@ class TestClient:
data = data[2:] data = data[2:]
if h[1] == WebSocket.TEXT: if h[1] == WebSocket.TEXT:
data = data.decode() data = data.decode()
self.buffer = self._next(data) self.buffer = await self._next(data)
ws_headers = { ws_headers = {
'Upgrade': 'websocket', 'Upgrade': 'websocket',
@@ -282,5 +307,6 @@ class TestClient:
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
} }
ws_headers.update(headers or {}) 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