86 Commits

Author SHA1 Message Date
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
Miguel Grinberg
453e133cc2 Release 0.8.1 2022-03-18 23:51:28 +00:00
Miguel Grinberg
29a9f6f46c Optimizations for request streams and bodies 2022-02-21 18:11:19 +01:00
Miguel Grinberg
9d3222ae4b Version 0.8.1.dev0 2022-02-18 17:41:16 +00:00
Miguel Grinberg
f23a6be2db Release 0.8.0 2022-02-18 17:40:59 +00:00
Miguel Grinberg
992fa722c1 Support streamed request payloads (Fixes #26) 2022-02-18 17:32:14 +00:00
Steve Li
e16fb94b2d Use case insensitive comparisons for HTTP headers (#33) 2022-01-31 12:10:23 +00:00
Miguel Grinberg
c130d8f2d4 simplified hello_async.py example 2022-01-22 23:23:31 +00:00
Miguel Grinberg
bd82c4deab More robust logic to read request body (Fixes #31) 2021-10-23 19:03:31 +01:00
Miguel Grinberg
7bc5d724f0 Version 0.7.3.dev0 2021-09-28 17:23:17 +01:00
Miguel Grinberg
f23c78533e Release 0.7.2 2021-09-28 17:21:05 +01:00
Miguel Grinberg
d29ed6aaa1 Document a security risk in the send_file function 2021-09-28 17:15:07 +01:00
Miguel Grinberg
8e5fb92ff1 Validate redirect URLs 2021-09-28 17:12:15 +01:00
Miguel Grinberg
06015934b8 Return a 400 error when request object could not be created 2021-09-28 17:09:02 +01:00
Miguel Grinberg
568cd51fd2 Version 0.7.2.dev0 2021-09-27 23:01:20 +01:00
Miguel Grinberg
2fe9793389 Release 0.7.1 2021-09-27 22:58:32 +01:00
Miguel Grinberg
de9c991a9a Limit the size of each request line 2021-09-27 20:03:18 +01:00
Miguel Grinberg
d75449eb32 Version 0.7.1.dev0 2021-09-27 17:14:48 +01:00
Miguel Grinberg
e508abc333 Release 0.7.0 2021-09-27 17:12:42 +01:00
Miguel Grinberg
5003a5b3d9 Limit the size of the request body 2021-09-27 17:01:43 +01:00
Miguel Grinberg
4ed101dfc6 Add security policy 2021-09-27 13:57:04 +01:00
Mark Blakeney
833fecb105 Add documentation for request.client_addr (#27) 2021-09-22 12:04:28 +01:00
Miguel Grinberg
d527bdb7c3 Added documentation for reason argument in the Response object 2021-08-11 12:00:46 +01:00
Miguel Grinberg
2516b296a7 Version 0.6.1.dev0 2021-08-11 10:37:04 +01:00
Miguel Grinberg
5061145f5c Release 0.6.0 2021-08-11 10:36:42 +01:00
Miguel Grinberg
122c638bae Fix codecov badge link #nolog 2021-08-11 10:33:10 +01:00
Miguel Grinberg
bd74bcab74 Accept a custom reason phrase for the HTTP response (Fixes #25) 2021-08-11 10:29:08 +01:00
Miguel Grinberg
5cd3ace516 More unit tests 2021-08-02 15:53:13 +01:00
Miguel Grinberg
da32f23e35 Better handling of content types in form and json methods (Fixes #24) 2021-08-02 15:39:32 +01:00
Mark Blakeney
0641466faa Copy client headers to avoid write back (#23) 2021-07-28 10:43:54 +01:00
Miguel Grinberg
dd3fc20507 Make mime type check for form submissions more robust 2021-06-06 20:05:32 +01:00
Miguel Grinberg
46963ba464 Work around a bug in uasyncio's create_server() function 2021-06-06 20:05:12 +01:00
Miguel Grinberg
1a8db51cb3 Installation instructions 2021-06-06 12:24:09 +01:00
Miguel Grinberg
d903c42370 Minor wording update in the documentation #nolog 2021-06-06 12:17:22 +01:00
Miguel Grinberg
8b4ebbd953 Run tests with pytest 2021-06-06 12:09:03 +01:00
Miguel Grinberg
a82ed55f56 Last version of the microdot-asyncio package 2021-06-06 11:54:51 +01:00
Miguel Grinberg
ac87f0542f Version 0.5.1.dev0 2021-06-06 11:49:01 +01:00
Miguel Grinberg
2de57498a8 Release 0.5.0 2021-06-06 11:47:09 +01:00
Miguel Grinberg
b7b881e3c7 merge microdot-asyncio package with microdot 2021-06-06 11:15:32 +01:00
Miguel Grinberg
9955ac99a6 change log 2021-06-06 00:38:46 +01:00
Miguel Grinberg
4b101d1597 Improve project structure 2021-06-06 00:29:52 +01:00
Miguel Grinberg
097cd9cf02 Documentation updates 2021-06-05 15:42:37 +01:00
Miguel Grinberg
b0c25a1a72 Support duplicate arguments in query string and form submissions
Fixes #21
2021-06-05 12:26:37 +01:00
Miguel Grinberg
b7b8e58d6a added documentation link 2021-06-05 00:56:16 +01:00
Miguel Grinberg
12cd60305b Documentation 2021-06-05 00:52:41 +01:00
108 changed files with 7485 additions and 1192 deletions

2
.coveragerc Normal file
View File

@@ -0,0 +1,2 @@
[run]
omit=src/utemplate/*

View File

@@ -2,10 +2,10 @@ name: build
on:
push:
branches:
- master
- main
pull_request:
branches:
- master
- main
jobs:
lint:
name: lint
@@ -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.6', '3.7', '3.8', '3.9', '3.10']
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

132
CHANGES.md Normal file
View File

@@ -0,0 +1,132 @@
# Microdot change log
**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))
**Release 0.8.0** - 2022-02-18
- Support streamed request payloads [#26](https://github.com/miguelgrinberg/microdot/issues/26) ([commit](https://github.com/miguelgrinberg/microdot/commit/992fa722c1312c0ac0ee9fbd5e23ad7b52d3caca))
- Use case insensitive comparisons for HTTP headers [#33](https://github.com/miguelgrinberg/microdot/issues/33) ([commit](https://github.com/miguelgrinberg/microdot/commit/e16fb94b2d1e88ef681d70f7f456c37ee9859df6)) (thanks **Steve Li**!)
- More robust logic to read request body [#31](https://github.com/miguelgrinberg/microdot/issues/31) ([commit](https://github.com/miguelgrinberg/microdot/commit/bd82c4deabf40d37e6b7397b08e8eb40ba2b6a42))
- Simplified `hello_async.py` example ([commit](https://github.com/miguelgrinberg/microdot/commit/c130d8f2d45dcce9606dda25d31d653ce91faf92))
**Release 0.7.2** - 2021-09-28
- Document a security risk in the send_file function ([commit](https://github.com/miguelgrinberg/microdot/commit/d29ed6aaa1f2080fcf471bf6ae0f480f95ff1716)) (thanks **Ky Tran**!)
- Validate redirect URLs ([commit](https://github.com/miguelgrinberg/microdot/commit/8e5fb92ff1ccd50972b0c1cb5a6c3bd5eb54d86b)) (thanks **Ky Tran**!)
- Return a 400 error when request object could not be created ([commit](https://github.com/miguelgrinberg/microdot/commit/06015934b834622d39f52b3e13d16bfee9dc8e5a))
**Release 0.7.1** - 2021-09-27
- Breaking change: Limit the size of each request line to 2KB. A different maximum can be set in `Request.max_readline`. ([commit](https://github.com/miguelgrinberg/microdot/commit/de9c991a9ab836d57d5c08bf4282f99f073b502a)) (thanks **Ky Tran**!)
**Release 0.7.0** - 2021-09-27
- Breaking change: Limit the size of the request body to 16KB. A different maximum can be set in `Request.max_content_length`. ([commit](https://github.com/miguelgrinberg/microdot/commit/5003a5b3d948a7cf365857b419bebf6e388593a1))
- Add documentation for `request.client_addr` [#27](https://github.com/miguelgrinberg/microdot/issues/27) ([commit](https://github.com/miguelgrinberg/microdot/commit/833fecb105ce456b95f1d2a6ea96dceca1075814)) (thanks **Mark Blakeney**!)
- Added documentation for reason argument in the Response object ([commit](https://github.com/miguelgrinberg/microdot/commit/d527bdb7c32ab918a1ecf6956cf3a9f544504354))
**Release 0.6.0** - 2021-08-11
- Better handling of content types in form and json methods [#24](https://github.com/miguelgrinberg/microdot/issues/24) ([commit](https://github.com/miguelgrinberg/microdot/commit/da32f23e35f871470a40638e7000e84b0ff6d17f))
- Accept a custom reason phrase for the HTTP response [#25](https://github.com/miguelgrinberg/microdot/issues/25) ([commit](https://github.com/miguelgrinberg/microdot/commit/bd74bcab74f283c89aadffc8f9c20d6ff0f771ce))
- Make mime type check for form submissions more robust ([commit](https://github.com/miguelgrinberg/microdot/commit/dd3fc20507715a23d0fa6fa3aae3715c8fbc0351))
- Copy client headers to avoid write back [#23](https://github.com/miguelgrinberg/microdot/issues/23) ([commit](https://github.com/miguelgrinberg/microdot/commit/0641466faa9dda0c54f78939ac05993c0812e84a)) (thanks **Mark Blakeney**!)
- Work around a bug in uasyncio's create_server() function ([commit](https://github.com/miguelgrinberg/microdot/commit/46963ba4644d7abc8dc653c99bc76222af526964))
- More unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/5cd3ace5166ec549579b0b1149ae3d7be195974a))
- Installation instructions ([commit](https://github.com/miguelgrinberg/microdot/commit/1a8db51cb3754308da6dcc227512dcdeb4ce4557))
- Run tests with pytest ([commit](https://github.com/miguelgrinberg/microdot/commit/8b4ebbd9535b3c083fb2a955284609acba07f05e))
- Deprecated the microdot-asyncio package ([commit](https://github.com/miguelgrinberg/microdot/commit/a82ed55f56e14fbcea93e8171af86ab42657fa96))
**Release 0.5.0** - 2021-06-06
- [Documentation](https://microdot.readthedocs.io/en/latest/) site ([commit](https://github.com/miguelgrinberg/microdot/commit/12cd60305b7b48ab151da52661fc5988684dbcd8))
- Support duplicate arguments in query string and form submissions [#21](https://github.com/miguelgrinberg/microdot/issues/21) ([commit](https://github.com/miguelgrinberg/microdot/commit/b0c25a1a7298189373be5df1668e0afb5532cdaf))
- Merge `microdot-asyncio` package with `microdot` ([commit](https://github.com/miguelgrinberg/microdot/commit/b7b881e3c7f1c6ede6546e498737e93928425c30))
- Added a change log ([commit](https://github.com/miguelgrinberg/microdot/commit/9955ac99a6ac20308644f02d6e6e32847d28b70c))
- Improve project structure ([commit](https://github.com/miguelgrinberg/microdot/commit/4b101d15971fa2883d187f0bab0be999ae30b583))
**Release v0.4.0** - 2021-06-04
- Add HTTP method-specific route decorators ([commit](https://github.com/miguelgrinberg/microdot/commit/a3288a63ed45f700f79b67d0b57fc4dd20e844c1))
- Server shutdown [#19](https://github.com/miguelgrinberg/microdot/issues/19) ([commit](https://github.com/miguelgrinberg/microdot/commit/0ad538df91f8b6b8a3885aa602c014ee7fe4526b))
- Update microypthon binary for tests to 1.15 ([commit](https://github.com/miguelgrinberg/microdot/commit/3bd7fe8cea4598a7dbd0efcb9c6ce57ec2b79f9c))
**Release v0.3.1** - 2021-02-06
- Support large downloads in send_file [#3](https://github.com/miguelgrinberg/microdot/issues/3) ([commit](https://github.com/miguelgrinberg/microdot/commit/3e29af57753dbb7961ff98719a4fc4f71c0b4e3e))
- Move socket import and add simple hello example [#12](https://github.com/miguelgrinberg/microdot/issues/12) ([commit](https://github.com/miguelgrinberg/microdot/commit/c5e1873523b609680ff67d7abfada72568272250)) (thanks **Damien George**!)
- Update python versions to build ([commit](https://github.com/miguelgrinberg/microdot/commit/dfbe2edd797153fc9be40bc1928d93bdee7e7be5))
- Handle Chrome preconnect [#8](https://github.com/miguelgrinberg/microdot/issues/8) ([commit](https://github.com/miguelgrinberg/microdot/commit/125af4b4a92b1d78acfa9d57ad2f507e759b6938)) (thanks **Ricardo Mendonça Ferreira**!)
- Readme update ([commit](https://github.com/miguelgrinberg/microdot/commit/1aacb3cf46bd0b634ec3bc852ff9439f3c5dd773))
- Switch to GitHub actions for builds ([commit](https://github.com/miguelgrinberg/microdot/commit/4c0afa2beca0c3b0f167fd25c6849d6937c412ba))
**Release v0.3.0** - 2019-05-05
- g, before_request and after_request ([commit](https://github.com/miguelgrinberg/microdot/commit/8aa50f171d2d04bc15c472ab1d9b3288518f7a21))
- Threaded mode ([commit](https://github.com/miguelgrinberg/microdot/commit/494800ff9ff474c38644979086057e3584573969))
- Optional asyncio support ([commit](https://github.com/miguelgrinberg/microdot/commit/3d9b5d7084d52e749553ca79206ed7060f963f9d))
- Debug mode ([commit](https://github.com/miguelgrinberg/microdot/commit/4c83cb75636572066958ef2cc0802909deafe542))
- Print exceptions ([commit](https://github.com/miguelgrinberg/microdot/commit/491202de1fce232b9629b7f1db63594fd13f84a3))
- Flake8 ([commit](https://github.com/miguelgrinberg/microdot/commit/92edc17522d7490544c7186d62a2964caf35c861))
- Unit testing framework ([commit](https://github.com/miguelgrinberg/microdot/commit/f741ed7cf83320d25ce16a1a29796af6fdfb91e9))
- More robust header checking in tests ([commit](https://github.com/miguelgrinberg/microdot/commit/03efe46a26e7074f960dd4c9a062c53d6f72bfa0))
- Response unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/cd71986a5042dcc308617a3db89476f28dd13ecf))
- Request unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/0b95feafc96dc91d7d34528ff2d8931a8aa3d612))
- More unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/76ab1fa6d72dd9deaa24aeaf4895a0c6fc883bcb))
- Async request and response unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/89f7f09b9a2d0dfccefabebbe9b83307133bd97c))
- More asyncio unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/ba986a89ff72ebbd9a65307b81ee769879961594))
- Improve code structure ([commit](https://github.com/miguelgrinberg/microdot/commit/b16466f1a9432a608eb23769907e8952fe304a9a))
- URL pattern matching unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/0a373775d54df571ceddaac090094bb62dbe6c72))
- Rename microdot_async to microdot_asyncio ([commit](https://github.com/miguelgrinberg/microdot/commit/e5525c5c485ae8901c9602da7e4582b58fb2da40))
**Release 0.2.0** - 2019-04-19
- Error handlers ([commit](https://github.com/miguelgrinberg/microdot/commit/0f2c749f6d1b9edbf124523160e10449c932ea45))
- Fleshed out example GPIO application ([commit](https://github.com/miguelgrinberg/microdot/commit/52f2d0c4918d00d1a7e46cc7fd9a909ef6d259c1))
- More robust parsing of cookie header ([commit](https://github.com/miguelgrinberg/microdot/commit/2f58c41cc89946d51646df83d4f9ae0e24e447b9))
**Release 0.1.1** - 2019-04-17
- Minor fixes for micropython ([commit](https://github.com/miguelgrinberg/microdot/commit/e4ff70cf8fe839f5b5297157bf028569188b9031))
- Initial commit ([commit](https://github.com/miguelgrinberg/microdot/commit/311a82a44430d427948866b09cb6136e60a5b1c9))

View File

@@ -1,8 +1,25 @@
# 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/master/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/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”*
## Documentation
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.
Coming soon!
```python
from microdot import Microdot
app = Microdot()
@app.route('/')
def index(request):
return 'Hello, world!'
app.run()
```
## Resources
- [Documentation](https://microdot.readthedocs.io/en/latest/)
- [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)

9
SECURITY.md Normal file
View File

@@ -0,0 +1,9 @@
# Security Policy
## Reporting a Vulnerability
If you think you've found a vulnerability on this project, please send me (Miguel Grinberg) an email at miguel.grinberg@gmail.com with a description of the problem. I will personally review the issue and respond to you with next steps.
If the issue is highly sensitive, you are welcome to encrypt your message. Here is my [PGP key](https://keyserver.ubuntu.com/pks/lookup?search=miguel.grinberg%40gmail.com&fingerprint=on&op=index).
Please do not disclose vulnerabilities publicly before discussing how to proceed with me.

View File

@@ -1,33 +0,0 @@
#!/bin/bash
VERSION=$1
if [[ "$VERSION" == "" ]]; then
echo Usage: $0 "<version>"
exit 1
fi
git diff --cached --exit-code >/dev/null
if [[ "$?" != "0" ]]; then
echo Commit your changes before using this script.
exit 1
fi
set -e
for PKG in microdot*; do
echo Building $PKG...
cd $PKG
sed -i "" "s/version.*$/version=\"$VERSION\",/" setup.py
git add setup.py
rm -rf dist
python setup.py sdist bdist_wheel --universal
cd ..
done
git commit -m "Release v$VERSION"
git tag v$VERSION
git push --tags origin master
for PKG in microdot*; do
echo Releasing $PKG...
cd $PKG
twine upload dist/*
cd ..
done

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

3
docs/_static/css/custom.css vendored Normal file
View File

@@ -0,0 +1,3 @@
.py .class, .py .method, .py .property {
margin-top: 20px;
}

82
docs/api.rst Normal file
View File

@@ -0,0 +1,82 @@
API Reference
=============
``microdot`` module
-------------------
.. autoclass:: microdot.Microdot
:members:
.. autoclass:: microdot.Request
:members:
.. autoclass:: microdot.Response
:members:
.. autoclass:: microdot.MultiDict
:members:
``microdot_asyncio`` module
---------------------------
.. autoclass:: microdot_asyncio.Microdot
:inherited-members:
:members:
.. autoclass:: microdot_asyncio.Request
:inherited-members:
:members:
.. autoclass:: microdot_asyncio.Response
:inherited-members:
:members:
``microdot_utemplate`` module
-----------------------------
.. automodule:: microdot_utemplate
:members:
``microdot_jinja`` module
-------------------------
.. automodule:: microdot_jinja
:members:
``microdot_session`` module
---------------------------
.. automodule:: microdot_session
:members:
``microdot_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

71
docs/conf.py Normal file
View File

@@ -0,0 +1,71 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('../src'))
sys.path.insert(1, os.path.abspath('../libs/common'))
# -- Project information -----------------------------------------------------
project = 'Microdot'
copyright = '2021, Miguel Grinberg'
author = 'Miguel Grinberg'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autosectionlabel',
'sphinx.ext.autodoc',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_css_files = [
'css/custom.css',
]
html_theme_options = {
'github_user': 'miguelgrinberg',
'github_repo': 'microdot',
'github_banner': True,
'github_button': True,
'github_type': 'star',
'fixed_sidebar': True,
}
autodoc_default_options = {
'member-order': 'bysource',
}

378
docs/extensions.rst Normal file
View File

@@ -0,0 +1,378 @@
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_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_utemplate.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_utemplate.py>`_
| `hello_utemplate_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_utemplate_async.py>`_
The :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_jinja.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_jinja.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/miguelgrinberg/micropython-lib/blob/ujwt-module/python-ecosys/ujwt/ujwt.py>`_,
`hmac <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_,
`hashlib <https://github.com/miguelgrinberg/micropython-lib/blob/ujwt-module/python-stdlib/hashlib>`_,
`warnings <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/warnings/warnings.py>`_
* - Examples
- | `login.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/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('/')
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_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_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

24
docs/index.rst Normal file
View File

@@ -0,0 +1,24 @@
.. Microdot documentation master file, created by
sphinx-quickstart on Fri Jun 4 17:40:19 2021.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Microdot
========
*"The impossibly small web framework for Python and MicroPython"*
Microdot is a minimalistic Python web framework inspired by
`Flask <https://flask.palletsprojects.com/>`_, and designed to run on
systems with limited resources such as microcontrollers. It runs on standard
Python and on `MicroPython <https://micropython.org>`_.
.. toctree::
:maxdepth: 3
intro
extensions
api
* :ref:`genindex`
* :ref:`search`

754
docs/intro.rst Normal file
View File

@@ -0,0 +1,754 @@
Installation
------------
For standard Python (CPython) projects, Microdot and all of its core extensions
can be installed with ``pip``::
pip install microdot
For MicroPython, you can install it with ``upip`` if that option is available,
but the recommended approach is to manually copy *microdot.py* and any
desired optional extension source files from the
`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.
Getting Started
---------------
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 index(request):
return 'Hello, world!'
app.run()
The script imports the :class:`Microdot <microdot.Microdot>` class and creates
an application instance from it.
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.
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.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.py>`_
| `gpio.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/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('/')
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()
@ap.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.
.. 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
a 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 an
exception of that class is raised. The next example provides a custom response
for division by zero errors::
@app.errorhandler(ZeroDivisionError)
def division_by_zero(request):
return {'error': 'division by zero'}, 500
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.

35
docs/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

BIN
examples/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
examples/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
examples/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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.6.15
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.19.1
toml==0.10.2
typing_extensions==4.3.0
urllib3==1.26.11
uvicorn==0.18.2
Werkzeug==2.2.1
wsproto==1.1.0

View File

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

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

@@ -0,0 +1,94 @@
import os
import subprocess
import time
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'], **env},
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
time.sleep(1)
if not name.startswith('baseline'):
r = requests.get('http://localhost:5000')
r.raise_for_status()
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}{humanize.naturalsize(mem):>10} {bar}')
p.terminate()
time.sleep(1)

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')

36
examples/hello_asgi.py Normal file
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')
@@ -33,8 +29,4 @@ async def shutdown(request):
return 'The server is shutting down...'
async def main():
await app.start_server(debug=True)
asyncio.run(main())
app.run(debug=True)

17
examples/hello_jinja.py Normal file
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_jinja.html', name=name)
if __name__ == '__main__':
app.run()

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_utemplate.html', name=name)
if __name__ == '__main__':
app.run()

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()

36
examples/hello_wsgi.py Normal file
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
''')

58
examples/login.py Normal file
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()

19
examples/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>

BIN
examples/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

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,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>

45
examples/video_stream.py Normal file
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)

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)

View File

@@ -0,0 +1,25 @@
try:
import uhashlib
except ImportError:
uhashlib = None
def init():
for i in ("sha1", "sha224", "sha256", "sha384", "sha512"):
try:
c = __import__("_" + i, None, None, (), 1)
except ImportError:
c = uhashlib
c = getattr(c, i, None)
globals()[i] = c
init()
def new(algo, data=b""):
try:
c = globals()[algo]
return c(data)
except KeyError:
raise ValueError(algo)

View File

@@ -0,0 +1 @@
from ._sha256 import sha224

View File

@@ -0,0 +1,301 @@
SHA_BLOCKSIZE = 64
SHA_DIGESTSIZE = 32
def new_shaobject():
return {
"digest": [0] * 8,
"count_lo": 0,
"count_hi": 0,
"data": [0] * SHA_BLOCKSIZE,
"local": 0,
"digestsize": 0,
}
ROR = lambda x, y: (((x & 0xFFFFFFFF) >> (y & 31)) | (x << (32 - (y & 31)))) & 0xFFFFFFFF
Ch = lambda x, y, z: (z ^ (x & (y ^ z)))
Maj = lambda x, y, z: (((x | y) & z) | (x & y))
S = lambda x, n: ROR(x, n)
R = lambda x, n: (x & 0xFFFFFFFF) >> n
Sigma0 = lambda x: (S(x, 2) ^ S(x, 13) ^ S(x, 22))
Sigma1 = lambda x: (S(x, 6) ^ S(x, 11) ^ S(x, 25))
Gamma0 = lambda x: (S(x, 7) ^ S(x, 18) ^ R(x, 3))
Gamma1 = lambda x: (S(x, 17) ^ S(x, 19) ^ R(x, 10))
def sha_transform(sha_info):
W = []
d = sha_info["data"]
for i in range(0, 16):
W.append((d[4 * i] << 24) + (d[4 * i + 1] << 16) + (d[4 * i + 2] << 8) + d[4 * i + 3])
for i in range(16, 64):
W.append((Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]) & 0xFFFFFFFF)
ss = sha_info["digest"][:]
def RND(a, b, c, d, e, f, g, h, i, ki):
t0 = h + Sigma1(e) + Ch(e, f, g) + ki + W[i]
t1 = Sigma0(a) + Maj(a, b, c)
d += t0
h = t0 + t1
return d & 0xFFFFFFFF, h & 0xFFFFFFFF
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 0, 0x428A2F98)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 1, 0x71374491)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 2, 0xB5C0FBCF)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 3, 0xE9B5DBA5)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 4, 0x3956C25B)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 5, 0x59F111F1)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 6, 0x923F82A4)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 7, 0xAB1C5ED5)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 8, 0xD807AA98)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 9, 0x12835B01)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 10, 0x243185BE)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 11, 0x550C7DC3)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 12, 0x72BE5D74)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 13, 0x80DEB1FE)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 14, 0x9BDC06A7)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 15, 0xC19BF174)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 16, 0xE49B69C1)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 17, 0xEFBE4786)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 18, 0x0FC19DC6)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 19, 0x240CA1CC)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 20, 0x2DE92C6F)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 21, 0x4A7484AA)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 22, 0x5CB0A9DC)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 23, 0x76F988DA)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 24, 0x983E5152)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 25, 0xA831C66D)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 26, 0xB00327C8)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 27, 0xBF597FC7)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 28, 0xC6E00BF3)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 29, 0xD5A79147)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 30, 0x06CA6351)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 31, 0x14292967)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 32, 0x27B70A85)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 33, 0x2E1B2138)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 34, 0x4D2C6DFC)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 35, 0x53380D13)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 36, 0x650A7354)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 37, 0x766A0ABB)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 38, 0x81C2C92E)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 39, 0x92722C85)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 40, 0xA2BFE8A1)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 41, 0xA81A664B)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 42, 0xC24B8B70)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 43, 0xC76C51A3)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 44, 0xD192E819)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 45, 0xD6990624)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 46, 0xF40E3585)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 47, 0x106AA070)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 48, 0x19A4C116)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 49, 0x1E376C08)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 50, 0x2748774C)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 51, 0x34B0BCB5)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 52, 0x391C0CB3)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 53, 0x4ED8AA4A)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 54, 0x5B9CCA4F)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 55, 0x682E6FF3)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 56, 0x748F82EE)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 57, 0x78A5636F)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 58, 0x84C87814)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 59, 0x8CC70208)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 60, 0x90BEFFFA)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 61, 0xA4506CEB)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 62, 0xBEF9A3F7)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 63, 0xC67178F2)
dig = []
for i, x in enumerate(sha_info["digest"]):
dig.append((x + ss[i]) & 0xFFFFFFFF)
sha_info["digest"] = dig
def sha_init():
sha_info = new_shaobject()
sha_info["digest"] = [
0x6A09E667,
0xBB67AE85,
0x3C6EF372,
0xA54FF53A,
0x510E527F,
0x9B05688C,
0x1F83D9AB,
0x5BE0CD19,
]
sha_info["count_lo"] = 0
sha_info["count_hi"] = 0
sha_info["local"] = 0
sha_info["digestsize"] = 32
return sha_info
def sha224_init():
sha_info = new_shaobject()
sha_info["digest"] = [
0xC1059ED8,
0x367CD507,
0x3070DD17,
0xF70E5939,
0xFFC00B31,
0x68581511,
0x64F98FA7,
0xBEFA4FA4,
]
sha_info["count_lo"] = 0
sha_info["count_hi"] = 0
sha_info["local"] = 0
sha_info["digestsize"] = 28
return sha_info
def getbuf(s):
if isinstance(s, str):
return s.encode("ascii")
else:
return bytes(s)
def sha_update(sha_info, buffer):
if isinstance(buffer, str):
raise TypeError("Unicode strings must be encoded before hashing")
count = len(buffer)
buffer_idx = 0
clo = (sha_info["count_lo"] + (count << 3)) & 0xFFFFFFFF
if clo < sha_info["count_lo"]:
sha_info["count_hi"] += 1
sha_info["count_lo"] = clo
sha_info["count_hi"] += count >> 29
if sha_info["local"]:
i = SHA_BLOCKSIZE - sha_info["local"]
if i > count:
i = count
# copy buffer
for x in enumerate(buffer[buffer_idx : buffer_idx + i]):
sha_info["data"][sha_info["local"] + x[0]] = x[1]
count -= i
buffer_idx += i
sha_info["local"] += i
if sha_info["local"] == SHA_BLOCKSIZE:
sha_transform(sha_info)
sha_info["local"] = 0
else:
return
while count >= SHA_BLOCKSIZE:
# copy buffer
sha_info["data"] = list(buffer[buffer_idx : buffer_idx + SHA_BLOCKSIZE])
count -= SHA_BLOCKSIZE
buffer_idx += SHA_BLOCKSIZE
sha_transform(sha_info)
# copy buffer
pos = sha_info["local"]
sha_info["data"][pos : pos + count] = list(buffer[buffer_idx : buffer_idx + count])
sha_info["local"] = count
def sha_final(sha_info):
lo_bit_count = sha_info["count_lo"]
hi_bit_count = sha_info["count_hi"]
count = (lo_bit_count >> 3) & 0x3F
sha_info["data"][count] = 0x80
count += 1
if count > SHA_BLOCKSIZE - 8:
# zero the bytes in data after the count
sha_info["data"] = sha_info["data"][:count] + ([0] * (SHA_BLOCKSIZE - count))
sha_transform(sha_info)
# zero bytes in data
sha_info["data"] = [0] * SHA_BLOCKSIZE
else:
sha_info["data"] = sha_info["data"][:count] + ([0] * (SHA_BLOCKSIZE - count))
sha_info["data"][56] = (hi_bit_count >> 24) & 0xFF
sha_info["data"][57] = (hi_bit_count >> 16) & 0xFF
sha_info["data"][58] = (hi_bit_count >> 8) & 0xFF
sha_info["data"][59] = (hi_bit_count >> 0) & 0xFF
sha_info["data"][60] = (lo_bit_count >> 24) & 0xFF
sha_info["data"][61] = (lo_bit_count >> 16) & 0xFF
sha_info["data"][62] = (lo_bit_count >> 8) & 0xFF
sha_info["data"][63] = (lo_bit_count >> 0) & 0xFF
sha_transform(sha_info)
dig = []
for i in sha_info["digest"]:
dig.extend([((i >> 24) & 0xFF), ((i >> 16) & 0xFF), ((i >> 8) & 0xFF), (i & 0xFF)])
return bytes(dig)
class sha256(object):
digest_size = digestsize = SHA_DIGESTSIZE
block_size = SHA_BLOCKSIZE
def __init__(self, s=None):
self._sha = sha_init()
if s:
sha_update(self._sha, getbuf(s))
def update(self, s):
sha_update(self._sha, getbuf(s))
def digest(self):
return sha_final(self._sha.copy())[: self._sha["digestsize"]]
def hexdigest(self):
return "".join(["%.2x" % i for i in self.digest()])
def copy(self):
new = sha256()
new._sha = self._sha.copy()
return new
class sha224(sha256):
digest_size = digestsize = 28
def __init__(self, s=None):
self._sha = sha224_init()
if s:
sha_update(self._sha, getbuf(s))
def copy(self):
new = sha224()
new._sha = self._sha.copy()
return new
def test():
a_str = "just a test string"
assert (
b"\xe3\xb0\xc4B\x98\xfc\x1c\x14\x9a\xfb\xf4\xc8\x99o\xb9$'\xaeA\xe4d\x9b\x93L\xa4\x95\x99\x1bxR\xb8U"
== sha256().digest()
)
assert (
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" == sha256().hexdigest()
)
assert (
"d7b553c6f09ac85d142415f857c5310f3bbbe7cdd787cce4b985acedd585266f"
== sha256(a_str).hexdigest()
)
assert (
"8113ebf33c97daa9998762aacafe750c7cefc2b2f173c90c59663a57fe626f21"
== sha256(a_str * 7).hexdigest()
)
s = sha256(a_str)
s.update(a_str)
assert "03d9963e05a094593190b6fc794cb1a3e1ac7d7883f0b5855268afeccc70d461" == s.hexdigest()
if __name__ == "__main__":
test()

View File

@@ -0,0 +1 @@
from ._sha512 import sha384

View File

@@ -0,0 +1,519 @@
"""
This code was Ported from CPython's sha512module.c
"""
SHA_BLOCKSIZE = 128
SHA_DIGESTSIZE = 64
def new_shaobject():
return {
"digest": [0] * 8,
"count_lo": 0,
"count_hi": 0,
"data": [0] * SHA_BLOCKSIZE,
"local": 0,
"digestsize": 0,
}
ROR64 = (
lambda x, y: (((x & 0xFFFFFFFFFFFFFFFF) >> (y & 63)) | (x << (64 - (y & 63))))
& 0xFFFFFFFFFFFFFFFF
)
Ch = lambda x, y, z: (z ^ (x & (y ^ z)))
Maj = lambda x, y, z: (((x | y) & z) | (x & y))
S = lambda x, n: ROR64(x, n)
R = lambda x, n: (x & 0xFFFFFFFFFFFFFFFF) >> n
Sigma0 = lambda x: (S(x, 28) ^ S(x, 34) ^ S(x, 39))
Sigma1 = lambda x: (S(x, 14) ^ S(x, 18) ^ S(x, 41))
Gamma0 = lambda x: (S(x, 1) ^ S(x, 8) ^ R(x, 7))
Gamma1 = lambda x: (S(x, 19) ^ S(x, 61) ^ R(x, 6))
def sha_transform(sha_info):
W = []
d = sha_info["data"]
for i in range(0, 16):
W.append(
(d[8 * i] << 56)
+ (d[8 * i + 1] << 48)
+ (d[8 * i + 2] << 40)
+ (d[8 * i + 3] << 32)
+ (d[8 * i + 4] << 24)
+ (d[8 * i + 5] << 16)
+ (d[8 * i + 6] << 8)
+ d[8 * i + 7]
)
for i in range(16, 80):
W.append(
(Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]) & 0xFFFFFFFFFFFFFFFF
)
ss = sha_info["digest"][:]
def RND(a, b, c, d, e, f, g, h, i, ki):
t0 = (h + Sigma1(e) + Ch(e, f, g) + ki + W[i]) & 0xFFFFFFFFFFFFFFFF
t1 = (Sigma0(a) + Maj(a, b, c)) & 0xFFFFFFFFFFFFFFFF
d = (d + t0) & 0xFFFFFFFFFFFFFFFF
h = (t0 + t1) & 0xFFFFFFFFFFFFFFFF
return d & 0xFFFFFFFFFFFFFFFF, h & 0xFFFFFFFFFFFFFFFF
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 0, 0x428A2F98D728AE22
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 1, 0x7137449123EF65CD
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 2, 0xB5C0FBCFEC4D3B2F
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 3, 0xE9B5DBA58189DBBC
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 4, 0x3956C25BF348B538
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 5, 0x59F111F1B605D019
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 6, 0x923F82A4AF194F9B
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 7, 0xAB1C5ED5DA6D8118
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 8, 0xD807AA98A3030242
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 9, 0x12835B0145706FBE
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 10, 0x243185BE4EE4B28C
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 11, 0x550C7DC3D5FFB4E2
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 12, 0x72BE5D74F27B896F
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 13, 0x80DEB1FE3B1696B1
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 14, 0x9BDC06A725C71235
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 15, 0xC19BF174CF692694
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 16, 0xE49B69C19EF14AD2
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 17, 0xEFBE4786384F25E3
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 18, 0x0FC19DC68B8CD5B5
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 19, 0x240CA1CC77AC9C65
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 20, 0x2DE92C6F592B0275
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 21, 0x4A7484AA6EA6E483
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 22, 0x5CB0A9DCBD41FBD4
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 23, 0x76F988DA831153B5
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 24, 0x983E5152EE66DFAB
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 25, 0xA831C66D2DB43210
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 26, 0xB00327C898FB213F
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 27, 0xBF597FC7BEEF0EE4
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 28, 0xC6E00BF33DA88FC2
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 29, 0xD5A79147930AA725
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 30, 0x06CA6351E003826F
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 31, 0x142929670A0E6E70
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 32, 0x27B70A8546D22FFC
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 33, 0x2E1B21385C26C926
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 34, 0x4D2C6DFC5AC42AED
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 35, 0x53380D139D95B3DF
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 36, 0x650A73548BAF63DE
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 37, 0x766A0ABB3C77B2A8
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 38, 0x81C2C92E47EDAEE6
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 39, 0x92722C851482353B
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 40, 0xA2BFE8A14CF10364
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 41, 0xA81A664BBC423001
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 42, 0xC24B8B70D0F89791
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 43, 0xC76C51A30654BE30
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 44, 0xD192E819D6EF5218
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 45, 0xD69906245565A910
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 46, 0xF40E35855771202A
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 47, 0x106AA07032BBD1B8
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 48, 0x19A4C116B8D2D0C8
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 49, 0x1E376C085141AB53
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 50, 0x2748774CDF8EEB99
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 51, 0x34B0BCB5E19B48A8
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 52, 0x391C0CB3C5C95A63
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 53, 0x4ED8AA4AE3418ACB
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 54, 0x5B9CCA4F7763E373
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 55, 0x682E6FF3D6B2B8A3
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 56, 0x748F82EE5DEFB2FC
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 57, 0x78A5636F43172F60
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 58, 0x84C87814A1F0AB72
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 59, 0x8CC702081A6439EC
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 60, 0x90BEFFFA23631E28
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 61, 0xA4506CEBDE82BDE9
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 62, 0xBEF9A3F7B2C67915
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 63, 0xC67178F2E372532B
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 64, 0xCA273ECEEA26619C
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 65, 0xD186B8C721C0C207
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 66, 0xEADA7DD6CDE0EB1E
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 67, 0xF57D4F7FEE6ED178
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 68, 0x06F067AA72176FBA
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 69, 0x0A637DC5A2C898A6
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 70, 0x113F9804BEF90DAE
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 71, 0x1B710B35131C471B
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 72, 0x28DB77F523047D84
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 73, 0x32CAAB7B40C72493
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 74, 0x3C9EBE0A15C9BEBC
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 75, 0x431D67C49C100D4C
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 76, 0x4CC5D4BECB3E42B6
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 77, 0x597F299CFC657E2A
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 78, 0x5FCB6FAB3AD6FAEC
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 79, 0x6C44198C4A475817
)
dig = []
for i, x in enumerate(sha_info["digest"]):
dig.append((x + ss[i]) & 0xFFFFFFFFFFFFFFFF)
sha_info["digest"] = dig
def sha_init():
sha_info = new_shaobject()
sha_info["digest"] = [
0x6A09E667F3BCC908,
0xBB67AE8584CAA73B,
0x3C6EF372FE94F82B,
0xA54FF53A5F1D36F1,
0x510E527FADE682D1,
0x9B05688C2B3E6C1F,
0x1F83D9ABFB41BD6B,
0x5BE0CD19137E2179,
]
sha_info["count_lo"] = 0
sha_info["count_hi"] = 0
sha_info["local"] = 0
sha_info["digestsize"] = 64
return sha_info
def sha384_init():
sha_info = new_shaobject()
sha_info["digest"] = [
0xCBBB9D5DC1059ED8,
0x629A292A367CD507,
0x9159015A3070DD17,
0x152FECD8F70E5939,
0x67332667FFC00B31,
0x8EB44A8768581511,
0xDB0C2E0D64F98FA7,
0x47B5481DBEFA4FA4,
]
sha_info["count_lo"] = 0
sha_info["count_hi"] = 0
sha_info["local"] = 0
sha_info["digestsize"] = 48
return sha_info
def getbuf(s):
if isinstance(s, str):
return s.encode("ascii")
else:
return bytes(s)
def sha_update(sha_info, buffer):
if isinstance(buffer, str):
raise TypeError("Unicode strings must be encoded before hashing")
count = len(buffer)
buffer_idx = 0
clo = (sha_info["count_lo"] + (count << 3)) & 0xFFFFFFFF
if clo < sha_info["count_lo"]:
sha_info["count_hi"] += 1
sha_info["count_lo"] = clo
sha_info["count_hi"] += count >> 29
if sha_info["local"]:
i = SHA_BLOCKSIZE - sha_info["local"]
if i > count:
i = count
# copy buffer
for x in enumerate(buffer[buffer_idx : buffer_idx + i]):
sha_info["data"][sha_info["local"] + x[0]] = x[1]
count -= i
buffer_idx += i
sha_info["local"] += i
if sha_info["local"] == SHA_BLOCKSIZE:
sha_transform(sha_info)
sha_info["local"] = 0
else:
return
while count >= SHA_BLOCKSIZE:
# copy buffer
sha_info["data"] = list(buffer[buffer_idx : buffer_idx + SHA_BLOCKSIZE])
count -= SHA_BLOCKSIZE
buffer_idx += SHA_BLOCKSIZE
sha_transform(sha_info)
# copy buffer
pos = sha_info["local"]
sha_info["data"][pos : pos + count] = list(buffer[buffer_idx : buffer_idx + count])
sha_info["local"] = count
def sha_final(sha_info):
lo_bit_count = sha_info["count_lo"]
hi_bit_count = sha_info["count_hi"]
count = (lo_bit_count >> 3) & 0x7F
sha_info["data"][count] = 0x80
count += 1
if count > SHA_BLOCKSIZE - 16:
# zero the bytes in data after the count
sha_info["data"] = sha_info["data"][:count] + ([0] * (SHA_BLOCKSIZE - count))
sha_transform(sha_info)
# zero bytes in data
sha_info["data"] = [0] * SHA_BLOCKSIZE
else:
sha_info["data"] = sha_info["data"][:count] + ([0] * (SHA_BLOCKSIZE - count))
sha_info["data"][112] = 0
sha_info["data"][113] = 0
sha_info["data"][114] = 0
sha_info["data"][115] = 0
sha_info["data"][116] = 0
sha_info["data"][117] = 0
sha_info["data"][118] = 0
sha_info["data"][119] = 0
sha_info["data"][120] = (hi_bit_count >> 24) & 0xFF
sha_info["data"][121] = (hi_bit_count >> 16) & 0xFF
sha_info["data"][122] = (hi_bit_count >> 8) & 0xFF
sha_info["data"][123] = (hi_bit_count >> 0) & 0xFF
sha_info["data"][124] = (lo_bit_count >> 24) & 0xFF
sha_info["data"][125] = (lo_bit_count >> 16) & 0xFF
sha_info["data"][126] = (lo_bit_count >> 8) & 0xFF
sha_info["data"][127] = (lo_bit_count >> 0) & 0xFF
sha_transform(sha_info)
dig = []
for i in sha_info["digest"]:
dig.extend(
[
((i >> 56) & 0xFF),
((i >> 48) & 0xFF),
((i >> 40) & 0xFF),
((i >> 32) & 0xFF),
((i >> 24) & 0xFF),
((i >> 16) & 0xFF),
((i >> 8) & 0xFF),
(i & 0xFF),
]
)
return bytes(dig)
class sha512(object):
digest_size = digestsize = SHA_DIGESTSIZE
block_size = SHA_BLOCKSIZE
def __init__(self, s=None):
self._sha = sha_init()
if s:
sha_update(self._sha, getbuf(s))
def update(self, s):
sha_update(self._sha, getbuf(s))
def digest(self):
return sha_final(self._sha.copy())[: self._sha["digestsize"]]
def hexdigest(self):
return "".join(["%.2x" % i for i in self.digest()])
def copy(self):
new = sha512()
new._sha = self._sha.copy()
return new
class sha384(sha512):
digest_size = digestsize = 48
def __init__(self, s=None):
self._sha = sha384_init()
if s:
sha_update(self._sha, getbuf(s))
def copy(self):
new = sha384()
new._sha = self._sha.copy()
return new
def test():
a_str = "just a test string"
assert (
sha512().digest()
== b"\xcf\x83\xe15~\xef\xb8\xbd\xf1T(P\xd6m\x80\x07\xd6 \xe4\x05\x0bW\x15\xdc\x83\xf4\xa9!\xd3l\xe9\xceG\xd0\xd1<]\x85\xf2\xb0\xff\x83\x18\xd2\x87~\xec/c\xb91\xbdGAz\x81\xa582z\xf9'\xda>"
)
assert (
sha512().hexdigest()
== "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
)
assert (
sha512(a_str).hexdigest()
== "68be4c6664af867dd1d01c8d77e963d87d77b702400c8fabae355a41b8927a5a5533a7f1c28509bbd65c5f3ac716f33be271fbda0ca018b71a84708c9fae8a53"
)
assert (
sha512(a_str * 7).hexdigest()
== "3233acdbfcfff9bff9fc72401d31dbffa62bd24e9ec846f0578d647da73258d9f0879f7fde01fe2cc6516af3f343807fdef79e23d696c923d79931db46bf1819"
)
s = sha512(a_str)
s.update(a_str)
assert (
s.hexdigest()
== "341aeb668730bbb48127d5531115f3c39d12cb9586a6ca770898398aff2411087cfe0b570689adf328cddeb1f00803acce6737a19f310b53bbdb0320828f75bb"
)
if __name__ == "__main__":
test()

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

@@ -0,0 +1,152 @@
"""HMAC (Keyed-Hashing for Message Authentication) Python module.
Implements the HMAC algorithm as described by RFC 2104.
"""
import warnings as _warnings
# from _operator import _compare_digest as compare_digest
import hashlib as _hashlib
PendingDeprecationWarning = None
RuntimeWarning = None
trans_5C = bytes((x ^ 0x5C) for x in range(256))
trans_36 = bytes((x ^ 0x36) for x in range(256))
def translate(d, t):
return bytes(t[x] for x in d)
# The size of the digests returned by HMAC depends on the underlying
# hashing module used. Use digest_size from the instance of HMAC instead.
digest_size = None
class HMAC:
"""RFC 2104 HMAC class. Also complies with RFC 4231.
This supports the API for Cryptographic Hash Functions (PEP 247).
"""
blocksize = 64 # 512-bit HMAC; can be changed in subclasses.
def __init__(self, key, msg=None, digestmod=None):
"""Create a new HMAC object.
key: key for the keyed hash object.
msg: Initial input for the hash, if provided.
digestmod: A module supporting PEP 247. *OR*
A hashlib constructor returning a new hash object. *OR*
A hash name suitable for hashlib.new().
Defaults to hashlib.md5.
Implicit default to hashlib.md5 is deprecated and will be
removed in Python 3.6.
Note: key and msg must be a bytes or bytearray objects.
"""
if not isinstance(key, (bytes, bytearray)):
raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__)
if digestmod is None:
_warnings.warn(
"HMAC() without an explicit digestmod argument " "is deprecated.",
PendingDeprecationWarning,
2,
)
digestmod = _hashlib.md5
if callable(digestmod):
self.digest_cons = digestmod
elif isinstance(digestmod, str):
self.digest_cons = lambda d=b"": _hashlib.new(digestmod, d)
else:
self.digest_cons = lambda d=b"": digestmod.new(d)
self.outer = self.digest_cons()
self.inner = self.digest_cons()
self.digest_size = self.inner.digest_size
if hasattr(self.inner, "block_size"):
blocksize = self.inner.block_size
if blocksize < 16:
_warnings.warn(
"block_size of %d seems too small; using our "
"default of %d." % (blocksize, self.blocksize),
RuntimeWarning,
2,
)
blocksize = self.blocksize
else:
_warnings.warn(
"No block_size attribute on given digest object; "
"Assuming %d." % (self.blocksize),
RuntimeWarning,
2,
)
blocksize = self.blocksize
# self.blocksize is the default blocksize. self.block_size is
# effective block size as well as the public API attribute.
self.block_size = blocksize
if len(key) > blocksize:
key = self.digest_cons(key).digest()
key = key + bytes(blocksize - len(key))
self.outer.update(translate(key, trans_5C))
self.inner.update(translate(key, trans_36))
if msg is not None:
self.update(msg)
@property
def name(self):
return "hmac-" + self.inner.name
def update(self, msg):
"""Update this hashing object with the string msg."""
self.inner.update(msg)
def copy(self):
"""Return a separate copy of this hashing object.
An update to this copy won't affect the original object.
"""
# Call __new__ directly to avoid the expensive __init__.
other = self.__class__.__new__(self.__class__)
other.digest_cons = self.digest_cons
other.digest_size = self.digest_size
other.inner = self.inner.copy()
other.outer = self.outer.copy()
return other
def _current(self):
"""Return a hash object for the current state.
To be used only internally with digest() and hexdigest().
"""
h = self.outer.copy()
h.update(self.inner.digest())
return h
def digest(self):
"""Return the hash value of this hashing object.
This returns a string containing 8-bit data. The object is
not altered in any way by this function; you can continue
updating the object after calling this function.
"""
h = self._current()
return h.digest()
def hexdigest(self):
"""Like digest(), but returns a string of hexadecimal digits instead."""
h = self._current()
return h.hexdigest()
def new(key, msg=None, digestmod=None):
"""Create a new hashing object and return it.
key: The starting key for the hash.
msg: if available, will immediately be hashed into the object's starting
state.
You can now feed arbitrary strings into the object using its update()
method, and can ask for the hash value at any time by calling its digest()
method.
"""
return HMAC(key, msg, digestmod)

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

@@ -0,0 +1,79 @@
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 ExpiredTokenError(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.ExpiredTokenError()
return payload

View File

@@ -0,0 +1,2 @@
def warn(msg, cat=None, stacklevel=1):
print("%s: %s" % ("Warning" if cat is None else cat.__name__, msg))

View File

@@ -1,173 +0,0 @@
try:
import uasyncio as asyncio
except ImportError:
import asyncio
from microdot import Microdot as BaseMicrodot
from microdot import print_exception
from microdot import Request as BaseRequest
from microdot import Response as BaseResponse
def _iscoroutine(coro):
return hasattr(coro, 'send') and hasattr(coro, 'throw')
class Request(BaseRequest):
@staticmethod
async def create(app, stream, client_addr):
# request line
line = (await stream.readline()).strip().decode()
if not line: # pragma: no cover
return None
method, url, http_version = line.split()
http_version = http_version.split('/', 1)[1]
# headers
headers = {}
content_length = 0
while True:
line = (await stream.readline()).strip().decode()
if line == '':
break
header, value = line.split(':', 1)
value = value.strip()
headers[header] = value
if header == 'Content-Length':
content_length = int(value)
# body
body = await stream.read(content_length) \
if content_length else b''
return Request(app, client_addr, method, url, http_version, headers,
body)
class Response(BaseResponse):
async def write(self, stream):
self.complete()
# status code
await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code,
reason='OK' if self.status_code == 200 else 'N/A').encode())
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
await stream.awrite('{header}: {value}\r\n'.format(
header=header, value=value).encode())
await stream.awrite(b'\r\n')
# body
if self.body:
if hasattr(self.body, 'read'):
while True:
buf = self.body.read(self.send_file_buffer_size)
if len(buf):
await stream.awrite(buf)
if len(buf) < self.send_file_buffer_size:
break
if hasattr(self.body, 'close'):
self.body.close()
else:
await stream.awrite(self.body)
class Microdot(BaseMicrodot):
async def start_server(self, host='0.0.0.0', port=5000, debug=False):
self.debug = debug
async def serve(reader, writer):
if not hasattr(writer, 'awrite'): # pragma: no cover
# CPython provides the awrite and aclose methods in 3.8+
async def awrite(self, data):
self.write(data)
await self.drain()
async def aclose(self):
self.close()
await self.wait_closed()
from types import MethodType
writer.awrite = MethodType(awrite, writer)
writer.aclose = MethodType(aclose, writer)
await self.dispatch_request(reader, writer)
if self.debug: # pragma: no cover
print('Starting async server on {host}:{port}...'.format(
host=host, port=port))
self.server = await asyncio.start_server(serve, host, port)
await self.server.wait_closed()
def run(self, host='0.0.0.0', port=5000, debug=False):
asyncio.run(self.start_server(host=host, port=port, debug=debug))
def shutdown(self):
self.server.close()
async def dispatch_request(self, reader, writer):
req = await Request.create(self, reader,
writer.get_extra_info('peername'))
if req:
f = self.find_route(req)
try:
res = None
if f:
for handler in self.before_request_handlers:
res = await self._invoke_handler(handler, req)
if res:
break
if res is None:
res = await self._invoke_handler(
f, req, **req.url_args)
if isinstance(res, tuple):
res = Response(*res)
elif not isinstance(res, Response):
res = Response(res)
for handler in self.after_request_handlers:
res = await self._invoke_handler(
handler, req, res) or res
elif 404 in self.error_handlers:
res = await self._invoke_handler(
self.error_handlers[404], req)
else:
res = 'Not found', 404
except Exception as exc:
print_exception(exc)
res = None
if exc.__class__ in self.error_handlers:
try:
res = await self._invoke_handler(
self.error_handlers[exc.__class__], req, exc)
except Exception as exc2: # pragma: no cover
print_exception(exc2)
if res is None:
if 500 in self.error_handlers:
res = await self._invoke_handler(
self.error_handlers[500], req)
else:
res = 'Internal server error', 500
if isinstance(res, tuple):
res = Response(*res)
elif not isinstance(res, Response):
res = Response(res)
await res.write(writer)
await writer.aclose()
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))
async def _invoke_handler(self, f_or_coro, *args, **kwargs):
ret = f_or_coro(*args, **kwargs)
if _iscoroutine(ret):
ret = await ret
return ret
redirect = Response.redirect
send_file = Response.send_file

View File

@@ -1,35 +0,0 @@
"""
Microdot-AsyncIO
----------------
AsyncIO support for the Microdot web framework.
"""
from setuptools import setup
setup(
name='microdot-asyncio',
version="0.4.0",
url='http://github.com/miguelgrinberg/microdot/',
license='MIT',
author='Miguel Grinberg',
author_email='miguel.grinberg@gmail.com',
description='AsyncIO support for the Microdot web framework',
long_description=__doc__,
py_modules=['microdot_asyncio'],
platforms='any',
install_requires=[
'microdot',
'micropython-uasyncio;implementation_name=="micropython"'
],
classifiers=[
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: Implementation :: MicroPython',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Topic :: Software Development :: Libraries :: Python Modules'
]
)

View File

@@ -1,452 +0,0 @@
try:
from sys import print_exception
except ImportError: # pragma: no cover
import traceback
def print_exception(exc):
traceback.print_exc()
try:
import uerrno as errno
except ImportError:
import errno
concurrency_mode = 'threaded'
try: # pragma: no cover
import threading
def create_thread(f, *args, **kwargs):
"""Use the threading module."""
threading.Thread(target=f, args=args, kwargs=kwargs).start()
except ImportError: # pragma: no cover
try:
import _thread
def create_thread(f, *args, **kwargs):
"""Use MicroPython's _thread module."""
def run():
f(*args, **kwargs)
_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:
import json
try:
import ure as re
except ImportError:
import re
try:
import usocket as socket
except ImportError:
try:
import socket
except ImportError: # pragma: no cover
socket = None
def urldecode(string):
string = string.replace('+', ' ')
parts = string.split('%')
if len(parts) == 1:
return string
result = [parts[0]]
for item in parts[1:]:
if item == '':
result.append('%')
else:
code = item[:2]
result.append(chr(int(code, 16)))
result.append(item[2:])
return ''.join(result)
class Request():
class G:
pass
def __init__(self, app, client_addr, method, url, http_version, headers,
body):
self.app = app
self.client_addr = client_addr
self.method = method
self.path = url
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():
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
self.body = body
self._json = None
self._form = None
self.g = Request.G()
@staticmethod
def create(app, client_stream, client_addr):
# request line
line = client_stream.readline().strip().decode()
if not line: # pragma: no cover
return None
method, url, http_version = line.split()
http_version = http_version.split('/', 1)[1]
# headers
headers = {}
content_length = 0
while True:
line = client_stream.readline().strip().decode()
if line == '':
break
header, value = line.split(':', 1)
value = value.strip()
headers[header] = value
if header == 'Content-Length':
content_length = int(value)
# body
body = client_stream.read(content_length) if content_length else b''
return Request(app, client_addr, method, url, http_version, headers,
body)
def _parse_urlencoded(self, urlencoded):
return {
urldecode(key): urldecode(value) for key, value in [
pair.split('=', 1) for pair in
urlencoded.split('&')]}
@property
def json(self):
if self.content_type != 'application/json':
return None
if self._json is None:
self._json = json.loads(self.body.decode())
return self._json
@property
def form(self):
if self.content_type != 'application/x-www-form-urlencoded':
return None
if self._form is None:
self._form = self._parse_urlencoded(self.body.decode())
return self._form
class Response():
types_map = {
'css': 'text/css',
'gif': 'image/gif',
'html': 'text/html',
'jpg': 'image/jpeg',
'js': 'application/javascript',
'json': 'application/json',
'png': 'image/png',
'txt': 'text/plain',
}
send_file_buffer_size = 1024
def __init__(self, body='', status_code=200, headers=None):
self.status_code = status_code
self.headers = headers or {}
if isinstance(body, (dict, list)):
self.body = json.dumps(body).encode()
self.headers['Content-Type'] = 'application/json'
elif isinstance(body, str):
self.body = body.encode()
else:
# this applies to bytes or file-like objects
self.body = body
def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
max_age=None, secure=False, http_only=False):
http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value)
if path:
http_cookie += '; Path=' + path
if domain:
http_cookie += '; Domain=' + domain
if expires:
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:
http_cookie += '; Secure'
if http_only:
http_cookie += '; HttpOnly'
if 'Set-Cookie' in self.headers:
self.headers['Set-Cookie'].append(http_cookie)
else:
self.headers['Set-Cookie'] = [http_cookie]
def complete(self):
if isinstance(self.body, bytes) and \
'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'
def write(self, stream):
self.complete()
# status code
stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code,
reason='OK' if self.status_code == 200 else 'N/A').encode())
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
stream.write('{header}: {value}\r\n'.format(
header=header, value=value).encode())
stream.write(b'\r\n')
# body
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)
if len(buf) < self.send_file_buffer_size:
break
if hasattr(self.body, 'close'): # pragma: no close
self.body.close()
else:
stream.write(self.body)
@classmethod
def redirect(cls, location, status_code=302):
return cls(status_code=status_code, headers={'Location': location})
@classmethod
def send_file(cls, filename, status_code=200, content_type=None):
if content_type is None:
ext = filename.split('.')[-1]
if ext in Response.types_map:
content_type = Response.types_map[ext]
else:
content_type = 'application/octet-stream'
f = open(filename, 'rb')
return cls(body=f, status_code=status_code,
headers={'Content-Type': content_type})
class URLPattern():
def __init__(self, url_pattern):
self.pattern = ''
self.args = []
use_regex = False
for segment in url_pattern.lstrip('/').split('/'):
if segment and segment[0] == '<':
if segment[-1] != '>':
raise ValueError('invalid URL pattern')
segment = segment[1:-1]
if ':' in segment:
type_, name = segment.rsplit(':', 1)
else:
type_ = 'string'
name = segment
if type_ == 'string':
pattern = '[^/]+'
elif type_ == 'int':
pattern = '\\d+'
elif type_ == 'path':
pattern = '.+'
elif type_.startswith('re:'):
pattern = type_[3:]
else:
raise ValueError('invalid URL segment type')
use_regex = True
self.pattern += '/({pattern})'.format(pattern=pattern)
self.args.append({'type': type_, 'name': name})
else:
self.pattern += '/{segment}'.format(segment=segment)
if use_regex:
self.pattern = re.compile('^' + self.pattern + '$')
def match(self, path):
if isinstance(self.pattern, str):
if path != self.pattern:
return
return {}
g = self.pattern.match(path)
if not g:
return
args = {}
i = 1
for arg in self.args:
value = g.group(i)
if arg['type'] == 'int':
value = int(value)
args[arg['name']] = value
i += 1
return args
class Microdot():
def __init__(self):
self.url_map = []
self.before_request_handlers = []
self.after_request_handlers = []
self.error_handlers = {}
self.shutdown_requested = False
self.debug = False
self.server = None
def route(self, url_pattern, methods=None):
def decorated(f):
self.url_map.append(
(methods or ['GET'], URLPattern(url_pattern), f))
return f
return decorated
def get(self, url_pattern):
return self.route(url_pattern, methods=['GET'])
def post(self, url_pattern):
return self.route(url_pattern, methods=['POST'])
def put(self, url_pattern):
return self.route(url_pattern, methods=['PUT'])
def patch(self, url_pattern):
return self.route(url_pattern, methods=['PATCH'])
def delete(self, url_pattern):
return self.route(url_pattern, methods=['DELETE'])
def before_request(self, f):
self.before_request_handlers.append(f)
return f
def after_request(self, f):
self.after_request_handlers.append(f)
return f
def errorhandler(self, status_code_or_exception_class):
def decorated(f):
self.error_handlers[status_code_or_exception_class] = f
return f
return decorated
def run(self, host='0.0.0.0', port=5000, debug=False):
self.debug = debug
self.shutdown_requested = False
self.server = socket.socket()
ai = socket.getaddrinfo(host, port)
addr = ai[0][-1]
if self.debug: # pragma: no cover
print('Starting {mode} server on {host}:{port}...'.format(
mode=concurrency_mode, host=host, port=port))
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind(addr)
self.server.listen(5)
while not self.shutdown_requested:
try:
sock, addr = self.server.accept()
except OSError as exc:
if exc.args[0] == errno.ECONNABORTED:
break
else:
raise
create_thread(self.dispatch_request, sock, addr)
def shutdown(self):
self.shutdown_requested = True
def find_route(self, req):
f = None
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:
f = route_handler
break
return f
def dispatch_request(self, sock, addr):
if not hasattr(sock, 'readline'): # pragma: no cover
stream = sock.makefile("rwb")
else:
stream = sock
req = Request.create(self, stream, addr)
if req:
f = self.find_route(req)
try:
res = None
if f:
for handler in self.before_request_handlers:
res = handler(req)
if res:
break
if res is None:
res = f(req, **req.url_args)
if isinstance(res, tuple):
res = Response(*res)
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)
else:
res = 'Not found', 404
except Exception as exc:
print_exception(exc)
res = None
if exc.__class__ in self.error_handlers:
try:
res = self.error_handlers[exc.__class__](req, exc)
except Exception as exc2: # pragma: no cover
print_exception(exc2)
if res is None:
if 500 in self.error_handlers:
res = self.error_handlers[500](req)
else:
res = 'Internal server error', 500
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))
redirect = Response.redirect
send_file = Response.send_file

View File

@@ -1,31 +0,0 @@
"""
Microdot
--------
The impossibly small web framework for MicroPython.
"""
from setuptools import setup
setup(
name='microdot',
version="0.4.0",
url='http://github.com/miguelgrinberg/microdot/',
license='MIT',
author='Miguel Grinberg',
author_email='miguel.grinberg@gmail.com',
description='The impossibly small web framework for MicroPython',
long_description=__doc__,
py_modules=['microdot'],
platforms='any',
classifiers=[
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: Implementation :: MicroPython',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Topic :: Software Development :: Libraries :: Python Modules'
]
)

6
pyproject.toml Normal file
View File

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

View File

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

34
setup.cfg Normal file
View File

@@ -0,0 +1,34 @@
[metadata]
name = microdot
version = 1.0.0
author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com
description = The impossibly small web framework for MicroPython
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/miguelgrinberg/microdot
project_urls =
Bug Tracker = https://github.com/miguelgrinberg/microdot/issues
classifiers =
Environment :: Web Environment
Intended Audience :: Developers
Programming Language :: Python :: 3
Programming Language :: Python :: Implementation :: MicroPython
License :: OSI Approved :: MIT License
Operating System :: OS Independent
[options]
zip_safe = False
include_package_data = True
package_dir =
= src
py_modules =
microdot
microdot_asyncio
microdot_utemplate
microdot_jinja
microdot_session
microdot_test_client
microdot_asyncio_test_client
microdot_wsgi
microdot_asgi

3
setup.py Executable file
View File

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

992
src/microdot.py Normal file
View File

@@ -0,0 +1,992 @@
"""
microdot
--------
The ``microdot`` module defines a few classes that help implement HTTP-based
servers for MicroPython and standard Python, with multithreading support for
Python interpreters that support it.
"""
try:
from sys import print_exception
except ImportError: # pragma: no cover
import traceback
def print_exception(exc):
traceback.print_exc()
try:
import uerrno as errno
except ImportError:
import errno
concurrency_mode = 'threaded'
try: # pragma: no cover
import threading
def create_thread(f, *args, **kwargs):
# use the threading module
threading.Thread(target=f, args=args, kwargs=kwargs).start()
except ImportError: # pragma: no cover
def create_thread(f, *args, **kwargs):
# no threads available, call function synchronously
f(*args, **kwargs)
concurrency_mode = 'sync'
try:
import ujson as json
except ImportError:
import json
try:
import ure as re
except ImportError:
import re
try:
import usocket as socket
except ImportError:
try:
import socket
except ImportError: # pragma: no cover
socket = None
def urldecode(string):
string = string.replace('+', ' ')
parts = string.split('%')
if len(parts) == 1:
return string
result = [parts[0]]
for item in parts[1:]:
if item == '':
result.append('%')
else:
code = item[:2]
result.append(chr(int(code, 16)))
result.append(item[2:])
return ''.join(result)
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
form submissions.
:param initial_dict: an initial dictionary of key/value pairs to
initialize this object with.
Example::
>>> d = MultiDict()
>>> d['sort'] = 'name'
>>> d['sort'] = 'email'
>>> print(d['sort'])
'name'
>>> print(d.getlist('sort'))
['name', 'email']
"""
def __init__(self, initial_dict=None):
super().__init__()
if initial_dict:
for key, value in initial_dict.items():
self[key] = value
def __setitem__(self, key, value):
if key not in self:
super().__setitem__(key, [])
super().__getitem__(key).append(value)
def __getitem__(self, key):
return super().__getitem__(key)[0]
def get(self, key, default=None, type=None):
"""Return the value for a given key.
:param key: The key to retrieve.
:param default: A default value to use if the key does not exist.
:param type: A type conversion callable to apply to the value.
If the multidict contains more than one value for the requested key,
this method returns the first value only.
Example::
>>> d = MultiDict()
>>> d['age'] = '42'
>>> d.get('age')
'42'
>>> d.get('age', type=int)
42
>>> d.get('name', default='noname')
'noname'
"""
if key not in self:
return default
value = self[key]
if type is not None:
value = type(value)
return value
def getlist(self, key, type=None):
"""Return all the values for a given key.
:param key: The key to retrieve.
:param type: A type conversion callable to apply to the values.
If the requested key does not exist in the dictionary, this method
returns an empty list.
Example::
>>> d = MultiDict()
>>> d.getlist('items')
[]
>>> d['items'] = '3'
>>> d.getlist('items')
['3']
>>> d['items'] = '56'
>>> d.getlist('items')
['3', '56']
>>> d.getlist('items', type=int)
[3, 56]
"""
if key not in self:
return []
values = super().__getitem__(key)
if type is not None:
values = [type(value) for value in values]
return values
class Request():
"""An HTTP request."""
#: Specify the maximum payload size that is accepted. Requests with larger
#: payloads will be rejected with a 413 status code. Applications can
#: change this maximum as necessary.
#:
#: Example::
#:
#: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed
max_content_length = 16 * 1024
#: Specify the maximum payload size that can be stored in ``body``.
#: Requests with payloads that are larger than this size and up to
#: ``max_content_length`` bytes will be accepted, but the application will
#: only be able to access the body of the request by reading from
#: ``stream``. Set to 0 if you always access the body as a stream.
#:
#: Example::
#:
#: Request.max_body_length = 4 * 1024 # up to 4KB bodies read
max_body_length = 16 * 1024
#: Specify the maximum length allowed for a line in the request. Requests
#: with longer lines will not be correctly interpreted. Applications can
#: change this maximum as necessary.
#:
#: Example::
#:
#: Request.max_readline = 16 * 1024 # 16KB lines allowed
max_readline = 2 * 1024
class G:
pass
def __init__(self, app, client_addr, method, url, http_version, headers,
body=None, stream=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 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)
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
self._body = body
self.body_used = False
self._stream = stream
self.stream_used = False
self._json = None
self._form = None
self.after_request_handlers = []
@staticmethod
def create(app, client_stream, client_addr):
"""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.
This method returns a newly created ``Request`` object.
"""
# request line
line = Request._safe_readline(client_stream).strip().decode()
if not line:
return None
method, url, http_version = line.split()
http_version = http_version.split('/', 1)[1]
# headers
headers = {}
while True:
line = Request._safe_readline(client_stream).strip().decode()
if line == '':
break
header, value = line.split(':', 1)
value = value.strip()
headers[header] = value
return Request(app, client_addr, method, url, http_version, headers,
stream=client_stream)
def _parse_urlencoded(self, urlencoded):
data = MultiDict()
if urlencoded:
for k, v in [pair.split('=', 1) for pair in urlencoded.split('&')]:
data[urldecode(k)] = urldecode(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:
self._body = b''
if self.content_length and \
self.content_length <= Request.max_body_length:
while len(self._body) < self.content_length:
data = self._stream.read(
self.content_length - len(self._body))
if len(data) == 0: # pragma: no cover
raise EOFError()
self._body += data
self.body_used = True
return self._body
@property
def stream(self):
"""The input stream, containing the request body."""
if self.body_used:
raise RuntimeError('Cannot use both stream and body')
self.stream_used = True
return self._stream
@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
mime_type = self.content_type.split(';')[0]
if mime_type != 'application/json':
return None
self._json = json.loads(self.body.decode())
return self._json
@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())
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!'
"""
self.after_request_handlers.append(f)
return f
@staticmethod
def _safe_readline(stream):
line = stream.readline(Request.max_readline + 1)
if len(line) > Request.max_readline:
raise ValueError('line too long')
return line
class Response():
"""An HTTP response class.
:param body: The body of the response. If a dictionary or list is given,
a JSON formatter is used to generate the body. If a file-like
object or a generator is given, a streaming response is used.
If a string is given, it is encoded from UTF-8. Else, the
body should be a byte sequence.
: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.
:param reason: A custom reason phrase to add after the status code. The
default is "OK" for responses with a 200 status code and
"N/A" for any other status codes.
"""
types_map = {
'css': 'text/css',
'gif': 'image/gif',
'html': 'text/html',
'jpg': 'image/jpeg',
'js': 'application/javascript',
'json': 'application/json',
'png': 'image/png',
'txt': 'text/plain',
}
send_file_buffer_size = 1024
#: The content type to use for responses that do not explicitly define a
#: ``Content-Type`` header.
default_content_type = 'text/plain'
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.reason = reason
if isinstance(body, (dict, list)):
self.body = json.dumps(body).encode()
self.headers['Content-Type'] = 'application/json'
elif isinstance(body, str):
self.body = body.encode()
else:
# this applies to bytes, file-like objects or generators
self.body = body
def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
max_age=None, secure=False, http_only=False):
"""Add a cookie to the response.
:param cookie: The cookie's name.
: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
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.
"""
http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value)
if path:
http_cookie += '; Path=' + path
if domain:
http_cookie += '; Domain=' + domain
if expires:
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:
http_cookie += '; Secure'
if http_only:
http_cookie += '; HttpOnly'
if 'Set-Cookie' in self.headers:
self.headers['Set-Cookie'].append(http_cookie)
else:
self.headers['Set-Cookie'] = [http_cookie]
def complete(self):
if isinstance(self.body, bytes) and \
'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'] = self.default_content_type
def write(self, stream):
self.complete()
# status code
reason = self.reason if self.reason is not None else \
('OK' if self.status_code == 200 else 'N/A')
stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code, reason=reason).encode())
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
stream.write('{header}: {value}\r\n'.format(
header=header, value=value).encode())
stream.write(b'\r\n')
# body
can_flush = hasattr(stream, 'flush')
try:
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 == 32: # errno.EPIPE
pass
else:
raise
def body_iter(self):
if self.body:
if hasattr(self.body, 'read'):
while True:
buf = self.body.read(self.send_file_buffer_size)
if len(buf):
yield buf
if len(buf) < self.send_file_buffer_size:
break
if hasattr(self.body, 'close'): # pragma: no cover
self.body.close()
elif hasattr(self.body, '__next__'):
yield from self.body
else:
yield self.body
@classmethod
def redirect(cls, location, status_code=302):
"""Return a redirect response.
:param location: The URL to redirect to.
:param status_code: The 3xx status code to use for the redirect. The
default is 302.
"""
if '\x0d' in location or '\x0a' in location:
raise ValueError('invalid redirect URL')
return cls(status_code=status_code, headers={'Location': location})
@classmethod
def send_file(cls, filename, status_code=200, content_type=None):
"""Send file contents in a response.
:param filename: The filename of the file.
:param status_code: The 3xx status code to use for the redirect. The
default is 302.
:param content_type: The ``Content-Type`` header to use in the
response. If omitted, it is generated
automatically from the file extension.
Security note: The filename is assumed to be trusted. Never pass
filenames provided by the user without validating and sanitizing them
first.
"""
if content_type is None:
ext = filename.split('.')[-1]
if ext in Response.types_map:
content_type = Response.types_map[ext]
else:
content_type = 'application/octet-stream'
f = open(filename, 'rb')
return cls(body=f, status_code=status_code,
headers={'Content-Type': content_type})
class URLPattern():
def __init__(self, url_pattern):
self.url_pattern = url_pattern
self.pattern = ''
self.args = []
use_regex = False
for segment in url_pattern.lstrip('/').split('/'):
if segment and segment[0] == '<':
if segment[-1] != '>':
raise ValueError('invalid URL pattern')
segment = segment[1:-1]
if ':' in segment:
type_, name = segment.rsplit(':', 1)
else:
type_ = 'string'
name = segment
if type_ == 'string':
pattern = '[^/]+'
elif type_ == 'int':
pattern = '\\d+'
elif type_ == 'path':
pattern = '.+'
elif type_.startswith('re:'):
pattern = type_[3:]
else:
raise ValueError('invalid URL segment type')
use_regex = True
self.pattern += '/({pattern})'.format(pattern=pattern)
self.args.append({'type': type_, 'name': name})
else:
self.pattern += '/{segment}'.format(segment=segment)
if use_regex:
self.pattern = re.compile('^' + self.pattern + '$')
def match(self, path):
if isinstance(self.pattern, str):
if path != self.pattern:
return
return {}
g = self.pattern.match(path)
if not g:
return
args = {}
i = 1
for arg in self.args:
value = g.group(i)
if arg['type'] == 'int':
value = int(value)
args[arg['name']] = value
i += 1
return args
class Microdot():
"""An HTTP application class.
This class implements an HTTP application instance and is heavily
influenced by the ``Flask`` class of the Flask framework. It is typically
declared near the start of the main application script.
Example::
from microdot import Microdot
app = Microdot()
"""
def __init__(self):
self.url_map = []
self.before_request_handlers = []
self.after_request_handlers = []
self.error_handlers = {}
self.shutdown_requested = False
self.debug = False
self.server = None
def route(self, url_pattern, methods=None):
"""Decorator that is used to register a function as a request handler
for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
:param methods: The list of HTTP methods to be handled by the
decorated function. If omitted, only ``GET`` requests
are handled.
The URL pattern can be a static path (for example, ``/users`` or
``/api/invoices/search``) or a path with dynamic components enclosed
in ``<`` and ``>`` (for example, ``/users/<id>`` or
``/invoices/<number>/products``). Dynamic path components can also
include a type prefix, separated from the name with a colon (for
example, ``/users/<int:id>``). The type can be ``string`` (the
default), ``int``, ``path`` or ``re:[regular-expression]``.
The first argument of the decorated function must be
the request object. Any path arguments that are specified in the URL
pattern are passed as keyword arguments. The return value of the
function must be a :class:`Response` instance, or the arguments to
be passed to this class.
Example::
@app.route('/')
def index(request):
return 'Hello, world!'
"""
def decorated(f):
self.url_map.append(
(methods or ['GET'], URLPattern(url_pattern), f))
return f
return decorated
def get(self, url_pattern):
"""Decorator that is used to register a function as a ``GET`` request
handler for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
This decorator can be used as an alias to the ``route`` decorator with
``methods=['GET']``.
Example::
@app.get('/users/<int:id>')
def get_user(request, id):
# ...
"""
return self.route(url_pattern, methods=['GET'])
def post(self, url_pattern):
"""Decorator that is used to register a function as a ``POST`` request
handler for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
This decorator can be used as an alias to the``route`` decorator with
``methods=['POST']``.
Example::
@app.post('/users')
def create_user(request):
# ...
"""
return self.route(url_pattern, methods=['POST'])
def put(self, url_pattern):
"""Decorator that is used to register a function as a ``PUT`` request
handler for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
This decorator can be used as an alias to the ``route`` decorator with
``methods=['PUT']``.
Example::
@app.put('/users/<int:id>')
def edit_user(request, id):
# ...
"""
return self.route(url_pattern, methods=['PUT'])
def patch(self, url_pattern):
"""Decorator that is used to register a function as a ``PATCH`` request
handler for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
This decorator can be used as an alias to the ``route`` decorator with
``methods=['PATCH']``.
Example::
@app.patch('/users/<int:id>')
def edit_user(request, id):
# ...
"""
return self.route(url_pattern, methods=['PATCH'])
def delete(self, url_pattern):
"""Decorator that is used to register a function as a ``DELETE``
request handler for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
This decorator can be used as an alias to the ``route`` decorator with
``methods=['DELETE']``.
Example::
@app.delete('/users/<int:id>')
def delete_user(request, id):
# ...
"""
return self.route(url_pattern, methods=['DELETE'])
def before_request(self, f):
"""Decorator to register a function to run before each request is
handled. The decorated function must take a single argument, the
request object.
Example::
@app.before_request
def func(request):
# ...
"""
self.before_request_handlers.append(f)
return f
def after_request(self, f):
"""Decorator to register a function to run after each request is
handled. The decorated function must take two arguments, the request
and response objects. The return value of the function must be an
updated response object.
Example::
@app.after_request
def func(request, response):
# ...
return response
"""
self.after_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,
the request object. Error handler functions for Python exceptions
must accept two arguments, the request object and the exception
object.
:param status_code_or_exception_class: The numeric HTTP status code or
Python exception class to
handle.
Examples::
@app.errorhandler(404)
def not_found(request):
return 'Not found'
@app.errorhandler(RuntimeError)
def runtime_error(request, exception):
return 'Runtime error'
"""
def decorated(f):
self.error_handlers[status_code_or_exception_class] = f
return f
return decorated
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 status_code, handler in subapp.error_handlers.items():
self.error_handlers[status_code] = handler
def run(self, host='0.0.0.0', port=5000, debug=False):
"""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.
:param host: The hostname or IP address of the network interface that
will be listening for requests. A value of ``'0.0.0.0'``
(the default) indicates that the server should listen for
requests on all the available interfaces, and a value of
``127.0.0.1`` indicates that the server should listen
for requests only on the internal networking interface of
the host.
:param port: The port number to listen for requests. The default is
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
Example::
from microdot import Microdot
app = Microdot()
@app.route('/')
def index():
return 'Hello, world!'
app.run(debug=True)
"""
self.debug = debug
self.shutdown_requested = False
self.server = socket.socket()
ai = socket.getaddrinfo(host, port)
addr = ai[0][-1]
if self.debug: # pragma: no cover
print('Starting {mode} server on {host}:{port}...'.format(
mode=concurrency_mode, host=host, port=port))
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind(addr)
self.server.listen(5)
while not self.shutdown_requested:
try:
sock, addr = self.server.accept()
except OSError as exc: # pragma: no cover
if exc.errno == errno.ECONNABORTED:
break
else:
raise
create_thread(self.handle_request, sock, addr)
def shutdown(self):
"""Request a server shutdown. The server will then exit its request
listening loop and the :func:`run` function will return. This function
can be safely called from a route handler, as it only schedules the
server to terminate as soon as the request completes.
Example::
@app.route('/shutdown')
def shutdown(request):
request.app.shutdown()
return 'The server is shutting down...'
"""
self.shutdown_requested = True
def find_route(self, req):
f = 404
for route_methods, route_pattern, route_handler in self.url_map:
req.url_args = route_pattern.match(req.path)
if req.url_args is not None:
if req.method in route_methods:
f = route_handler
break
else:
f = 405
return f
def handle_request(self, sock, addr):
if not hasattr(sock, 'readline'): # pragma: no cover
stream = sock.makefile("rwb")
else:
stream = sock
req = None
try:
req = Request.create(self, stream, addr)
except Exception as exc: # pragma: no cover
print_exception(exc)
res = self.dispatch_request(req)
res.write(stream)
try:
stream.close()
except OSError as exc: # pragma: no cover
if exc.errno == 32: # errno.EPIPE
pass
else:
raise
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):
if req:
if req.content_length > req.max_content_length:
if 413 in self.error_handlers:
res = self.error_handlers[413](req)
else:
res = 'Payload too large', 413
else:
f = self.find_route(req)
try:
res = None
if callable(f):
for handler in self.before_request_handlers:
res = handler(req)
if res:
break
if res is None:
res = f(req, **req.url_args)
if isinstance(res, tuple):
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
for handler in req.after_request_handlers:
res = handler(req, res) or res
elif f in self.error_handlers:
res = self.error_handlers[f](req)
else:
res = 'Not found', f
except Exception as exc:
print_exception(exc)
res = None
if exc.__class__ in self.error_handlers:
try:
res = self.error_handlers[exc.__class__](req, exc)
except Exception as exc2: # pragma: no cover
print_exception(exc2)
if res is None:
if 500 in self.error_handlers:
res = self.error_handlers[500](req)
else:
res = 'Internal server error', 500
else:
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)
return res
redirect = Response.redirect
send_file = Response.send_file

148
src/microdot_asgi.py Normal file
View File

@@ -0,0 +1,148 @@
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
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'] != 'http': # pragma: no cover
return
path = scope['path']
if 'query_string' in scope and scope['query_string']:
path += '?' + scope['query_string'].decode()
headers = {}
content_length = 0
for key, value in scope.get('headers', []):
headers[key] = value
if key.lower() == 'content-length':
content_length = int(value)
body = b''
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['method'],
path,
'HTTP/' + scope['http_version'],
headers,
body=body,
stream=stream)
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, value))
else:
for v in value:
header_list.append((name, v))
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)

397
src/microdot_asyncio.py Normal file
View File

@@ -0,0 +1,397 @@
"""
microdot_asyncio
----------------
The ``microdot_asyncio`` module defines a few classes that help implement
HTTP-based servers for MicroPython and standard Python that use ``asyncio``
and coroutines.
"""
try:
import uasyncio as asyncio
except ImportError:
import asyncio
try:
import uio as io
except ImportError:
import io
from microdot import Microdot as BaseMicrodot
from microdot import print_exception
from microdot import Request as BaseRequest
from microdot import Response as BaseResponse
def _iscoroutine(coro):
return hasattr(coro, 'send') and hasattr(coro, 'throw')
class _AsyncBytesIO:
def __init__(self, data):
self.stream = io.BytesIO(data)
async def read(self, n=-1):
return self.stream.read(n)
async def readline(self): # pragma: no cover
return self.stream.readline()
async def readexactly(self, n): # pragma: no cover
return self.stream.read(n)
async def readuntil(self, separator=b'\n'): # pragma: no cover
return self.stream.readuntil(separator=separator)
class Request(BaseRequest):
@staticmethod
async def create(app, client_stream, client_addr):
"""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.
This method is a coroutine. It returns a newly created ``Request``
object.
"""
# request line
line = (await Request._safe_readline(client_stream)).strip().decode()
if not line:
return None
method, url, http_version = line.split()
http_version = http_version.split('/', 1)[1]
# headers
headers = {}
content_length = 0
while True:
line = (await Request._safe_readline(
client_stream)).strip().decode()
if line == '':
break
header, value = line.split(':', 1)
value = value.strip()
headers[header] = value
if header.lower() == 'content-length':
content_length = int(value)
# body
body = b''
if content_length and content_length <= Request.max_body_length:
body = await client_stream.readexactly(content_length)
stream = None
else:
body = b''
stream = client_stream
return Request(app, client_addr, method, url, http_version, headers,
body=body, stream=stream)
@property
def stream(self):
if self._stream is None:
self._stream = _AsyncBytesIO(self._body)
return self._stream
@staticmethod
async def _safe_readline(stream):
line = (await stream.readline())
if len(line) > Request.max_readline:
raise ValueError('line too long')
return line
class Response(BaseResponse):
"""An HTTP response class.
:param body: The body of the response. If a dictionary or list is given,
a JSON formatter is used to generate the body. If a file-like
object or an async generator is given, a streaming response is
used. If a string is given, it is encoded from UTF-8. Else,
the body should be a byte sequence.
:param status_code: The numeric HTTP status code of the response. The
default is 200.
:param headers: A dictionary of headers to include in the response.
:param reason: A custom reason phrase to add after the status code. The
default is "OK" for responses with a 200 status code and
"N/A" for any other status codes.
"""
async def write(self, stream):
self.complete()
# status code
reason = self.reason if self.reason is not None else \
('OK' if self.status_code == 200 else 'N/A')
await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code, reason=reason).encode())
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
await stream.awrite('{header}: {value}\r\n'.format(
header=header, value=value).encode())
await stream.awrite(b'\r\n')
# body
try:
async for body in self.body_iter():
if isinstance(body, str): # pragma: no cover
body = body.encode()
await stream.awrite(body)
except OSError as exc: # pragma: no cover
if exc.errno == 32 or exc.args[0] == 'Connection lost':
pass
else:
raise
def body_iter(self):
if hasattr(self.body, '__anext__'):
# response body is an async generator
return self.body
response = self
class iter:
def __aiter__(self):
if response.body:
self.i = 0 # need to determine type of response.body
else:
self.i = -1 # no response body
return self
async def __anext__(self):
if self.i == -1:
raise StopAsyncIteration
if self.i == 0:
if hasattr(response.body, 'read'):
self.i = 2 # response body is a file-like object
elif hasattr(response.body, '__next__'):
self.i = 1 # response body is a sync generator
return next(response.body)
else:
self.i = -1 # response body is a plain string
return response.body
elif self.i == 1:
try:
return next(response.body)
except StopIteration:
raise StopAsyncIteration
buf = response.body.read(response.send_file_buffer_size)
if _iscoroutine(buf): # pragma: no cover
buf = await buf
if len(buf) < response.send_file_buffer_size:
self.i = -1
if hasattr(response.body, 'close'): # pragma: no cover
result = response.body.close()
if _iscoroutine(result):
await result
return buf
return iter()
class Microdot(BaseMicrodot):
async def start_server(self, host='0.0.0.0', port=5000, debug=False):
"""Start the Microdot web server as a coroutine. This coroutine does
not normally return, as the server enters an endless listening loop.
The :func:`shutdown` function provides a method for terminating the
server gracefully.
:param host: The hostname or IP address of the network interface that
will be listening for requests. A value of ``'0.0.0.0'``
(the default) indicates that the server should listen for
requests on all the available interfaces, and a value of
``127.0.0.1`` indicates that the server should listen
for requests only on the internal networking interface of
the host.
:param port: The port number to listen for requests. The default is
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
This method is a coroutine.
Example::
import asyncio
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
async def index():
return 'Hello, world!'
async def main():
await app.start_server(debug=True)
asyncio.run(main())
"""
self.debug = debug
async def serve(reader, writer):
if not hasattr(writer, 'awrite'): # pragma: no cover
# CPython provides the awrite and aclose methods in 3.8+
async def awrite(self, data):
self.write(data)
await self.drain()
async def aclose(self):
self.close()
await self.wait_closed()
from types import MethodType
writer.awrite = MethodType(awrite, writer)
writer.aclose = MethodType(aclose, writer)
await self.handle_request(reader, writer)
if self.debug: # pragma: no cover
print('Starting async server on {host}:{port}...'.format(
host=host, port=port))
self.server = await asyncio.start_server(serve, host, port)
while True:
try:
await self.server.wait_closed()
break
except AttributeError: # pragma: no cover
# the task hasn't been initialized in the server object yet
# wait a bit and try again
await asyncio.sleep(0.1)
def run(self, host='0.0.0.0', port=5000, debug=False):
"""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.
:param host: The hostname or IP address of the network interface that
will be listening for requests. A value of ``'0.0.0.0'``
(the default) indicates that the server should listen for
requests on all the available interfaces, and a value of
``127.0.0.1`` indicates that the server should listen
for requests only on the internal networking interface of
the host.
:param port: The port number to listen for requests. The default is
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
Example::
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
async def index():
return 'Hello, world!'
app.run(debug=True)
"""
asyncio.run(self.start_server(host=host, port=port, debug=debug))
def shutdown(self):
self.server.close()
async def handle_request(self, reader, writer):
req = None
try:
req = await Request.create(self, reader,
writer.get_extra_info('peername'))
except Exception as exc: # pragma: no cover
print_exception(exc)
res = await self.dispatch_request(req)
await res.write(writer)
try:
await writer.aclose()
except OSError as exc: # pragma: no cover
if exc.errno == 32: # errno.EPIPE
pass
else:
raise
if self.debug and req: # pragma: no cover
print('{method} {path} {status_code}'.format(
method=req.method, path=req.path,
status_code=res.status_code))
async def dispatch_request(self, req):
if req:
if req.content_length > req.max_content_length:
if 413 in self.error_handlers:
res = await self._invoke_handler(
self.error_handlers[413], req)
else:
res = 'Payload too large', 413
else:
f = self.find_route(req)
try:
res = None
if callable(f):
for handler in self.before_request_handlers:
res = await self._invoke_handler(handler, req)
if res:
break
if res is None:
res = await self._invoke_handler(
f, req, **req.url_args)
if isinstance(res, tuple):
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 = await self._invoke_handler(
handler, req, res) or res
for handler in req.after_request_handlers:
res = await handler(req, res) or res
elif f in self.error_handlers:
res = await self._invoke_handler(
self.error_handlers[f], req)
else:
res = 'Not found', f
except Exception as exc:
print_exception(exc)
res = None
if exc.__class__ in self.error_handlers:
try:
res = await self._invoke_handler(
self.error_handlers[exc.__class__], req, exc)
except Exception as exc2: # pragma: no cover
print_exception(exc2)
if res is None:
if 500 in self.error_handlers:
res = await self._invoke_handler(
self.error_handlers[500], req)
else:
res = 'Internal server error', 500
else:
if 400 in self.error_handlers:
res = await self._invoke_handler(self.error_handlers[400], req)
else:
res = 'Bad request', 400
if isinstance(res, tuple):
res = Response(*res)
elif not isinstance(res, Response):
res = Response(res)
return res
async def _invoke_handler(self, f_or_coro, *args, **kwargs):
ret = f_or_coro(*args, **kwargs)
if _iscoroutine(ret):
ret = await ret
return ret
redirect = Response.redirect
send_file = Response.send_file

View File

@@ -0,0 +1,126 @@
from microdot_asyncio import Request, _AsyncBytesIO
from microdot_test_client import TestClient as BaseTestClient, \
TestResponse as BaseTestResponse
class TestResponse(BaseTestResponse):
"""A response object issued by the Microdot test client."""
@classmethod
async def create(cls, res):
test_res = cls()
test_res._initialize_response(res)
await test_res._initialize_body(res)
test_res._process_text_body()
test_res._process_json_body()
return test_res
async def _initialize_body(self, res):
self.body = b''
async for body in res.body_iter():
if isinstance(body, str):
body = body.encode()
self.body += body
class TestClient(BaseTestClient):
"""A test client for Microdot's Asynchronous web server.
:param app: The Microdot application instance.
:param cookies: A dictionary of cookies to use when sending requests to the
application.
The following example shows how to create a test client for an application
and send a test request::
from microdot_asyncio import Microdot
app = Microdot()
@app.get('/')
async def index():
return 'Hello, World!'
async def test_hello_world(self):
client = TestClient(app)
res = await client.get('/')
assert res.status_code == 200
assert res.text == 'Hello, World!'
"""
async def request(self, method, path, headers=None, body=None):
headers = headers or {}
body, headers = self._process_body(body, headers)
cookies, headers = self._process_cookies(headers)
request_bytes = self._render_request(method, path, headers, body)
req = await Request.create(self.app, _AsyncBytesIO(request_bytes),
('127.0.0.1', 1234))
res = await self.app.dispatch_request(req)
res.complete()
self._update_cookies(res)
return await TestResponse.create(res)
async def get(self, path, headers=None):
"""Send a GET request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return await self.request('GET', path, headers=headers)
async def post(self, path, headers=None, body=None):
"""Send a POST request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
:param body: The request body. If a dictionary or list is provided,
a JSON-encoded body will be sent. A string body is encoded
to bytes as UTF-8. A bytes body is sent as-is.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return await self.request('POST', path, headers=headers, body=body)
async def put(self, path, headers=None, body=None):
"""Send a PUT request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
:param body: The request body. If a dictionary or list is provided,
a JSON-encoded body will be sent. A string body is encoded
to bytes as UTF-8. A bytes body is sent as-is.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return await self.request('PUT', path, headers=headers, body=body)
async def patch(self, path, headers=None, body=None):
"""Send a PATCH request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
:param body: The request body. If a dictionary or list is provided,
a JSON-encoded body will be sent. A string body is encoded
to bytes as UTF-8. A bytes body is sent as-is.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return await self.request('PATCH', path, headers=headers, body=body)
async def delete(self, path, headers=None):
"""Send a DELETE request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return await self.request('DELETE', path, headers=headers)

33
src/microdot_jinja.py Normal file
View File

@@ -0,0 +1,33 @@
from jinja2 import Environment, FileSystemLoader, select_autoescape
_jinja_env = None
def init_templates(template_dir='templates'):
"""Initialize the templating subsystem.
:param template_dir: the directory where templates are stored. This
argument is optional. The default is to load templates
from a *templates* subdirectory.
"""
global _jinja_env
_jinja_env = Environment(
loader=FileSystemLoader(template_dir),
autoescape=select_autoescape()
)
def render_template(template, *args, **kwargs):
"""Render a template.
:param template: The filename of the template to render, relative to the
configured template directory.
:param args: Positional arguments to be passed to the render engine.
:param kwargs: Keyword arguments to be passed to the render engine.
The return value is a string with the rendered template.
"""
if _jinja_env is None: # pragma: no cover
init_templates()
template = _jinja_env.get_template(template)
return template.render(*args, **kwargs)

94
src/microdot_session.py Normal file
View File

@@ -0,0 +1,94 @@
import jwt
secret_key = None
def set_session_secret_key(key):
"""Set the secret key for signing user sessions.
:param key: The secret key, as a string or bytes object.
"""
global secret_key
secret_key = key
def get_session(request):
"""Retrieve the user session.
:param request: The client request.
The return value is a dictionary with the data stored in the user's
session, or ``{}`` if the session data is not available or invalid.
"""
global secret_key
if not secret_key:
raise ValueError('The session secret key is not configured')
session = request.cookies.get('session')
if session is None:
return {}
try:
session = jwt.decode(session, secret_key, algorithms=['HS256'])
except jwt.exceptions.PyJWTError: # pragma: no cover
raise
return {}
return session
def update_session(request, session):
"""Update the user session.
:param request: The client request.
:param session: A dictionary with the update session data for the user.
Calling this function adds a cookie with the updated session to the request
currently being processed.
"""
if not secret_key:
raise ValueError('The session secret key is not configured')
encoded_session = jwt.encode(session, secret_key, algorithm='HS256')
@request.after_request
def _update_session(request, response):
response.set_cookie('session', encoded_session, http_only=True)
return response
def delete_session(request):
"""Remove the user session.
:param request: The client request.
Calling this function adds a cookie removal header to the request currently
being processed.
"""
@request.after_request
def _delete_session(request, response):
response.set_cookie('session', '', http_only=True,
expires='Thu, 01 Jan 1970 00:00:01 GMT')
return response
def with_session(f):
"""Decorator that passes the user session to the route handler.
The session dictionary is passed to the decorated function as an argument
after the request object. Example::
@app.route('/')
@with_session
def index(request, session):
return 'Hello, World!'
Note that the decorator does not save the session. To update the session,
call the :func:`update_session <microdot_session.update_session>` function.
"""
def wrapper(request, *args, **kwargs):
return f(request, get_session(request), *args, **kwargs)
for attr in ['__name__', '__doc__', '__module__', '__qualname__']:
try:
setattr(wrapper, attr, getattr(f, attr))
except AttributeError: # pragma: no cover
pass
return wrapper

226
src/microdot_test_client.py Normal file
View File

@@ -0,0 +1,226 @@
from io import BytesIO
import json
from microdot import Request
class TestResponse:
"""A response object issued by the Microdot test client."""
def __init__(self):
#: The numeric status code returned by the server.
self.status_code = None
#: The text reason associated with the status response, such as
#: ``'OK'`` or ``'NOT FOUND'``. Set to ``None`` unless the application
#: explicitly sets it on the response object.
self.reason = None
#: A dictionary with the response headers.
self.headers = None
#: The body of the response, as a bytes object.
self.body = None
#: The body of the response, decoded to a UTF-8 string. Set to
#: ``None`` if the response cannot be represented as UTF-8 text.
self.text = None
#: The body of the JSON response, decoded to a dictionary or list. Set
#: ``Note`` if the response does not have a JSON payload.
self.json = None
def _initialize_response(self, res):
self.status_code = res.status_code
self.reason = res.reason
self.headers = res.headers
def _initialize_body(self, res):
self.body = b''
for body in res.body_iter():
if isinstance(body, str):
body = body.encode()
self.body += body
def _process_text_body(self):
try:
self.text = self.body.decode()
except ValueError:
pass
def _process_json_body(self):
for name, value in self.headers.items(): # pragma: no branch
if name.lower() == 'content-type':
if value.lower() == 'application/json':
self.json = json.loads(self.text)
break
@classmethod
def create(cls, res):
test_res = cls()
test_res._initialize_response(res)
test_res._initialize_body(res)
test_res._process_text_body()
test_res._process_json_body()
return test_res
class TestClient:
"""A test client for Microdot.
:param app: The Microdot application instance.
:param cookies: A dictionary of cookies to use when sending requests to the
application.
The following example shows how to create a test client for an application
and send a test request::
from microdot import Microdot
app = Microdot()
@app.get('/')
def index():
return 'Hello, World!'
def test_hello_world(self):
client = TestClient(app)
res = client.get('/')
assert res.status_code == 200
assert res.text == 'Hello, World!'
"""
def __init__(self, app, cookies=None):
self.app = app
self.cookies = cookies or {}
def _process_body(self, body, headers):
if body is None:
body = b''
elif isinstance(body, (dict, list)):
body = json.dumps(body).encode()
if 'Content-Type' not in headers and \
'content-type' not in headers: # pragma: no cover
headers['Content-Type'] = 'application/json'
elif isinstance(body, str):
body = body.encode()
if body and 'Content-Length' not in headers and \
'content-length' not in headers:
headers['Content-Length'] = str(len(body))
if 'Host' not in headers: # pragma: no branch
headers['Host'] = 'example.com:1234'
return body, headers
def _process_cookies(self, headers):
cookies = ''
for name, value in self.cookies.items():
if cookies:
cookies += '; '
cookies += name + '=' + value
if cookies:
if 'Cookie' in headers:
headers['Cookie'] += '; ' + cookies
else:
headers['Cookie'] = cookies
return cookies, headers
def _render_request(self, method, path, headers, body):
request_bytes = '{method} {path} HTTP/1.0\n'.format(
method=method, path=path)
for header, value in headers.items():
request_bytes += '{header}: {value}\n'.format(
header=header, value=value)
request_bytes = request_bytes.encode() + b'\n' + body
return request_bytes
def _update_cookies(self, res):
for name, value in res.headers.items():
if name.lower() == 'set-cookie':
for cookie in value:
cookie_name, cookie_value = cookie.split('=', 1)
cookie_options = cookie_value.split(';')
delete = False
for option in cookie_options[1:]:
if option.strip().lower().startswith('expires='):
_, e = option.strip().split('=', 1)
# this is a very limited parser for cookie expiry
# that only detects a cookie deletion request when
# the date is 1/1/1970
if '1 jan 1970' in e.lower(): # pragma: no branch
delete = True
break
if delete:
if cookie_name in self.cookies: # pragma: no branch
del self.cookies[cookie_name]
else:
self.cookies[cookie_name] = cookie_options[0]
def request(self, method, path, headers=None, body=None):
headers = headers or {}
body, headers = self._process_body(body, headers)
cookies, headers = self._process_cookies(headers)
request_bytes = self._render_request(method, path, headers, body)
req = Request.create(self.app, BytesIO(request_bytes),
('127.0.0.1', 1234))
res = self.app.dispatch_request(req)
res.complete()
self._update_cookies(res)
return TestResponse.create(res)
def get(self, path, headers=None):
"""Send a GET request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return self.request('GET', path, headers=headers)
def post(self, path, headers=None, body=None):
"""Send a POST request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
:param body: The request body. If a dictionary or list is provided,
a JSON-encoded body will be sent. A string body is encoded
to bytes as UTF-8. A bytes body is sent as-is.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return self.request('POST', path, headers=headers, body=body)
def put(self, path, headers=None, body=None):
"""Send a PUT request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
:param body: The request body. If a dictionary or list is provided,
a JSON-encoded body will be sent. A string body is encoded
to bytes as UTF-8. A bytes body is sent as-is.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return self.request('PUT', path, headers=headers, body=body)
def patch(self, path, headers=None, body=None):
"""Send a PATCH request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
:param body: The request body. If a dictionary or list is provided,
a JSON-encoded body will be sent. A string body is encoded
to bytes as UTF-8. A bytes body is sent as-is.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return self.request('PATCH', path, headers=headers, body=body)
def delete(self, path, headers=None):
"""Send a DELETE request to the application.
:param path: The request URL.
:param headers: A dictionary of headers to send with the request.
This method returns a
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return self.request('DELETE', path, headers=headers)

34
src/microdot_utemplate.py Normal file
View File

@@ -0,0 +1,34 @@
from utemplate import recompile
_loader = None
def init_templates(template_dir='templates', loader_class=recompile.Loader):
"""Initialize the templating subsystem.
:param template_dir: the directory where templates are stored. This
argument is optional. The default is to load templates
from a *templates* subdirectory.
:param loader_class: the ``utemplate.Loader`` class to use when loading
templates. This argument is optional. The default is
the ``recompile.Loader`` class, which automatically
recompiles templates when they change.
"""
global _loader
_loader = loader_class(None, template_dir)
def render_template(template, *args, **kwargs):
"""Render a template.
:param template: The filename of the template to render, relative to the
configured template directory.
:param args: Positional arguments to be passed to the render engine.
:param kwargs: Keyword arguments to be passed to the render engine.
The return value is an iterator that returns sections of rendered template.
"""
if _loader is None: # pragma: no cover
init_templates()
render = _loader.load(template)
return render(*args, **kwargs)

64
src/microdot_wsgi.py Normal file
View File

@@ -0,0 +1,64 @@
import os
import signal
from microdot import * # noqa: F401, F403
from microdot import Microdot as BaseMicrodot
from microdot import Request
class Microdot(BaseMicrodot):
def __init__(self):
super().__init__()
self.embedded_server = False
def wsgi_app(self, environ, start_response):
"""A WSGI application callable."""
path = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '')
if 'QUERY_STRING' in environ and environ['QUERY_STRING']:
path += '?' + environ['QUERY_STRING']
headers = {}
for k, v in environ.items():
if k.startswith('HTTP_'):
h = '-'.join([p.title() for p in k[5:].split('_')])
headers[h] = v
req = Request(
self,
(environ['REMOTE_ADDR'], int(environ.get('REMOTE_PORT', '0'))),
environ['REQUEST_METHOD'],
path,
environ['SERVER_PROTOCOL'],
headers,
stream=environ['wsgi.input'])
req.environ = environ
res = self.dispatch_request(req)
res.complete()
reason = res.reason or ('OK' if res.status_code == 200 else 'N/A')
header_list = []
for name, value in res.headers.items():
if not isinstance(value, list):
header_list.append((name, value))
else:
for v in value:
header_list.append((name, v))
start_response(str(res.status_code) + ' ' + reason, header_list)
return res.body_iter()
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
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 WSGI web server and pass the ``Microdot``
instance as the WSGI callable.
"""
self.embedded_server = True
super().run(host=host, port=port, debug=debug, **options)

View File

@@ -1,8 +1,13 @@
from tests.microdot.test_request import TestRequest
from tests.microdot.test_response import TestResponse
from tests.microdot.test_url_pattern import TestURLPattern
from tests.microdot.test_microdot import TestMicrodot
from .test_multidict import TestMultiDict
from .test_request import TestRequest
from .test_response import TestResponse
from .test_url_pattern import TestURLPattern
from .test_microdot import TestMicrodot
from tests.microdot_asyncio.test_request_asyncio import TestRequestAsync
from tests.microdot_asyncio.test_response_asyncio import TestResponseAsync
from tests.microdot_asyncio.test_microdot_asyncio import TestMicrodotAsync
from .test_request_asyncio import TestRequestAsync
from .test_response_asyncio import TestResponseAsync
from .test_microdot_asyncio import TestMicrodotAsync
from .test_utemplate import TestUTemplate
from .test_session import TestSession

View File

@@ -1,206 +0,0 @@
import sys
import unittest
from microdot import Microdot, Response
from tests import mock_socket
def mock_create_thread(f, *args, **kwargs):
f(*args, **kwargs)
class TestMicrodot(unittest.TestCase):
def setUp(self):
# mock socket module
self.original_socket = sys.modules['microdot'].socket
self.original_create_thread = sys.modules['microdot'].create_thread
sys.modules['microdot'].socket = mock_socket
sys.modules['microdot'].create_thread = mock_create_thread
def tearDown(self):
# restore original socket module
sys.modules['microdot'].socket = self.original_socket
sys.modules['microdot'].create_thread = self.original_create_thread
def _add_shutdown(self, app):
@app.route('/shutdown')
def shutdown(req):
app.shutdown()
return ''
mock_socket.add_request('GET', '/shutdown')
def test_get_request(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nfoo'))
def test_post_request(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.route('/', methods=['POST'])
def index_post(req):
return Response('bar')
mock_socket.clear_requests()
fd = mock_socket.add_request('POST', '/')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nbar'))
def test_before_after_request(self):
app = Microdot()
@app.before_request
def before_request(req):
if req.path == '/bar':
return 'bar', 202
req.g.message = 'baz'
@app.after_request
def after_request_one(req, res):
res.headers['X-One'] = '1'
@app.after_request
def after_request_two(req, res):
res.set_cookie('foo', 'bar')
return res
@app.route('/bar')
def bar(req):
return 'foo'
@app.route('/baz')
def baz(req):
return req.g.message
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/bar')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 202 N/A\r\n'))
self.assertIn(b'X-One: 1\r\n', fd.response)
self.assertIn(b'Set-Cookie: foo=bar\r\n', fd.response)
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nbar'))
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/baz')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'X-One: 1\r\n', fd.response)
self.assertIn(b'Set-Cookie: foo=bar\r\n', fd.response)
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nbaz'))
def test_404(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/foo')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 404 N/A\r\n'))
self.assertIn(b'Content-Length: 9\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nNot found'))
def test_404_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.errorhandler(404)
def handle_404(req):
return '404'
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/foo')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n404'))
def test_500(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 500 N/A\r\n'))
self.assertIn(b'Content-Length: 21\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nInternal server error'))
def test_500_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
@app.errorhandler(500)
def handle_500(req):
return '501', 501
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 501 N/A\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n501'))
def test_exception_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
@app.errorhandler(ZeroDivisionError)
def handle_div_zero(req, exc):
return '501', 501
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 501 N/A\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n501'))

View File

@@ -1,217 +0,0 @@
import sys
import unittest
from microdot_asyncio import Microdot, Response
from tests import mock_asyncio, mock_socket
class TestMicrodotAsync(unittest.TestCase):
def setUp(self):
# mock socket module
self.original_asyncio = sys.modules['microdot_asyncio'].asyncio
sys.modules['microdot_asyncio'].asyncio = mock_asyncio
def tearDown(self):
# restore original socket module
sys.modules['microdot_asyncio'].asyncio = self.original_asyncio
def _add_shutdown(self, app):
@app.route('/shutdown')
def shutdown(req):
app.shutdown()
return ''
mock_socket.add_request('GET', '/shutdown')
def test_get_request(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.route('/async')
async def index2(req):
return 'foo-async'
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/')
fd2 = mock_socket.add_request('GET', '/async')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nfoo'))
self.assertTrue(fd2.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 9\r\n', fd2.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd2.response)
self.assertTrue(fd2.response.endswith(b'\r\n\r\nfoo-async'))
def test_post_request(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.route('/', methods=['POST'])
def index_post(req):
return Response('bar')
@app.route('/async', methods=['POST'])
async def index_post2(req):
return Response('bar-async')
mock_socket.clear_requests()
fd = mock_socket.add_request('POST', '/')
fd2 = mock_socket.add_request('POST', '/async')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nbar'))
self.assertTrue(fd2.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 9\r\n', fd2.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd2.response)
self.assertTrue(fd2.response.endswith(b'\r\n\r\nbar-async'))
def test_before_after_request(self):
app = Microdot()
@app.before_request
def before_request(req):
if req.path == '/bar':
return 'bar', 202
req.g.message = 'baz'
@app.after_request
def after_request_one(req, res):
res.headers['X-One'] = '1'
@app.after_request
async def after_request_two(req, res):
res.set_cookie('foo', 'bar')
return res
@app.route('/bar')
def bar(req):
return 'foo'
@app.route('/baz')
def baz(req):
return req.g.message
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/bar')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 202 N/A\r\n'))
self.assertIn(b'X-One: 1\r\n', fd.response)
self.assertIn(b'Set-Cookie: foo=bar\r\n', fd.response)
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nbar'))
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/baz')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'X-One: 1\r\n', fd.response)
self.assertIn(b'Set-Cookie: foo=bar\r\n', fd.response)
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nbaz'))
def test_404(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/foo')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 404 N/A\r\n'))
self.assertIn(b'Content-Length: 9\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nNot found'))
def test_404_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.errorhandler(404)
async def handle_404(req):
return '404'
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/foo')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n404'))
def test_500(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 500 N/A\r\n'))
self.assertIn(b'Content-Length: 21\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nInternal server error'))
def test_500_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
@app.errorhandler(500)
def handle_500(req):
return '501', 501
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 501 N/A\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n501'))
def test_exception_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
@app.errorhandler(ZeroDivisionError)
async def handle_div_zero(req, exc):
return '501', 501
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 501 N/A\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n501'))

View File

@@ -56,7 +56,10 @@ class FakeStreamAsync:
async def readline(self):
return self.stream.readline()
async def read(self, n):
async def read(self, n=-1):
return self.stream.read(n)
async def readexactly(self, n):
return self.stream.read(n)
async def awrite(self, data):

View File

@@ -0,0 +1 @@
Hello, {{ name }}!

View File

@@ -0,0 +1,2 @@
{% args name %}
Hello, {{ name }}!

58
tests/test_jinja.py Normal file
View File

@@ -0,0 +1,58 @@
try:
import uasyncio as asyncio
except ImportError:
import asyncio
import sys
import unittest
from microdot import Microdot, Request
from microdot_asyncio import Microdot as MicrodotAsync, Request as RequestAsync
from microdot_jinja import render_template, init_templates
from tests.mock_socket import get_request_fd, get_async_request_fd
init_templates('tests/templates')
def _run(coro):
return asyncio.get_event_loop().run_until_complete(coro)
@unittest.skipIf(sys.implementation.name == 'micropython',
'not supported under MicroPython')
class TestUTemplate(unittest.TestCase):
def test_render_template(self):
s = render_template('hello.jinja.txt', name='foo')
self.assertEqual(s, 'Hello, foo!')
def test_render_template_in_app(self):
app = Microdot()
@app.route('/')
def index(req):
return render_template('hello.jinja.txt', name='foo')
req = Request.create(app, get_request_fd('GET', '/'), 'addr')
res = app.dispatch_request(req)
self.assertEqual(res.status_code, 200)
self.assertEqual(list(res.body_iter()), [b'Hello, foo!'])
def test_render_template_in_app_async(self):
app = MicrodotAsync()
@app.route('/')
async def index(req):
return render_template('hello.jinja.txt', name='foo')
req = _run(RequestAsync.create(
app, get_async_request_fd('GET', '/'), 'addr'))
res = _run(app.dispatch_request(req))
self.assertEqual(res.status_code, 200)
async def get_result():
result = []
async for chunk in res.body_iter():
result.append(chunk)
return result
result = _run(get_result())
self.assertEqual(result, [b'Hello, foo!'])

529
tests/test_microdot.py Normal file
View File

@@ -0,0 +1,529 @@
import sys
import unittest
from microdot import Microdot, Response
from microdot_test_client import TestClient
from tests import mock_socket
class TestMicrodot(unittest.TestCase):
def _mock(self):
def mock_create_thread(f, *args, **kwargs):
f(*args, **kwargs)
self.original_socket = sys.modules['microdot'].socket
self.original_create_thread = sys.modules['microdot'].create_thread
sys.modules['microdot'].socket = mock_socket
sys.modules['microdot'].create_thread = mock_create_thread
def _unmock(self):
sys.modules['microdot'].socket = self.original_socket
sys.modules['microdot'].create_thread = self.original_create_thread
def _add_shutdown(self, app):
@app.route('/shutdown')
def shutdown(req):
app.shutdown()
return ''
mock_socket.add_request('GET', '/shutdown')
def test_get_request(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'foo')
self.assertEqual(res.body, b'foo')
self.assertEqual(res.json, None)
def test_post_request(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.route('/', methods=['POST'])
def index_post(req):
return Response('bar')
client = TestClient(app)
res = client.post('/')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'bar')
def test_empty_request(self):
self._mock()
app = Microdot()
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 11\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
self._unmock()
def test_method_decorators(self):
app = Microdot()
@app.get('/get')
def get(req):
return 'GET'
@app.post('/post')
def post(req):
return 'POST'
@app.put('/put')
def put(req):
return 'PUT'
@app.patch('/patch')
def patch(req):
return 'PATCH'
@app.delete('/delete')
def delete(req):
return 'DELETE'
client = TestClient(app)
methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
for method in methods:
res = getattr(client, method.lower())('/' + method.lower())
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, method)
def test_headers(self):
app = Microdot()
@app.route('/')
def index(req):
return req.headers.get('X-Foo')
client = TestClient(app)
res = client.get('/', headers={'X-Foo': 'bar'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'bar')
def test_cookies(self):
app = Microdot()
@app.route('/')
def index(req):
return req.cookies['one'] + req.cookies['two'] + \
req.cookies['three']
client = TestClient(app, cookies={'one': '1', 'two': '2'})
res = client.get('/', headers={'Cookie': 'three=3'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '123')
def test_binary_payload(self):
app = Microdot()
@app.post('/')
def index(req):
return req.body
client = TestClient(app)
res = client.post('/', body=b'foo')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'foo')
def test_json_payload(self):
app = Microdot()
@app.post('/dict')
def json_dict(req):
print(req.headers)
return req.json.get('foo')
@app.post('/list')
def json_list(req):
return req.json[0]
client = TestClient(app)
res = client.post('/dict', body={'foo': 'bar'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'bar')
res = client.post('/list', body=['foo', 'bar'])
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'foo')
def test_tuple_responses(self):
app = Microdot()
@app.route('/body')
def one(req):
return 'one'
@app.route('/body-status')
def two(req):
return 'two', 202
@app.route('/body-headers')
def three(req):
return '<p>three</p>', {'Content-Type': 'text/html'}
@app.route('/body-status-headers')
def four(req):
return '<p>four</p>', 202, {'Content-Type': 'text/html'}
client = TestClient(app)
res = client.get('/body')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'one')
res = client.get('/body-status')
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'two')
res = client.get('/body-headers')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/html')
self.assertEqual(res.text, '<p>three</p>')
res = client.get('/body-status-headers')
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/html')
self.assertEqual(res.text, '<p>four</p>')
def test_before_after_request(self):
app = Microdot()
@app.before_request
def before_request(req):
if req.path == '/bar':
@req.after_request
def after_request(req, res):
res.headers['X-Two'] = '2'
return res
return 'bar', 202
req.g.message = 'baz'
@app.after_request
def after_request_one(req, res):
res.headers['X-One'] = '1'
@app.after_request
def after_request_two(req, res):
res.set_cookie('foo', 'bar')
return res
@app.route('/bar')
def bar(req):
return 'foo'
@app.route('/baz')
def baz(req):
return req.g.message
client = TestClient(app)
res = client.get('/bar')
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertEqual(res.headers['X-Two'], '2')
self.assertEqual(res.text, 'bar')
self.assertEqual(client.cookies['foo'], 'bar')
res = client.get('/baz')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertFalse('X-Two' in res.headers)
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'baz')
def test_400(self):
self._mock()
app = Microdot()
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 11\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
self._unmock()
def test_400_handler(self):
self._mock()
app = Microdot()
@app.errorhandler(400)
def handle_400(req):
return '400'
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n400'))
self._unmock()
def test_404(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
client = TestClient(app)
res = client.post('/foo')
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'Not found')
def test_404_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.errorhandler(404)
def handle_404(req):
return '404'
client = TestClient(app)
res = client.post('/foo')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '404')
def test_405(self):
app = Microdot()
@app.route('/foo')
def index(req):
return 'foo'
client = TestClient(app)
res = client.post('/foo')
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'Not found')
def test_405_handler(self):
app = Microdot()
@app.route('/foo')
def index(req):
return 'foo'
@app.errorhandler(405)
def handle_405(req):
return '405', 405
client = TestClient(app)
res = client.patch('/foo')
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '405')
def test_413(self):
app = Microdot()
@app.post('/')
def index(req):
return 'foo'
client = TestClient(app)
res = client.post('/foo', body='x' * 17000)
self.assertEqual(res.status_code, 413)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'Payload too large')
def test_413_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.errorhandler(413)
def handle_413(req):
return '413', 400
client = TestClient(app)
res = client.post('/foo', body='x' * 17000)
self.assertEqual(res.status_code, 400)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '413')
def test_500(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 500)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'Internal server error')
def test_500_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
@app.errorhandler(500)
def handle_500(req):
return '501', 501
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '501')
def test_exception_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
@app.errorhandler(ZeroDivisionError)
def handle_div_zero(req, exc):
return '501', 501
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '501')
def test_json_response(self):
app = Microdot()
@app.route('/dict')
def json_dict(req):
return {'foo': 'bar'}
@app.route('/list')
def json_list(req):
return ['foo', 'bar']
client = TestClient(app)
res = client.get('/dict')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'application/json')
self.assertEqual(res.json, {'foo': 'bar'})
res = client.get('/list')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'application/json')
self.assertEqual(res.json, ['foo', 'bar'])
def test_binary_response(self):
app = Microdot()
@app.route('/bin')
def index(req):
return b'\xff\xfe', {'Content-Type': 'application/octet-stream'}
client = TestClient(app)
res = client.get('/bin')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'application/octet-stream')
self.assertEqual(res.text, None)
self.assertEqual(res.json, None)
self.assertEqual(res.body, b'\xff\xfe')
def test_streaming(self):
app = Microdot()
@app.route('/')
def index(req):
def stream():
yield 'foo'
yield b'bar'
return stream()
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'foobar')
def test_mount(self):
subapp = Microdot()
@subapp.before_request
def before(req):
req.g.before = 'before'
@subapp.after_request
def after(req, res):
return res.body + b':after'
@subapp.errorhandler(404)
def not_found(req):
return '404', 404
@subapp.route('/app')
def index(req):
return req.g.before + ':foo'
app = Microdot()
app.mount(subapp, url_prefix='/sub')
client = TestClient(app)
res = client.get('/app')
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '404')
res = client.get('/sub/app')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'before:foo:after')

170
tests/test_microdot_asgi.py Normal file
View File

@@ -0,0 +1,170 @@
import unittest
import sys
try:
import asyncio
except ImportError:
pass
try:
from unittest import mock
except ImportError:
mock = None
from microdot_asgi import Microdot, Response
from tests import mock_asyncio
@unittest.skipIf(sys.implementation.name == 'micropython',
'not supported under MicroPython')
class TestMicrodotASGI(unittest.TestCase):
def test_asgi_request_with_query_string(self):
app = Microdot()
@app.post('/foo/bar')
async def index(req):
self.assertEqual(req.app, app)
self.assertEqual(req.client_addr, ('1.2.3.4', 1234))
self.assertEqual(req.method, 'POST')
self.assertEqual(req.http_version, 'HTTP/1.1')
self.assertEqual(req.path, '/foo/bar')
self.assertEqual(req.args, {'baz': ['1']})
self.assertEqual(req.cookies, {'session': 'xyz'})
self.assertEqual(req.body, b'body')
class R:
def __init__(self):
self.i = 0
self.body = [b're', b'sp', b'on', b'se', b'']
async def read(self, n):
data = self.body[self.i]
self.i += 1
return data
return Response(body=R(), headers={'Content-Length': '8'})
@app.after_request
def after_request(req, res):
res.set_cookie('foo', 'foo')
res.set_cookie('bar', 'bar', http_only=True)
scope = {
'type': 'http',
'path': '/foo/bar',
'query_string': b'baz=1',
'headers': [('Authorization', 'Bearer 123'),
('Cookie', 'session=xyz'),
('Content-Length', 4)],
'client': ['1.2.3.4', 1234],
'method': 'POST',
'http_version': '1.1',
}
event_index = 0
async def receive():
nonlocal event_index
if event_index == 0:
event_index = 1
return {
'type': 'http.request',
'body': b'body',
'more_body': False,
}
await asyncio.sleep(0.1)
return {
'type': 'http.disconnect',
}
async def send(packet):
if packet['type'] == 'http.response.start':
self.assertEqual(packet['status'], 200)
expected_headers = [('Content-Length', '8'),
('Content-Type', 'text/plain'),
('Set-Cookie', 'foo=foo'),
('Set-Cookie', 'bar=bar; HttpOnly')]
self.assertEqual(len(packet['headers']), len(expected_headers))
for header in expected_headers:
self.assertIn(header, packet['headers'])
elif packet['type'] == 'http.response.body':
self.assertIn(packet['body'],
[b're', b'sp', b'on', b'se', b''])
original_buffer_size = Response.send_file_buffer_size
Response.send_file_buffer_size = 2
mock_asyncio.run(app(scope, receive, send))
Response.send_file_buffer_size = original_buffer_size
def test_wsgi_request_without_query_string(self):
app = Microdot()
@app.route('/foo/bar')
async def index(req):
self.assertEqual(req.path, '/foo/bar')
self.assertEqual(req.args, {})
return 'response'
scope = {
'type': 'http',
'path': '/foo/bar',
'headers': [('Authorization', 'Bearer 123'),
('Cookie', 'session=xyz'),
('Content-Length', 4)],
'client': ['1.2.3.4', 1234],
'method': 'POST',
'http_version': '1.1',
}
event_index = 0
async def receive():
nonlocal event_index
if event_index == 0:
event_index = 1
return {
'type': 'http.request',
'body': b'body',
'more_body': False,
}
await asyncio.sleep(0.1)
return {
'type': 'http.disconnect',
}
async def send(packet):
pass
mock_asyncio.run(app(scope, receive, send))
def test_shutdown(self):
app = Microdot()
@app.route('/shutdown')
async def shutdown(request):
request.app.shutdown()
scope = {
'type': 'http',
'path': '/shutdown',
'client': ['1.2.3.4', 1234],
'method': 'GET',
'http_version': '1.1',
}
async def receive():
pass
async def send(packet):
pass
with mock.patch('microdot_asgi.os.kill') as kill:
mock_asyncio.run(app(scope, receive, send))
kill.assert_called()

View File

@@ -0,0 +1,540 @@
try:
import uasyncio as asyncio
except ImportError:
import asyncio
import sys
import unittest
from microdot_asyncio import Microdot, Response
from microdot_asyncio_test_client import TestClient
from tests import mock_asyncio, mock_socket
class TestMicrodotAsync(unittest.TestCase):
def setUp(self):
self._mock()
def tearDown(self):
self._unmock()
def _run(self, coro):
loop = asyncio.get_event_loop()
return loop.run_until_complete(coro)
def _mock(self):
self.original_asyncio = sys.modules['microdot_asyncio'].asyncio
sys.modules['microdot_asyncio'].asyncio = mock_asyncio
def _unmock(self):
sys.modules['microdot_asyncio'].asyncio = self.original_asyncio
def _add_shutdown(self, app):
@app.route('/shutdown')
def shutdown(req):
app.shutdown()
return ''
mock_socket.add_request('GET', '/shutdown')
def test_get_request(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.route('/async')
async def index2(req):
return 'foo-async'
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'foo')
self.assertEqual(res.body, b'foo')
self.assertEqual(res.json, None)
res = self._run(client.get('/async'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Length'], '9')
self.assertEqual(res.text, 'foo-async')
self.assertEqual(res.body, b'foo-async')
self.assertEqual(res.json, None)
def test_post_request(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.route('/', methods=['POST'])
def index_post(req):
return Response('bar')
@app.route('/async', methods=['POST'])
async def index_post2(req):
return Response('bar-async')
client = TestClient(app)
res = self._run(client.post('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'bar')
self.assertEqual(res.body, b'bar')
self.assertEqual(res.json, None)
res = self._run(client.post('/async'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Length'], '9')
self.assertEqual(res.text, 'bar-async')
self.assertEqual(res.body, b'bar-async')
self.assertEqual(res.json, None)
def test_empty_request(self):
app = Microdot()
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 11\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
def test_method_decorators(self):
app = Microdot()
@app.get('/get')
def get(req):
return 'GET'
@app.post('/post')
async def post(req):
return 'POST'
@app.put('/put')
def put(req):
return 'PUT'
@app.patch('/patch')
async def patch(req):
return 'PATCH'
@app.delete('/delete')
def delete(req):
return 'DELETE'
client = TestClient(app)
methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
for method in methods:
res = self._run(getattr(
client, method.lower())('/' + method.lower()))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, method)
def test_headers(self):
app = Microdot()
@app.route('/')
def index(req):
return req.headers.get('X-Foo')
client = TestClient(app)
res = self._run(client.get('/', headers={'X-Foo': 'bar'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'bar')
def test_cookies(self):
app = Microdot()
@app.route('/')
def index(req):
return req.cookies['one'] + req.cookies['two'] + \
req.cookies['three']
client = TestClient(app, cookies={'one': '1', 'two': '2'})
res = self._run(client.get('/', headers={'Cookie': 'three=3'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '123')
def test_binary_payload(self):
app = Microdot()
@app.post('/')
def index(req):
return req.body
client = TestClient(app)
res = self._run(client.post('/', body=b'foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'foo')
def test_json_payload(self):
app = Microdot()
@app.post('/dict')
def json_dict(req):
print(req.headers)
return req.json.get('foo')
@app.post('/list')
def json_list(req):
return req.json[0]
client = TestClient(app)
res = self._run(client.post('/dict', body={'foo': 'bar'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'bar')
res = self._run(client.post('/list', body=['foo', 'bar']))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'foo')
def test_tuple_responses(self):
app = Microdot()
@app.route('/body')
def one(req):
return 'one'
@app.route('/body-status')
def two(req):
return 'two', 202
@app.route('/body-headers')
def three(req):
return '<p>three</p>', {'Content-Type': 'text/html'}
@app.route('/body-status-headers')
def four(req):
return '<p>four</p>', 202, {'Content-Type': 'text/html'}
client = TestClient(app)
res = self._run(client.get('/body'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'one')
res = self._run(client.get('/body-status'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'two')
res = self._run(client.get('/body-headers'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/html')
self.assertEqual(res.text, '<p>three</p>')
res = self._run(client.get('/body-status-headers'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/html')
self.assertEqual(res.text, '<p>four</p>')
def test_before_after_request(self):
app = Microdot()
@app.before_request
def before_request(req):
if req.path == '/bar':
@req.after_request
async def after_request(req, res):
res.headers['X-Two'] = '2'
return res
return 'bar', 202
req.g.message = 'baz'
@app.after_request
def after_request_one(req, res):
res.headers['X-One'] = '1'
@app.after_request
async def after_request_two(req, res):
res.set_cookie('foo', 'bar')
return res
@app.route('/bar')
def bar(req):
return 'foo'
@app.route('/baz')
def baz(req):
return req.g.message
client = TestClient(app)
res = self._run(client.get('/bar'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertEqual(res.headers['X-Two'], '2')
self.assertEqual(res.text, 'bar')
self.assertEqual(client.cookies['foo'], 'bar')
res = self._run(client.get('/baz'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertFalse('X-Two' in res.headers)
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'baz')
def test_400(self):
self._mock()
app = Microdot()
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 11\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
self._unmock()
def test_400_handler(self):
self._mock()
app = Microdot()
@app.errorhandler(400)
async def handle_404(req):
return '400'
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n400'))
self._unmock()
def test_404(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
client = TestClient(app)
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'Not found')
def test_404_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.errorhandler(404)
async def handle_404(req):
return '404'
client = TestClient(app)
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '404')
def test_405(self):
app = Microdot()
@app.route('/foo')
def index(req):
return 'foo'
client = TestClient(app)
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'Not found')
def test_405_handler(self):
app = Microdot()
@app.route('/foo')
def index(req):
return 'foo'
@app.errorhandler(405)
async def handle_405(req):
return '405', 405
client = TestClient(app)
res = self._run(client.patch('/foo'))
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '405')
def test_413(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
client = TestClient(app)
res = self._run(client.post('/foo', body='x' * 17000))
self.assertEqual(res.status_code, 413)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'Payload too large')
def test_413_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.errorhandler(413)
async def handle_413(req):
return '413', 400
client = TestClient(app)
res = self._run(client.post('/foo', body='x' * 17000))
self.assertEqual(res.status_code, 400)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '413')
def test_500(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 500)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'Internal server error')
def test_500_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
@app.errorhandler(500)
def handle_500(req):
return '501', 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '501')
def test_exception_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 1 / 0
@app.errorhandler(ZeroDivisionError)
async def handle_div_zero(req, exc):
return '501', 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, '501')
def test_json_response(self):
app = Microdot()
@app.route('/dict')
async def json_dict(req):
return {'foo': 'bar'}
@app.route('/list')
def json_list(req):
return ['foo', 'bar']
client = TestClient(app)
res = self._run(client.get('/dict'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'application/json')
self.assertEqual(res.json, {'foo': 'bar'})
res = self._run(client.get('/list'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'application/json')
self.assertEqual(res.json, ['foo', 'bar'])
def test_binary_response(self):
app = Microdot()
@app.route('/bin')
def index(req):
return b'\xff\xfe', {'Content-Type': 'application/octet-stream'}
client = TestClient(app)
res = self._run(client.get('/bin'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'application/octet-stream')
self.assertEqual(res.text, None)
self.assertEqual(res.json, None)
self.assertEqual(res.body, b'\xff\xfe')
def test_streaming(self):
app = Microdot()
@app.route('/')
def index(req):
class stream():
def __init__(self):
self.i = 0
self.data = ['foo', b'bar']
def __aiter__(self):
return self
async def __anext__(self):
if self.i >= len(self.data):
raise StopAsyncIteration
data = self.data[self.i]
self.i += 1
return data
return stream()
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.text, 'foobar')

113
tests/test_microdot_wsgi.py Normal file
View File

@@ -0,0 +1,113 @@
import unittest
import sys
try:
import uio as io
except ImportError:
import io
try:
from unittest import mock
except ImportError:
mock = None
from microdot_wsgi import Microdot
@unittest.skipIf(sys.implementation.name == 'micropython',
'not supported under MicroPython')
class TestMicrodotWSGI(unittest.TestCase):
def test_wsgi_request_with_query_string(self):
app = Microdot()
@app.post('/foo/bar')
def index(req):
self.assertEqual(req.app, app)
self.assertEqual(req.client_addr, ('1.2.3.4', 1234))
self.assertEqual(req.method, 'POST')
self.assertEqual(req.http_version, 'HTTP/1.1')
self.assertEqual(req.path, '/foo/bar')
self.assertEqual(req.args, {'baz': ['1']})
self.assertEqual(req.cookies, {'session': 'xyz'})
self.assertEqual(req.body, b'body')
return 'response'
@app.after_request
def after_request(req, resp):
resp.set_cookie('foo', 'foo')
resp.set_cookie('bar', 'bar', http_only=True)
environ = {
'SCRIPT_NAME': '/foo',
'PATH_INFO': '/bar',
'QUERY_STRING': 'baz=1',
'HTTP_AUTHORIZATION': 'Bearer 123',
'HTTP_COOKIE': 'session=xyz',
'HTTP_CONTENT_LENGTH': '4',
'REMOTE_ADDR': '1.2.3.4',
'REMOTE_PORT': '1234',
'REQUEST_METHOD': 'POST',
'SERVER_PROTOCOL': 'HTTP/1.1',
'wsgi.input': io.BytesIO(b'body'),
}
def start_response(status, headers):
self.assertEqual(status, '200 OK')
expected_headers = [('Content-Length', '8'),
('Content-Type', 'text/plain'),
('Set-Cookie', 'foo=foo'),
('Set-Cookie', 'bar=bar; HttpOnly')]
self.assertEqual(len(headers), len(expected_headers))
for header in expected_headers:
self.assertIn(header, headers)
r = app(environ, start_response)
self.assertEqual(next(r), b'response')
def test_wsgi_request_without_query_string(self):
app = Microdot()
@app.route('/foo/bar')
def index(req):
self.assertEqual(req.path, '/foo/bar')
self.assertEqual(req.args, {})
return 'response'
environ = {
'SCRIPT_NAME': '/foo',
'PATH_INFO': '/bar',
'REMOTE_ADDR': '1.2.3.4',
'REMOTE_PORT': '1234',
'REQUEST_METHOD': 'GET',
'SERVER_PROTOCOL': 'HTTP/1.1',
'wsgi.input': io.BytesIO(b''),
}
def start_response(status, headers):
pass
app(environ, start_response)
def test_shutdown(self):
app = Microdot()
@app.route('/shutdown')
def shutdown(request):
request.app.shutdown()
environ = {
'PATH_INFO': '/shutdown',
'REMOTE_ADDR': '1.2.3.4',
'REMOTE_PORT': '1234',
'REQUEST_METHOD': 'GET',
'SERVER_PROTOCOL': 'HTTP/1.1',
'wsgi.input': io.BytesIO(b''),
}
def start_response(status, headers):
pass
with mock.patch('microdot_wsgi.os.kill') as kill:
app(environ, start_response)
kill.assert_called()

31
tests/test_multidict.py Normal file
View File

@@ -0,0 +1,31 @@
import unittest
from microdot import MultiDict
class TestMultiDict(unittest.TestCase):
def test_multidict(self):
d = MultiDict()
assert dict(d) == {}
assert d.get('zero') is None
assert d.get('zero', default=0) == 0
assert d.getlist('zero') == []
assert d.getlist('zero', type=int) == []
d['one'] = 1
assert d['one'] == 1
assert d.get('one') == 1
assert d.get('one', default=2) == 1
assert d.get('one', type=int) == 1
assert d.get('one', type=str) == '1'
d['two'] = 1
d['two'] = 2
assert d['two'] == 1
assert d.get('two') == 1
assert d.get('two', default=2) == 1
assert d.get('two', type=int) == 1
assert d.get('two', type=str) == '1'
assert d.getlist('two') == [1, 2]
assert d.getlist('two', type=int) == [1, 2]
assert d.getlist('two', type=str) == ['1', '2']

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