96 Commits
v1.2.4 ... auth

Author SHA1 Message Date
Miguel Grinberg
e373d037f0 login tests 2024-04-28 00:32:22 +01:00
Miguel Grinberg
4321d93a82 auth documentation 2024-04-28 00:32:20 +01:00
Miguel Grinberg
68b6cb9862 Example updates 2024-04-28 00:32:06 +01:00
Miguel Grinberg
c7f9b3ff3b lint issues 2024-04-28 00:32:06 +01:00
Miguel Grinberg
261dd2f980 documentation 2024-04-28 00:32:06 +01:00
Miguel Grinberg
f204416e36 remember tests 2024-04-28 00:32:06 +01:00
Miguel Grinberg
7bc66ce3bb auth examples 2024-04-28 00:32:06 +01:00
Miguel Grinberg
43f2227140 remember cookie 2024-04-28 00:32:06 +01:00
Miguel Grinberg
b0cddde6ec Basic, token and login authentication 2024-04-28 00:32:06 +01:00
Miguel Grinberg
dac6df7a7a use codecov token for coverage uploads #nolog 2024-04-28 00:31:53 +01:00
dependabot[bot]
5d6e838f3c Bump gunicorn from 21.2.0 to 22.0.0 in /examples/benchmark (#224) #nolog
Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 21.2.0 to 22.0.0.
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/21.2.0...22.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-17 23:52:37 +01:00
dependabot[bot]
563bfdc8f5 Bump idna from 3.6 to 3.7 in /examples/benchmark (#223) #nolog
Bumps [idna](https://github.com/kjd/idna) from 3.6 to 3.7.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)
- [Commits](https://github.com/kjd/idna/compare/v3.6...v3.7)

---
updated-dependencies:
- dependency-name: idna
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-12 15:16:52 +01:00
Miguel Grinberg
679d8e63b8 Fix docs build #nolog 2024-03-24 19:56:43 +00:00
Miguel Grinberg
4cb155ee41 Improved cookie support in the test client 2024-03-24 19:45:22 +00:00
Miguel Grinberg
dea79c5ce2 Make Session class more reusable 2024-03-23 16:29:36 +00:00
Carlo Colombo
6b1fd61917 removed outdated import from documentation (Fixes #216) 2024-03-15 11:03:10 +00:00
Miguel Grinberg
f6876c0d15 Use @wraps on decorated functions 2024-03-14 00:16:35 +00:00
Hamsanger
904d5fcaa2 Add event ID to the SSE implementation (#213) 2024-03-10 23:52:43 +00:00
Miguel Grinberg
a0ea439def Add roadmap details to readme 2024-03-10 17:15:14 +00:00
Miguel Grinberg
a1801d9a53 Version 2.0.6.dev0 2024-03-09 10:48:18 +00:00
Miguel Grinberg
14f2c9d345 Release 2.0.5 2024-03-09 10:48:09 +00:00
Miguel Grinberg
d0a4cf8fa7 Handle 0 as an integer argument (Fixes #212) 2024-03-06 20:34:00 +00:00
Miguel Grinberg
901f4e55b8 Version 2.0.5.dev0 2024-02-20 23:15:01 +00:00
Miguel Grinberg
53b28f9938 Release 2.0.4 2024-02-20 23:12:31 +00:00
Miguel Grinberg
f6cba2c0f7 More URLPattern unit tests 2024-02-20 23:09:24 +00:00
Miguel Grinberg
38262c56d3 Do not use regexes for parsing simple URLs (Fixes #207) 2024-02-18 15:05:41 +00:00
dependabot[bot]
a3363c7b8c Bump fastapi from 0.104.1 to 0.109.1 in /examples/benchmark (#203) #nolog
Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.104.1 to 0.109.1.
- [Release notes](https://github.com/tiangolo/fastapi/releases)
- [Commits](https://github.com/tiangolo/fastapi/compare/0.104.1...0.109.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 17:43:31 +00:00
Miguel Grinberg
e44c271bae Circuitpython build 2024-01-31 23:43:17 +00:00
Miguel Grinberg
bf519478cb Added documentation on using alternative utemplate loaders 2024-01-14 12:47:28 +00:00
dependabot[bot]
8d1ca808cb Bump jinja2 from 3.1.2 to 3.1.3 in /examples/benchmark (#199) #nolog
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-11 22:56:46 +00:00
Miguel Grinberg
1f804f869c Version 2.0.4.dev0 2024-01-07 10:50:36 +00:00
Miguel Grinberg
7a6026006f Release 2.0.3 2024-01-07 10:50:28 +00:00
Miguel Grinberg
6712c47400 Pass keyword arguments to thread executor in the correct way (Fixes #195) 2024-01-07 10:44:16 +00:00
Miguel Grinberg
c8c91e8345 Update uasyncio to include new TLS support 2024-01-04 20:41:05 +00:00
Miguel Grinberg
5d188e8c0d Add a limit to WebSocket message size (Fixes #193) 2024-01-03 00:04:02 +00:00
Miguel Grinberg
b80b6b64d0 Documentation improvements 2023-12-28 12:59:19 +00:00
Miguel Grinberg
28007ea583 Version 2.0.3.dev0 2023-12-28 12:11:32 +00:00
Miguel Grinberg
300f8563ed Release 2.0.2 2023-12-28 12:10:46 +00:00
Miguel Grinberg
1fc11193da Support binary data in the SSE extension 2023-12-28 12:04:17 +00:00
Miguel Grinberg
79452a4699 Upgrade micropython tests to use v1.22, initial circuitpython work 2023-12-27 20:39:20 +00:00
Miguel Grinberg
84842e39c3 Improvements to migration guide 2023-12-26 20:00:07 +00:00
Miguel Grinberg
2a3c889717 typo in documentation #nolog 2023-12-26 17:07:20 +00:00
Tak Tran
ad368be993 Remove spurious async in documentation example (#187) 2023-12-23 14:08:12 +00:00
Miguel Grinberg
3df56c6ffe Version 2.0.2.dev0 2023-12-23 12:50:03 +00:00
Miguel Grinberg
c2e18004f7 Release 2.0.1 2023-12-23 12:49:52 +00:00
Miguel Grinberg
bd18ceb442 Addressed some inadvertent mistakes in the template extensions 2023-12-23 12:48:27 +00:00
Miguel Grinberg
f38d6d760a Updated readme and change log #nolog 2023-12-23 00:04:22 +00:00
Miguel Grinberg
dee4914bdd Version 2.0.1.dev0 2023-12-22 20:42:53 +00:00
Miguel Grinberg
92e571ee32 Release 2.0.0 2023-12-22 20:41:15 +00:00
Miguel Grinberg
655f23ee7e Documentation links update #nolog 2023-12-22 20:34:48 +00:00
Miguel Grinberg
20ea305fe7 v2 (#186) 2023-12-22 20:26:07 +00:00
Miguel Grinberg
7a329d98a8 Version 1.3.5.dev0 2023-11-08 00:15:07 +00:00
Miguel Grinberg
93411c6a9f Release 1.3.4 2023-11-08 00:14:21 +00:00
Miguel Grinberg
5550b20cdd Handle change in wait_closed() behavior in python 3.12 (Fixes #177) 2023-11-08 00:11:14 +00:00
dependabot[bot]
d8d2667053 Bump urllib3 from 1.26.17 to 1.26.18 in /examples/benchmark (#173) #nolog
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.17 to 1.26.18.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.17...1.26.18)

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 11:19:39 +01:00
Miguel Grinberg
f81de6d958 Explicitly set UTF-8 encoding for HTML in examples (Fixes #132) 2023-05-12 09:17:54 +01:00
Miguel Grinberg
efec9f14be More robust check for socket timeout error code (Fixes #106) 2023-04-24 18:24:16 +01:00
Miguel Grinberg
239cf4ff37 Use a more conservative default for socket timeout (Fixes #130) 2023-04-24 18:19:41 +01:00
Miguel Grinberg
87cd098f66 WebSocket error when handling PING packet (Fixes #129) 2023-04-14 15:29:26 +01:00
Miguel Grinberg
bb75e15b2d Upgrade GitHub actions #nolog 2023-04-14 12:56:33 +01:00
Miguel Grinberg
b7ad02eaf1 Version 1.3.1.dev0 2023-04-08 17:23:33 +01:00
Miguel Grinberg
79e11262d1 Release 1.3.0 2023-04-08 17:21:30 +01:00
Miguel Grinberg
a1b061656f Tolerate slightly invalid formats in query strings (Fixes #126) 2023-04-08 17:15:54 +01:00
Miguel Grinberg
67798f7dbf Cross-Origin Resource Sharing (CORS) support (Fixes #45) 2023-03-23 00:03:21 +00:00
Miguel Grinberg
ea6766cea9 Add update() method to NoCaseDict class 2023-03-22 20:22:29 +00:00
Miguel Grinberg
6a31f89673 Respond to HEAD and OPTIONS requests 2023-03-22 12:25:21 +00:00
Miguel Grinberg
eaf2ef62d1 Documentation typo #nolog 2023-03-21 00:32:22 +00:00
Miguel Grinberg
a350e8fd1e Set exit code to 1 for failed MicroPython test runs 2023-03-21 00:28:23 +00:00
Miguel Grinberg
daf1001ec5 Support compressed files in send_file() (Fixes #93) 2023-03-21 00:24:57 +00:00
Miguel Grinberg
e684ee32d9 Add max_age argument to send_file() 2023-03-20 12:11:01 +00:00
Miguel Grinberg
573e303a98 Issue templates #nolog 2023-03-03 14:49:10 +00:00
Miguel Grinberg
3592f53999 Update gitignore #nolog 2023-03-03 11:06:50 +00:00
Miguel Grinberg
ea3722ca5c Version 1.2.5.dev0 2023-03-03 08:46:27 +00:00
161 changed files with 7280 additions and 6396 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,22 +11,23 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions
- run: tox -eflake8
- run: tox -edocs
tests:
name: tests
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ['3.7', '3.8', '3.9', '3.10', '3.11']
python: ['3.8', '3.9', '3.10', '3.11', '3.12']
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python }}
- run: python -m pip install --upgrade pip wheel
@@ -36,27 +37,40 @@ jobs:
name: tests-micropython
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions
- run: tox -eupy
tests-circuitpython:
name: tests-circuitpython
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions
- run: tox -ecpy
coverage:
name: coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions codecov
- run: pip install tox tox-gh-actions
- run: tox
- run: codecov
- uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
benchmark:
name: benchmark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions
- run: tox -ebenchmark

5
.gitignore vendored
View File

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

16
.readthedocs.yaml Normal file
View File

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

View File

@@ -1,5 +1,81 @@
# Microdot change log
**Release 2.0.5** - 2024-03-09
- Correct handling of 0 as an integer argument (regression from #207) [#212](https://github.com/miguelgrinberg/microdot/issues/212) ([commit](https://github.com/miguelgrinberg/microdot/commit/d0a4cf8fa7dfb1da7466157b18d3329a8cf9a5df))
**Release 2.0.4** - 2024-02-20
- Do not use regexes for parsing simple URLs [#207](https://github.com/miguelgrinberg/microdot/issues/207) ([commit #1](https://github.com/miguelgrinberg/microdot/commit/38262c56d34784401659639b482a4a1224e1e59a) [commit #2](https://github.com/miguelgrinberg/microdot/commit/f6cba2c0f7e18e2f32b5adb779fb037b6c473eab))
- Added documentation on using alternative uTemplate loaders ([commit](https://github.com/miguelgrinberg/microdot/commit/bf519478cbc6e296785241cd7d01edb23c317cd3))
- Added CircuitPython builds ([commit](https://github.com/miguelgrinberg/microdot/commit/e44c271bae88f4327d3eda16d8780ac264d1ebab))
**Release 2.0.3** - 2024-01-07
- Add a limit to WebSocket message size [#193](https://github.com/miguelgrinberg/microdot/issues/193) ([commit](https://github.com/miguelgrinberg/microdot/commit/5d188e8c0ddef6ce633ca702dbdd4a90f2799597))
- Pass keyword arguments to thread executor in the correct way [#195](https://github.com/miguelgrinberg/microdot/issues/195) ([commit](https://github.com/miguelgrinberg/microdot/commit/6712c47400d7c426c88032f65ab74466524eccab))
- Update uasyncio library used in tests to include new TLS support ([commit](https://github.com/miguelgrinberg/microdot/commit/c8c91e83457d24320f22c9a74e80b15e06b072ca))
- Documentation improvements ([commit](https://github.com/miguelgrinberg/microdot/commit/b80b6b64d02d21400ca8a5077f5ed1127cc202ae))
**Release 2.0.2** - 2023-12-28
- Support binary data in the SSE extension ([commit](https://github.com/miguelgrinberg/microdot/commit/1fc11193da0d298f5539e2ad218836910a13efb2))
- Upgrade micropython tests to use v1.22 + initial CircuitPython testing work ([commit](https://github.com/miguelgrinberg/microdot/commit/79452a46992351ccad2c0317c20bf50be0d76641))
- Improvements to migration guide ([commit](https://github.com/miguelgrinberg/microdot/commit/84842e39c360a8b3ddf36feac8af201fb19bbb0b))
- Remove spurious async in documentation example [#187](https://github.com/miguelgrinberg/microdot/issues/187) ([commit](https://github.com/miguelgrinberg/microdot/commit/ad368be993e2e3007579f1d3880e36d60c71da92)) (thanks **Tak Tran**!)
**Release 2.0.1** - 2023-12-23
- Addressed some inadvertent mistakes in the template extensions ([commit](https://github.com/miguelgrinberg/microdot/commit/bd18ceb4424e9dfb52b1e6d498edd260aa24fc53))
**Release 2.0.0** - 2023-12-22
- Major redesign [#186](https://github.com/miguelgrinberg/microdot/issues/186) ([commit](https://github.com/miguelgrinberg/microdot/commit/20ea305fe793eb206b52af9eb5c5f3c1e9f57dbb))
- Code reorganization as a `microdot` package
- Asyncio is now the core implementation
- New support for Server-Sent Events (SSE)
- Several extensions redesigned
- Support for "partitioned" cookies
- [Cross-compiling and freezing](https://microdot.readthedocs.io/en/stable/freezing.html) guidance
- A [Migration Guide](https://microdot.readthedocs.io/en/stable/migrating.html) to help transition to version 2 from older releases
**Release 1.3.4** - 2023-11-08
- Handle change in `wait_closed()` behavior in Python 3.12 [#177](https://github.com/miguelgrinberg/microdot/issues/177) ([commit](https://github.com/miguelgrinberg/microdot/commit/5550b20cdd347d59e2aa68f6ebf9e9abffaff9fc))
- Added missing request argument in some documentation examples [#163](https://github.com/miguelgrinberg/microdot/issues/163) ([commit](https://github.com/miguelgrinberg/microdot/commit/744548f8dc33a72512b34c4001ee9c6c1edd22ee))
- Fix minor documentation typos [#161](https://github.com/miguelgrinberg/microdot/issues/161) ([commit](https://github.com/miguelgrinberg/microdot/commit/2e4911d10826cbb3914de4a45e495c3be36543fa)) (thanks **Andy Piper**!)
**Release 1.3.3** - 2023-07-16
- Handle query string arguments without value [#149](https://github.com/miguelgrinberg/microdot/issues/149) ([commit](https://github.com/miguelgrinberg/microdot/commit/3554bc91cb1523efa5b66fe3ef173f8e86e8c2a0))
- Support empty responses with ASGI adapter ([commit](https://github.com/miguelgrinberg/microdot/commit/e09e9830f43af41d38775547637558494151a385))
- Added CORS extension to Python package ([commit](https://github.com/miguelgrinberg/microdot/commit/304ca2ef6881fe718126b3e308211e760109d519))
- Document access to WSGI and ASGI attributes [#153](https://github.com/miguelgrinberg/microdot/issues/153) ([commit](https://github.com/miguelgrinberg/microdot/commit/d99df2c4010ab70c60b86ab334d656903e04eb26))
- Upgrade micropython tests to use v1.20 ([commit](https://github.com/miguelgrinberg/microdot/commit/e0f0565551966ee0238a5a1819c78a13639ad704))
**Release 1.3.2** - 2023-06-13
- In ASGI, return headers as strings and not binary [#144](https://github.com/miguelgrinberg/microdot/issues/144) ([commit](https://github.com/miguelgrinberg/microdot/commit/e92310fa55bbffcdcbb33f560e27c3579d7ac451))
- Incorrect import in `static_async.py` example ([commit](https://github.com/miguelgrinberg/microdot/commit/c07a53943508e64baea160748e67efc92e75b036))
**Release 1.3.1** - 2023-05-21
- Support negative numbers for int path components [#137](https://github.com/miguelgrinberg/microdot/issues/137) ([commit](https://github.com/miguelgrinberg/microdot/commit/a0dd7c8ab6d681932324e56ed101aba861a105a0))
- Use a more conservative default for socket timeout [#130](https://github.com/miguelgrinberg/microdot/issues/130) ([commit](https://github.com/miguelgrinberg/microdot/commit/239cf4ff37268a7e2467b93be44fe9f91cee8aee))
- More robust check for socket timeout error code [#106](https://github.com/miguelgrinberg/microdot/issues/106) ([commit](https://github.com/miguelgrinberg/microdot/commit/efec9f14be7b6f3451e4d1d0fe7e528ce6ca74dc))
- WebSocket error when handling PING packet [#129](https://github.com/miguelgrinberg/microdot/issues/129) ([commit](https://github.com/miguelgrinberg/microdot/commit/87cd098f66e24bed6bbad29b1490a129e355bbb3))
- Explicitly set UTF-8 encoding for HTML files in examples [#132](https://github.com/miguelgrinberg/microdot/issues/132) ([commit](https://github.com/miguelgrinberg/microdot/commit/f81de6d9582f4905b9c2735d3c639b92d7e77994))
**Release 1.3.0** - 2023-04-08
- Cross-Origin Resource Sharing (CORS) extension [#45](https://github.com/miguelgrinberg/microdot/issues/45) ([commit](https://github.com/miguelgrinberg/microdot/commit/67798f7dbffb30018ab4b62a9aaa297f63bc9e64))
- Respond to `HEAD` and `OPTIONS` requests ([commit](https://github.com/miguelgrinberg/microdot/commit/6a31f89673518e79fef5659c04e609b7976a5e34))
- Tolerate slightly invalid formats in query strings [#126](https://github.com/miguelgrinberg/microdot/issues/126) ([commit](https://github.com/miguelgrinberg/microdot/commit/a1b061656fa19dae583951596b0f1f0603652a56))
- Support compressed files in `send_file()` [#93](https://github.com/miguelgrinberg/microdot/issues/93) ([commit](https://github.com/miguelgrinberg/microdot/commit/daf1001ec55ab38e6cdfee4931729a3b7506858b))
- Add `max_age` argument to `send_file()` ([commit](https://github.com/miguelgrinberg/microdot/commit/e684ee32d91d3e2ab9569bb5fd342986c010ffeb))
- Add `update()` method to `NoCaseDict` class ([commit](https://github.com/miguelgrinberg/microdot/commit/ea6766cea96b756b36ed777f9c1b6a6680db09ba))
- Set exit code to 1 for failed MicroPython test runs ([commit](https://github.com/miguelgrinberg/microdot/commit/a350e8fd1e55fac12c9e5b909cfa82d880b177ef))
**Release 1.2.4** - 2023-03-03
- One more attempt to correct build issues ([commit](https://github.com/miguelgrinberg/microdot/commit/cb39898829f4edc233ab4e7ba3f7ef3c5c50f196))

5
MANIFEST.in Normal file
View File

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

View File

@@ -3,9 +3,9 @@
*“The impossibly small web framework for Python and MicroPython”*
Microdot is a minimalistic Python web framework inspired by Flask, and designed
to run on systems with limited resources such as microcontrollers. It runs on
standard Python and on MicroPython.
Microdot is a minimalistic Python web framework inspired by Flask. Given its
small size, it can run on systems with limited resources such as
microcontrollers. Both standard Python (CPython) and MicroPython are supported.
```python
from microdot import Microdot
@@ -13,13 +13,44 @@ from microdot import Microdot
app = Microdot()
@app.route('/')
def index(request):
async def index(request):
return 'Hello, world!'
app.run()
```
## Migrating to Microdot 2
Version 2 of Microdot incorporates feedback received from users of earlier
releases, and attempts to improve and correct some design decisions that have
proven to be problematic.
For this reason most applications built for earlier versions will need to be
updated to work correctly with Microdot 2. The
[Migration Guide](https://microdot.readthedocs.io/en/stable/migrating.html)
describes the backwards incompatible changes that were made.
## Resources
- [Documentation](https://microdot.readthedocs.io/en/latest/)
- [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)
- Documentation
- [Latest](https://microdot.readthedocs.io/en/latest/)
- [Stable (v2)](https://microdot.readthedocs.io/en/stable/)
- [Legacy (v1)](https://microdot.readthedocs.io/en/v1/) ([Code](https://github.com/miguelgrinberg/microdot/tree/v1))
## Roadmap
The following features are planned for future releases of Microdot, both for
MicroPython and CPython:
- Support for forms encoded in `multipart/form-data` format
- Authentication support, similar to [Flask-Login](https://github.com/maxcountryman/flask-login) for Flask
- OpenAPI integration, similar to [APIFairy](https://github.com/miguelgrinberg/apifairy) for Flask
In addition to the above, the following extensions are also under consideration,
but only for CPython:
- Database integration through [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)
- Socket.IO support through [python-socketio](https://github.com/miguelgrinberg/python-socketio)
Do you have other ideas to propose? Let's [discuss them](https://github.com/miguelgrinberg/microdot/discussions/new?category=ideas)!

BIN
bin/circuitpython Executable file

Binary file not shown.

Binary file not shown.

View File

@@ -1,8 +1,8 @@
API Reference
=============
``microdot`` module
-------------------
Core API
--------
.. autoclass:: microdot.Microdot
:members:
@@ -13,97 +13,75 @@ API Reference
.. autoclass:: microdot.Response
:members:
.. autoclass:: microdot.NoCaseDict
WebSocket
---------
.. automodule:: microdot.websocket
:members:
.. autoclass:: microdot.MultiDict
:members:
``microdot_asyncio`` module
---------------------------
.. autoclass:: microdot_asyncio.Microdot
:inherited-members:
:members:
.. autoclass:: microdot_asyncio.Request
:inherited-members:
:members:
.. autoclass:: microdot_asyncio.Response
:inherited-members:
:members:
``microdot_utemplate`` module
-----------------------------
.. automodule:: microdot_utemplate
:members:
``microdot_jinja`` module
-------------------------
.. automodule:: microdot_jinja
:members:
``microdot_session`` module
---------------------------
.. automodule:: microdot_session
:members:
``microdot_websocket`` module
------------------------------
.. automodule:: microdot_websocket
:members:
``microdot_asyncio_websocket`` module
-------------------------------------
.. automodule:: microdot_asyncio_websocket
:members:
``microdot_asgi_websocket`` module
-------------------------------------
.. automodule:: microdot_asgi_websocket
:members:
``microdot_ssl`` module
-----------------------
.. automodule:: microdot_ssl
:members:
``microdot_test_client`` module
-------------------------------
.. autoclass:: microdot_test_client.TestClient
:members:
.. autoclass:: microdot_test_client.TestResponse
:members:
``microdot_asyncio_test_client`` module
---------------------------------------
.. autoclass:: microdot_asyncio_test_client.TestClient
:members:
.. autoclass:: microdot_asyncio_test_client.TestResponse
:members:
``microdot_wsgi`` module
Server-Sent Events (SSE)
------------------------
.. autoclass:: microdot_wsgi.Microdot
.. automodule:: microdot.sse
:members:
Templates (uTemplate)
---------------------
.. automodule:: microdot.utemplate
:members:
Templates (Jinja)
-----------------
.. automodule:: microdot.jinja
:members:
User Sessions
-------------
.. automodule:: microdot.session
:members:
Authentication
--------------
.. automodule:: microdot.auth
:inherited-members:
:special-members: __call__
:members:
User Logins
-----------
.. automodule:: microdot.login
:inherited-members:
:special-members: __call__
:members:
Cross-Origin Resource Sharing (CORS)
------------------------------------
.. automodule:: microdot.cors
:members:
Test Client
-----------
.. automodule:: microdot.test_client
:members:
ASGI
----
.. autoclass:: microdot.asgi.Microdot
:members:
:exclude-members: shutdown, run
``microdot_asgi`` module
------------------------
WSGI
----
.. autoclass:: microdot_asgi.Microdot
.. autoclass:: microdot.wsgi.Microdot
:members:
:exclude-members: shutdown, run

View File

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

110
docs/freezing.rst Normal file
View File

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

View File

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

View File

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

145
docs/migrating.rst Normal file
View File

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

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

@@ -0,0 +1 @@
This directory contains examples that demonstrate basic and token authentication.

View File

@@ -0,0 +1,31 @@
from hashlib import sha1
from microdot import Microdot
from microdot.auth import BasicAuth
def create_hash(password):
return sha1(password).hexdigest()
USERS = {
'susan': create_hash(b'hello'),
'david': create_hash(b'bye'),
}
app = Microdot()
auth = BasicAuth()
@auth.authenticate
async def check_credentials(request, username, password):
if username in USERS and USERS[username] == create_hash(password.encode()):
return username
@app.route('/')
@auth
async def index(request):
return f'Hello, {request.g.current_user}!'
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -0,0 +1,26 @@
from microdot import Microdot
from microdot.auth import TokenAuth
app = Microdot()
auth = TokenAuth()
TOKENS = {
'susan-token': 'susan',
'david-token': 'david',
}
@auth.authenticate
async def check_token(request, token):
if token in TOKENS:
return TOKENS[token]
@app.route('/')
@auth
async def index(request):
return f'Hello, {request.g.current_user}!'
if __name__ == '__main__':
app.run(debug=True)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1 @@
This directory contains examples that demonstrate user logins.

122
examples/login/login.py Normal file
View File

@@ -0,0 +1,122 @@
from hashlib import sha1
from microdot import Microdot, redirect
from microdot.session import Session
from microdot.login import Login
class User:
def __init__(self, id, username, password):
self.id = id
self.username = username
self.password_hash = self.create_hash(password)
def create_hash(self, password):
# note: to keep this example simple, passwords are hashed with the SHA1
# algorithm. In a real application, you should use a stronger
# algorithm, such as bcrypt.
return sha1(password.encode()).hexdigest()
def check_password(self, password):
return self.create_hash(password) == self.password_hash
USERS = {
'user001': User('user001', 'susan', 'hello'),
'user002': User('user002', 'david', 'bye'),
}
app = Microdot()
Session(app, secret_key='top-secret!')
auth = Login()
@auth.id_to_user
async def get_user(user_id):
print('get_user', user_id)
return USERS.get(user_id)
@app.route('/login', methods=['GET', 'POST'])
async def login(request):
if request.method == 'GET':
return '''
<!doctype html>
<html>
<body>
<h1>Please Login</h1>
<form method="POST">
<p>
Username<br>
<input name="username" autofocus>
</p>
<p>
Password:<br>
<input name="password" type="password">
<br>
</p>
<p>
<input name="remember_me" type="checkbox"> Remember me
<br>
</p>
<p>
<button type="submit">Login</button>
</p>
</form>
</body>
</html>
''', {'Content-Type': 'text/html'}
username = request.form['username']
password = request.form['password']
remember_me = bool(request.form.get('remember_me'))
for user in USERS.values():
if user.username == username:
if user.check_password(password):
return await auth.login_user(request, user,
remember=remember_me)
return redirect('/login')
@app.route('/')
@auth
async def index(request):
return f'''
<!doctype html>
<html>
<body>
<h1>Hello, {request.g.current_user.username}!</h1>
<p>
<a href="/fresh">Click here</a> to access the fresh login page.
</p>
<form method="POST" action="/logout">
<button type="submit">Logout</button>
</form>
</body>
</html>
''', {'Content-Type': 'text/html'}
@app.get('/fresh')
@auth.fresh
async def fresh(request):
return f'''
<!doctype html>
<html>
<body>
<h1>Hello, {request.g.current_user.username}!</h1>
<p>This page requires a fresh login session.</p>
<p><a href="/">Go back</a> to the main page.</p>
</body>
</html>
''', {'Content-Type': 'text/html'}
@app.post('/logout')
@auth
async def logout(request):
await auth.logout_user(request)
return redirect('/')
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -1,11 +1,15 @@
# This is a simple example that demonstrates how to use the user session, but
# is not intended as a complete login solution. See the login subdirectory for
# a more complete example.
from microdot import Microdot, Response, redirect
from microdot_session import set_session_secret_key, with_session, \
update_session, delete_session
from microdot.session import Session, with_session
BASE_TEMPLATE = '''<!doctype html>
<html>
<head>
<title>Microdot login example</title>
<meta charset="UTF-8">
</head>
<body>
<h1>Microdot login example</h1>
@@ -17,7 +21,7 @@ LOGGED_OUT = '''<p>You are not logged in.</p>
<form method="POST">
<p>
Username:
<input type="text" name="username" autofocus />
<input name="username" autofocus />
</p>
<input type="submit" value="Submit" />
</form>'''
@@ -28,18 +32,19 @@ LOGGED_IN = '''<p>Hello <b>{username}</b>!</p>
</form>'''
app = Microdot()
set_session_secret_key('top-secret')
Session(app, secret_key='top-secret')
Response.default_content_type = 'text/html'
@app.get('/')
@app.post('/')
@with_session
def index(req, session):
async def index(req, session):
username = session.get('username')
if req.method == 'POST':
username = req.form.get('username')
update_session(req, {'username': username})
session['username'] = username
session.save()
return redirect('/')
if username is None:
return BASE_TEMPLATE.format(content=LOGGED_OUT)
@@ -49,8 +54,9 @@ def index(req, session):
@app.post('/logout')
def logout(req):
delete_session(req)
@with_session
async def logout(req, session):
session.delete()
return redirect('/')

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

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

View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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
Template.initialize('templates', enable_async=True)
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/', methods=['GET', 'POST'])
async def index(req):
name = None
if req.method == 'POST':
name = req.form.get('name')
return await Template('index.html').render_async(name=name)
if __name__ == '__main__':
app.run()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,14 @@
import ssl
from microdot_asyncio import Microdot
import sys
from microdot import Microdot
app = Microdot()
htmldoc = '''<!DOCTYPE html>
html = '''<!DOCTYPE html>
<html>
<head>
<title>Microdot Example Page</title>
<meta charset="UTF-8">
</head>
<body>
<div>
@@ -21,7 +23,7 @@ htmldoc = '''<!DOCTYPE html>
@app.route('/')
async def hello(request):
return htmldoc, 200, {'Content-Type': 'text/html'}
return html, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')
@@ -30,6 +32,7 @@ async def shutdown(request):
return 'The server is shutting down...'
ext = 'der' if sys.implementation.name == 'micropython' else 'pem'
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
sslctx.load_cert_chain('cert.pem', 'key.pem')
sslctx.load_cert_chain('cert.' + ext, 'key.' + ext)
app.run(port=4443, debug=True, ssl=sslctx)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,139 @@
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
# SPDX-FileCopyrightText: Copyright (c) 2021 Jeff Epler for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_ticks`
================================================================================
Work with intervals and deadlines in milliseconds
* Author(s): Jeff Epler
Implementation Notes
--------------------
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases
"""
# imports
from micropython import const
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ticks.git"
_TICKS_PERIOD = const(1 << 29)
_TICKS_MAX = const(_TICKS_PERIOD - 1)
_TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2)
# Get the correct implementation of ticks_ms. There are three possibilities:
#
# - supervisor.ticks_ms is present. This will be the case starting in CP7.0
#
# - time.ticks_ms is present. This is the case for MicroPython & for the "unix
# port" of CircuitPython, used for some automated testing.
#
# - time.monotonic_ns is present, and works. This is the case on most
# Express boards in CP6.x, and most host computer versions of Python.
#
# - Otherwise, time.monotonic is assumed to be present. This is the case
# on most non-express boards in CP6.x, and some old host computer versions
# of Python.
#
# Note that on microcontrollers, this time source becomes increasingly
# inaccurate when the board has not been reset in a long time, losing the
# ability to measure 1ms intervals after about 1 hour, and losing the
# ability to meausre 128ms intervals after 6 days. The only solution is to
# either upgrade to a version with supervisor.ticks_ms, or to switch to a
# board with time.monotonic_ns.
try:
from supervisor import ticks_ms # pylint: disable=unused-import
except (ImportError, NameError):
import time
if _ticks_ms := getattr(time, "ticks_ms", None):
def ticks_ms() -> int:
"""Return the time in milliseconds since an unspecified moment,
wrapping after 2**29ms.
The wrap value was chosen so that it is always possible to add or
subtract two `ticks_ms` values without overflow on a board without
long ints (or without allocating any long integer objects, on
boards with long ints).
This ticks value comes from a low-accuracy clock internal to the
microcontroller, just like `time.monotonic`. Due to its low
accuracy and the fact that it "wraps around" every few days, it is
intended for working with short term events like advancing an LED
animation, not for long term events like counting down the time
until a holiday."""
return _ticks_ms() & _TICKS_MAX # pylint: disable=not-callable
else:
try:
from time import monotonic_ns as _monotonic_ns
_monotonic_ns() # Check that monotonic_ns is usable
def ticks_ms() -> int:
"""Return the time in milliseconds since an unspecified moment,
wrapping after 2**29ms.
The wrap value was chosen so that it is always possible to add or
subtract two `ticks_ms` values without overflow on a board without
long ints (or without allocating any long integer objects, on
boards with long ints).
This ticks value comes from a low-accuracy clock internal to the
microcontroller, just like `time.monotonic`. Due to its low
accuracy and the fact that it "wraps around" every few days, it is
intended for working with short term events like advancing an LED
animation, not for long term events like counting down the time
until a holiday."""
return (_monotonic_ns() // 1_000_000) & _TICKS_MAX
except (ImportError, NameError, NotImplementedError):
from time import monotonic as _monotonic
def ticks_ms() -> int:
"""Return the time in milliseconds since an unspecified moment,
wrapping after 2**29ms.
The wrap value was chosen so that it is always possible to add or
subtract two `ticks_ms` values without overflow on a board without
long ints (or without allocating any long integer objects, on
boards with long ints).
This ticks value comes from a low-accuracy clock internal to the
microcontroller, just like `time.monotonic`. Due to its low
accuracy and the fact that it "wraps around" every few days, it is
intended for working with short term events like advancing an LED
animation, not for long term events like counting down the time
until a holiday."""
return int(_monotonic() * 1000) & _TICKS_MAX
def ticks_add(ticks: int, delta: int) -> int:
"Add a delta to a base number of ticks, performing wraparound at 2**29ms."
return (ticks + delta) % _TICKS_PERIOD
def ticks_diff(ticks1: int, ticks2: int) -> int:
"""Compute the signed difference between two ticks values,
assuming that they are within 2**28 ticks"""
diff = (ticks1 - ticks2) & _TICKS_MAX
diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD
return diff
def ticks_less(ticks1: int, ticks2: int) -> bool:
"""Return true if ticks1 is before ticks2 and false otherwise,
assuming that they are within 2**28 ticks"""
return ticks_diff(ticks1, ticks2) < 0

View File

@@ -0,0 +1,41 @@
# SPDX-FileCopyrightText: 2019 Damien P. George
#
# SPDX-License-Identifier: MIT
#
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019 Damien P. George
#
# This code comes from MicroPython, and has not been run through black or pylint there.
# Altering these files significantly would make merging difficult, so we will not use
# pylint or black.
# pylint: skip-file
# fmt: off
from .core import *
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/Adafruit/Adafruit_CircuitPython_asyncio.git"
_attrs = {
"wait_for": "funcs",
"wait_for_ms": "funcs",
"gather": "funcs",
"Event": "event",
"ThreadSafeFlag": "event",
"Lock": "lock",
"open_connection": "stream",
"start_server": "stream",
"StreamReader": "stream",
"StreamWriter": "stream",
}
# Lazy loader, effectively does:
# global attr
# from .mod import attr
def __getattr__(attr):
mod = _attrs.get(attr, None)
if mod is None:
raise AttributeError(attr)
value = getattr(__import__(mod, None, None, True, 1), attr)
globals()[attr] = value
return value

View File

@@ -0,0 +1,430 @@
# SPDX-FileCopyrightText: 2019 Damien P. George
#
# SPDX-License-Identifier: MIT
#
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019 Damien P. George
#
# This code comes from MicroPython, and has not been run through black or pylint there.
# Altering these files significantly would make merging difficult, so we will not use
# pylint or black.
# pylint: skip-file
# fmt: off
"""
Core
====
"""
from adafruit_ticks import ticks_ms as ticks, ticks_diff, ticks_add
import sys, select
try:
from traceback import print_exception
except:
from .traceback import print_exception
# Import TaskQueue and Task, preferring built-in C code over Python code
try:
from _asyncio import TaskQueue, Task
except ImportError:
from .task import TaskQueue, Task
################################################################################
# Exceptions
# Depending on the release of CircuitPython these errors may or may not
# exist in the C implementation of `_asyncio`. However, when they
# do exist, they must be preferred over the Python code.
try:
from _asyncio import CancelledError, InvalidStateError
except (ImportError, AttributeError):
class CancelledError(BaseException):
"""Injected into a task when calling `Task.cancel()`"""
pass
class InvalidStateError(Exception):
"""Can be raised in situations like setting a result value for a task object that already has a result value set."""
pass
class TimeoutError(Exception):
"""Raised when waiting for a task longer than the specified timeout."""
pass
# Used when calling Loop.call_exception_handler
_exc_context = {"message": "Task exception wasn't retrieved", "exception": None, "future": None}
################################################################################
# Sleep functions
# "Yield" once, then raise StopIteration
class SingletonGenerator:
def __init__(self):
self.state = None
self.exc = StopIteration()
def __iter__(self):
return self
def __await__(self):
return self
def __next__(self):
if self.state is not None:
_task_queue.push_sorted(cur_task, self.state)
self.state = None
return None
else:
self.exc.__traceback__ = None
raise self.exc
# Pause task execution for the given time (integer in milliseconds, uPy extension)
# Use a SingletonGenerator to do it without allocating on the heap
def sleep_ms(t, sgen=SingletonGenerator()):
"""Sleep for *t* milliseconds.
This is a coroutine, and a MicroPython extension.
"""
assert sgen.state is None, "Check for a missing `await` in your code"
sgen.state = ticks_add(ticks(), max(0, t))
return sgen
# Pause task execution for the given time (in seconds)
def sleep(t):
"""Sleep for *t* seconds
This is a coroutine.
"""
return sleep_ms(int(t * 1000))
################################################################################
# "Never schedule" object"
# Don't re-schedule the object that awaits _never().
# For internal use only. Some constructs, like `await event.wait()`,
# work by NOT re-scheduling the task which calls wait(), but by
# having some other task schedule it later.
class _NeverSingletonGenerator:
def __init__(self):
self.state = None
self.exc = StopIteration()
def __iter__(self):
return self
def __await__(self):
return self
def __next__(self):
if self.state is not None:
self.state = None
return None
else:
self.exc.__traceback__ = None
raise self.exc
def _never(sgen=_NeverSingletonGenerator()):
# assert sgen.state is None, "Check for a missing `await` in your code"
sgen.state = False
return sgen
################################################################################
# Queue and poller for stream IO
class IOQueue:
def __init__(self):
self.poller = select.poll()
self.map = {} # maps id(stream) to [task_waiting_read, task_waiting_write, stream]
def _enqueue(self, s, idx):
if id(s) not in self.map:
entry = [None, None, s]
entry[idx] = cur_task
self.map[id(s)] = entry
self.poller.register(s, select.POLLIN if idx == 0 else select.POLLOUT)
else:
sm = self.map[id(s)]
assert sm[idx] is None
assert sm[1 - idx] is not None
sm[idx] = cur_task
self.poller.modify(s, select.POLLIN | select.POLLOUT)
# Link task to this IOQueue so it can be removed if needed
cur_task.data = self
def _dequeue(self, s):
del self.map[id(s)]
self.poller.unregister(s)
async def queue_read(self, s):
self._enqueue(s, 0)
await _never()
async def queue_write(self, s):
self._enqueue(s, 1)
await _never()
def remove(self, task):
while True:
del_s = None
for k in self.map: # Iterate without allocating on the heap
q0, q1, s = self.map[k]
if q0 is task or q1 is task:
del_s = s
break
if del_s is not None:
self._dequeue(s)
else:
break
def wait_io_event(self, dt):
for s, ev in self.poller.ipoll(dt):
sm = self.map[id(s)]
# print('poll', s, sm, ev)
if ev & ~select.POLLOUT and sm[0] is not None:
# POLLIN or error
_task_queue.push_head(sm[0])
sm[0] = None
if ev & ~select.POLLIN and sm[1] is not None:
# POLLOUT or error
_task_queue.push_head(sm[1])
sm[1] = None
if sm[0] is None and sm[1] is None:
self._dequeue(s)
elif sm[0] is None:
self.poller.modify(s, select.POLLOUT)
else:
self.poller.modify(s, select.POLLIN)
################################################################################
# Main run loop
# Ensure the awaitable is a task
def _promote_to_task(aw):
return aw if isinstance(aw, Task) else create_task(aw)
# Create and schedule a new task from a coroutine
def create_task(coro):
"""Create a new task from the given coroutine and schedule it to run.
Returns the corresponding `Task` object.
"""
if not hasattr(coro, "send"):
raise TypeError("coroutine expected")
t = Task(coro, globals())
_task_queue.push_head(t)
return t
# Keep scheduling tasks until there are none left to schedule
def run_until_complete(main_task=None):
"""Run the given *main_task* until it completes."""
global cur_task
excs_all = (CancelledError, Exception) # To prevent heap allocation in loop
excs_stop = (CancelledError, StopIteration) # To prevent heap allocation in loop
while True:
# Wait until the head of _task_queue is ready to run
dt = 1
while dt > 0:
dt = -1
t = _task_queue.peek()
if t:
# A task waiting on _task_queue; "ph_key" is time to schedule task at
dt = max(0, ticks_diff(t.ph_key, ticks()))
elif not _io_queue.map:
# No tasks can be woken so finished running
return
# print('(poll {})'.format(dt), len(_io_queue.map))
_io_queue.wait_io_event(dt)
# Get next task to run and continue it
t = _task_queue.pop_head()
cur_task = t
try:
# Continue running the coroutine, it's responsible for rescheduling itself
exc = t.data
if not exc:
t.coro.send(None)
else:
# If the task is finished and on the run queue and gets here, then it
# had an exception and was not await'ed on. Throwing into it now will
# raise StopIteration and the code below will catch this and run the
# call_exception_handler function.
t.data = None
t.coro.throw(exc)
except excs_all as er:
# Check the task is not on any event queue
assert t.data is None
# This task is done, check if it's the main task and then loop should stop
if t is main_task:
if isinstance(er, StopIteration):
return er.value
raise er
if t.state:
# Task was running but is now finished.
waiting = False
if t.state is True:
# "None" indicates that the task is complete and not await'ed on (yet).
t.state = None
elif callable(t.state):
# The task has a callback registered to be called on completion.
t.state(t, er)
t.state = False
waiting = True
else:
# Schedule any other tasks waiting on the completion of this task.
while t.state.peek():
_task_queue.push_head(t.state.pop_head())
waiting = True
# "False" indicates that the task is complete and has been await'ed on.
t.state = False
if not waiting and not isinstance(er, excs_stop):
# An exception ended this detached task, so queue it for later
# execution to handle the uncaught exception if no other task retrieves
# the exception in the meantime (this is handled by Task.throw).
_task_queue.push_head(t)
# Save return value of coro to pass up to caller.
t.data = er
elif t.state is None:
# Task is already finished and nothing await'ed on the task,
# so call the exception handler.
_exc_context["exception"] = exc
_exc_context["future"] = t
Loop.call_exception_handler(_exc_context)
# Create a new task from a coroutine and run it until it finishes
def run(coro):
"""Create a new task from the given coroutine and run it until it completes.
Returns the value returned by *coro*.
"""
return run_until_complete(create_task(coro))
################################################################################
# Event loop wrapper
async def _stopper():
pass
_stop_task = None
class Loop:
"""Class representing the event loop"""
_exc_handler = None
def create_task(coro):
"""Create a task from the given *coro* and return the new `Task` object."""
return create_task(coro)
def run_forever():
"""Run the event loop until `Loop.stop()` is called."""
global _stop_task
_stop_task = Task(_stopper(), globals())
run_until_complete(_stop_task)
# TODO should keep running until .stop() is called, even if there're no tasks left
def run_until_complete(aw):
"""Run the given *awaitable* until it completes. If *awaitable* is not a task then
it will be promoted to one.
"""
return run_until_complete(_promote_to_task(aw))
def stop():
"""Stop the event loop"""
global _stop_task
if _stop_task is not None:
_task_queue.push_head(_stop_task)
# If stop() is called again, do nothing
_stop_task = None
def close():
"""Close the event loop."""
pass
def set_exception_handler(handler):
"""Set the exception handler to call when a Task raises an exception that is not
caught. The *handler* should accept two arguments: ``(loop, context)``
"""
Loop._exc_handler = handler
def get_exception_handler():
"""Get the current exception handler. Returns the handler, or ``None`` if no
custom handler is set.
"""
return Loop._exc_handler
def default_exception_handler(loop, context):
"""The default exception handler that is called."""
exc = context["exception"]
print_exception(None, exc, exc.__traceback__)
def call_exception_handler(context):
"""Call the current exception handler. The argument *context* is passed through
and is a dictionary containing keys:
``'message'``, ``'exception'``, ``'future'``
"""
(Loop._exc_handler or Loop.default_exception_handler)(Loop, context)
# The runq_len and waitq_len arguments are for legacy uasyncio compatibility
def get_event_loop(runq_len=0, waitq_len=0):
"""Return the event loop used to schedule and run tasks. See `Loop`."""
return Loop
def current_task():
"""Return the `Task` object associated with the currently running task."""
return cur_task
def new_event_loop():
"""Reset the event loop and return it.
**NOTE**: Since MicroPython only has a single event loop, this function just resets
the loop's state, it does not create a new one
"""
global _task_queue, _io_queue, _exc_context, cur_task
# TaskQueue of Task instances
_task_queue = TaskQueue()
# Task queue and poller for stream IO
_io_queue = IOQueue()
cur_task = None
_exc_context['exception'] = None
_exc_context['future'] = None
return Loop
# Initialise default event loop
new_event_loop()

View File

@@ -0,0 +1,92 @@
# SPDX-FileCopyrightText: 2019-2020 Damien P. George
#
# SPDX-License-Identifier: MIT
#
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
#
# This code comes from MicroPython, and has not been run through black or pylint there.
# Altering these files significantly would make merging difficult, so we will not use
# pylint or black.
# pylint: skip-file
# fmt: off
"""
Events
======
"""
from . import core
# Event class for primitive events that can be waited on, set, and cleared
class Event:
"""Create a new event which can be used to synchronize tasks. Events
start in the cleared state.
"""
def __init__(self):
self.state = False # False=unset; True=set
self.waiting = core.TaskQueue() # Queue of Tasks waiting on completion of this event
def is_set(self):
"""Returns ``True`` if the event is set, ``False`` otherwise."""
return self.state
def set(self):
"""Set the event. Any tasks waiting on the event will be scheduled to run.
"""
# Event becomes set, schedule any tasks waiting on it
# Note: This must not be called from anything except the thread running
# the asyncio loop (i.e. neither hard or soft IRQ, or a different thread).
while self.waiting.peek():
core._task_queue.push_head(self.waiting.pop_head())
self.state = True
def clear(self):
"""Clear the event."""
self.state = False
async def wait(self):
"""Wait for the event to be set. If the event is already set then it returns
immediately.
This is a coroutine.
"""
if not self.state:
# Event not set, put the calling task on the event's waiting queue
self.waiting.push_head(core.cur_task)
# Set calling task's data to the event's queue so it can be removed if needed
core.cur_task.data = self.waiting
await core._never()
return True
# MicroPython-extension: This can be set from outside the asyncio event loop,
# such as other threads, IRQs or scheduler context. Implementation is a stream
# that asyncio will poll until a flag is set.
# Note: Unlike Event, this is self-clearing.
try:
import uio
class ThreadSafeFlag(uio.IOBase):
def __init__(self):
self._flag = 0
def ioctl(self, req, flags):
if req == 3: # MP_STREAM_POLL
return self._flag * flags
return None
def set(self):
self._flag = 1
async def wait(self):
if not self._flag:
yield core._io_queue.queue_read(self)
self._flag = 0
except ImportError:
pass

View File

@@ -0,0 +1,165 @@
# SPDX-FileCopyrightText: 2019-2020 Damien P. George
#
# SPDX-License-Identifier: MIT
#
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2022 Damien P. George
#
# This code comes from MicroPython, and has not been run through black or pylint there.
# Altering these files significantly would make merging difficult, so we will not use
# pylint or black.
# pylint: skip-file
# fmt: off
"""
Functions
=========
"""
from . import core
async def _run(waiter, aw):
try:
result = await aw
status = True
except BaseException as er:
result = None
status = er
if waiter.data is None:
# The waiter is still waiting, cancel it.
if waiter.cancel():
# Waiter was cancelled by us, change its CancelledError to an instance of
# CancelledError that contains the status and result of waiting on aw.
# If the wait_for task subsequently gets cancelled externally then this
# instance will be reset to a CancelledError instance without arguments.
waiter.data = core.CancelledError(status, result)
async def wait_for(aw, timeout, sleep=core.sleep):
"""Wait for the *aw* awaitable to complete, but cancel if it takes longer
than *timeout* seconds. If *aw* is not a task then a task will be created
from it.
If a timeout occurs, it cancels the task and raises ``asyncio.TimeoutError``:
this should be trapped by the caller.
Returns the return value of *aw*.
This is a coroutine.
"""
aw = core._promote_to_task(aw)
if timeout is None:
return await aw
# Run aw in a separate runner task that manages its exceptions.
runner_task = core.create_task(_run(core.cur_task, aw))
try:
# Wait for the timeout to elapse.
await sleep(timeout)
except core.CancelledError as er:
status = er.args[0] if er.args else None
if status is None:
# This wait_for was cancelled externally, so cancel aw and re-raise.
runner_task.cancel()
raise er
elif status is True:
# aw completed successfully and cancelled the sleep, so return aw's result.
return er.args[1]
else:
# aw raised an exception, propagate it out to the caller.
raise status
# The sleep finished before aw, so cancel aw and raise TimeoutError.
runner_task.cancel()
await runner_task
raise core.TimeoutError
def wait_for_ms(aw, timeout):
"""Similar to `wait_for` but *timeout* is an integer in milliseconds.
This is a coroutine, and a MicroPython extension.
"""
return wait_for(aw, timeout, core.sleep_ms)
class _Remove:
@staticmethod
def remove(t):
pass
async def gather(*aws, return_exceptions=False):
"""Run all *aws* awaitables concurrently. Any *aws* that are not tasks
are promoted to tasks.
Returns a list of return values of all *aws*
"""
if not aws:
return []
def done(t, er):
# Sub-task "t" has finished, with exception "er".
nonlocal state
if gather_task.data is not _Remove:
# The main gather task has already been scheduled, so do nothing.
# This happens if another sub-task already raised an exception and
# woke the main gather task (via this done function), or if the main
# gather task was cancelled externally.
return
elif not return_exceptions and not isinstance(er, StopIteration):
# A sub-task raised an exception, indicate that to the gather task.
state = er
else:
state -= 1
if state:
# Still some sub-tasks running.
return
# Gather waiting is done, schedule the main gather task.
core._task_queue.push_head(gather_task)
ts = [core._promote_to_task(aw) for aw in aws]
for i in range(len(ts)):
if ts[i].state is not True:
# Task is not running, gather not currently supported for this case.
raise RuntimeError("can't gather")
# Register the callback to call when the task is done.
ts[i].state = done
# Set the state for execution of the gather.
gather_task = core.cur_task
state = len(ts)
cancel_all = False
# Wait for the a sub-task to need attention.
gather_task.data = _Remove
try:
await core._never()
except core.CancelledError as er:
cancel_all = True
state = er
# Clean up tasks.
for i in range(len(ts)):
if ts[i].state is done:
# Sub-task is still running, deregister the callback and cancel if needed.
ts[i].state = True
if cancel_all:
ts[i].cancel()
elif isinstance(ts[i].data, StopIteration):
# Sub-task ran to completion, get its return value.
ts[i] = ts[i].data.value
else:
# Sub-task had an exception with return_exceptions==True, so get its exception.
ts[i] = ts[i].data
# Either this gather was cancelled, or one of the sub-tasks raised an exception with
# return_exceptions==False, so reraise the exception here.
if state is not 0:
raise state
# Return the list of return values of each sub-task.
return ts

View File

@@ -0,0 +1,87 @@
# SPDX-FileCopyrightText: 2019-2020 Damien P. George
#
# SPDX-License-Identifier: MIT
#
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
#
# This code comes from MicroPython, and has not been run through black or pylint there.
# Altering these files significantly would make merging difficult, so we will not use
# pylint or black.
# pylint: skip-file
# fmt: off
"""
Locks
=====
"""
from . import core
# Lock class for primitive mutex capability
class Lock:
"""Create a new lock which can be used to coordinate tasks. Locks start in
the unlocked state.
In addition to the methods below, locks can be used in an ``async with``
statement.
"""
def __init__(self):
# The state can take the following values:
# - 0: unlocked
# - 1: locked
# - <Task>: unlocked but this task has been scheduled to acquire the lock next
self.state = 0
# Queue of Tasks waiting to acquire this Lock
self.waiting = core.TaskQueue()
def locked(self):
"""Returns ``True`` if the lock is locked, otherwise ``False``."""
return self.state == 1
def release(self):
"""Release the lock. If any tasks are waiting on the lock then the next
one in the queue is scheduled to run and the lock remains locked. Otherwise,
no tasks are waiting and the lock becomes unlocked.
"""
if self.state != 1:
raise RuntimeError("Lock not acquired")
if self.waiting.peek():
# Task(s) waiting on lock, schedule next Task
self.state = self.waiting.pop_head()
core._task_queue.push_head(self.state)
else:
# No Task waiting so unlock
self.state = 0
async def acquire(self):
"""Wait for the lock to be in the unlocked state and then lock it in an
atomic way. Only one task can acquire the lock at any one time.
This is a coroutine.
"""
if self.state != 0:
# Lock unavailable, put the calling Task on the waiting queue
self.waiting.push_head(core.cur_task)
# Set calling task's data to the lock's queue so it can be removed if needed
core.cur_task.data = self.waiting
try:
await core._never()
except core.CancelledError as er:
if self.state == core.cur_task:
# Cancelled while pending on resume, schedule next waiting Task
self.state = 1
self.release()
raise er
# Lock available, set it as locked
self.state = 1
return True
async def __aenter__(self):
return await self.acquire()
async def __aexit__(self, exc_type, exc, tb):
return self.release()

View File

@@ -0,0 +1,24 @@
# SPDX-FileCopyrightText: 2019 Damien P. George
#
# SPDX-License-Identifier: MIT
#
#
# This code comes from MicroPython, and has not been run through black or pylint there.
# Altering these files significantly would make merging difficult, so we will not use
# pylint or black.
# pylint: skip-file
# fmt: off
# This list of frozen files doesn't include task.py because that's provided by the C module.
freeze(
"..",
(
"uasyncio/__init__.py",
"uasyncio/core.py",
"uasyncio/event.py",
"uasyncio/funcs.py",
"uasyncio/lock.py",
"uasyncio/stream.py",
),
opt=3,
)

View File

@@ -0,0 +1,263 @@
# SPDX-FileCopyrightText: 2019-2020 Damien P. George
#
# SPDX-License-Identifier: MIT
#
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
#
# This code comes from MicroPython, and has not been run through black or pylint there.
# Altering these files significantly would make merging difficult, so we will not use
# pylint or black.
# pylint: skip-file
# fmt: off
"""
Streams
=======
"""
from . import core
class Stream:
"""This represents a TCP stream connection. To minimise code this class
implements both a reader and a writer, and both ``StreamReader`` and
``StreamWriter`` alias to this class.
"""
def __init__(self, s, e={}):
self.s = s
self.e = e
self.out_buf = b""
def get_extra_info(self, v):
"""Get extra information about the stream, given by *v*. The valid
values for *v* are: ``peername``.
"""
return self.e[v]
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
await self.close()
def close(self):
pass
async def wait_closed(self):
"""Wait for the stream to close.
This is a coroutine.
"""
# TODO yield?
self.s.close()
async def read(self, n):
"""Read up to *n* bytes and return them.
This is a coroutine.
"""
await core._io_queue.queue_read(self.s)
return self.s.read(n)
async def readinto(self, buf):
"""Read up to n bytes into *buf* with n being equal to the length of *buf*
Return the number of bytes read into *buf*
This is a coroutine, and a MicroPython extension.
"""
await core._io_queue.queue_read(self.s)
return self.s.readinto(buf)
async def readexactly(self, n):
"""Read exactly *n* bytes and return them as a bytes object.
Raises an ``EOFError`` exception if the stream ends before reading
*n* bytes.
This is a coroutine.
"""
r = b""
while n:
await core._io_queue.queue_read(self.s)
r2 = self.s.read(n)
if r2 is not None:
if not len(r2):
raise EOFError
r += r2
n -= len(r2)
return r
async def readline(self):
"""Read a line and return it.
This is a coroutine.
"""
l = b""
while True:
await core._io_queue.queue_read(self.s)
l2 = self.s.readline() # may do multiple reads but won't block
l += l2
if not l2 or l[-1] == 10: # \n (check l in case l2 is str)
return l
def write(self, buf):
"""Accumulated *buf* to the output buffer. The data is only flushed when
`Stream.drain` is called. It is recommended to call `Stream.drain`
immediately after calling this function.
"""
if not self.out_buf:
# Try to write immediately to the underlying stream.
ret = self.s.write(buf)
if ret == len(buf):
return
if ret is not None:
buf = buf[ret:]
self.out_buf += buf
async def drain(self):
"""Drain (write) all buffered output data out to the stream.
This is a coroutine.
"""
mv = memoryview(self.out_buf)
off = 0
while off < len(mv):
await core._io_queue.queue_write(self.s)
ret = self.s.write(mv[off:])
if ret is not None:
off += ret
self.out_buf = b""
# Stream can be used for both reading and writing to save code size
StreamReader = Stream
StreamWriter = Stream
# Create a TCP stream connection to a remote host
async def open_connection(host, port):
"""Open a TCP connection to the given *host* and *port*. The *host* address will
be resolved using `socket.getaddrinfo`, which is currently a blocking call.
Returns a pair of streams: a reader and a writer stream. Will raise a socket-specific
``OSError`` if the host could not be resolved or if the connection could not be made.
This is a coroutine.
"""
from uerrno import EINPROGRESS
import usocket as socket
ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0] # TODO this is blocking!
s = socket.socket(ai[0], ai[1], ai[2])
s.setblocking(False)
ss = Stream(s)
try:
s.connect(ai[-1])
except OSError as er:
if er.errno != EINPROGRESS:
raise er
await core._io_queue.queue_write(s)
return ss, ss
# Class representing a TCP stream server, can be closed and used in "async with"
class Server:
"""This represents the server class returned from `start_server`. It can be used in
an ``async with`` statement to close the server upon exit.
"""
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
self.close()
await self.wait_closed()
def close(self):
"""Close the server."""
self.task.cancel()
async def wait_closed(self):
"""Wait for the server to close.
This is a coroutine.
"""
await self.task
async def _serve(self, s, cb):
# Accept incoming connections
while True:
try:
await core._io_queue.queue_read(s)
except core.CancelledError:
# Shutdown server
s.close()
return
try:
s2, addr = s.accept()
except:
# Ignore a failed accept
continue
s2.setblocking(False)
s2s = Stream(s2, {"peername": addr})
core.create_task(cb(s2s, s2s))
# Helper function to start a TCP stream server, running as a new task
# TODO could use an accept-callback on socket read activity instead of creating a task
async def start_server(cb, host, port, backlog=5):
"""Start a TCP server on the given *host* and *port*. The *cb* callback will be
called with incoming, accepted connections, and be passed 2 arguments: reader
writer streams for the connection.
Returns a `Server` object.
This is a coroutine.
"""
import usocket as socket
# Create and bind server socket.
host = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
s = socket.socket()
s.setblocking(False)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(host[-1])
s.listen(backlog)
# Create and return server object and task.
srv = Server()
srv.task = core.create_task(srv._serve(s, cb))
return srv
################################################################################
# Legacy uasyncio compatibility
async def stream_awrite(self, buf, off=0, sz=-1):
if off != 0 or sz != -1:
buf = memoryview(buf)
if sz == -1:
sz = len(buf)
buf = buf[off : off + sz]
self.write(buf)
await self.drain()
Stream.aclose = Stream.wait_closed
Stream.awrite = stream_awrite
Stream.awritestr = stream_awrite # TODO explicitly convert to bytes?

View File

@@ -0,0 +1,215 @@
# SPDX-FileCopyrightText: 2019-2020 Damien P. George
#
# SPDX-License-Identifier: MIT
#
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
#
# This code comes from MicroPython, and has not been run through black or pylint there.
# Altering these files significantly would make merging difficult, so we will not use
# pylint or black.
# pylint: skip-file
# fmt: off
"""
Tasks
=====
"""
# This file contains the core TaskQueue based on a pairing heap, and the core Task class.
# They can optionally be replaced by C implementations.
from . import core
# pairing-heap meld of 2 heaps; O(1)
def ph_meld(h1, h2):
if h1 is None:
return h2
if h2 is None:
return h1
lt = core.ticks_diff(h1.ph_key, h2.ph_key) < 0
if lt:
if h1.ph_child is None:
h1.ph_child = h2
else:
h1.ph_child_last.ph_next = h2
h1.ph_child_last = h2
h2.ph_next = None
h2.ph_rightmost_parent = h1
return h1
else:
h1.ph_next = h2.ph_child
h2.ph_child = h1
if h1.ph_next is None:
h2.ph_child_last = h1
h1.ph_rightmost_parent = h2
return h2
# pairing-heap pairing operation; amortised O(log N)
def ph_pairing(child):
heap = None
while child is not None:
n1 = child
child = child.ph_next
n1.ph_next = None
if child is not None:
n2 = child
child = child.ph_next
n2.ph_next = None
n1 = ph_meld(n1, n2)
heap = ph_meld(heap, n1)
return heap
# pairing-heap delete of a node; stable, amortised O(log N)
def ph_delete(heap, node):
if node is heap:
child = heap.ph_child
node.ph_child = None
return ph_pairing(child)
# Find parent of node
parent = node
while parent.ph_next is not None:
parent = parent.ph_next
parent = parent.ph_rightmost_parent
# Replace node with pairing of its children
if node is parent.ph_child and node.ph_child is None:
parent.ph_child = node.ph_next
node.ph_next = None
return heap
elif node is parent.ph_child:
child = node.ph_child
next = node.ph_next
node.ph_child = None
node.ph_next = None
node = ph_pairing(child)
parent.ph_child = node
else:
n = parent.ph_child
while node is not n.ph_next:
n = n.ph_next
child = node.ph_child
next = node.ph_next
node.ph_child = None
node.ph_next = None
node = ph_pairing(child)
if node is None:
node = n
else:
n.ph_next = node
node.ph_next = next
if next is None:
node.ph_rightmost_parent = parent
parent.ph_child_last = node
return heap
# TaskQueue class based on the above pairing-heap functions.
class TaskQueue:
def __init__(self):
self.heap = None
def peek(self):
return self.heap
def push(self, v, key=None):
assert v.ph_child is None
assert v.ph_next is None
v.data = None
v.ph_key = key if key is not None else core.ticks()
self.heap = ph_meld(v, self.heap)
def pop(self):
v = self.heap
assert v.ph_next is None
self.heap = ph_pairing(v.ph_child)
v.ph_child = None
return v
def remove(self, v):
self.heap = ph_delete(self.heap, v)
# Compatibility aliases, remove after they are no longer used
push_head = push
push_sorted = push
pop_head = pop
# Task class representing a coroutine, can be waited on and cancelled.
class Task:
"""This object wraps a coroutine into a running task. Tasks can be waited on
using ``await task``, which will wait for the task to complete and return the
return value of the task.
Tasks should not be created directly, rather use ``create_task`` to create them.
"""
def __init__(self, coro, globals=None):
self.coro = coro # Coroutine of this Task
self.data = None # General data for queue it is waiting on
self.state = True # None, False, True, a callable, or a TaskQueue instance
self.ph_key = 0 # Pairing heap
self.ph_child = None # Paring heap
self.ph_child_last = None # Paring heap
self.ph_next = None # Paring heap
self.ph_rightmost_parent = None # Paring heap
def __iter__(self):
if not self.state:
# Task finished, signal that is has been await'ed on.
self.state = False
elif self.state is True:
# Allocated head of linked list of Tasks waiting on completion of this task.
self.state = TaskQueue()
elif type(self.state) is not TaskQueue:
# Task has state used for another purpose, so can't also wait on it.
raise RuntimeError("can't wait")
return self
# CircuitPython needs __await()__.
__await__ = __iter__
def __next__(self):
if not self.state:
if self.data is None:
# Task finished but has already been sent to the loop's exception handler.
raise StopIteration
else:
# Task finished, raise return value to caller so it can continue.
raise self.data
else:
# Put calling task on waiting queue.
self.state.push(core.cur_task)
# Set calling task's data to this task that it waits on, to double-link it.
core.cur_task.data = self
def done(self):
"""Whether the task is complete."""
return not self.state
def cancel(self):
"""Cancel the task by injecting a ``CancelledError`` into it. The task
may or may not ignore this exception.
"""
# Check if task is already finished.
if not self.state:
return False
# Can't cancel self (not supported yet).
if self is core.cur_task:
raise RuntimeError("can't cancel self")
# If Task waits on another task then forward the cancel to the one it's waiting on.
while isinstance(self.data, Task):
self = self.data
# Reschedule Task as a cancelled task.
if hasattr(self.data, "remove"):
# Not on the main running queue, remove the task from the queue it's on.
self.data.remove(self)
core._task_queue.push(self)
elif core.ticks_diff(self.ph_key, core.ticks()) > 0:
# On the main running queue but scheduled in the future, so bring it forward to now.
core._task_queue.remove(self)
core._task_queue.push(self)
self.data = core.CancelledError
return True

View File

@@ -0,0 +1,57 @@
# SPDX-FileCopyrightText: 2019-2020 Damien P. George
#
# SPDX-License-Identifier: MIT
#
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
"""
Fallback traceback module if the system traceback is missing.
"""
try:
from typing import List
except ImportError:
pass
import sys
def _print_traceback(traceback, limit=None, file=sys.stderr) -> List[str]:
if limit is None:
if hasattr(sys, "tracebacklimit"):
limit = sys.tracebacklimit
n = 0
while traceback is not None:
frame = traceback.tb_frame
line_number = traceback.tb_lineno
frame_code = frame.f_code
filename = frame_code.co_filename
name = frame_code.co_name
print(' File "%s", line %d, in %s' % (filename, line_number, name), file=file)
traceback = traceback.tb_next
n = n + 1
if limit is not None and n >= limit:
break
def print_exception(exception, value=None, traceback=None, limit=None, file=sys.stderr):
"""
Print exception information and stack trace to file.
"""
if traceback:
print("Traceback (most recent call last):", file=file)
_print_traceback(traceback, limit=limit, file=file)
if isinstance(exception, BaseException):
exception_type = type(exception).__name__
elif hasattr(exception, "__name__"):
exception_type = exception.__name__
else:
exception_type = type(value).__name__
valuestr = str(value)
if value is None or not valuestr:
print(exception_type, file=file)
else:
print("%s: %s" % (str(exception_type), valuestr), file=file)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
# MicroPython uasyncio module
# MicroPython asyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
from . import core
@@ -13,12 +13,6 @@ class Stream:
def get_extra_info(self, v):
return self.e[v]
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
await self.close()
def close(self):
pass
@@ -26,7 +20,8 @@ class Stream:
# TODO yield?
self.s.close()
async def read(self, n=-1):
# async
def read(self, n=-1):
r = b""
while True:
yield core._io_queue.queue_read(self.s)
@@ -38,11 +33,13 @@ class Stream:
return r
r += r2
async def readinto(self, buf):
# async
def readinto(self, buf):
yield core._io_queue.queue_read(self.s)
return self.s.readinto(buf)
async def readexactly(self, n):
# async
def readexactly(self, n):
r = b""
while n:
yield core._io_queue.queue_read(self.s)
@@ -54,11 +51,14 @@ class Stream:
n -= len(r2)
return r
async def readline(self):
# async
def readline(self):
l = b""
while True:
yield core._io_queue.queue_read(self.s)
l2 = self.s.readline() # may do multiple reads but won't block
if l2 is None:
continue
l += l2
if not l2 or l[-1] == 10: # \n (check l in case l2 is str)
return l
@@ -73,10 +73,11 @@ class Stream:
buf = buf[ret:]
self.out_buf += buf
async def drain(self):
# async
def drain(self):
if not self.out_buf:
# Drain must always yield, so a tight loop of write+drain can't block the scheduler.
return await core.sleep_ms(0)
return (yield from core.sleep_ms(0))
mv = memoryview(self.out_buf)
off = 0
while off < len(mv):
@@ -93,19 +94,31 @@ StreamWriter = Stream
# Create a TCP stream connection to a remote host
async def open_connection(host, port):
from uerrno import EINPROGRESS
import usocket as socket
#
# async
def open_connection(host, port, ssl=None, server_hostname=None):
from errno import EINPROGRESS
import socket
ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0] # TODO this is blocking!
s = socket.socket(ai[0], ai[1], ai[2])
s.setblocking(False)
ss = Stream(s)
try:
s.connect(ai[-1])
except OSError as er:
if er.errno != EINPROGRESS:
raise er
# wrap with SSL, if requested
if ssl:
if ssl is True:
import ssl as _ssl
ssl = _ssl.SSLContext(_ssl.PROTOCOL_TLS_CLIENT)
if not server_hostname:
server_hostname = host
s = ssl.wrap_socket(s, server_hostname=server_hostname, do_handshake_on_connect=False)
s.setblocking(False)
ss = Stream(s)
yield core._io_queue.queue_write(s)
return ss, ss
@@ -120,25 +133,42 @@ class Server:
await self.wait_closed()
def close(self):
# Note: the _serve task must have already started by now due to the sleep
# in start_server, so `state` won't be clobbered at the start of _serve.
self.state = True
self.task.cancel()
async def wait_closed(self):
await self.task
async def _serve(self, s, cb):
async def _serve(self, s, cb, ssl):
self.state = False
# Accept incoming connections
while True:
try:
yield core._io_queue.queue_read(s)
except core.CancelledError:
# Shutdown server
except core.CancelledError as er:
# The server task was cancelled, shutdown server and close socket.
s.close()
return
if self.state:
# If the server was explicitly closed, ignore the cancellation.
return
else:
# Otherwise e.g. the parent task was cancelled, propagate
# cancellation.
raise er
try:
s2, addr = s.accept()
except:
# Ignore a failed accept
continue
if ssl:
try:
s2 = ssl.wrap_socket(s2, server_side=True, do_handshake_on_connect=False)
except OSError as e:
core.sys.print_exception(e)
s2.close()
continue
s2.setblocking(False)
s2s = Stream(s2, {"peername": addr})
core.create_task(cb(s2s, s2s))
@@ -146,8 +176,8 @@ class Server:
# Helper function to start a TCP stream server, running as a new task
# TODO could use an accept-callback on socket read activity instead of creating a task
async def start_server(cb, host, port, backlog=5):
import usocket as socket
async def start_server(cb, host, port, backlog=5, ssl=None):
import socket
# Create and bind server socket.
host = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
@@ -159,7 +189,17 @@ async def start_server(cb, host, port, backlog=5):
# Create and return server object and task.
srv = Server()
srv.task = core.create_task(srv._serve(s, cb))
srv.task = core.create_task(srv._serve(s, cb, ssl))
try:
# Ensure that the _serve task has been scheduled so that it gets to
# handle cancellation.
await core.sleep_ms(0)
except core.CancelledError as er:
# If the parent task is cancelled during this first sleep, then
# we will leak the task and it will sit waiting for the socket, so
# cancel it.
srv.task.cancel()
raise er
return srv

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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