101 Commits

Author SHA1 Message Date
Miguel Grinberg
46b120bc87 Release 1.2.2 2023-03-03 07:19:47 +00:00
Miguel Grinberg
ddb3b8f442 Return headers as lowercase byte sequences as required by ASGI 2023-03-03 07:15:09 +00:00
Miguel Grinberg
9398c96075 Add CPU timing to benchmark 2023-02-28 23:30:58 +00:00
Miguel Grinberg
4d432a7d6c More robust timeout handling (Fixes #106) 2023-02-28 18:31:54 +00:00
Miguel Grinberg
d0d358f94a Add a socket read timeout to abort incomplete requests (Fixes #99) 2023-02-22 00:42:20 +00:00
Miguel Grinberg
680cd9c023 Async example of static file serving 2023-02-21 15:18:04 +00:00
dependabot[bot]
ec72d54203 Bump werkzeug from 2.2.1 to 2.2.3 in /examples/benchmark (#102) #nolog
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 2.2.1 to 2.2.3.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/2.2.1...2.2.3)

---
updated-dependencies:
- dependency-name: werkzeug
  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-02-16 10:02:41 +00:00
Eric Welch
c00b24c943 Fixing broken links to examples in documentation (#101) 2023-02-15 16:07:17 +00:00
dependabot[bot]
878a911afc Bump starlette from 0.19.1 to 0.25.0 in /examples/benchmark (#100) 2023-02-14 23:27:49 +00:00
Miguel Grinberg
ecd84ecb7b Update unittest library for MicroPython 2023-02-07 00:03:53 +00:00
Miguel Grinberg
fcaeee6905 Add @after_error_handler decorator (Fixes #97) 2023-02-06 23:53:11 +00:00
Miguel Grinberg
427a4d49de Correct path in benchmark test #nolog 2023-01-13 14:58:16 +00:00
Miguel Grinberg
f56c826149 Update tox.ini #nolog 2023-01-13 11:39:23 +00:00
Miguel Grinberg
2aa90d4245 Add scrollbar to documentation's left sidebar 2023-01-13 10:27:51 +00:00
William Wheeler
8139498023 Documentation typo (#90) 2022-12-20 07:24:49 +00:00
Miguel Grinberg
3d6815119c Upgrade uasyncio release used in tests 2022-12-09 17:12:57 +00:00
Miguel Grinberg
818f98d9a4 New build of micropython 2022-12-09 12:15:35 +00:00
Miguel Grinberg
dd15d90239 Remove 3.6, add 3.11 2022-12-09 11:20:07 +00:00
dependabot[bot]
d42388d6fe Bump certifi from 2022.6.15 to 2022.12.7 in /examples/benchmark (#88) #nolog
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.6.15 to 2022.12.7.
- [Release notes](https://github.com/certifi/python-certifi/releases)
- [Commits](https://github.com/certifi/python-certifi/compare/2022.06.15...2022.12.07)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-09 11:07:23 +00:00
Miguel Grinberg
1abe8edc56 Version 1.2.2.dev0 2022-12-06 12:38:26 +00:00
Miguel Grinberg
e69c2dc42f Release 1.2.1 2022-12-06 12:37:51 +00:00
Miguel Grinberg
5a589afd5e Addressed error when deleting a user session in async app (Fixes #86) 2022-12-06 12:01:16 +00:00
Miguel Grinberg
c841cbedda Add asyncio file upload example 2022-11-16 19:10:44 +00:00
Diego Pomares
24d74fb848 Error handling invokes parent exceptions (Fixes #74) 2022-11-08 00:27:11 +00:00
Diego Pomares
4a9b92b800 Fix typos in documentation (#77) 2022-10-21 10:35:22 +01:00
Diego Pomares
c443599089 Add missing exception argument to error handler example in documentation (#73) 2022-10-15 12:44:52 +01:00
Miguel Grinberg
6554f29ddc Remove unused file #nolog 2022-10-08 12:26:18 +01:00
Miguel Grinberg
211ad953ae New Jinja and uTemplate examples with Bootstrap 2022-10-08 12:21:00 +01:00
Miguel Grinberg
63f43e1e7e Version 1.2.1.dev0 2022-09-25 12:19:46 +01:00
Miguel Grinberg
cb2a23285e Release 1.2.0 2022-09-25 12:19:31 +01:00
Miguel Grinberg
b133dcc343 URL encode/decode unit tests 2022-09-24 20:15:22 +01:00
Miguel Grinberg
01947b101e Cache user session 2022-09-24 19:40:28 +01:00
Miguel Grinberg
1547e861ee request.url attribute with the complete URL of the request 2022-09-24 19:33:46 +01:00
Miguel Grinberg
672512e086 urlencode() function 2022-09-24 19:33:10 +01:00
Miguel Grinberg
a8515c97b0 Small performance improvement for NoCaseDict 2022-09-24 15:37:52 +01:00
Miguel Grinberg
8ebe81c09b File upload example 2022-09-22 17:52:48 +01:00
Miguel Grinberg
4f263c63ab Minor documentation styling fixes 2022-09-21 23:38:51 +01:00
Miguel Grinberg
b0fd6c4323 Use a case insensitive dict for headers 2022-09-21 23:29:01 +01:00
Miguel Grinberg
cbefb6bf3a Do not log HTTPException occurrences 2022-09-19 23:50:04 +01:00
Miguel Grinberg
c81a2649c5 Version 1.1.2.dev0 2022-09-18 11:28:48 +01:00
Miguel Grinberg
ff178508f9 Release 1.1.1 2022-09-18 11:26:04 +01:00
Miguel Grinberg
5693b812ce Make WebSocket internals consistent between TLS and non-TLS (Fixes #61) 2022-09-18 11:17:57 +01:00
Miguel Grinberg
f540e04ffe Updated API section of the documentation #nolog 2022-09-17 23:28:45 +01:00
Miguel Grinberg
c028e4eddb Version 1.1.1.dev0 2022-09-17 23:21:55 +01:00
Miguel Grinberg
51a0aa62e1 Release 1.1.0 2022-09-17 23:17:38 +01:00
Miguel Grinberg
dc7a041ebd Recover from errors writing the response 2022-09-17 23:11:19 +01:00
Miguel Grinberg
59453a52a1 unit test fixes #nolog 2022-09-17 20:46:11 +01:00
Miguel Grinberg
75725795b4 Charset handling in Content-Type headers (Fixes #60) 2022-09-17 19:34:34 +01:00
Miguel Grinberg
019eb4d6bb Update README.md 2022-09-12 16:53:22 +01:00
Miguel Grinberg
fe750feb03 TLS fixes for WebSocket under MicroPython 2022-09-08 23:21:43 +01:00
Miguel Grinberg
b61f51f243 SSL/TLS Support 2022-09-05 10:27:59 +01:00
Miguel Grinberg
2399c29c8a Websocket standard and asyncio extensions (#55) 2022-09-03 20:04:34 +01:00
Sterling G. Baird
ec0f9ba855 Fix links to hello and gpio examples in documentation (#53) 2022-08-27 14:40:41 +01:00
Miguel Grinberg
a01fc9c3f0 Reorganized examples into subdirectories 2022-08-14 16:35:17 +01:00
Miguel Grinberg
3c125c43d2 Add abort function 2022-08-09 23:53:44 +01:00
Miguel Grinberg
e767426228 Update micropython libraries 2022-08-08 18:20:50 +01:00
Miguel Grinberg
42b6d69793 Update micropython tests to use release 1.19 2022-08-07 16:40:25 +01:00
Miguel Grinberg
2dc34a463b updated links to micropython libraries #nolog 2022-08-07 15:55:42 +01:00
Miguel Grinberg
abb7900691 Version 1.0.1.dev0 2022-08-07 15:53:29 +01:00
Miguel Grinberg
74998e7f68 Release 1.0.0 2022-08-07 15:52:13 +01:00
Miguel Grinberg
56d11964ab Updated readme #nolog 2022-08-07 15:45:50 +01:00
Miguel Grinberg
2f496db50b Concurrency section added to the documentation 2022-08-07 15:41:35 +01:00
Miguel Grinberg
998c197058 Do not use _thread for multithreading 2022-08-07 15:20:14 +01:00
Miguel Grinberg
5054813dc8 Added new modules to package #nolog 2022-08-07 14:11:34 +01:00
Miguel Grinberg
d090bbf8e2 memory comparison benchmark 2022-08-07 12:42:41 +01:00
Miguel Grinberg
09dc3ef7aa Documentation for all official extensions 2022-08-07 00:13:00 +01:00
Miguel Grinberg
3bcdf4d496 Async test client 2022-08-06 20:31:33 +01:00
Miguel Grinberg
355ffefcb2 User sessions 2022-08-06 15:30:02 +01:00
Miguel Grinberg
199d23f2c7 Test client 2022-08-06 15:30:02 +01:00
Miguel Grinberg
3a54984b67 Cookie expiration can also be given as a string 2022-08-04 11:27:19 +01:00
Miguel Grinberg
e8d16cf3f9 Support responses with more than one cookie in WSGI and ASGI extensions 2022-08-04 11:20:05 +01:00
Miguel Grinberg
c9e148bd04 Added MicroPython libs required by sessions module 2022-08-04 00:26:51 +01:00
Miguel Grinberg
037024320f Getting Started documentation chapter 2022-07-31 20:10:43 +01:00
Miguel Grinberg
a3d7772b8a Example that serves static files from a directory 2022-07-31 18:51:49 +01:00
Miguel Grinberg
16f3775fa2 Allow routes to only return a body and headers 2022-07-31 16:49:21 +01:00
Miguel Grinberg
8177b9c7f1 Improved handling of 400 and 405 errors 2022-07-31 12:21:31 +01:00
Miguel Grinberg
cd5b35d86f Mount sub-applications 2022-07-30 15:44:19 +01:00
Miguel Grinberg
f1a93ec35e Remove legacy microdot-asyncio package files 2022-07-30 15:00:46 +01:00
Miguel Grinberg
bf3aff6c35 Accept POST request with empty body 2022-07-30 14:57:36 +01:00
Miguel Grinberg
120abe45ec Request-specific after_request handlers 2022-07-30 14:52:56 +01:00
Miguel Grinberg
7686b2ae38 Extension that renders templates with Jinja 2022-07-29 20:19:51 +01:00
Miguel Grinberg
7df74b0537 Reorganized vendored micropython libraries 2022-07-28 00:24:31 +01:00
Miguel Grinberg
54c1329582 Render templates with uTemplate 2022-07-25 09:35:56 +01:00
Miguel Grinberg
7f1e546067 add missing asgi module to package 2022-07-25 00:44:39 +01:00
Miguel Grinberg
1271527c36 Update api.rst 2022-06-23 09:43:37 +01:00
Miguel Grinberg
03fe654693 Update CHANGES.md 2022-06-23 09:40:27 +01:00
Miguel Grinberg
c19343cc06 Version 0.9.1.dev0 2022-06-04 16:48:11 +01:00
Miguel Grinberg
aac022ba43 Release 0.9.0 2022-06-04 16:48:03 +01:00
Miguel Grinberg
c18ccccb8e Run linter on examples 2022-06-04 16:41:40 +01:00
Miguel Grinberg
bcbad51675 Documentation updates 2022-06-04 16:41:02 +01:00
Miguel Grinberg
d71665fd38 Stream responses (Fixes #44) 2022-06-04 15:56:13 +01:00
Miguel Grinberg
4182ba6380 Uvicorn support for ASGI implementation 2022-06-04 15:08:30 +01:00
Miguel Grinberg
5b5eb907d8 Add Python 3.10 to build 2022-05-26 10:50:55 +01:00
Miguel Grinberg
71009b4978 Return 204 when view function returns None 2022-05-26 10:50:55 +01:00
Miguel Grinberg
35c72125a0 Make body_iter async generator compatible with MicroPython 2022-05-26 10:50:55 +01:00
Miguel Grinberg
7e8ecb1997 ASGI support 2022-05-25 23:47:37 +01:00
Miguel Grinberg
1ae51ccdf7 WSGI support 2022-05-25 00:31:18 +01:00
Miguel Grinberg
0ca1e01e00 Version 0.8.3.dev0 2022-04-20 10:15:24 +01:00
Miguel Grinberg
5f7efcc3f8 Release 0.8.2 2022-04-20 10:15:17 +01:00
Mark Blakeney
0f278321c8 Remove stray/debug remnant print() (#38) 2022-04-20 10:13:38 +01:00
Miguel Grinberg
acf20cc20c Version 0.8.2.dev0 2022-03-18 23:51:38 +00:00
142 changed files with 8206 additions and 1284 deletions

5
.coveragerc Normal file
View File

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

View File

@@ -21,7 +21,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ['3.6', '3.7', '3.8', '3.9']
python: ['3.7', '3.8', '3.9', '3.10', '3.11']
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
@@ -51,3 +51,12 @@ jobs:
- run: pip install tox tox-gh-actions codecov
- run: tox
- run: codecov
benchmark:
name: benchmark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions
- run: tox -ebenchmark

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
__pycache__/
*.py[cod]
*$py.class
*_html.py
# C extensions
*.so

View File

@@ -1,5 +1,95 @@
# Microdot change log
**Release 1.2.2** - 2023-03-03
- Add a socket read timeout to abort incomplete requests [#99](https://github.com/miguelgrinberg/microdot/issues/99) ([commit](https://github.com/miguelgrinberg/microdot/commit/d0d358f94a63f8565d6406feff0c6e7418cc7f81))
- More robust timeout handling [#106](https://github.com/miguelgrinberg/microdot/issues/106) ([commit](https://github.com/miguelgrinberg/microdot/commit/4d432a7d6cd88b874a8b825fb62891ed22881f74))
- Add @after_error_handler decorator [#97](https://github.com/miguelgrinberg/microdot/issues/97) ([commit](https://github.com/miguelgrinberg/microdot/commit/fcaeee69052b5681706f65b022e667baeee30d4d))
- Return headers as lowercase byte sequences as required by ASGI ([commit](https://github.com/miguelgrinberg/microdot/commit/ddb3b8f442d3683df04554104edaf8acd9c68148))
- Async example of static file serving ([commit](https://github.com/miguelgrinberg/microdot/commit/680cd9c023352f0ff03d67f1041ea174b7b7385b))
- Fixing broken links to examples in documentation [#101](https://github.com/miguelgrinberg/microdot/issues/101) ([commit](https://github.com/miguelgrinberg/microdot/commit/c00b24c9436e1b8f3d4c9bb6f2adfca988902e91)) (thanks **Eric Welch**!)
- Add scrollbar to documentation's left sidebar ([commit](https://github.com/miguelgrinberg/microdot/commit/2aa90d42451dc64c84efcc4f40a1b6c8d1ef1e8d))
- Documentation typo [#90](https://github.com/miguelgrinberg/microdot/issues/90) ([commit](https://github.com/miguelgrinberg/microdot/commit/81394980234f24aac834faf8e2e8225231e9014b)) (thanks **William Wheeler**!)
- Add CPU timing to benchmark ([commit](https://github.com/miguelgrinberg/microdot/commit/9398c960752f87bc32d7c4349cbf594e5d678e99))
- Upgrade uasyncio release used in tests ([commit](https://github.com/miguelgrinberg/microdot/commit/3d6815119ca1ec989f704f626530f938c857a8e5))
- Update unittest library for MicroPython ([commit](https://github.com/miguelgrinberg/microdot/commit/ecd84ecb7bd3c29d5af96739442b908badeab804))
- New build of micropython for unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/818f98d9a4e531e01c0f913813425ab2b40c289d))
- Remove 3.6, add 3.11 to builds ([commit](https://github.com/miguelgrinberg/microdot/commit/dd15d90239b73b5fd413515c9cd4ac23f6d42f67))
**Release 1.2.1** - 2022-12-06
- Error handling invokes parent exceptions [#74](https://github.com/miguelgrinberg/microdot/issues/74) ([commit](https://github.com/miguelgrinberg/microdot/commit/24d74fb8483b04e8abe6e303e06f0a310f32700b)) (thanks **Diego Pomares**!)
- Addressed error when deleting a user session in async app [#86](https://github.com/miguelgrinberg/microdot/issues/86) ([commit](https://github.com/miguelgrinberg/microdot/commit/5a589afd5e519e94e84fc1ee69033f2dad51c3ea))
- Add asyncio file upload example ([commit](https://github.com/miguelgrinberg/microdot/commit/c841cbedda40f59a9d87f6895fdf9fd954f854a2))
- New Jinja and uTemplate examples with Bootstrap ([commit](https://github.com/miguelgrinberg/microdot/commit/211ad953aeedb4c7f73fe210424aa173b4dc7fee))
- Fix typos in documentation [#77](https://github.com/miguelgrinberg/microdot/issues/77) ([commit](https://github.com/miguelgrinberg/microdot/commit/4a9b92b800d3fd87110f7bc9f546c10185ee13bc)) (thanks **Diego Pomares**!)
- Add missing exception argument to error handler example in documentation [#73](https://github.com/miguelgrinberg/microdot/issues/73) ([commit](https://github.com/miguelgrinberg/microdot/commit/c443599089f2127d1cb052dfba8a05c1969d65e3)) (thanks **Diego Pomares**!)
**Release 1.2.0** - 2022-09-25
- Use a case insensitive dict for headers ([commit #1](https://github.com/miguelgrinberg/microdot/commit/b0fd6c432371ca5cb10d07ff84c4deed7aa0ce2e) [commit #2](https://github.com/miguelgrinberg/microdot/commit/a8515c97b030f942fa6ca85cbe1772291468fb0d))
- urlencode() helper function ([commit #1](https://github.com/miguelgrinberg/microdot/commit/672512e086384e808489305502e6ebebcc5a888f) [commit #2](https://github.com/miguelgrinberg/microdot/commit/b133dcc34368853ee685396a1bcb50360e807813))
- Added `request.url` attribute with the complete URL of the request ([commit](https://github.com/miguelgrinberg/microdot/commit/1547e861ee28d43d10fe4c4ed1871345d4b81086))
- Do not log HTTPException occurrences ([commit](https://github.com/miguelgrinberg/microdot/commit/cbefb6bf3a3fdcff8b7a8bacad3449be18e46e3b))
- Cache user session for performance ([commit](https://github.com/miguelgrinberg/microdot/commit/01947b101ebe198312c88d73872e3248024918f0))
- File upload example ([commit](https://github.com/miguelgrinberg/microdot/commit/8ebe81c09b604ddc1123e78ad6bc87ceda5f8597))
- Minor documentation styling fixes ([commit](https://github.com/miguelgrinberg/microdot/commit/4f263c63ab7bb1ce0dd48d8e00f3c6891e1bf07e))
**Release 1.1.1** - 2022-09-18
- Make WebSocket internals consistent between TLS and non-TLS [#61](https://github.com/miguelgrinberg/microdot/issues/61) ([commit](https://github.com/miguelgrinberg/microdot/commit/5693b812ceb2c0d51ec3c991adf6894a87e6fcc7))
**Release 1.1.0** - 2022-09-17
- Websocket support [#55](https://github.com/miguelgrinberg/microdot/issues/55) ([commit](https://github.com/miguelgrinberg/microdot/commit/2399c29c8a45289f009f47fd66438452da93cdab))
- SSL/TLS support ([commit #1](https://github.com/miguelgrinberg/microdot/commit/b61f51f2434465b7a0ee197aabf46e8f99f6e8ad) [commit #2](https://github.com/miguelgrinberg/microdot/commit/fe750feb0373b41cb022521a6a3edf1973847a74))
- Add `abort()` function ([commit](https://github.com/miguelgrinberg/microdot/commit/3c125c43d2e037ce64138e22c1ff4186ea107471))
- Charset handling in Content-Type headers [#60](https://github.com/miguelgrinberg/microdot/issues/60) ([commit](https://github.com/miguelgrinberg/microdot/commit/75725795b45d275deaee133204e400e8fbb3de70))
- Recover from errors writing the response ([commit](https://github.com/miguelgrinberg/microdot/commit/dc7a041ebd30f38b9f6b22c4bbcd61993c43944e))
- Reorganized examples into subdirectories ([commit](https://github.com/miguelgrinberg/microdot/commit/a01fc9c3f070e21e705b8f12ceb8288b0f304569))
- Update tests to use MicroPython 1.19 ([commit](https://github.com/miguelgrinberg/microdot/commit/42b6d6979381d9cd8ccc6ab6e079f12ec5987b80))
- Update MicroPython libraries used by tests ([commit](https://github.com/miguelgrinberg/microdot/commit/e767426228eeacd58886bccb5046049e994c0479))
- Fix links to hello and gpio examples in documentation [#53](https://github.com/miguelgrinberg/microdot/issues/53) ([commit](https://github.com/miguelgrinberg/microdot/commit/ec0f9ba855cca7dd35cddad40c4cb7eb17d8842a)) (thanks **Sterling G. Baird**!)
**Release 1.0.0** - 2022-08-07
- User sessions with signed JWTs ([commit](https://github.com/miguelgrinberg/microdot/commit/355ffefcb2697b30d03359d35283835901f375d6))
- Mount sub-applications ([commit](https://github.com/miguelgrinberg/microdot/commit/cd5b35d86f2bdd2924234d19943b06dbad6db7c0))
- Request-specific `after_request` handlers ([commit](https://github.com/miguelgrinberg/microdot/commit/120abe45ecee3ef215c2201337fcb399d5602d59))
- Render templates with uTemplate ([commit](https://github.com/miguelgrinberg/microdot/commit/54c13295827548a9258a9af914d199f06d8ae5cd))
- Render templates with Jinja ([commit](https://github.com/miguelgrinberg/microdot/commit/7686b2ae38fb980de0de33c1585f430af11e1cdf))
- Test client ([commit](https://github.com/miguelgrinberg/microdot/commit/199d23f2c72356072a32fa7bdc85b094c8a63766))
- Async test client ([commit](https://github.com/miguelgrinberg/microdot/commit/3bcdf4d496630672ed702677b1e22e5364b2b95a))
- Example that serves static files from a directory ([commit](https://github.com/miguelgrinberg/microdot/commit/a3d7772b8a8e49526f895d10af52a4c0568922b2))
- Allow routes to only return a body and headers ([commit](https://github.com/miguelgrinberg/microdot/commit/16f3775fa26ea08600898f6a244d5baabea32813))
- Improved handling of 400 and 405 errors ([commit](https://github.com/miguelgrinberg/microdot/commit/8177b9c7f1c1dfedcd10dcd1562caf6e442d941f))
- Support responses with more than one cookie in WSGI and ASGI extensions ([commit](https://github.com/miguelgrinberg/microdot/commit/e8d16cf3f90270c5cd3fb13168c5cc983708989c))
- Cookie expiration can also be given as a string ([commit](https://github.com/miguelgrinberg/microdot/commit/3a54984b674148b6e590eb989de18c1ff0aa9217))
- Accept POST request with empty body ([commit](https://github.com/miguelgrinberg/microdot/commit/bf3aff6c35982c7dc4a42ae5415933b252cebc0d))
- Add missing asgi module to package ([commit](https://github.com/miguelgrinberg/microdot/commit/7f1e546067d2222fa1499af69a6a697e5b7188be))
- Memory usage comparison and benchmark ([commit](https://github.com/miguelgrinberg/microdot/commit/d090bbf8e2b7ce07c802b06de7ebb29de68d788d))
- Do not use `_thread` for multithreading ([commit](https://github.com/miguelgrinberg/microdot/commit/998c1970586bf5298b6f749460ab88496e429612))
- Getting Started documentation chapter ([commit](https://github.com/miguelgrinberg/microdot/commit/037024320f08e294601d7b4e206b309dc77b1d90))
- Concurrency section added to the documentation ([commit](https://github.com/miguelgrinberg/microdot/commit/2f496db50b3d3629c68178b5915454cf1d87bc89))
- Documentation for all official extensions ([commit](https://github.com/miguelgrinberg/microdot/commit/09dc3ef7aa8e37c64f6ee919e4603c53b05bc156))
- Remove legacy `microdot-asyncio` package files ([commit](https://github.com/miguelgrinberg/microdot/commit/f1a93ec35e2e758015360b753cb9b07dbf4e96d1))
- Added MicroPython libraries required by user sessions ([commit](https://github.com/miguelgrinberg/microdot/commit/c9e148bd04aa70df2d8cc8db766eb52fa87cda31))
- Reorganized vendored MicroPython libraries ([commit](https://github.com/miguelgrinberg/microdot/commit/7df74b05374cfc398fcdeb280e93ec3f46047c2a))
**Release 0.9.0** - 2022-06-04
- Streaming responses [#44](https://github.com/miguelgrinberg/microdot/issues/44) ([commit](https://github.com/miguelgrinberg/microdot/commit/d71665fd388c92a50198faf0d761235f0138797a))
- Return 204 when view function returns None ([commit](https://github.com/miguelgrinberg/microdot/commit/71009b49781ce356155df661a66dc98170f35d63))
- ASGI support (CPython only) ([commit](https://github.com/miguelgrinberg/microdot/commit/7e8ecb199717dd90c6cb374cb0d24b54dd6ea33e))
- WSGI support (CPython only) ([commit](https://github.com/miguelgrinberg/microdot/commit/1ae51ccdf75991a2958b06f7a3439d64f92f1b69))
- Documentation updates ([commit](https://github.com/miguelgrinberg/microdot/commit/bcbad516751f1ea9928f4a6d0e8843a4334b885a))
- Add Python 3.10 to build ([commit](https://github.com/miguelgrinberg/microdot/commit/5b5eb907d83d94dde544b266e6659071e4d47ee1))
- Run linter on examples ([commit](https://github.com/miguelgrinberg/microdot/commit/c18ccccb8e0744d8670433aeeba068c5654f32df))
**Release 0.8.2** - 2022-04-20
- Remove debugging print statement [#38](https://github.com/miguelgrinberg/microdot/issues/38) ([commit](https://github.com/miguelgrinberg/microdot/commit/0f278321c8bd65c5cb67425eb837e6581cbb0054)) (thanks **Mark Blakeney**!)
**Release 0.8.1** - 2022-03-18
- Optimizations for request streams and bodies ([commit](https://github.com/miguelgrinberg/microdot/commit/29a9f6f46c737aa0fd452766c23bd83008594ac4))

View File

@@ -1,7 +1,23 @@
# microdot
[![Build status](https://github.com/miguelgrinberg/microdot/workflows/build/badge.svg)](https://github.com/miguelgrinberg/microdot/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/microdot/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/microdot)
A minimalistic Python web framework for microcontrollers inspired by Flask
*“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.
```python
from microdot import Microdot
app = Microdot()
@app.route('/')
def index(request):
return 'Hello, world!'
app.run()
```
## Resources

Binary file not shown.

View File

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

View File

@@ -4,30 +4,17 @@ API Reference
``microdot`` module
-------------------
The ``microdot`` module defines a few classes that help implement HTTP-based
servers for MicroPython and standard Python, with multithreading support for
Python interpreters that support it.
``Microdot`` class
~~~~~~~~~~~~~~~~~~
.. autoclass:: microdot.Microdot
:members:
``Request`` class
~~~~~~~~~~~~~~~~~
.. autoclass:: microdot.Request
:members:
``Response`` class
~~~~~~~~~~~~~~~~~~
.. autoclass:: microdot.Response
:members:
``MultiDict`` class
~~~~~~~~~~~~~~~~~~~
.. autoclass:: microdot.NoCaseDict
:members:
.. autoclass:: microdot.MultiDict
:members:
@@ -35,28 +22,88 @@ Python interpreters that support it.
``microdot_asyncio`` module
---------------------------
The ``microdot_asyncio`` module defines a few classes that help implement
HTTP-based servers for MicroPython and standard Python that use ``asyncio``
and coroutines.
``Microdot`` class
~~~~~~~~~~~~~~~~~~
.. autoclass:: microdot_asyncio.Microdot
:inherited-members:
:members:
``Request`` class
~~~~~~~~~~~~~~~~~
.. autoclass:: microdot_asyncio.Request
:inherited-members:
:members:
``Response`` class
~~~~~~~~~~~~~~~~~~
.. 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
------------------------
.. autoclass:: microdot_wsgi.Microdot
:members:
:exclude-members: shutdown, run
``microdot_asgi`` module
------------------------
.. autoclass:: microdot_asgi.Microdot
:members:
:exclude-members: shutdown, run

View File

@@ -13,7 +13,7 @@
import os
import sys
sys.path.insert(0, os.path.abspath('../src'))
sys.path.insert(1, os.path.abspath('../libs/common'))
# -- Project information -----------------------------------------------------
@@ -28,6 +28,7 @@ author = 'Miguel Grinberg'
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autosectionlabel',
'sphinx.ext.autodoc',
]

495
docs/extensions.rst Normal file
View File

@@ -0,0 +1,495 @@
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.
Asynchronous Support with Asyncio
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. 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>`_
* - Required external dependencies
- | CPython: None
| MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_
* - Examples
- | `hello_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_async.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 example that follows uses ``asyncio`` coroutines for concurrency::
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
async def hello(request):
return 'Hello, world!'
app.run()
Rendering HTML Templates
~~~~~~~~~~~~~~~~~~~~~~~~
Many web applications use HTML templates for rendering content to clients.
Microdot includes extensions to render templates with the
`utemplate <https://github.com/pfalcon/utemplate>`_ package on CPython and
MicroPython, and with `Jinja <https://jinja.palletsprojects.com/>`_ only on
CPython.
Using the uTemplate Engine
^^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:align: left
* - Compatibility
- | 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>`_
* - 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.
Example::
from microdot_utemplate import render_template
@app.get('/')
def index(req):
return render_template('index.html')
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::
from microdot_utemplate import init_templates
init_templates('my_templates')
Using the Jinja Engine
^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:align: left
* - Compatibility
- | 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>`_
* - Required external dependencies
- | `Jinja2 <https://jinja.palletsprojects.com/>`_
* - 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.
Example::
from microdot_jinja import render_template
@app.get('/')
def index(req):
return render_template('index.html')
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::
from microdot_jinja import init_templates
init_templates('my_templates')
.. note::
The Jinja extension is not compatible with MicroPython.
Maintaing Secure User Sessions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | 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>`_
* - 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>`_
* - 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
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
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.
To set the secret key, use the :func:`set_session_secret_key <microdot_session.set_session_secret_key>` function::
from microdot_session import set_session_secret_key
set_session_secret_key('top-secret!')
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
app = Microdot()
set_session_secret_key('top-secret')
@app.route('/', methods=['GET', 'POST'])
@with_session
def index(req, session):
username = session.get('username')
if req.method == 'POST':
username = req.form.get('username')
update_session(req, {'username': username})
return redirect('/')
if username is None:
return 'Not logged in'
else:
return 'Logged in as ' + username
@app.post('/logout')
def logout(req):
delete_session(req)
return redirect('/')
WebSocket Support
~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_websocket.py>`_
* - Required external dependencies
- | None
* - Examples
- | `echo.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo.py>`_
| `echo_wsgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_wsgi.py>`_
The WebSocket extension provides a way for the application to handle WebSocket
requests. The :func:`websocket <microdot_websocket.with_websocket>` decorator
is used to mark a route handler as a WebSocket handler. The handler receives
a WebSocket object as a second argument. The WebSocket object provides
``send()`` and ``receive()`` methods to send and receive messages respectively.
Example::
@app.route('/echo')
@with_websocket
def echo(request, ws):
while True:
message = ws.receive()
ws.send(message)
.. note::
An unsupported *microdot_websocket_alt.py* module, with the same
interface, is also provided. This module uses the native WebSocket support
in MicroPython that powers the WebREPL, and may provide slightly better
performance for MicroPython low-end boards. This module is not compatible
with CPython.
Asynchronous WebSocket
~~~~~~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
| `microdot_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_websocket.py>`_
| `microdot_asyncio_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio_websocket.py>`_
* - Required external dependencies
- | CPython: None
| MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_
* - Examples
- | `echo_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_async.py>`_
This extension has the same interface as the synchronous WebSocket extension,
but the ``receive()`` and ``send()`` methods are asynchronous.
.. note::
An unsupported *microdot_asgi_websocket.py* module, with the same
interface, is also provided. This module must be used instead of
*microdot_asyncio_websocket.py* when the ASGI support is used. The
`echo_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_asgi.py>`_
example shows how to use this module.
HTTPS Support
~~~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_ssl.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_ssl.py>`_
* - Examples
- | `hello_tls.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/tls/hello_tls.py>`_
| `hello_async_tls.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/tls/hello_async_tls.py>`_
The ``run()`` function accepts an optional ``ssl`` argument, through which an
initialized ``SSLContext`` object can be passed. MicroPython does not currently
have a ``SSLContext`` implementation, so the ``microdot_ssl`` module provides
a basic implementation that can be used to create a context.
Example::
from microdot import Microdot
from microdot_ssl import create_ssl_context
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.
Test Client
~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_test_client.py>`_
* - Required external dependencies
- | None
The Microdot Test Client is a utility class that can be used during testing to
send requests into the application.
Example::
from microdot import Microdot
from microdot_test_client import TestClient
app = Microdot()
@app.route('/')
def index(req):
return 'Hello, World!'
def test_app():
client = TestClient(app)
response = client.get('/')
assert response.text == 'Hello, World!'
See the documentation for the :class:`TestClient <microdot_test_client.TestClient>`
class for more details.
Asynchronous Test Client
~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
| `microdot_test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_test_client.py>`_
| `microdot_asyncio_test_client.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio_test_client.py>`_
* - Required external dependencies
- | None
Similar to the :class:`TestClient <microdot_test_client.TestClient>` class
above, but for asynchronous applications.
Example usage::
from microdot_asyncio_test_client import TestClient
async def test_app():
client = TestClient(app)
response = await client.get('/')
assert response.text == 'Hello, World!'
See the :class:`reference documentation <microdot_asyncio_test_client.TestClient>`
for details.
Deploying on a Production Web Server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``Microdot`` class creates its own simple web server. This is enough for an
application deployed with MicroPython, but when using CPython it may be useful
to use a separate, battle-tested web server. To address this need, Microdot
provides extensions that implement the WSGI and ASGI protocols.
Using a WSGI Web Server
^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:align: left
* - Compatibility
- | CPython only
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_wsgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_wsgi.py>`_
* - Required external dependencies
- | A WSGI web server, such as `Gunicorn <https://gunicorn.org/>`_.
* - Examples
- | `hello_wsgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_wsgi.py>`_
The ``microdot_wsgi`` module provides an extended ``Microdot`` class that
implements the WSGI protocol and can be used with a compliant WSGI web server
such as `Gunicorn <https://gunicorn.org/>`_ or
`uWSGI <https://uwsgi-docs.readthedocs.io/en/latest/>`_.
To use a WSGI web server, the application must import the
:class:`Microdot <microdot_wsgi.Microdot>` class from the ``microdot_wsgi``
module::
from microdot_wsgi import Microdot
app = Microdot()
@app.route('/')
def index(req):
return 'Hello, World!'
The ``app`` application instance created from this class is a WSGI application
that can be used with any complaint WSGI web server. If the above application
is stored in a file called *test.py*, then the following command runs the
web application using the Gunicorn web server::
gunicorn test:app
Using an ASGI 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_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
| `microdot_asgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asgi.py>`_
* - Required external dependencies
- | An ASGI web server, such as `Uvicorn <https://uvicorn.org/>`_.
* - Examples
- | `hello_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_asgi.py>`_
The ``microdot_asgi`` module provides an extended ``Microdot`` class that
implements the ASGI protocol and can be used with a compliant ASGI server such
as `Uvicorn <https://www.uvicorn.org/>`_.
To use an ASGI web server, the application must import the
:class:`Microdot <microdot_asgi.Microdot>` class from the ``microdot_asgi``
module::
from microdot_asgi import Microdot
app = Microdot()
@app.route('/')
async def index(req):
return 'Hello, World!'
The ``app`` application instance created from this class is an ASGI application
that can be used with any complaint ASGI web server. If the above application
is stored in a file called *test.py*, then the following command runs the
web application using the Uvicorn web server::
uvicorn test:app

View File

@@ -6,6 +6,8 @@
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
@@ -15,6 +17,7 @@ Python and on `MicroPython <https://micropython.org>`_.
:maxdepth: 3
intro
extensions
api
* :ref:`genindex`

View File

@@ -1,39 +1,764 @@
Installation
------------
Microdot can be installed with ``pip``::
For standard Python (CPython) projects, Microdot and all of its core extensions
can be installed with ``pip``::
pip install microdot
For platforms that do not support or cannot run ``pip``, you can also manually
copy and install the ``microdot.py`` and ``microdot_asyncio.py`` source files.
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
`GitHub repository <https://github.com/miguelgrinberg/microdot/tree/main/src>`_
into your device, possibly 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.
Examples
--------
Getting Started
---------------
The following is an example of a standard single or multi-threaded web
server::
This section describes the main features of Microdot in an informal manner. For
detailed reference information, consult the :ref:`API Reference`.
A Simple Microdot Web Server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following is an example of a simple web server::
from microdot import Microdot
app = Microdot()
@app.route('/')
def hello(request):
def index(request):
return 'Hello, world!'
app.run()
Microdot also supports the asynchronous model and can be used under
``asyncio``. The example that follows is equivalent to the one above, but uses
coroutines for concurrency::
The script imports the :class:`Microdot <microdot.Microdot>` class and creates
an application instance from it.
from microdot_asyncio import Microdot
The application instance provides a :func:`route() <microdot.Microdot.route>`
decorator, which is used to define one or more routes, as needed by the
application.
app = Microdot()
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.
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.
Running with CPython
~~~~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
* - Required external dependencies
- | None
* - Examples
- | `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::
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``.
Running with MicroPython
~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
* - Required external dependencies
- | None
* - Examples
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello.py>`_
| `gpio.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/gpio/gpio.py>`_
When using MicroPython, you can upload a *main.py* file containing the web
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.
.. note::
Microdot does not configure the network interface of the device in which it
is running. If your device requires a network connection to be made in
advance, for example to a Wi-Fi access point, this must be configured before
the ``run()`` method is invoked.
Defining Routes
~~~~~~~~~~~~~~~
The :func:`route() <microdot.Microdot.route>` decorator is used to associate an
application URL with the function that handles it. The only required argument
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('/')
async def hello(request):
def index(request):
return 'Hello, world!'
When a client requests the root URL (for example, *http://localhost:5000/*),
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
in its path::
@app.route('/users/active')
def active_users(request):
return 'Active users: Susan, Joe, and Bob'
The complete URL that maps to this route is
*http://localhost:5000/users/active*.
An application can include multiple routes. Microdot uses the path portion of
the URL to determine the correct route function to call for each incoming
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.
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):
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::
@app.route('/invoices', methods=['GET'])
def get_invoices(request):
return 'get invoices'
@app.route('/invoices', methods=['POST'])
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::
@app.get('/invoices')
def get_invoices(request):
return 'get invoices'
@app.post('/invoices')
def create_invoice(request):
return 'create an invoice'
Including Dynamic Components in the URL Path
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The examples shown above all use hardcoded URL paths. Microdot also supports
the definition of routes that have dynamic components in the path. For example,
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):
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.
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):
return 'User: ' + firstname + ' ' + lastname
Dynamic path components are considered to be strings by default. An explicit
type can be specified as a prefix, separated from the dynamic component name by
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):
return 'User: ' + username + ' (' + str(id) + ')'
If a dynamic path component is defined as an integer, the value passed to the
route function is also an integer. If the client sends a value that is not an
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::
@app.get('/tests/<path:path>')
def get_test(request, path):
return 'Test: ' + path
For the most control, the ``re`` type allows the application to provide a
custom regular expression for the dynamic component. The next example defines
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):
return 'User: ' + username
.. note::
Dynamic path components are passed to route functions as keyword arguments,
so the names of the function arguments must match the names declared in the
path specification.
Before and After Request Handlers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
It is common for applications to need to perform one or more actions before a
request is handled. Examples include authenticating and/or authorizing the
client, opening a connection to a database, or checking if the requested
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
client is authenticated before the request is handled::
@app.before_request
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
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
power to intercept a request if necessary. The example above uses this
technique to prevent an unauthorized user from accessing the requested
resource.
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::
@app.before_request
def start_timer(request):
request.g.start_time = time.time()
@app.after_request
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.
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.
.. 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.
Error Handlers
^^^^^^^^^^^^^^
When an error occurs during the handling of a request, Microdot ensures that
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.
- 413 for requests that are larger than the allowed size.
- 500 when the application raises an exception.
While the above errors are fully complaint with the HTTP specification, the
application might want to provide custom responses for them. The
:func:`errorhandler() <microdot.Microdot.errorhandler>` decorator registers
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):
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::
@app.errorhandler(ZeroDivisionError)
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
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
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.
Consider, for example, a *customers.py* sub-application that implements
operations on customers::
from microdot import Microdot
customers_app = Microdot()
@customers_app.get('/')
def get_customers(request):
# return all customers
@customers_app.post('/')
def new_customer(request):
# create a new customer
In the same way, the *orders.py* sub-application implements operations on
customer orders::
from microdot import Microdot
orders_app = Microdot()
@orders_app.get('/')
def get_orders(request):
# return all orders
@orders_app.post('/')
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::
from microdot import Microdot
from customers import customers_app
from orders import orders_app
def create_app():
app = Microdot()
app.mount(customers_app, url_prefix='/customers')
app.mount(orders_app, url_prefix='/orders')
return app
app = create_app()
app.run()
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
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
created.
Shutting Down the Server
^^^^^^^^^^^^^^^^^^^^^^^^
Web servers are designed to run forever, and are often stopped by sending them
an interrupt signal. But having a way to gracefully stop the server is
sometimes useful, especially in testing environments. Microdot provides a
:func:`shutdown() <microdot.Microdot.shutdown>` method that can be invoked
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):
request.app.shutdown()
return 'The server is shutting down...'
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.
Request Attributes
^^^^^^^^^^^^^^^^^^
The request object provides access to the request attributes, including:
- :attr:`method <microdot.Request.method>`: The HTTP method of the request.
- :attr:`path <microdot.Request.path>`: The path of the request.
- :attr:`args <microdot.Request.args>`: The query string parameters of the
request, as a :class:`MultiDict <microdot.MultiDict>` object.
- :attr:`headers <microdot.Request.headers>`: The headers of the request, as a
dictionary.
- :attr:`cookies <microdot.Request.cookies>`: The cookies that the client sent
with the request, as a dictionary.
- :attr:`content_type <microdot.Request.content_type>`: The content type
specified by the client, or ``None`` if no content type was specified.
- :attr:`content_length <microdot.Request.content_length>`: The content
length of the request, or 0 if no content length was specified.
- :attr:`client_addr <microdot.Request.client_addr>`: The network address of
the client, as a tuple (host, port).
- :attr:`app <microdot.Request.app>`: The application instance that created the
request.
JSON Payloads
^^^^^^^^^^^^^
When the client sends a request that contains JSON data in the body, the
application can access the parsed JSON data using the
:attr:`json <microdot.Request.json>` attribute. The following example shows how
to use this attribute::
@app.post('/customers')
def create_customer(request):
customer = request.json
# do something with customer
return {'success': True}
.. note::
The client must set the ``Content-Type`` header to ``application/json`` for
the ``json`` attribute of the request object to be populated.
URLEncoded Form Data
^^^^^^^^^^^^^^^^^^^^
The request object also supports standard HTML form submissions through the
:attr:`form <microdot.Request.form>` attribute, which presents the form data
as a :class:`MultiDict <microdot.MultiDict>` object. Example::
@app.route('/', methods=['GET', 'POST'])
def index(req):
name = 'Unknown'
if req.method == 'POST':
name = req.form.get('name')
return f'Hello {name}'
.. note::
Form submissions are only parsed when the ``Content-Type`` header is set by
the client to ``application/x-www-form-urlencoded``. Form submissions using
the ``multipart/form-data`` content type are currently not supported.
Accessing the Raw Request Body
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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.
Cookies
^^^^^^^
Cookies that are sent by the client are made available throught the
:attr:`cookies <microdot.Request.cookies>` attribute of the request object in
dictionary form.
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.
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):
username = authenticate_user(request)
if not username:
return 'Unauthorized', 401
request.g.username = username
@app.get('/')
def index(request):
return f'Hello, {request.g.username}!'
Request-Specific After Request Handlers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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
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
called.
The next example shows how a cookie can be updated using a request-specific
after request handler defined inside a route function::
@app.post('/logout')
def logout(request):
@request.after_request
def reset_session(request, response):
response.set_cookie('session', '', http_only=True)
return response
return 'Logged out'
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
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
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
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
being loaded into memory::
Request.max_content_length = 1024 * 1024
Request.max_body_length = 8 * 1024
Responses
~~~~~~~~~
The value or values that are returned from the route function are used by
Microdot to build the response that is sent to the client. The following
sections describe the different types of responses that are supported.
The Three Parts of a Response
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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):
return 'Hello, World!'
In the above example, Microdot issues a standard 200 status code response, and
inserts the necessary 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
code::
@app.get('/')
def index(request):
return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'}
The application can also return a :class:`Response <microdot.Response>` object
containing all the details of the response as a single value.
JSON Responses
^^^^^^^^^^^^^^
If the application needs to return a response with JSON formatted data, it can
return a dictionary or a list as the first value, and Microdot will
automatically format the response as JSON.
Example::
@app.get('/')
def index(request):
return {'hello': 'world'}
.. note::
A ``Content-Type`` header set to ``application/json`` is automatically added
to the response.
Redirects
^^^^^^^^^
The :func:`redirect <microdot.Response.redirect>` function is a helper that
creates redirect responses::
from microdot import redirect
@app.get('/')
def index(request):
return redirect('/about')
File Responses
^^^^^^^^^^^^^^
The :func:`send_file <microdot.Response.send_file>` function builds a response
object for a file::
from microdot import send_file
@app.get('/')
def index(request):
return send_file('/static/index.html')
.. 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
added to the application to serve static files from a *static* directory in
the project::
@app.route('/static/<path:path>')
def static(request, path):
if '..' in path:
# directory traversal is not allowed
return 'Not found', 404
return send_file('static/' + path)
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::
@app.get('/fibonacci')
def fibonacci(request):
def generate_fibonacci():
a, b = 0, 1
while a < 100:
yield str(a) + '\n'
a, b = b, a + b
return generate_fibonacci()
Changing the Default Response Content Type
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Microdot uses a ``text/plain`` content type by default for responses that do
not explicitly include the ``Content-Type`` header. The application can change
this default by setting the desired content type in the
:attr:`default_content_type <microdot.Response.default_content_type>` attribute
of the :class:`Response <microdot.Response>` class.
The example that follows configures the application to use ``text/html`` as
default content type::
from microdot import Response
Response.default_content_type = 'text/html'
Setting Cookies
^^^^^^^^^^^^^^^
Many web applications rely on cookies to maintain client state between
requests. Cookies can be set with the ``Set-Cookie`` header in the response,
but since this is such a common practice, Microdot provides the
:func:`set_cookie() <microdot.Response.set_cookie>` method in the response
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>`.
Example::
@app.get('/')
def index(request):
@request.after_request
def set_cookie(request, response):
response.set_cookie('name', 'value')
return response
return 'Hello, World!'
Another option is to create a response object directly in the route function::
@app.get('/')
def index(request):
response = Response('Hello, World!')
response.set_cookie('name', 'value')
return response
.. note::
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
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.
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.
The :ref:`micropython_asyncio <Asynchronous Support with Asyncio>` extension
provides a more robust concurrency option that is supported even on low-end
MicroPython boards.

View File

@@ -0,0 +1,5 @@
This directory contains a few example applications for different
configurations of Microdot, plus similar implementations for other web
frameworks.
The *run.py* script runs these applications and reports memory usage for each.

11
examples/benchmark/mem.py Normal file
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
def index():
return {'hello': 'world'}

View File

@@ -0,0 +1,8 @@
from flask import Flask
app = Flask(__name__)
@app.get('/')
def index():
return {'hello': 'world'}

View File

@@ -0,0 +1,8 @@
from quart import Quart
app = Quart(__name__)
@app.get('/')
def index():
return {'hello': 'world'}

View File

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

View File

@@ -0,0 +1,33 @@
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
h2==4.1.0
hpack==4.0.0
humanize==4.3.0
hypercorn==0.13.2
hyperframe==6.0.1
idna==3.3
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.1
microdot
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

99
examples/benchmark/run.py Normal file
View File

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

2
examples/hello/README.md Normal file
View File

@@ -0,0 +1,2 @@
This directory contains several "Hello, World!" type examples for different
platforms and configurations supported by Microdot.

View File

@@ -1,4 +1,4 @@
from microdot import Microdot, Response
from microdot import Microdot
app = Microdot()
@@ -20,7 +20,7 @@ htmldoc = '''<!DOCTYPE html>
@app.route('/')
def hello(request):
return Response(body=htmldoc, headers={'Content-Type': 'text/html'})
return htmldoc, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')

View File

@@ -0,0 +1,36 @@
from microdot_asgi 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...'
if __name__ == '__main__':
print('''Use an ASGI web server to run this applicaton.
Example:
uvicorn hello_asgi:app
''')

View File

@@ -1,8 +1,4 @@
try:
import uasyncio as asyncio
except ImportError:
import asyncio
from microdot_asyncio import Microdot, Response
from microdot_asyncio import Microdot
app = Microdot()
@@ -24,7 +20,7 @@ htmldoc = '''<!DOCTYPE html>
@app.route('/')
async def hello(request):
return Response(body=htmldoc, headers={'Content-Type': 'text/html'})
return htmldoc, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')

View File

@@ -0,0 +1,17 @@
from microdot_asyncio import Microdot, Response
from microdot_utemplate import render_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 render_template('index.html', name=name)
if __name__ == '__main__':
app.run()

View File

@@ -0,0 +1,36 @@
from microdot_wsgi 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('/')
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...'
if __name__ == '__main__':
print('''Use a WSGI web server to run this applicaton.
Example:
gunicorn hello_wsgi:app
''')

View File

@@ -0,0 +1 @@
This directory contains examples that take advantage of user sessions.

View File

@@ -0,0 +1,58 @@
from microdot import Microdot, Response, redirect
from microdot_session import set_session_secret_key, with_session, \
update_session, delete_session
BASE_TEMPLATE = '''<!doctype html>
<html>
<head>
<title>Microdot login example</title>
</head>
<body>
<h1>Microdot login example</h1>
{content}
</body>
</html>'''
LOGGED_OUT = '''<p>You are not logged in.</p>
<form method="POST">
<p>
Username:
<input type="text" name="username" autofocus />
</p>
<input type="submit" value="Submit" />
</form>'''
LOGGED_IN = '''<p>Hello <b>{username}</b>!</p>
<form method="POST" action="/logout">
<input type="submit" value="Logout" />
</form>'''
app = Microdot()
set_session_secret_key('top-secret')
Response.default_content_type = 'text/html'
@app.get('/')
@app.post('/')
@with_session
def index(req, session):
username = session.get('username')
if req.method == 'POST':
username = req.form.get('username')
update_session(req, {'username': username})
return redirect('/')
if username is None:
return BASE_TEMPLATE.format(content=LOGGED_OUT)
else:
return BASE_TEMPLATE.format(content=LOGGED_IN.format(
username=username))
@app.post('/logout')
def logout(req):
delete_session(req)
return redirect('/')
if __name__ == '__main__':
app.run()

View File

@@ -0,0 +1,2 @@
The example in this directory demonstrates how to serve static files out of a
directory.

19
examples/static/static.py Normal file
View File

@@ -0,0 +1,19 @@
from microdot import Microdot, send_file
app = Microdot()
@app.route('/')
def index(request):
return send_file('static/index.html')
@app.route('/static/<path:path>')
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

@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>Static File Serving Demo</title>
</head>
<body>
<h1>Static File Serving Demo</h1>
<img src="static/logo.png" alt="logo">
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

BIN
examples/streaming/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
examples/streaming/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
examples/streaming/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1 @@
This directory contain examples that demonstrate how to use streaming responses.

View File

@@ -0,0 +1,45 @@
try:
import utime as time
except ImportError:
import time
from microdot 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')
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)
return stream(), 200, {'Content-Type':
'multipart/x-mixed-replace; boundary=frame'}
if __name__ == '__main__':
app.run(debug=True)

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<title>Microdot + Jinja example</title>
</head>
<body>
<h1>Microdot + Jinja example</h1>
{% if name %}
<p>Hello, <b>{{ name }}</b>!</p>
{% endif %}
<form method="POST">
<p>
What is your name?
<input type="text" name="name" autofocus />
</p>
<input type="submit" value="Submit" />
</form>
</body>
</html>

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
from microdot import Microdot, Response
from microdot_utemplate import render_template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/')
def index(req):
return render_template('page1.html', page='Page 1')
@app.route('/page2')
def page2(req):
return render_template('page2.html', page='Page 2')
if __name__ == '__main__':
app.run(debug=True)

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
{% args name %}
<!doctype html>
<html>
<head>
<title>Microdot + uTemplate example</title>
</head>
<body>
<h1>Microdot + uTemplate example</h1>
{% if name %}
<p>Hello, <b>{{ name }}</b>!</p>
{% endif %}
<form method="POST">
<p>
What is your name?
<input type="text" name="name" autofocus />
</p>
<input type="submit" value="Submit" />
</form>
</body>
</html>

View File

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

View File

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

20
examples/tls/README.md Normal file
View File

@@ -0,0 +1,20 @@
This directory contains examples that demonstrate how to start TLS servers.
To run these examples, SSL certificate and private key files need to be
created. When running under CPython, the files should be in PEM format, named
`cert.pem` and `key.pem`. When running under MicroPython, they should be in DER
format, and named `cert.der` and `key.der`.
To quickly create a self-signed SSL certificate, use the following command:
```bash
openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365
```
To convert the resulting PEM files to DER format for MicroPython, use these
commands:
```bash
openssl x509 -in cert.pem -out cert.der -outform DER
openssl rsa -in key.pem -out key.der -outform DER
```

View File

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

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

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

View File

@@ -0,0 +1,35 @@
import ssl
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...'
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
sslctx.load_cert_chain('cert.pem', 'key.pem')
app.run(port=4443, debug=True, ssl=sslctx)

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

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

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

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

View File

@@ -0,0 +1 @@
This directory contains file upload examples.

View File

@@ -0,0 +1 @@
Uploaded files are saved to this directory.

View File

@@ -0,0 +1,34 @@
<!doctype html>
<html>
<head>
<title>Microdot Upload Example</title>
</head>
<body>
<h1>Microdot Upload Example</h1>
<form id="form">
<input type="file" id="file" name="file" />
<input type="submit" value="Upload" />
</form>
<script>
async function upload(ev) {
ev.preventDefault();
const file = document.getElementById('file').files[0];
if (!file) {
return;
}
await fetch('/upload', {
method: 'POST',
body: file,
headers: {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${file.name}"`,
},
}).then(res => {
console.log('Upload accepted');
window.location.href = '/';
});
}
document.getElementById('form').addEventListener('submit', upload);
</script>
</body>
</html>

View File

@@ -0,0 +1,34 @@
from microdot import Microdot, send_file, Request
app = Microdot()
Request.max_content_length = 1024 * 1024 # 1MB (change as needed)
@app.get('/')
def index(request):
return send_file('index.html')
@app.post('/upload')
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 = 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

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

@@ -0,0 +1 @@
This directory contains WebSocket examples.

View File

@@ -0,0 +1,20 @@
from microdot import Microdot, send_file
from microdot_websocket import with_websocket
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)
app.run()

View File

@@ -0,0 +1,17 @@
from microdot_asgi import Microdot, send_file
from microdot_asgi_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)

View File

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

View File

@@ -0,0 +1,17 @@
from microdot_wsgi import Microdot, send_file
from microdot_websocket import with_websocket
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)

View File

@@ -0,0 +1,35 @@
<!doctype html>
<html>
<head>
<title>Microdot WebSocket Demo</title>
</head>
<body>
<h1>Microdot 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('ws://' + 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

@@ -1,5 +0,0 @@
microdot-asyncio
================
This package has been merged with the ``microdot`` package. It currently
installs as an empty package that depends on it.

View File

@@ -1,6 +0,0 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"

View File

@@ -1,24 +0,0 @@
[metadata]
name = microdot-asyncio
version = 0.5.0
author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com
description = AsyncIO support for the Microdot web framework'
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/miguelgrinberg/microdot
project_urls =
Bug Tracker = https://github.com/miguelgrinberg/microdot/issues
classifiers =
Environment :: Web Environment
Intended Audience :: Developers
Programming Language :: Python :: 3
Programming Language :: Python :: Implementation :: MicroPython
License :: OSI Approved :: MIT License
Operating System :: OS Independent
[options]
zip_safe = False
include_package_data = True
py_modules =
install_requires =
microdot

View File

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

9
libs/README.md Normal file
View File

@@ -0,0 +1,9 @@
# Vendored MicroPyton libraries
This directory contains some libraries that are required by examples and unit
tests.
All libraries except `utemplate` were copied from the
[micropython-lib](https://github.com/micropython/micropython-lib) project. See
the README file in the `common/utemplate` subdirectory for details about this
library.

View File

@@ -0,0 +1,116 @@
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
compatible with CPython and other compliant Python implementations.
`utemplate` syntax is roughly based on Django/Jinja2 syntax (e.g.
`{% if %}`, `{{var}}`), but only the most needed features are offered
(for example, "filters" (`{{var|filter}}`) are syntactic sugar for
function calls, and so far are not planned to be implemented, function
calls can be used directly instead: `{{filter(var)}}`).
`utemplate` compiles templates to Python source code, specifically to
a generator function which, being iterated over, produces consecutive
parts (substrings) of the rendered template. This allows for minimal
memory usage during template substitution (with Pycopy, it starts
from mere hundreds of bytes). Generated Python code can be imported as
a module directly, or a simple loader class (`utemplate.compiled.Loader`)
is provided for convenience.
There is also a loader class which will compile templates on the fly,
if not already compiled - `utemplate.source.Loader`.
Finally, there's a loader which will automatically recompile a template
module if source template is changed - `utemplate.recompile.Loader`.
This loader class is the most convenient to use during development, but
on the other hand, it performs extra processing not required for a
finished/deployed application.
To test/manage templates, `utemplate_util.py` tool is provided. For
example, to quickly try a template (assuming you are already in
`examples/` dir):
pycopy ../utemplate_util.py run squares.tpl
or
python3 ../utemplate_util.py run squares.tpl
Templates can take parameters (that's how dynamic content is generated).
Template parameters are passed as arguments to a generator function
produced from a template. They also can be passed on the `utemplate_util.py`
command line (arguments will be treated as strings in this case, but
can be of any types if called from your code):
pycopy ../utemplate_util.py run test1.tpl foo bar
Quick Syntax Reference
----------------------
Evaluating Python expression, converting it to a string and outputting to
rendered content:
* `{{<expr>}}`
Where `expr` is an arbitrary Python expression - from a bare variable name,
to function calls, `yield from`/`await` expressions, etc.
Supported statements:
* `{% args <var1>, <var2>, ... %}` - specify arguments to a template
(optional, should be at the beginning of a template if you want to
pass any arguments). All argument types as supported by Python can
be used: positional and keyword, with default values, `*args` and
`**kwargs` forms, etc.
* `{% if <expr> %}`, `{% elif <expr> %}`, `{% else %}`, `{% endif %}` -
similar to Python's `if` statement
* `{% for <var> in <expr> %}`, `{% endfor %}` - similar to Python's
`for` statement
* `{% while <expr> %}`, `{% endwhile %}` - similar to Python's `while`
statement
* `{% set <var> = <expr> %}` - assignment statement
* `{% include "name.tpl" %}` - statically include another template
* `{% include {{name}} %}` - dynamically include template whose name is
stored in variable `name`.
File Naming Conventions
-----------------------
* The recommended extension for templates is `.tpl`, e.g. `example.tpl`.
* When template is compiled, dot (`.`) in its name is replaced
with underscore (`_`) and `.py` appended, e.g. `example_tpl.py`. It
thus can be imported with `import example_tpl`.
* The name passed to `{% include %}` statement should be full name of
a template with extension, e.g. `{% include "example.tpl" %}`.
* For dynamic form of the `include`, a variable should similarly contain
a full name of the template, e.g. `{% set name = "example.tpl" %}` /
`{% include {{name}} %}`.
Examples
--------
`examples/squares.tpl` as mentioned in the usage examples above has the
following content:
```
{% args n=5 %}
{% for i in range(n) %}
| {{i}} | {{"%2d" % i ** 2}} |
{% endfor %}
```
More examples are available in the [examples/](examples/) directory.
If you want to see a complete example web application which uses `utemplate`,
refer to https://github.com/pfalcon/notes-pico .
License
-------
`utemplate` is written and maintained by Paul Sokolovsky. It's available
under the MIT license.

View File

@@ -0,0 +1,14 @@
class Loader:
def __init__(self, pkg, dir):
if dir == ".":
dir = ""
else:
dir = dir.replace("/", ".") + "."
if pkg and pkg != "__main__":
dir = pkg + "." + dir
self.p = dir
def load(self, name):
name = name.replace(".", "_")
return __import__(self.p + name, None, None, (name,)).render

View File

@@ -0,0 +1,21 @@
# (c) 2014-2020 Paul Sokolovsky. MIT license.
try:
from uos import stat, remove
except:
from os import stat, remove
from . import source
class Loader(source.Loader):
def load(self, name):
o_path = self.pkg_path + self.compiled_path(name)
i_path = self.pkg_path + self.dir + "/" + name
try:
o_stat = stat(o_path)
i_stat = stat(i_path)
if i_stat[8] > o_stat[8]:
# input file is newer, remove output to force recompile
remove(o_path)
finally:
return super().load(name)

View File

@@ -0,0 +1,188 @@
# (c) 2014-2019 Paul Sokolovsky. MIT license.
from . import compiled
class Compiler:
START_CHAR = "{"
STMNT = "%"
STMNT_END = "%}"
EXPR = "{"
EXPR_END = "}}"
def __init__(self, file_in, file_out, indent=0, seq=0, loader=None):
self.file_in = file_in
self.file_out = file_out
self.loader = loader
self.seq = seq
self._indent = indent
self.stack = []
self.in_literal = False
self.flushed_header = False
self.args = "*a, **d"
def indent(self, adjust=0):
if not self.flushed_header:
self.flushed_header = True
self.indent()
self.file_out.write("def render%s(%s):\n" % (str(self.seq) if self.seq else "", self.args))
self.stack.append("def")
self.file_out.write(" " * (len(self.stack) + self._indent + adjust))
def literal(self, s):
if not s:
return
if not self.in_literal:
self.indent()
self.file_out.write('yield """')
self.in_literal = True
self.file_out.write(s.replace('"', '\\"'))
def close_literal(self):
if self.in_literal:
self.file_out.write('"""\n')
self.in_literal = False
def render_expr(self, e):
self.indent()
self.file_out.write('yield str(' + e + ')\n')
def parse_statement(self, stmt):
tokens = stmt.split(None, 1)
if tokens[0] == "args":
if len(tokens) > 1:
self.args = tokens[1]
else:
self.args = ""
elif tokens[0] == "set":
self.indent()
self.file_out.write(stmt[3:].strip() + "\n")
elif tokens[0] == "include":
if not self.flushed_header:
# If there was no other output, we still need a header now
self.indent()
tokens = tokens[1].split(None, 1)
args = ""
if len(tokens) > 1:
args = tokens[1]
if tokens[0][0] == "{":
self.indent()
# "1" as fromlist param is uPy hack
self.file_out.write('_ = __import__(%s.replace(".", "_"), None, None, 1)\n' % tokens[0][2:-2])
self.indent()
self.file_out.write("yield from _.render(%s)\n" % args)
return
with self.loader.input_open(tokens[0][1:-1]) as inc:
self.seq += 1
c = Compiler(inc, self.file_out, len(self.stack) + self._indent, self.seq)
inc_id = self.seq
self.seq = c.compile()
self.indent()
self.file_out.write("yield from render%d(%s)\n" % (inc_id, args))
elif len(tokens) > 1:
if tokens[0] == "elif":
assert self.stack[-1] == "if"
self.indent(-1)
self.file_out.write(stmt + ":\n")
else:
self.indent()
self.file_out.write(stmt + ":\n")
self.stack.append(tokens[0])
else:
if stmt.startswith("end"):
assert self.stack[-1] == stmt[3:]
self.stack.pop(-1)
elif stmt == "else":
assert self.stack[-1] == "if"
self.indent(-1)
self.file_out.write("else:\n")
else:
assert False
def parse_line(self, l):
while l:
start = l.find(self.START_CHAR)
if start == -1:
self.literal(l)
return
self.literal(l[:start])
self.close_literal()
sel = l[start + 1]
#print("*%s=%s=" % (sel, EXPR))
if sel == self.STMNT:
end = l.find(self.STMNT_END)
assert end > 0
stmt = l[start + len(self.START_CHAR + self.STMNT):end].strip()
self.parse_statement(stmt)
end += len(self.STMNT_END)
l = l[end:]
if not self.in_literal and l == "\n":
break
elif sel == self.EXPR:
# print("EXPR")
end = l.find(self.EXPR_END)
assert end > 0
expr = l[start + len(self.START_CHAR + self.EXPR):end].strip()
self.render_expr(expr)
end += len(self.EXPR_END)
l = l[end:]
else:
self.literal(l[start])
l = l[start + 1:]
def header(self):
self.file_out.write("# Autogenerated file\n")
def compile(self):
self.header()
for l in self.file_in:
self.parse_line(l)
self.close_literal()
return self.seq
class Loader(compiled.Loader):
def __init__(self, pkg, dir):
super().__init__(pkg, dir)
self.dir = dir
if pkg == "__main__":
# if pkg isn't really a package, don't bother to use it
# it means we're running from "filesystem directory", not
# from a package.
pkg = None
self.pkg_path = ""
if pkg:
p = __import__(pkg)
if isinstance(p.__path__, str):
# uPy
self.pkg_path = p.__path__
else:
# CPy
self.pkg_path = p.__path__[0]
self.pkg_path += "/"
def input_open(self, template):
path = self.pkg_path + self.dir + "/" + template
return open(path)
def compiled_path(self, template):
return self.dir + "/" + template.replace(".", "_") + ".py"
def load(self, name):
try:
return super().load(name)
except (OSError, ImportError):
pass
compiled_path = self.pkg_path + self.compiled_path(name)
f_in = self.input_open(name)
f_out = open(compiled_path, "w")
c = Compiler(f_in, f_out, loader=self)
c.compile()
f_in.close()
f_out.close()
return super().load(name)

87
libs/micropython/hmac.py Normal file
View File

@@ -0,0 +1,87 @@
# Implements the hmac module from the Python standard library.
class HMAC:
def __init__(self, key, msg=None, digestmod=None):
if not isinstance(key, (bytes, bytearray)):
raise TypeError("key: expected bytes/bytearray")
import hashlib
if digestmod is None:
# TODO: Default hash algorithm is now deprecated.
digestmod = hashlib.md5
if callable(digestmod):
# A hashlib constructor returning a new hash object.
make_hash = digestmod # A
elif isinstance(digestmod, str):
# A hash name suitable for hashlib.new().
make_hash = lambda d=b"": hashlib.new(digestmod, d) # B
else:
# A module supporting PEP 247.
make_hash = digestmod.new # C
self._outer = make_hash()
self._inner = make_hash()
self.digest_size = getattr(self._inner, "digest_size", None)
# If the provided hash doesn't support block_size (e.g. built-in
# hashlib), 64 is the correct default for all built-in hash
# functions (md5, sha1, sha256).
self.block_size = getattr(self._inner, "block_size", 64)
# Truncate to digest_size if greater than block_size.
if len(key) > self.block_size:
key = make_hash(key).digest()
# Pad to block size.
key = key + bytes(self.block_size - len(key))
self._outer.update(bytes(x ^ 0x5C for x in key))
self._inner.update(bytes(x ^ 0x36 for x in key))
if msg is not None:
self.update(msg)
@property
def name(self):
return "hmac-" + getattr(self._inner, "name", type(self._inner).__name__)
def update(self, msg):
self._inner.update(msg)
def copy(self):
if not hasattr(self._inner, "copy"):
# Not supported for built-in hash functions.
raise NotImplementedError()
# Call __new__ directly to avoid the expensive __init__.
other = self.__class__.__new__(self.__class__)
other.block_size = self.block_size
other.digest_size = self.digest_size
other._inner = self._inner.copy()
other._outer = self._outer.copy()
return other
def _current(self):
h = self._outer
if hasattr(h, "copy"):
# built-in hash functions don't support this, and as a result,
# digest() will finalise the hmac and further calls to
# update/digest will fail.
h = h.copy()
h.update(self._inner.digest())
return h
def digest(self):
h = self._current()
return h.digest()
def hexdigest(self):
import binascii
return str(binascii.hexlify(self.digest()), "utf-8")
def new(key, msg=None, digestmod=None):
return HMAC(key, msg, digestmod)

78
libs/micropython/jwt.py Normal file
View File

@@ -0,0 +1,78 @@
import binascii
import hashlib
import hmac
import json
from time import time
def _to_b64url(data):
return (
binascii.b2a_base64(data)
.rstrip(b"\n")
.rstrip(b"=")
.replace(b"+", b"-")
.replace(b"/", b"_")
)
def _from_b64url(data):
return binascii.a2b_base64(data.replace(b"-", b"+").replace(b"_", b"/") + b"===")
class exceptions:
class PyJWTError(Exception):
pass
class InvalidTokenError(PyJWTError):
pass
class InvalidAlgorithmError(PyJWTError):
pass
class InvalidSignatureError(PyJWTError):
pass
class ExpiredSignatureError(PyJWTError):
pass
def encode(payload, key, algorithm="HS256"):
if algorithm != "HS256":
raise exceptions.InvalidAlgorithmError
if isinstance(key, str):
key = key.encode()
header = _to_b64url(json.dumps({"typ": "JWT", "alg": algorithm}).encode())
payload = _to_b64url(json.dumps(payload).encode())
signature = _to_b64url(hmac.new(key, header + b"." + payload, hashlib.sha256).digest())
return (header + b"." + payload + b"." + signature).decode()
def decode(token, key, algorithms=["HS256"]):
if "HS256" not in algorithms:
raise exceptions.InvalidAlgorithmError
parts = token.encode().split(b".")
if len(parts) != 3:
raise exceptions.InvalidTokenError
try:
header = json.loads(_from_b64url(parts[0]).decode())
payload = json.loads(_from_b64url(parts[1]).decode())
signature = _from_b64url(parts[2])
except Exception:
raise exceptions.InvalidTokenError
if header["alg"] not in algorithms or header["alg"] != "HS256":
raise exceptions.InvalidAlgorithmError
if isinstance(key, str):
key = key.encode()
calculated_signature = hmac.new(key, parts[0] + b"." + parts[1], hashlib.sha256).digest()
if signature != calculated_signature:
raise exceptions.InvalidSignatureError
if "exp" in payload:
if time() > payload["exp"]:
raise exceptions.ExpiredSignatureError
return payload

View File

@@ -41,7 +41,7 @@ class SingletonGenerator:
def __next__(self):
if self.state is not None:
_task_queue.push_sorted(cur_task, self.state)
_task_queue.push(cur_task, self.state)
self.state = None
return None
else:
@@ -115,11 +115,11 @@ class IOQueue:
# print('poll', s, sm, ev)
if ev & ~select.POLLOUT and sm[0] is not None:
# POLLIN or error
_task_queue.push_head(sm[0])
_task_queue.push(sm[0])
sm[0] = None
if ev & ~select.POLLIN and sm[1] is not None:
# POLLOUT or error
_task_queue.push_head(sm[1])
_task_queue.push(sm[1])
sm[1] = None
if sm[0] is None and sm[1] is None:
self._dequeue(s)
@@ -142,7 +142,7 @@ def create_task(coro):
if not hasattr(coro, "send"):
raise TypeError("coroutine expected")
t = Task(coro, globals())
_task_queue.push_head(t)
_task_queue.push(t)
return t
@@ -167,7 +167,7 @@ def run_until_complete(main_task=None):
_io_queue.wait_io_event(dt)
# Get next task to run and continue it
t = _task_queue.pop_head()
t = _task_queue.pop()
cur_task = t
try:
# Continue running the coroutine, it's responsible for rescheduling itself
@@ -175,6 +175,10 @@ def run_until_complete(main_task=None):
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:
@@ -185,22 +189,37 @@ def run_until_complete(main_task=None):
if isinstance(er, StopIteration):
return er.value
raise er
# Schedule any other tasks waiting on the completion of this task
waiting = False
if hasattr(t, "waiting"):
while t.waiting.peek():
_task_queue.push_head(t.waiting.pop_head())
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
t.waiting = None # Free waiting queue head
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)
# Indicate task is done by setting coro to the task object itself
t.coro = t
# Save return value of coro to pass up to caller
t.data = er
else:
# Schedule any other tasks waiting on the completion of this task.
while t.state.peek():
_task_queue.push(t.state.pop())
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(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
@@ -237,7 +256,7 @@ class Loop:
def stop():
global _stop_task
if _stop_task is not None:
_task_queue.push_head(_stop_task)
_task_queue.push(_stop_task)
# If stop() is called again, do nothing
_stop_task = None

View File

@@ -17,7 +17,7 @@ class Event:
# 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())
core._task_queue.push(self.waiting.pop())
self.state = True
def clear(self):
@@ -26,7 +26,7 @@ class Event:
async def wait(self):
if not self.state:
# Event not set, put the calling task on the event's waiting queue
self.waiting.push_head(core.cur_task)
self.waiting.push(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
yield
@@ -36,27 +36,29 @@ class Event:
# 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.
# Note: Unlike Event, this is self-clearing after a wait().
try:
import uio
class ThreadSafeFlag(uio.IOBase):
def __init__(self):
self._flag = 0
self.state = 0
def ioctl(self, req, flags):
if req == 3: # MP_STREAM_POLL
return self._flag * flags
return self.state * flags
return None
def set(self):
self._flag = 1
self.state = 1
def clear(self):
self.state = 0
async def wait(self):
if not self._flag:
if not self.state:
yield core._io_queue.queue_read(self)
self._flag = 0
self.state = 0
except ImportError:
pass

View File

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

View File

@@ -22,8 +22,8 @@ class Lock:
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)
self.state = self.waiting.pop()
core._task_queue.push(self.state)
else:
# No Task waiting so unlock
self.state = 0
@@ -31,7 +31,7 @@ class Lock:
async def acquire(self):
if self.state != 0:
# Lock unavailable, put the calling Task on the waiting queue
self.waiting.push_head(core.cur_task)
self.waiting.push(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:

View File

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

View File

@@ -26,9 +26,21 @@ class Stream:
# TODO yield?
self.s.close()
async def read(self, n):
async def read(self, n=-1):
r = b""
while True:
yield core._io_queue.queue_read(self.s)
r2 = self.s.read(n)
if r2 is not None:
if n >= 0:
return r2
if not len(r2):
return r
r += r2
async def readinto(self, buf):
yield core._io_queue.queue_read(self.s)
return self.s.read(n)
return self.s.readinto(buf)
async def readexactly(self, n):
r = b""
@@ -52,9 +64,19 @@ class Stream:
return l
def write(self, buf):
if not self.out_buf:
# Try to write immediately to the underlying stream.
ret = self.s.write(buf)
if ret == len(buf):
return
if ret is not None:
buf = buf[ret:]
self.out_buf += buf
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)
mv = memoryview(self.out_buf)
off = 0
while off < len(mv):
@@ -75,8 +97,8 @@ async def open_connection(host, port):
from uerrno import EINPROGRESS
import usocket as socket
ai = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
s = socket.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:
@@ -103,16 +125,7 @@ class Server:
async def wait_closed(self):
await self.task
async def _serve(self, cb, host, port, backlog):
import usocket as socket
ai = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
s = socket.socket()
s.setblocking(False)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(ai[-1])
s.listen(backlog)
self.task = core.cur_task
async def _serve(self, s, cb):
# Accept incoming connections
while True:
try:
@@ -134,9 +147,20 @@ 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):
s = Server()
core.create_task(s._serve(cb, host, port, backlog))
return s
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
################################################################################

View File

@@ -99,19 +99,18 @@ class TaskQueue:
def peek(self):
return self.heap
def push_sorted(self, v, key):
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
v.ph_child = None
v.ph_next = None
v.ph_key = key if key is not None else core.ticks()
self.heap = ph_meld(v, self.heap)
def push_head(self, v):
self.push_sorted(v, core.ticks())
def pop_head(self):
def pop(self):
v = self.heap
self.heap = ph_pairing(self.heap.ph_child)
assert v.ph_next is None
self.heap = ph_pairing(v.ph_child)
v.ph_child = None
return v
def remove(self, v):
@@ -123,6 +122,7 @@ class Task:
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
@@ -130,30 +130,33 @@ class Task:
self.ph_rightmost_parent = None # Paring heap
def __iter__(self):
if self.coro is self:
# Signal that the completed-task has been await'ed on.
self.waiting = None
elif not hasattr(self, "waiting"):
# Lazily allocated head of linked list of Tasks waiting on completion of this task.
self.waiting = TaskQueue()
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
def __next__(self):
if self.coro is self:
if not self.state:
# Task finished, raise return value to caller so it can continue.
raise self.data
else:
# Put calling task on waiting queue.
self.waiting.push_head(core.cur_task)
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):
return self.coro is self
return not self.state
def cancel(self):
# Check if task is already finished.
if self.coro is self:
if not self.state:
return False
# Can't cancel self (not supported yet).
if self is core.cur_task:
@@ -165,20 +168,10 @@ class 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_head(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_head(self)
core._task_queue.push(self)
self.data = core.CancelledError
return True
def throw(self, value):
# This task raised an exception which was uncaught; handle that now.
# Set the data because it was cleared by the main scheduling loop.
self.data = value
if not hasattr(self, "waiting"):
# Nothing await'ed on the task so call the exception handler.
core._exc_context["exception"] = value
core._exc_context["future"] = self
core.Loop.call_exception_handler(core._exc_context)

View File

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

View File

@@ -1,7 +1,8 @@
import sys
sys.path.insert(0, 'src')
sys.path.insert(2, 'tests/libs')
sys.path.insert(2, 'libs/common')
sys.path.insert(3, 'libs/micropython')
import unittest

View File

@@ -1,6 +1,6 @@
[metadata]
name = microdot
version = 0.8.1
version = 1.2.2
author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com
description = The impossibly small web framework for MicroPython
@@ -22,6 +22,22 @@ zip_safe = False
include_package_data = True
package_dir =
= src
py_modules =
microdot
microdot_asyncio
packages = find:
[options.packages.find]
where = src
#py_modules =
# microdot
# microdot_asyncio
# microdot_utemplate
# microdot_jinja
# microdot_session
# microdot_websocket
# microdot_websocket_alt
# microdot_asyncio_websocket
# microdot_test_client
# microdot_asyncio_test_client
# microdot_wsgi
# microdot_asgi
# microdot_asgi_websocket

View File

@@ -27,21 +27,12 @@ try: # pragma: no cover
# use the threading module
threading.Thread(target=f, args=args, kwargs=kwargs).start()
except ImportError: # pragma: no cover
try:
import _thread
def create_thread(f, *args, **kwargs):
# no threads available, call function synchronously
f(*args, **kwargs)
def create_thread(f, *args, **kwargs):
# use MicroPython's _thread module
def run():
f(*args, **kwargs)
concurrency_mode = 'sync'
_thread.start_new_thread(run, ())
except ImportError:
def create_thread(f, *args, **kwargs):
# no threads available, call function synchronously
f(*args, **kwargs)
concurrency_mode = 'sync'
try:
import ujson as json
except ImportError:
@@ -52,20 +43,29 @@ try:
except ImportError:
import re
socket_timeout_error = OSError
try:
import usocket as socket
except ImportError:
try:
import socket
socket_timeout_error = socket.timeout
except ImportError: # pragma: no cover
socket = None
MUTED_SOCKET_ERRORS = [
32, # Broken pipe
54, # Connection reset by peer
104, # Connection reset by peer
128, # Operation on closed socket
]
def urldecode(string):
string = string.replace('+', ' ')
parts = string.split('%')
def urldecode_str(s):
s = s.replace('+', ' ')
parts = s.split('%')
if len(parts) == 1:
return string
return s
result = [parts[0]]
for item in parts[1:]:
if item == '':
@@ -77,6 +77,109 @@ def urldecode(string):
return ''.join(result)
def urldecode_bytes(s):
s = s.replace(b'+', b' ')
parts = s.split(b'%')
if len(parts) == 1:
return s.decode()
result = [parts[0]]
for item in parts[1:]:
if item == b'':
result.append(b'%')
else:
code = item[:2]
result.append(bytes([int(code, 16)]))
result.append(item[2:])
return b''.join(result).decode()
def urlencode(s):
return s.replace('+', '%2B').replace(' ', '+').replace(
'%', '%25').replace('?', '%3F').replace('#', '%23').replace(
'&', '%26').replace('=', '%3D')
class NoCaseDict(dict):
"""A subclass of dictionary that holds case-insensitive keys.
:param initial_dict: an initial dictionary of key/value pairs to
initialize this object with.
Example::
>>> d = NoCaseDict()
>>> d['Content-Type'] = 'text/html'
>>> print(d['Content-Type'])
text/html
>>> print(d['content-type'])
text/html
>>> print(d['CONTENT-TYPE'])
text/html
>>> del d['cOnTeNt-TyPe']
>>> print(d)
{}
"""
def __init__(self, initial_dict=None):
super().__init__(initial_dict or {})
self.keymap = {k.lower(): k for k in self.keys() if k.lower() != k}
def __setitem__(self, key, value):
kl = key.lower()
key = self.keymap.get(kl, key)
if kl != key:
self.keymap[kl] = key
super().__setitem__(key, value)
def __getitem__(self, key):
kl = key.lower()
return super().__getitem__(self.keymap.get(kl, kl))
def __delitem__(self, key):
kl = key.lower()
super().__delitem__(self.keymap.get(kl, kl))
def __contains__(self, key):
kl = key.lower()
return self.keymap.get(kl, kl) in self.keys()
def get(self, key, default=None):
kl = key.lower()
return super().get(self.keymap.get(kl, kl), default)
def mro(cls): # pragma: no cover
"""Return the method resolution order of a class.
This is a helper function that returns the method resolution order of a
class. It is used by Microdot to find the best error handler to invoke for
the raised exception.
In CPython, this function returns the ``__mro__`` attribute of the class.
In MicroPython, this function implements a recursive depth-first scanning
of the class hierarchy.
"""
if hasattr(cls, 'mro'):
return cls.__mro__
def _mro(cls):
m = [cls]
for base in cls.__bases__:
m += _mro(base)
return m
mro_list = _mro(cls)
# If a class appears multiple times (due to multiple inheritance) remove
# all but the last occurence. This matches the method resolution order
# of MicroPython, but not CPython.
mro_pruned = []
for i in range(len(mro_list)):
base = mro_list.pop(0)
if base not in mro_list:
mro_pruned.append(base)
return mro_pruned
class MultiDict(dict):
"""A subclass of dictionary that can hold multiple values for the same
key. It is used to hold key/value pairs decoded from query strings and
@@ -169,27 +272,7 @@ class MultiDict(dict):
class Request():
"""An HTTP request class.
:var app: The application instance to which this request belongs.
:var client_addr: The address of the client, as a tuple (host, port).
:var method: The HTTP method of the request.
:var path: The path portion of the URL.
:var query_string: The query string portion of the URL.
:var args: The parsed query string, as a :class:`MultiDict` object.
:var headers: A dictionary with the headers included in the request.
:var cookies: A dictionary with the cookies included in the request.
:var content_length: The parsed ``Content-Length`` header.
:var content_type: The parsed ``Content-Type`` header.
:var stream: The input stream, containing the request body.
:var body: The body of the request, as bytes.
:var json: The parsed JSON body, as a dictionary or list, or ``None`` if
the request does not have a JSON body.
:var form: The parsed form submission body, as a :class:`MultiDict` object,
or ``None`` if the request does not have a form submission.
:var g: A general purpose container for applications to store data during
the life of the request.
"""
"""An HTTP request."""
#: Specify the maximum payload size that is accepted. Requests with larger
#: payloads will be rejected with a 413 status code. Applications can
#: change this maximum as necessary.
@@ -219,52 +302,76 @@ class Request():
#: Request.max_readline = 16 * 1024 # 16KB lines allowed
max_readline = 2 * 1024
#: Specify a suggested read timeout to use when reading the request. Set to
#: 0 to disable the use of a timeout. This timeout should be considered a
#: suggestion only, as some platforms may not support it.
socket_read_timeout = 0.1
class G:
pass
def __init__(self, app, client_addr, method, url, http_version, headers,
body=None, stream=None):
body=None, stream=None, sock=None):
#: The application instance to which this request belongs.
self.app = app
#: The address of the client, as a tuple (host, port).
self.client_addr = client_addr
#: The HTTP method of the request.
self.method = method
#: The request URL, including the path and query string.
self.url = url
#: The path portion of the URL.
self.path = url
#: The query string portion of the URL.
self.query_string = None
#: The parsed query string, as a
#: :class:`MultiDict <microdot.MultiDict>` object.
self.args = {}
#: A dictionary with the headers included in the request.
self.headers = headers
#: A dictionary with the cookies included in the request.
self.cookies = {}
#: The parsed ``Content-Length`` header.
self.content_length = 0
#: The parsed ``Content-Type`` header.
self.content_type = None
#: A general purpose container for applications to store data during
#: the life of the request.
self.g = Request.G()
self.http_version = http_version
if '?' in self.path:
self.path, self.query_string = self.path.split('?', 1)
self.args = self._parse_urlencoded(self.query_string)
else:
self.query_string = None
self.args = {}
self.headers = headers
self.cookies = {}
self.content_length = 0
self.content_type = None
for header, value in self.headers.items():
header = header.lower()
if header == 'content-length':
self.content_length = int(value)
elif header == 'content-type':
self.content_type = value
elif header == 'cookie':
for cookie in value.split(';'):
name, value = cookie.strip().split('=', 1)
self.cookies[name] = value
if 'Content-Length' in self.headers:
self.content_length = int(self.headers['Content-Length'])
if 'Content-Type' in self.headers:
self.content_type = self.headers['Content-Type']
if 'Cookie' in self.headers:
for cookie in self.headers['Cookie'].split(';'):
name, value = cookie.strip().split('=', 1)
self.cookies[name] = value
self._body = body
self.body_used = False
self._stream = stream
self.stream_used = False
self.sock = sock
self._json = None
self._form = None
self.g = Request.G()
self.after_request_handlers = []
@staticmethod
def create(app, client_stream, client_addr):
def create(app, client_stream, client_addr, client_sock=None):
"""Create a request object.
:param app: The Microdot application instance.
:param client_stream: An input stream from where the request data can
be read.
:param client_addr: The address of the client, as a tuple.
:param client_sock: The low-level socket associated with the request.
This method returns a newly created ``Request`` object.
"""
@@ -276,7 +383,7 @@ class Request():
http_version = http_version.split('/', 1)[1]
# headers
headers = {}
headers = NoCaseDict()
while True:
line = Request._safe_readline(client_stream).strip().decode()
if line == '':
@@ -286,16 +393,24 @@ class Request():
headers[header] = value
return Request(app, client_addr, method, url, http_version, headers,
stream=client_stream)
stream=client_stream, sock=client_sock)
def _parse_urlencoded(self, urlencoded):
data = MultiDict()
for k, v in [pair.split('=', 1) for pair in urlencoded.split('&')]:
data[urldecode(k)] = urldecode(v)
if len(urlencoded) > 0:
if isinstance(urlencoded, str):
for k, v in [pair.split('=', 1)
for pair in urlencoded.split('&')]:
data[urldecode_str(k)] = urldecode_str(v)
elif isinstance(urlencoded, bytes): # pragma: no branch
for k, v in [pair.split(b'=', 1)
for pair in urlencoded.split(b'&')]:
data[urldecode_bytes(k)] = urldecode_bytes(v)
return data
@property
def body(self):
"""The body of the request, as bytes."""
if self.stream_used:
raise RuntimeError('Cannot use both stream and body')
if self._body is None:
@@ -313,6 +428,7 @@ class Request():
@property
def stream(self):
"""The input stream, containing the request body."""
if self.body_used:
raise RuntimeError('Cannot use both stream and body')
self.stream_used = True
@@ -320,6 +436,8 @@ class Request():
@property
def json(self):
"""The parsed JSON body, or ``None`` if the request does not have a
JSON body."""
if self._json is None:
if self.content_type is None:
return None
@@ -331,15 +449,43 @@ class Request():
@property
def form(self):
"""The parsed form submission body, as a
:class:`MultiDict <microdot.MultiDict>` object, or ``None`` if the
request does not have a form submission."""
if self._form is None:
if self.content_type is None:
return None
mime_type = self.content_type.split(';')[0]
if mime_type != 'application/x-www-form-urlencoded':
return None
self._form = self._parse_urlencoded(self.body.decode())
self._form = self._parse_urlencoded(self.body)
return self._form
def after_request(self, f):
"""Register a request-specific function to run after the request is
handled. Request-specific after request handlers run at the very end,
after the application's own after request handlers. The function must
take two arguments, the request and response objects. The return value
of the function must be the updated response object.
Example::
@app.route('/')
def index(request):
# register a request-specific after request handler
@req.after_request
def func(request, response):
# ...
return response
return 'Hello, World!'
Note that the function is not called if the request handler raises an
exception and an error response is returned instead.
"""
self.after_request_handlers.append(f)
return f
@staticmethod
def _safe_readline(stream):
line = stream.readline(Request.max_readline + 1)
@@ -352,7 +498,10 @@ class Response():
"""An HTTP response class.
:param body: The body of the response. If a dictionary or list is given,
a JSON formatter is used to generate the body.
a JSON formatter is used to generate the body. If a file-like
object or a generator is given, a streaming response is used.
If a string is given, it is encoded from UTF-8. Else, the
body should be a byte sequence.
:param status_code: The numeric HTTP status code of the response. The
default is 200.
:param headers: A dictionary of headers to include in the response.
@@ -372,17 +521,28 @@ class Response():
}
send_file_buffer_size = 1024
#: The content type to use for responses that do not explicitly define a
#: ``Content-Type`` header.
default_content_type = 'text/plain'
#: Special response used to signal that a response does not need to be
#: written to the client. Used to exit WebSocket connections cleanly.
already_handled = None
def __init__(self, body='', status_code=200, headers=None, reason=None):
if body is None and status_code == 200:
body = ''
status_code = 204
self.status_code = status_code
self.headers = headers.copy() if headers else {}
self.headers = NoCaseDict(headers or {})
self.reason = reason
if isinstance(body, (dict, list)):
self.body = json.dumps(body).encode()
self.headers['Content-Type'] = 'application/json'
self.headers['Content-Type'] = 'application/json; charset=UTF-8'
elif isinstance(body, str):
self.body = body.encode()
else:
# this applies to bytes or file-like objects
# this applies to bytes, file-like objects or generators
self.body = body
def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
@@ -393,7 +553,8 @@ class Response():
:param value: The cookie's value.
:param path: The cookie's path.
:param domain: The cookie's domain.
:param expires: The cookie expiration time, as a ``datetime`` object.
:param expires: The cookie expiration time, as a ``datetime`` object
or a correctly formatted string.
:param max_age: The cookie's ``Max-Age`` value.
:param secure: The cookie's ``secure`` flag.
:param http_only: The cookie's ``HttpOnly`` flag.
@@ -404,8 +565,11 @@ class Response():
if domain:
http_cookie += '; Domain=' + domain
if expires:
http_cookie += '; Expires=' + expires.strftime(
"%a, %d %b %Y %H:%M:%S GMT")
if isinstance(expires, str):
http_cookie += '; Expires=' + expires
else:
http_cookie += '; Expires=' + expires.strftime(
'%a, %d %b %Y %H:%M:%S GMT')
if max_age:
http_cookie += '; Max-Age=' + str(max_age)
if secure:
@@ -422,7 +586,9 @@ class Response():
'Content-Length' not in self.headers:
self.headers['Content-Length'] = str(len(self.body))
if 'Content-Type' not in self.headers:
self.headers['Content-Type'] = 'text/plain'
self.headers['Content-Type'] = self.default_content_type
if 'charset=' not in self.headers['Content-Type']:
self.headers['Content-Type'] += '; charset=UTF-8'
def write(self, stream):
self.complete()
@@ -442,18 +608,35 @@ class Response():
stream.write(b'\r\n')
# body
can_flush = hasattr(stream, 'flush')
try:
for body in self.body_iter():
if isinstance(body, str): # pragma: no cover
body = body.encode()
stream.write(body)
if can_flush: # pragma: no cover
stream.flush()
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS:
pass
else:
raise
def body_iter(self):
if self.body:
if hasattr(self.body, 'read'):
while True:
buf = self.body.read(self.send_file_buffer_size)
if len(buf):
stream.write(buf)
yield buf
if len(buf) < self.send_file_buffer_size:
break
if hasattr(self.body, 'close'): # pragma: no cover
self.body.close()
elif hasattr(self.body, '__next__'):
yield from self.body
else:
stream.write(self.body)
yield self.body
@classmethod
def redirect(cls, location, status_code=302):
@@ -479,7 +662,7 @@ class Response():
automatically from the file extension.
Security note: The filename is assumed to be trusted. Never pass
filenames provided by the user before validating and sanitizing them
filenames provided by the user without validating and sanitizing them
first.
"""
if content_type is None:
@@ -495,6 +678,7 @@ class Response():
class URLPattern():
def __init__(self, url_pattern):
self.url_pattern = url_pattern
self.pattern = ''
self.args = []
use_regex = False
@@ -545,6 +729,15 @@ class URLPattern():
return args
class HTTPException(Exception):
def __init__(self, status_code, reason=None):
self.status_code = status_code
self.reason = reason or str(status_code) + ' error'
def __repr__(self): # pragma: no cover
return 'HTTPException: {}'.format(self.status_code)
class Microdot():
"""An HTTP application class.
@@ -563,6 +756,7 @@ class Microdot():
self.url_map = []
self.before_request_handlers = []
self.after_request_handlers = []
self.after_error_request_handlers = []
self.error_handlers = {}
self.shutdown_requested = False
self.debug = False
@@ -716,13 +910,32 @@ class Microdot():
Example::
@app.before_request
@app.after_request
def func(request, response):
# ...
return response
"""
self.after_request_handlers.append(f)
return f
def after_error_request(self, f):
"""Decorator to register a function to run after an error response is
generated. The decorated function must take two arguments, the request
and response objects. The return value of the function must be an
updated response object. The handler is invoked for error responses
generated by Microdot, as well as those returned by application-defined
error handlers.
Example::
@app.after_error_request
def func(request, response):
# ...
return response
"""
self.after_error_request_handlers.append(f)
return f
def errorhandler(self, status_code_or_exception_class):
"""Decorator to register a function as an error handler. Error handler
functions for numeric HTTP status codes must accept a single argument,
@@ -749,7 +962,48 @@ class Microdot():
return f
return decorated
def run(self, host='0.0.0.0', port=5000, debug=False):
def mount(self, subapp, url_prefix=''):
"""Mount a sub-application, optionally under the given URL prefix.
:param subapp: The sub-application to mount.
:param url_prefix: The URL prefix to mount the application under.
"""
for methods, pattern, handler in subapp.url_map:
self.url_map.append(
(methods, URLPattern(url_prefix + pattern.url_pattern),
handler))
for handler in subapp.before_request_handlers:
self.before_request_handlers.append(handler)
for handler in subapp.after_request_handlers:
self.after_request_handlers.append(handler)
for handler in subapp.after_error_request_handlers:
self.after_error_request_handlers.append(handler)
for status_code, handler in subapp.error_handlers.items():
self.error_handlers[status_code] = handler
@staticmethod
def abort(status_code, reason=None):
"""Abort the current request and return an error response with the
given status code.
:param status_code: The numeric status code of the response.
:param reason: The reason for the response, which is included in the
response body.
Example::
from microdot import abort
@app.route('/users/<int:id>')
def get_user(id):
user = get_user_by_id(id)
if user is None:
abort(404)
return user.to_dict()
"""
raise HTTPException(status_code, reason)
def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None):
"""Start the web server. This function does not normally return, as
the server enters an endless listening loop. The :func:`shutdown`
function provides a method for terminating the server gracefully.
@@ -765,6 +1019,8 @@ class Microdot():
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
:param ssl: An ``SSLContext`` instance or ``None`` if the server should
not use TLS. The default is ``None``.
Example::
@@ -792,15 +1048,21 @@ class Microdot():
self.server.bind(addr)
self.server.listen(5)
if ssl:
self.server = ssl.wrap_socket(self.server, server_side=True)
while not self.shutdown_requested:
try:
sock, addr = self.server.accept()
except OSError as exc: # pragma: no cover
if exc.args[0] == errno.ECONNABORTED:
if exc.errno == errno.ECONNABORTED:
break
else:
raise
create_thread(self.dispatch_request, sock, addr)
print_exception(exc)
except Exception as exc: # pragma: no cover
print_exception(exc)
else:
create_thread(self.handle_request, sock, addr)
def shutdown(self):
"""Request a server shutdown. The server will then exit its request
@@ -818,26 +1080,58 @@ class Microdot():
self.shutdown_requested = True
def find_route(self, req):
f = None
f = 404
for route_methods, route_pattern, route_handler in self.url_map:
if req.method in route_methods:
req.url_args = route_pattern.match(req.path)
if req.url_args is not None:
req.url_args = route_pattern.match(req.path)
if req.url_args is not None:
if req.method in route_methods:
f = route_handler
break
else:
f = 405
return f
def dispatch_request(self, sock, addr):
def handle_request(self, sock, addr):
if Request.socket_read_timeout and \
hasattr(sock, 'settimeout'): # pragma: no cover
sock.settimeout(Request.socket_read_timeout)
if not hasattr(sock, 'readline'): # pragma: no cover
stream = sock.makefile("rwb")
else:
stream = sock
req = None
res = None
try:
req = Request.create(self, stream, addr)
req = Request.create(self, stream, addr, sock)
res = self.dispatch_request(req)
except socket_timeout_error as exc: # pragma: no cover
if exc.errno and exc.errno not in [60, 110]:
print_exception(exc) # not a timeout
except Exception as exc: # pragma: no cover
print_exception(exc)
try:
if res and res != Response.already_handled: # pragma: no branch
res.write(stream)
stream.close()
except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS:
pass
else:
print_exception(exc)
except Exception as exc: # pragma: no cover
print_exception(exc)
if stream != sock: # pragma: no cover
sock.close()
if self.shutdown_requested: # pragma: no cover
self.server.close()
if self.debug and req: # pragma: no cover
print('{method} {path} {status_code}'.format(
method=req.method, path=req.path,
status_code=res.status_code))
def dispatch_request(self, req):
after_request_handled = False
if req:
if req.content_length > req.max_content_length:
if 413 in self.error_handlers:
@@ -848,7 +1142,7 @@ class Microdot():
f = self.find_route(req)
try:
res = None
if f:
if callable(f):
for handler in self.before_request_handlers:
res = handler(req)
if res:
@@ -856,21 +1150,44 @@ class Microdot():
if res is None:
res = f(req, **req.url_args)
if isinstance(res, tuple):
res = Response(*res)
body = res[0]
if isinstance(res[1], int):
status_code = res[1]
headers = res[2] if len(res) > 2 else {}
else:
status_code = 200
headers = res[1]
res = Response(body, status_code, headers)
elif not isinstance(res, Response):
res = Response(res)
for handler in self.after_request_handlers:
res = handler(req, res) or res
elif 404 in self.error_handlers:
res = self.error_handlers[404](req)
for handler in req.after_request_handlers:
res = handler(req, res) or res
after_request_handled = True
elif f in self.error_handlers:
res = self.error_handlers[f](req)
else:
res = 'Not found', 404
res = 'Not found', f
except HTTPException as exc:
if exc.status_code in self.error_handlers:
res = self.error_handlers[exc.status_code](req)
else:
res = exc.reason, exc.status_code
except Exception as exc:
print_exception(exc)
exc_class = None
res = None
if exc.__class__ in self.error_handlers:
exc_class = exc.__class__
else:
for c in mro(exc.__class__)[1:]:
if c in self.error_handlers:
exc_class = c
break
if exc_class:
try:
res = self.error_handlers[exc.__class__](req, exc)
res = self.error_handlers[exc_class](req, exc)
except Exception as exc2: # pragma: no cover
print_exception(exc2)
if res is None:
@@ -879,22 +1196,22 @@ class Microdot():
else:
res = 'Internal server error', 500
else:
res = 'Bad request', 400
if 400 in self.error_handlers:
res = self.error_handlers[400](req)
else:
res = 'Bad request', 400
if isinstance(res, tuple):
res = Response(*res)
elif not isinstance(res, Response):
res = Response(res)
res.write(stream)
stream.close()
if stream != sock: # pragma: no cover
sock.close()
if self.shutdown_requested: # pragma: no cover
self.server.close()
if self.debug and req: # pragma: no cover
print('{method} {path} {status_code}'.format(
method=req.method, path=req.path,
status_code=res.status_code))
if not after_request_handled:
for handler in self.after_error_request_handlers:
res = handler(req, res) or res
return res
abort = Microdot.abort
Response.already_handled = Response()
redirect = Response.redirect
send_file = Response.send_file

152
src/microdot_asgi.py Normal file
View File

@@ -0,0 +1,152 @@
import asyncio
import os
import signal
from microdot_asyncio import * # noqa: F401, F403
from microdot_asyncio import Microdot as BaseMicrodot
from microdot_asyncio import Request
from microdot import NoCaseDict
class _BodyStream: # pragma: no cover
def __init__(self, receive):
self.receive = receive
self.data = b''
self.more = True
async def read_more(self):
if self.more:
packet = await self.receive()
self.data += packet.get('body', b'')
self.more = packet.get('more_body', False)
async def read(self, n=-1):
while self.more and len(self.data) < n:
self.read_more()
if len(self.data) < n:
data = self.data
self.data = b''
return data
data = self.data[:n]
self.data = self.data[n:]
return data
async def readline(self):
return self.readuntil()
async def readexactly(self, n):
return self.read(n)
async def readuntil(self, separator=b'\n'):
if self.more and separator not in self.data:
self.read_more()
data, self.data = self.data.split(separator, 1)
return data
class Microdot(BaseMicrodot):
def __init__(self):
super().__init__()
self.embedded_server = False
async def asgi_app(self, scope, receive, send):
"""An ASGI application."""
if scope['type'] not in ['http', 'websocket']: # pragma: no cover
return
path = scope['path']
if 'query_string' in scope and scope['query_string']:
path += '?' + scope['query_string'].decode()
headers = NoCaseDict()
content_length = 0
for key, value in scope.get('headers', []):
headers[key] = value
if key.lower() == 'content-length':
content_length = int(value)
if content_length and content_length <= Request.max_body_length:
body = b''
more = True
while more:
packet = await receive()
body += packet.get('body', b'')
more = packet.get('more_body', False)
stream = None
else:
body = b''
stream = _BodyStream(receive)
req = Request(
self,
(scope['client'][0], scope['client'][1]),
scope.get('method', 'GET'),
path,
'HTTP/' + scope['http_version'],
headers,
body=body,
stream=stream,
sock=(receive, send))
req.asgi_scope = scope
res = await self.dispatch_request(req)
res.complete()
header_list = []
for name, value in res.headers.items():
if not isinstance(value, list):
header_list.append((name.lower().encode(), value.encode()))
else:
for v in value:
header_list.append((name.lower().encode(), v.encode()))
if scope['type'] != 'http': # pragma: no cover
return
await send({'type': 'http.response.start',
'status': res.status_code,
'headers': header_list})
cancelled = False
async def cancel_monitor():
nonlocal cancelled
while True:
event = await receive()
if event['type'] == 'http.disconnect': # pragma: no branch
cancelled = True
break
asyncio.ensure_future(cancel_monitor())
body_iter = res.body_iter().__aiter__()
try:
body = await body_iter.__anext__()
while not cancelled: # pragma: no branch
next_body = await body_iter.__anext__()
await send({'type': 'http.response.body',
'body': body,
'more_body': True})
body = next_body
except StopAsyncIteration:
await send({'type': 'http.response.body',
'body': body,
'more_body': False})
async def __call__(self, scope, receive, send):
return await self.asgi_app(scope, receive, send)
def shutdown(self):
if self.embedded_server: # pragma: no cover
super().shutdown()
else:
pid = os.getpgrp() if hasattr(os, 'getpgrp') else os.getpid()
os.kill(pid, signal.SIGTERM)
def run(self, host='0.0.0.0', port=5000, debug=False,
**options): # pragma: no cover
"""Normally you would not start the server by invoking this method.
Instead, start your chosen ASGI web server and pass the ``Microdot``
instance as the ASGI application.
"""
self.embedded_server = True
super().run(host=host, port=port, debug=debug, **options)

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