microdot 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {microdot-2.1.0/src/microdot.egg-info → microdot-2.2.0}/PKG-INFO +6 -5
- {microdot-2.1.0 → microdot-2.2.0}/README.md +3 -3
- {microdot-2.1.0 → microdot-2.2.0}/docs/api.rst +6 -0
- {microdot-2.1.0 → microdot-2.2.0}/docs/extensions.rst +89 -7
- {microdot-2.1.0 → microdot-2.2.0}/docs/freezing.rst +8 -5
- {microdot-2.1.0 → microdot-2.2.0}/docs/intro.rst +71 -16
- {microdot-2.1.0 → microdot-2.2.0}/pyproject.toml +1 -1
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/__init__.py +1 -1
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/jinja.py +15 -8
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/microdot.py +62 -85
- microdot-2.2.0/src/microdot/multipart.py +291 -0
- {microdot-2.1.0 → microdot-2.2.0/src/microdot.egg-info}/PKG-INFO +6 -5
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot.egg-info/SOURCES.txt +2 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/__init__.py +1 -0
- microdot-2.2.0/tests/test_multipart.py +192 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_url_pattern.py +27 -2
- microdot-2.2.0/tests/test_urlencode.py +13 -0
- microdot-2.1.0/tests/test_urlencode.py +0 -11
- {microdot-2.1.0 → microdot-2.2.0}/LICENSE +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/MANIFEST.in +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/docs/Makefile +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/docs/_static/css/custom.css +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/docs/conf.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/docs/index.rst +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/docs/make.bat +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/docs/migrating.rst +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/setup.cfg +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/asgi.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/auth.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/cors.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/helpers.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/login.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/session.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/sse.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/test_client.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/utemplate.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/websocket.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot/wsgi.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot.egg-info/dependency_links.txt +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot.egg-info/not-zip-safe +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot.egg-info/requires.txt +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/src/microdot.egg-info/top_level.txt +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.bin +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.css +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.gif +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.gz +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.html +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.jpg +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.js +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.json +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.png +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.txt +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.txt.gz +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/mock_socket.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/templates/hello.jinja.txt +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/templates/hello.utemplate.txt +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/templates/hello_utemplate_txt.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_asgi.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_auth.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_cors.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_end2end.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_jinja.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_login.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_microdot.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_multidict.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_request.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_response.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_session.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_sse.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_utemplate.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_websocket.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tests/test_wsgi.py +0 -0
- {microdot-2.1.0 → microdot-2.2.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: microdot
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: The impossibly small web framework for MicroPython
|
|
5
5
|
Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/miguelgrinberg/microdot
|
|
@@ -19,6 +19,7 @@ Requires-Dist: tox; extra == "dev"
|
|
|
19
19
|
Provides-Extra: docs
|
|
20
20
|
Requires-Dist: sphinx; extra == "docs"
|
|
21
21
|
Requires-Dist: pyjwt; extra == "docs"
|
|
22
|
+
Dynamic: license-file
|
|
22
23
|
|
|
23
24
|
# microdot
|
|
24
25
|
[](https://github.com/miguelgrinberg/microdot/actions) [](https://codecov.io/gh/miguelgrinberg/microdot)
|
|
@@ -65,8 +66,8 @@ describes the backwards incompatible changes that were made.
|
|
|
65
66
|
The following features are planned for future releases of Microdot, both for
|
|
66
67
|
MicroPython and CPython:
|
|
67
68
|
|
|
68
|
-
-
|
|
69
|
-
-
|
|
69
|
+
- Authentication support, similar to [Flask-Login](https://github.com/maxcountryman/flask-login) for Flask (**Added in version 2.1**)
|
|
70
|
+
- Support for forms encoded in `multipart/form-data` format (**Added in version 2.2**)
|
|
70
71
|
- OpenAPI integration, similar to [APIFairy](https://github.com/miguelgrinberg/apifairy) for Flask
|
|
71
72
|
|
|
72
73
|
In addition to the above, the following extensions are also under consideration,
|
|
@@ -75,4 +76,4 @@ but only for CPython:
|
|
|
75
76
|
- Database integration through [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)
|
|
76
77
|
- Socket.IO support through [python-socketio](https://github.com/miguelgrinberg/python-socketio)
|
|
77
78
|
|
|
78
|
-
Do you have other ideas to propose? Let's [discuss them](https://github.com
|
|
79
|
+
Do you have other ideas to propose? Let's [discuss them](https://github.com/:miguelgrinberg/microdot/discussions/new?category=ideas)!
|
|
@@ -43,8 +43,8 @@ describes the backwards incompatible changes that were made.
|
|
|
43
43
|
The following features are planned for future releases of Microdot, both for
|
|
44
44
|
MicroPython and CPython:
|
|
45
45
|
|
|
46
|
-
-
|
|
47
|
-
-
|
|
46
|
+
- Authentication support, similar to [Flask-Login](https://github.com/maxcountryman/flask-login) for Flask (**Added in version 2.1**)
|
|
47
|
+
- Support for forms encoded in `multipart/form-data` format (**Added in version 2.2**)
|
|
48
48
|
- OpenAPI integration, similar to [APIFairy](https://github.com/miguelgrinberg/apifairy) for Flask
|
|
49
49
|
|
|
50
50
|
In addition to the above, the following extensions are also under consideration,
|
|
@@ -53,4 +53,4 @@ but only for CPython:
|
|
|
53
53
|
- Database integration through [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)
|
|
54
54
|
- Socket.IO support through [python-socketio](https://github.com/miguelgrinberg/python-socketio)
|
|
55
55
|
|
|
56
|
-
Do you have other ideas to propose? Let's [discuss them](https://github.com
|
|
56
|
+
Do you have other ideas to propose? Let's [discuss them](https://github.com/:miguelgrinberg/microdot/discussions/new?category=ideas)!
|
|
@@ -5,8 +5,82 @@ Microdot is a highly extensible web application framework. The extensions
|
|
|
5
5
|
described in this section are maintained as part of the Microdot project in
|
|
6
6
|
the same source code repository.
|
|
7
7
|
|
|
8
|
+
Multipart Forms
|
|
9
|
+
~~~~~~~~~~~~~~~
|
|
10
|
+
|
|
11
|
+
.. list-table::
|
|
12
|
+
:align: left
|
|
13
|
+
|
|
14
|
+
* - Compatibility
|
|
15
|
+
- | CPython & MicroPython
|
|
16
|
+
|
|
17
|
+
* - Required Microdot source files
|
|
18
|
+
- | `multipart.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/multipart.py>`_
|
|
19
|
+
| `helpers.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/helpers.py>`_
|
|
20
|
+
|
|
21
|
+
* - Required external dependencies
|
|
22
|
+
- | None
|
|
23
|
+
|
|
24
|
+
* - Examples
|
|
25
|
+
- | `formdata.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/uploads/formdata.py>`_
|
|
26
|
+
|
|
27
|
+
The multipart extension handles multipart forms, including those that have file
|
|
28
|
+
uploads.
|
|
29
|
+
|
|
30
|
+
The :func:`with_form_data <microdot.multipart.with_form_data>` decorator
|
|
31
|
+
provides the simplest way to work with these forms. With this decorator added
|
|
32
|
+
to the route, whenever the client sends a multipart request the
|
|
33
|
+
:attr:`request.form <microdot.Request.form>` and
|
|
34
|
+
:attr:`request.files <microdot.Request.files>` properties are populated with
|
|
35
|
+
the submitted data. For form fields the field values are always strings. For
|
|
36
|
+
files, they are instances of the
|
|
37
|
+
:class:`FileUpload <microdot.multipart.FileUpload>` class.
|
|
38
|
+
|
|
39
|
+
Example::
|
|
40
|
+
|
|
41
|
+
from microdot.multipart import with_form_data
|
|
42
|
+
|
|
43
|
+
@app.post('/upload')
|
|
44
|
+
@with_form_data
|
|
45
|
+
async def upload(request):
|
|
46
|
+
print('form fields:', request.form)
|
|
47
|
+
print('files:', request.files)
|
|
48
|
+
|
|
49
|
+
One disadvantage of the ``@with_form_data`` decorator is that it has to copy
|
|
50
|
+
any uploaded files to memory or temporary disk files, depending on their size.
|
|
51
|
+
The :attr:`FileUpload.max_memory_size <microdot.multipart.FileUpload.max_memory_size>`
|
|
52
|
+
attribute can be used to control the cutoff size above which a file upload
|
|
53
|
+
is transferred to a temporary file.
|
|
54
|
+
|
|
55
|
+
A more performant alternative to the ``@with_form_data`` decorator is the
|
|
56
|
+
:class:`FormDataIter <microdot.multipart.FormDataIter>` class, which iterates
|
|
57
|
+
over the form fields sequentially, giving the application the option to parse
|
|
58
|
+
the form fields on the fly and decide what to copy and what to discard. When
|
|
59
|
+
using ``FormDataIter`` the ``request.form`` and ``request.files`` attributes
|
|
60
|
+
are not used.
|
|
61
|
+
|
|
62
|
+
Example::
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
from microdot.multipart import FormDataIter
|
|
66
|
+
|
|
67
|
+
@app.post('/upload')
|
|
68
|
+
async def upload(request):
|
|
69
|
+
async for name, value in FormDataIter(request):
|
|
70
|
+
print(name, value)
|
|
71
|
+
|
|
72
|
+
For fields that contain an uploaded file, the ``value`` returned by the
|
|
73
|
+
iterator is the same ``FileUpload`` instance. The application can choose to
|
|
74
|
+
save the file with the :meth:`save() <microdot.multipart.FileUpload.save>`
|
|
75
|
+
method, or read it with the :meth:`read() <microdot.multipart.FileUpload.read>`
|
|
76
|
+
method, optionally passing a size to read it in chunks. The
|
|
77
|
+
:meth:`copy() <microdot.multipart.FileUpload.copy>` method is also available to
|
|
78
|
+
apply the copying logic used by the ``@with_form_data`` decorator, which is
|
|
79
|
+
inefficient but allows the file to be set aside to be processed later, after
|
|
80
|
+
the remaining form fields.
|
|
81
|
+
|
|
8
82
|
WebSocket
|
|
9
|
-
|
|
83
|
+
~~~~~~~~~
|
|
10
84
|
|
|
11
85
|
.. list-table::
|
|
12
86
|
:align: left
|
|
@@ -16,6 +90,7 @@ WebSocket
|
|
|
16
90
|
|
|
17
91
|
* - Required Microdot source files
|
|
18
92
|
- | `websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/websocket.py>`_
|
|
93
|
+
| `helpers.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/helpers.py>`_
|
|
19
94
|
|
|
20
95
|
* - Required external dependencies
|
|
21
96
|
- | None
|
|
@@ -32,12 +107,14 @@ messages respectively.
|
|
|
32
107
|
|
|
33
108
|
Example::
|
|
34
109
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
110
|
+
from microdot.websocket import with_websocket
|
|
111
|
+
|
|
112
|
+
@app.route('/echo')
|
|
113
|
+
@with_websocket
|
|
114
|
+
async def echo(request, ws):
|
|
115
|
+
while True:
|
|
116
|
+
message = await ws.receive()
|
|
117
|
+
await ws.send(message)
|
|
41
118
|
|
|
42
119
|
Server-Sent Events
|
|
43
120
|
~~~~~~~~~~~~~~~~~~
|
|
@@ -50,6 +127,7 @@ Server-Sent Events
|
|
|
50
127
|
|
|
51
128
|
* - Required Microdot source files
|
|
52
129
|
- | `sse.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/sse.py>`_
|
|
130
|
+
| `helpers.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/helpers.py>`_
|
|
53
131
|
|
|
54
132
|
* - Required external dependencies
|
|
55
133
|
- | None
|
|
@@ -65,6 +143,8 @@ asynchronous method to send an event to the client.
|
|
|
65
143
|
|
|
66
144
|
Example::
|
|
67
145
|
|
|
146
|
+
from microdot.sse import with_sse
|
|
147
|
+
|
|
68
148
|
@app.route('/events')
|
|
69
149
|
@with_sse
|
|
70
150
|
async def events(request, sse):
|
|
@@ -213,6 +293,7 @@ Secure User Sessions
|
|
|
213
293
|
|
|
214
294
|
* - Required Microdot source files
|
|
215
295
|
- | `session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/session.py>`_
|
|
296
|
+
| `helpers.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/helpers.py>`_
|
|
216
297
|
|
|
217
298
|
* - Required external dependencies
|
|
218
299
|
- | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
|
|
@@ -369,6 +450,7 @@ User Logins
|
|
|
369
450
|
* - Required Microdot source files
|
|
370
451
|
- | `login.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/auth.py>`_
|
|
371
452
|
| `session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/session.py>`_
|
|
453
|
+
| `helpers.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/helpers.py>`_
|
|
372
454
|
* - Required external dependencies
|
|
373
455
|
- | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
|
|
374
456
|
| MicroPython: `jwt.py <https://github.com/micropython/micropython-lib/blob/master/python-ecosys/pyjwt/jwt.py>`_,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
Cross-Compiling and Freezing Microdot
|
|
2
|
-
|
|
1
|
+
Cross-Compiling and Freezing Microdot
|
|
2
|
+
-------------------------------------
|
|
3
|
+
|
|
4
|
+
.. note::
|
|
5
|
+
This section only applies when using Microdot on MicroPython.
|
|
3
6
|
|
|
4
7
|
Microdot is a fairly small framework, so its size is not something you need to
|
|
5
8
|
be concerned about unless you are working with MicroPython on hardware with a
|
|
@@ -36,7 +39,7 @@ Cross-Compiling
|
|
|
36
39
|
|
|
37
40
|
An issue that is common with low-end microcontroller boards is that they do not
|
|
38
41
|
have enough RAM for the MicroPython compiler to compile the source files, but
|
|
39
|
-
once the code is compiled they are able to run it
|
|
42
|
+
once the code is compiled they are able to run it just fine.
|
|
40
43
|
|
|
41
44
|
To address this, MicroPython allows you to cross-compile source files on your
|
|
42
45
|
desktop or laptop computer and then upload their compiled versions to the
|
|
@@ -82,8 +85,8 @@ imported directly from the device's ROM, leaving more RAM available for
|
|
|
82
85
|
application use.
|
|
83
86
|
|
|
84
87
|
The process to create a custom firmware is unfortunately non-trivial and
|
|
85
|
-
different
|
|
86
|
-
documentation that applies to your device to learn how to do this.
|
|
88
|
+
different for each microcontroller platform, so you will need to consult the
|
|
89
|
+
MicroPython documentation that applies to your device to learn how to do this.
|
|
87
90
|
|
|
88
91
|
The part of the process that is common to all devices is the creation of a
|
|
89
92
|
`manifest file <https://docs.micropython.org/en/latest/reference/manifest.html>`_
|
|
@@ -329,15 +329,52 @@ URL::
|
|
|
329
329
|
async def get_test(request, path):
|
|
330
330
|
return 'Test: ' + path
|
|
331
331
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
The ``re`` type allows the application to provide a custom regular expression
|
|
333
|
+
for the dynamic component. The next example defines a route that only matches
|
|
334
|
+
usernames that begin with an upper or lower case letter, followed by a sequence
|
|
335
|
+
of letters or numbers::
|
|
336
336
|
|
|
337
337
|
@app.get('/users/<re:[a-zA-Z][a-zA-Z0-9]*:username>')
|
|
338
338
|
async def get_user(request, username):
|
|
339
339
|
return 'User: ' + username
|
|
340
340
|
|
|
341
|
+
The ``re`` type returns the URL component as a string, which sometimes may not
|
|
342
|
+
be the most convenient. To convert a path component to something more
|
|
343
|
+
meaningful than a string, the application can register a custom URL component
|
|
344
|
+
type and provide a parser function that performs the conversion. In the
|
|
345
|
+
following example, a ``hex`` custom type is registered to automatically
|
|
346
|
+
convert hex numbers given in the path to numbers::
|
|
347
|
+
|
|
348
|
+
from microdot import URLPattern
|
|
349
|
+
|
|
350
|
+
URLPattern.register_type('hex', parser=lambda value: int(value, 16))
|
|
351
|
+
|
|
352
|
+
@app.get('/users/<hex:user_id>')
|
|
353
|
+
async def get_user(request, user_id):
|
|
354
|
+
user = get_user_by_id(user_id)
|
|
355
|
+
# ...
|
|
356
|
+
|
|
357
|
+
In addition to the parser, the custom URL component can include a pattern,
|
|
358
|
+
given as a regular expression. When a pattern is provided, the URL component
|
|
359
|
+
will only match if the regular expression matches the value passed in the URL.
|
|
360
|
+
The ``hex`` example above can be expanded with a pattern as follows::
|
|
361
|
+
|
|
362
|
+
URLPattern.register_type('hex', pattern='[0-9a-fA-F]+',
|
|
363
|
+
parser=lambda value: int(value, 16))
|
|
364
|
+
|
|
365
|
+
In cases where a pattern isn't provided, or when the pattern is unable to
|
|
366
|
+
filter out all invalid values, the parser function can return ``None`` to
|
|
367
|
+
indicate a failed match. The next example shows how the parser for the ``hex``
|
|
368
|
+
type can be expanded to do that::
|
|
369
|
+
|
|
370
|
+
def hex_parser(value):
|
|
371
|
+
try:
|
|
372
|
+
return int(value, 16)
|
|
373
|
+
except ValueError:
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
URLPattern.register_type('hex', parser=hex_parser)
|
|
377
|
+
|
|
341
378
|
.. note::
|
|
342
379
|
Dynamic path components are passed to route functions as keyword arguments,
|
|
343
380
|
so the names of the function arguments must match the names declared in the
|
|
@@ -895,18 +932,36 @@ Another option is to create a response object directly in the route function::
|
|
|
895
932
|
Concurrency
|
|
896
933
|
~~~~~~~~~~~
|
|
897
934
|
|
|
898
|
-
Microdot implements concurrency through the ``asyncio`` package
|
|
899
|
-
|
|
900
|
-
|
|
935
|
+
Microdot implements concurrency through the ``asyncio`` package, which means
|
|
936
|
+
that applications must be careful to prevent blocking in their handlers.
|
|
937
|
+
|
|
938
|
+
"async def" handlers
|
|
939
|
+
^^^^^^^^^^^^^^^^^^^^
|
|
940
|
+
|
|
941
|
+
The recommendation for route handlers in Microdot is to use asynchronous
|
|
942
|
+
functions, declared as ``async def``. Microdot executes these handler
|
|
943
|
+
functions as native asynchronous tasks. The standard considerations for writing
|
|
944
|
+
asynchronous code apply, and in particular blocking calls should be avoided to
|
|
945
|
+
ensure the application runs smoothly and is always responsive.
|
|
946
|
+
|
|
947
|
+
"def" handlers
|
|
948
|
+
^^^^^^^^^^^^^^
|
|
949
|
+
|
|
950
|
+
Microdot also supports the use of synchronous route handlers, declared as
|
|
951
|
+
standard ``def`` functions. These handlers are handled differently under
|
|
952
|
+
CPython and MicroPython.
|
|
901
953
|
|
|
902
|
-
When running
|
|
903
|
-
asyncio
|
|
904
|
-
|
|
905
|
-
|
|
954
|
+
When running on CPython, Microdot executes synchronous handlers in a
|
|
955
|
+
`thread executor <https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor>`_,
|
|
956
|
+
which uses a thread pool. The use of blocking or CPU intensive code in these
|
|
957
|
+
handlers does not have such a negative effect on the application, because
|
|
958
|
+
handlers do not run on the same thread as the asynchronous loop. On the other
|
|
959
|
+
hand, the application will be affected by threading issues such as those caused
|
|
960
|
+
by the Global Interpreter Lock.
|
|
906
961
|
|
|
907
962
|
Under MicroPython the situation is different. Most microcontroller boards
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
complete
|
|
912
|
-
|
|
963
|
+
do not have or have very limited threading support, so Microdot executes
|
|
964
|
+
synchronous handlers in the main and often only thread available. This means
|
|
965
|
+
that these functions will block the asynchronous loop when they take too long
|
|
966
|
+
to complete. The use of properly written asynchronous handlers should be
|
|
967
|
+
preferred.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
from microdot.microdot import Microdot, Request, Response, abort, redirect, \
|
|
2
|
-
send_file # noqa: F401
|
|
2
|
+
send_file, URLPattern, AsyncBytesIO, iscoroutine # noqa: F401
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
2
2
|
|
|
3
|
-
_jinja_env = None
|
|
4
|
-
|
|
5
3
|
|
|
6
4
|
class Template:
|
|
7
5
|
"""A template object.
|
|
8
6
|
|
|
9
7
|
:param template: The filename of the template to render, relative to the
|
|
10
8
|
configured template directory.
|
|
9
|
+
:param kwargs: any additional options to be passed to the Jinja
|
|
10
|
+
environment's ``get_template()`` method.
|
|
11
11
|
"""
|
|
12
|
+
#: The Jinja environment. The ``initialize()`` method must be called before
|
|
13
|
+
#: this attribute is accessed.
|
|
14
|
+
jinja_env = None
|
|
15
|
+
|
|
12
16
|
@classmethod
|
|
13
17
|
def initialize(cls, template_dir='templates', enable_async=False,
|
|
14
18
|
**kwargs):
|
|
15
19
|
"""Initialize the templating subsystem.
|
|
16
20
|
|
|
21
|
+
This method is automatically invoked when the first template is
|
|
22
|
+
created. The application can call it explicitly if custom options need
|
|
23
|
+
to be provided.
|
|
24
|
+
|
|
17
25
|
:param template_dir: the directory where templates are stored. This
|
|
18
26
|
argument is optional. The default is to load
|
|
19
27
|
templates from a *templates* subdirectory.
|
|
@@ -23,20 +31,19 @@ class Template:
|
|
|
23
31
|
:param kwargs: any additional options to be passed to Jinja's
|
|
24
32
|
``Environment`` class.
|
|
25
33
|
"""
|
|
26
|
-
|
|
27
|
-
_jinja_env = Environment(
|
|
34
|
+
cls.jinja_env = Environment(
|
|
28
35
|
loader=FileSystemLoader(template_dir),
|
|
29
36
|
autoescape=select_autoescape(),
|
|
30
37
|
enable_async=enable_async,
|
|
31
38
|
**kwargs
|
|
32
39
|
)
|
|
33
40
|
|
|
34
|
-
def __init__(self, template):
|
|
35
|
-
if
|
|
41
|
+
def __init__(self, template, **kwargs):
|
|
42
|
+
if self.jinja_env is None: # pragma: no cover
|
|
36
43
|
self.initialize()
|
|
37
|
-
#: The name of the template
|
|
44
|
+
#: The name of the template.
|
|
38
45
|
self.name = template
|
|
39
|
-
self.template =
|
|
46
|
+
self.template = self.jinja_env.get_template(template, **kwargs)
|
|
40
47
|
|
|
41
48
|
def generate(self, *args, **kwargs):
|
|
42
49
|
"""Return a generator that renders the template in chunks, with the
|
|
@@ -8,6 +8,7 @@ servers for MicroPython and standard Python.
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import io
|
|
10
10
|
import json
|
|
11
|
+
import re
|
|
11
12
|
import time
|
|
12
13
|
|
|
13
14
|
try:
|
|
@@ -56,23 +57,9 @@ MUTED_SOCKET_ERRORS = [
|
|
|
56
57
|
]
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if len(parts) == 1:
|
|
63
|
-
return s
|
|
64
|
-
result = [parts[0]]
|
|
65
|
-
for item in parts[1:]:
|
|
66
|
-
if item == '':
|
|
67
|
-
result.append('%')
|
|
68
|
-
else:
|
|
69
|
-
code = item[:2]
|
|
70
|
-
result.append(chr(int(code, 16)))
|
|
71
|
-
result.append(item[2:])
|
|
72
|
-
return ''.join(result)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def urldecode_bytes(s):
|
|
60
|
+
def urldecode(s):
|
|
61
|
+
if isinstance(s, str):
|
|
62
|
+
s = s.encode()
|
|
76
63
|
s = s.replace(b'+', b' ')
|
|
77
64
|
parts = s.split(b'%')
|
|
78
65
|
if len(parts) == 1:
|
|
@@ -384,6 +371,7 @@ class Request:
|
|
|
384
371
|
self.sock = sock
|
|
385
372
|
self._json = None
|
|
386
373
|
self._form = None
|
|
374
|
+
self._files = None
|
|
387
375
|
self.after_request_handlers = []
|
|
388
376
|
|
|
389
377
|
@staticmethod
|
|
@@ -440,12 +428,12 @@ class Request:
|
|
|
440
428
|
if isinstance(urlencoded, str):
|
|
441
429
|
for kv in [pair.split('=', 1)
|
|
442
430
|
for pair in urlencoded.split('&') if pair]:
|
|
443
|
-
data[
|
|
431
|
+
data[urldecode(kv[0])] = urldecode(kv[1]) \
|
|
444
432
|
if len(kv) > 1 else ''
|
|
445
433
|
elif isinstance(urlencoded, bytes): # pragma: no branch
|
|
446
434
|
for kv in [pair.split(b'=', 1)
|
|
447
435
|
for pair in urlencoded.split(b'&') if pair]:
|
|
448
|
-
data[
|
|
436
|
+
data[urldecode(kv[0])] = urldecode(kv[1]) \
|
|
449
437
|
if len(kv) > 1 else b''
|
|
450
438
|
return data
|
|
451
439
|
|
|
@@ -478,7 +466,13 @@ class Request:
|
|
|
478
466
|
def form(self):
|
|
479
467
|
"""The parsed form submission body, as a
|
|
480
468
|
:class:`MultiDict <microdot.MultiDict>` object, or ``None`` if the
|
|
481
|
-
request does not have a form submission.
|
|
469
|
+
request does not have a form submission.
|
|
470
|
+
|
|
471
|
+
Forms that are URL encoded are processed by default. For multipart
|
|
472
|
+
forms to be processed, the
|
|
473
|
+
:func:`with_form_data <microdot.multipart.with_form_data>`
|
|
474
|
+
decorator must be added to the route.
|
|
475
|
+
"""
|
|
482
476
|
if self._form is None:
|
|
483
477
|
if self.content_type is None:
|
|
484
478
|
return None
|
|
@@ -488,6 +482,17 @@ class Request:
|
|
|
488
482
|
self._form = self._parse_urlencoded(self.body)
|
|
489
483
|
return self._form
|
|
490
484
|
|
|
485
|
+
@property
|
|
486
|
+
def files(self):
|
|
487
|
+
"""The files uploaded in the request as a dictionary, or ``None`` if
|
|
488
|
+
the request does not have any files.
|
|
489
|
+
|
|
490
|
+
The :func:`with_form_data <microdot.multipart.with_form_data>`
|
|
491
|
+
decorator must be added to the route that receives file uploads for
|
|
492
|
+
this property to be set.
|
|
493
|
+
"""
|
|
494
|
+
return self._files
|
|
495
|
+
|
|
491
496
|
def after_request(self, f):
|
|
492
497
|
"""Register a request-specific function to run after the request is
|
|
493
498
|
handled. Request-specific after request handlers run at the very end,
|
|
@@ -805,13 +810,23 @@ class Response:
|
|
|
805
810
|
|
|
806
811
|
|
|
807
812
|
class URLPattern():
|
|
813
|
+
segment_patterns = {
|
|
814
|
+
'string': '/([^/]+)',
|
|
815
|
+
'int': '/(-?\\d+)',
|
|
816
|
+
'path': '/(.+)',
|
|
817
|
+
}
|
|
818
|
+
segment_parsers = {
|
|
819
|
+
'int': lambda value: int(value),
|
|
820
|
+
}
|
|
821
|
+
|
|
808
822
|
def __init__(self, url_pattern):
|
|
809
823
|
self.url_pattern = url_pattern
|
|
810
824
|
self.segments = []
|
|
811
825
|
self.regex = None
|
|
826
|
+
|
|
827
|
+
def compile(self):
|
|
812
828
|
pattern = ''
|
|
813
|
-
|
|
814
|
-
for segment in url_pattern.lstrip('/').split('/'):
|
|
829
|
+
for segment in self.url_pattern.lstrip('/').split('/'):
|
|
815
830
|
if segment and segment[0] == '<':
|
|
816
831
|
if segment[-1] != '>':
|
|
817
832
|
raise ValueError('invalid URL pattern')
|
|
@@ -822,82 +837,44 @@ class URLPattern():
|
|
|
822
837
|
type_ = 'string'
|
|
823
838
|
name = segment
|
|
824
839
|
parser = None
|
|
825
|
-
if type_
|
|
826
|
-
parser = self._string_segment
|
|
827
|
-
pattern += '/([^/]+)'
|
|
828
|
-
elif type_ == 'int':
|
|
829
|
-
parser = self._int_segment
|
|
830
|
-
pattern += '/(-?\\d+)'
|
|
831
|
-
elif type_ == 'path':
|
|
832
|
-
use_regex = True
|
|
833
|
-
pattern += '/(.+)'
|
|
834
|
-
elif type_.startswith('re:'):
|
|
835
|
-
use_regex = True
|
|
840
|
+
if type_.startswith('re:'):
|
|
836
841
|
pattern += '/({pattern})'.format(pattern=type_[3:])
|
|
837
842
|
else:
|
|
838
|
-
|
|
843
|
+
if type_ not in self.segment_patterns:
|
|
844
|
+
raise ValueError('invalid URL segment type')
|
|
845
|
+
pattern += self.segment_patterns[type_]
|
|
846
|
+
parser = self.segment_parsers.get(type_)
|
|
839
847
|
self.segments.append({'parser': parser, 'name': name,
|
|
840
848
|
'type': type_})
|
|
841
849
|
else:
|
|
842
850
|
pattern += '/' + segment
|
|
843
|
-
self.segments.append({'parser':
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
851
|
+
self.segments.append({'parser': None})
|
|
852
|
+
self.regex = re.compile('^' + pattern + '$')
|
|
853
|
+
return self.regex
|
|
854
|
+
|
|
855
|
+
@classmethod
|
|
856
|
+
def register_type(cls, type_name, pattern='[^/]+', parser=None):
|
|
857
|
+
cls.segment_patterns[type_name] = '/({})'.format(pattern)
|
|
858
|
+
cls.segment_parsers[type_name] = parser
|
|
847
859
|
|
|
848
860
|
def match(self, path):
|
|
849
861
|
args = {}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
value = int(value)
|
|
861
|
-
args[segment['name']] = value
|
|
862
|
-
i += 1
|
|
863
|
-
else:
|
|
864
|
-
if len(path) == 0 or path[0] != '/':
|
|
865
|
-
return
|
|
866
|
-
path = path[1:]
|
|
867
|
-
args = {}
|
|
868
|
-
for segment in self.segments:
|
|
869
|
-
if path is None:
|
|
870
|
-
return
|
|
871
|
-
arg, path = segment['parser'](path)
|
|
862
|
+
g = (self.regex or self.compile()).match(path)
|
|
863
|
+
if not g:
|
|
864
|
+
return
|
|
865
|
+
i = 1
|
|
866
|
+
for segment in self.segments:
|
|
867
|
+
if 'name' not in segment:
|
|
868
|
+
continue
|
|
869
|
+
arg = g.group(i)
|
|
870
|
+
if segment['parser']:
|
|
871
|
+
arg = self.segment_parsers[segment['type']](arg)
|
|
872
872
|
if arg is None:
|
|
873
873
|
return
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
if path is not None:
|
|
877
|
-
return
|
|
874
|
+
args[segment['name']] = arg
|
|
875
|
+
i += 1
|
|
878
876
|
return args
|
|
879
877
|
|
|
880
|
-
def _static_segment(self, segment):
|
|
881
|
-
def _static(value):
|
|
882
|
-
s = value.split('/', 1)
|
|
883
|
-
if s[0] == segment:
|
|
884
|
-
return '', s[1] if len(s) > 1 else None
|
|
885
|
-
return None, None
|
|
886
|
-
return _static
|
|
887
|
-
|
|
888
|
-
def _string_segment(self, value):
|
|
889
|
-
s = value.split('/', 1)
|
|
890
|
-
if len(s[0]) == 0:
|
|
891
|
-
return None, None
|
|
892
|
-
return s[0], s[1] if len(s) > 1 else None
|
|
893
|
-
|
|
894
|
-
def _int_segment(self, value):
|
|
895
|
-
s = value.split('/', 1)
|
|
896
|
-
try:
|
|
897
|
-
return int(s[0]), s[1] if len(s) > 1 else None
|
|
898
|
-
except ValueError:
|
|
899
|
-
return None, None
|
|
900
|
-
|
|
901
878
|
def __repr__(self): # pragma: no cover
|
|
902
879
|
return 'URLPattern: {}'.format(self.url_pattern)
|
|
903
880
|
|