microdot 2.0.7__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.0.7/src/microdot.egg-info → microdot-2.2.0}/PKG-INFO +6 -5
- {microdot-2.0.7 → microdot-2.2.0}/README.md +3 -3
- {microdot-2.0.7 → microdot-2.2.0}/docs/api.rst +22 -0
- {microdot-2.0.7 → microdot-2.2.0}/docs/extensions.rst +272 -18
- {microdot-2.0.7 → microdot-2.2.0}/docs/freezing.rst +8 -5
- {microdot-2.0.7 → microdot-2.2.0}/docs/intro.rst +91 -22
- {microdot-2.0.7 → microdot-2.2.0}/pyproject.toml +1 -1
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/__init__.py +1 -1
- microdot-2.2.0/src/microdot/auth.py +144 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/jinja.py +15 -8
- microdot-2.2.0/src/microdot/login.py +163 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/microdot.py +181 -135
- microdot-2.2.0/src/microdot/multipart.py +291 -0
- {microdot-2.0.7 → microdot-2.2.0/src/microdot.egg-info}/PKG-INFO +6 -5
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot.egg-info/SOURCES.txt +6 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/__init__.py +3 -0
- microdot-2.2.0/tests/test_auth.py +125 -0
- microdot-2.2.0/tests/test_login.py +188 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_microdot.py +201 -2
- microdot-2.2.0/tests/test_multipart.py +192 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_session.py +1 -1
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_url_pattern.py +27 -2
- microdot-2.2.0/tests/test_urlencode.py +13 -0
- microdot-2.0.7/tests/test_urlencode.py +0 -11
- {microdot-2.0.7 → microdot-2.2.0}/LICENSE +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/MANIFEST.in +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/docs/Makefile +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/docs/_static/css/custom.css +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/docs/conf.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/docs/index.rst +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/docs/make.bat +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/docs/migrating.rst +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/setup.cfg +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/asgi.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/cors.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/helpers.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/session.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/sse.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/test_client.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/utemplate.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/websocket.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot/wsgi.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot.egg-info/dependency_links.txt +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot.egg-info/not-zip-safe +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot.egg-info/requires.txt +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/src/microdot.egg-info/top_level.txt +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.bin +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.css +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.gif +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.gz +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.html +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.jpg +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.js +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.json +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.png +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.txt +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.txt.gz +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/mock_socket.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/templates/hello.jinja.txt +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/templates/hello.utemplate.txt +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/templates/hello_utemplate_txt.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_asgi.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_cors.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_end2end.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_jinja.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_multidict.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_request.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_response.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_sse.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_utemplate.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_websocket.py +0 -0
- {microdot-2.0.7 → microdot-2.2.0}/tests/test_wsgi.py +0 -0
- {microdot-2.0.7 → 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.0
|
|
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)!
|
|
@@ -14,6 +14,12 @@ Core API
|
|
|
14
14
|
:members:
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
Multipart Forms
|
|
18
|
+
---------------
|
|
19
|
+
|
|
20
|
+
.. automodule:: microdot.multipart
|
|
21
|
+
:members:
|
|
22
|
+
|
|
17
23
|
WebSocket
|
|
18
24
|
---------
|
|
19
25
|
|
|
@@ -44,6 +50,22 @@ User Sessions
|
|
|
44
50
|
.. automodule:: microdot.session
|
|
45
51
|
:members:
|
|
46
52
|
|
|
53
|
+
Authentication
|
|
54
|
+
--------------
|
|
55
|
+
|
|
56
|
+
.. automodule:: microdot.auth
|
|
57
|
+
:inherited-members:
|
|
58
|
+
:special-members: __call__
|
|
59
|
+
:members:
|
|
60
|
+
|
|
61
|
+
User Logins
|
|
62
|
+
-----------
|
|
63
|
+
|
|
64
|
+
.. automodule:: microdot.login
|
|
65
|
+
:inherited-members:
|
|
66
|
+
:special-members: __call__
|
|
67
|
+
:members:
|
|
68
|
+
|
|
47
69
|
Cross-Origin Resource Sharing (CORS)
|
|
48
70
|
------------------------------------
|
|
49
71
|
|
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
+
|
|
82
|
+
WebSocket
|
|
83
|
+
~~~~~~~~~
|
|
10
84
|
|
|
11
85
|
.. list-table::
|
|
12
86
|
:align: left
|
|
@@ -16,6 +90,7 @@ WebSocket Support
|
|
|
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,15 +107,17 @@ messages respectively.
|
|
|
32
107
|
|
|
33
108
|
Example::
|
|
34
109
|
|
|
35
|
-
|
|
36
|
-
@with_websocket
|
|
37
|
-
async def echo(request, ws):
|
|
38
|
-
while True:
|
|
39
|
-
message = await ws.receive()
|
|
40
|
-
await ws.send(message)
|
|
110
|
+
from microdot.websocket import with_websocket
|
|
41
111
|
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
|
118
|
+
|
|
119
|
+
Server-Sent Events
|
|
120
|
+
~~~~~~~~~~~~~~~~~~
|
|
44
121
|
|
|
45
122
|
.. list-table::
|
|
46
123
|
:align: left
|
|
@@ -50,6 +127,7 @@ Server-Sent Events Support
|
|
|
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):
|
|
@@ -78,8 +158,8 @@ Example::
|
|
|
78
158
|
the SSE object. For bidirectional communication with the client, use the
|
|
79
159
|
WebSocket extension.
|
|
80
160
|
|
|
81
|
-
|
|
82
|
-
|
|
161
|
+
Templates
|
|
162
|
+
~~~~~~~~~
|
|
83
163
|
|
|
84
164
|
Many web applications use HTML templates for rendering content to clients.
|
|
85
165
|
Microdot includes extensions to render templates with the
|
|
@@ -202,8 +282,8 @@ must be used.
|
|
|
202
282
|
.. note::
|
|
203
283
|
The Jinja extension is not compatible with MicroPython.
|
|
204
284
|
|
|
205
|
-
|
|
206
|
-
|
|
285
|
+
Secure User Sessions
|
|
286
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
207
287
|
|
|
208
288
|
.. list-table::
|
|
209
289
|
:align: left
|
|
@@ -213,6 +293,7 @@ Maintaining 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/>`_
|
|
@@ -270,6 +351,179 @@ The :func:`save() <microdot.session.SessionDict.save>` and
|
|
|
270
351
|
:func:`delete() <microdot.session.SessionDict.delete>` methods are used to update
|
|
271
352
|
and destroy the user session respectively.
|
|
272
353
|
|
|
354
|
+
Authentication
|
|
355
|
+
~~~~~~~~~~~~~~
|
|
356
|
+
|
|
357
|
+
.. list-table::
|
|
358
|
+
:align: left
|
|
359
|
+
|
|
360
|
+
* - Compatibility
|
|
361
|
+
- | CPython & MicroPython
|
|
362
|
+
|
|
363
|
+
* - Required Microdot source files
|
|
364
|
+
- | `auth.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/auth.py>`_
|
|
365
|
+
|
|
366
|
+
* - Required external dependencies
|
|
367
|
+
- | None
|
|
368
|
+
|
|
369
|
+
* - Examples
|
|
370
|
+
- | `basic_auth.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/auth/basic_auth.py>`_
|
|
371
|
+
| `token_auth.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/auth/token_auth.py>`_
|
|
372
|
+
|
|
373
|
+
The authentication extension provides helper classes for two commonly used
|
|
374
|
+
authentication patterns, described below.
|
|
375
|
+
|
|
376
|
+
Basic Authentication
|
|
377
|
+
^^^^^^^^^^^^^^^^^^^^
|
|
378
|
+
|
|
379
|
+
`Basic Authentication <https://en.wikipedia.org/wiki/Basic_access_authentication>`_
|
|
380
|
+
is a method of authentication that is part of the HTTP specification. It allows
|
|
381
|
+
clients to authenticate to a server using a username and a password. Web
|
|
382
|
+
browsers have native support for Basic Authentication and will automatically
|
|
383
|
+
prompt the user for a username and a password when a protected resource is
|
|
384
|
+
accessed.
|
|
385
|
+
|
|
386
|
+
To use Basic Authentication, create an instance of the :class:`BasicAuth <microdot.auth.BasicAuth>`
|
|
387
|
+
class::
|
|
388
|
+
|
|
389
|
+
from microdot.auth import BasicAuth
|
|
390
|
+
|
|
391
|
+
auth = BasicAuth(app)
|
|
392
|
+
|
|
393
|
+
Next, create an authentication function. The function must accept a request
|
|
394
|
+
object and a username and password pair provided by the user. If the
|
|
395
|
+
credentials are valid, the function must return an object that represents the
|
|
396
|
+
user. If the authentication function cannot validate the user provided
|
|
397
|
+
credentials it must return ``None``. Decorate the function with
|
|
398
|
+
``@auth.authenticate``::
|
|
399
|
+
|
|
400
|
+
@auth.authenticate
|
|
401
|
+
async def verify_user(request, username, password):
|
|
402
|
+
user = await load_user_from_database(username)
|
|
403
|
+
if user and user.verify_password(password):
|
|
404
|
+
return user
|
|
405
|
+
|
|
406
|
+
To protect a route with authentication, add the ``auth`` instance as a
|
|
407
|
+
decorator::
|
|
408
|
+
|
|
409
|
+
@app.route('/')
|
|
410
|
+
@auth
|
|
411
|
+
async def index(request):
|
|
412
|
+
return f'Hello, {request.g.current_user}!'
|
|
413
|
+
|
|
414
|
+
While running an authenticated request, the user object returned by the
|
|
415
|
+
authenticaction function is accessible as ``request.g.current_user``.
|
|
416
|
+
|
|
417
|
+
Token Authentication
|
|
418
|
+
^^^^^^^^^^^^^^^^^^^^
|
|
419
|
+
|
|
420
|
+
To set up token authentication, create an instance of :class:`TokenAuth <microdot.auth.TokenAuth>`::
|
|
421
|
+
|
|
422
|
+
from microdot.auth import TokenAuth
|
|
423
|
+
|
|
424
|
+
auth = TokenAuth()
|
|
425
|
+
|
|
426
|
+
Then add a function that verifies the token and returns the user it belongs to,
|
|
427
|
+
or ``None`` if the token is invalid or expired::
|
|
428
|
+
|
|
429
|
+
@auth.authenticate
|
|
430
|
+
async def verify_token(request, token):
|
|
431
|
+
return load_user_from_token(token)
|
|
432
|
+
|
|
433
|
+
As with Basic authentication, the ``auth`` instance is used as a decorator to
|
|
434
|
+
protect your routes::
|
|
435
|
+
|
|
436
|
+
@app.route('/')
|
|
437
|
+
@auth
|
|
438
|
+
async def index(request):
|
|
439
|
+
return f'Hello, {request.g.current_user}!'
|
|
440
|
+
|
|
441
|
+
User Logins
|
|
442
|
+
~~~~~~~~~~~
|
|
443
|
+
|
|
444
|
+
.. list-table::
|
|
445
|
+
:align: left
|
|
446
|
+
|
|
447
|
+
* - Compatibility
|
|
448
|
+
- | CPython & MicroPython
|
|
449
|
+
|
|
450
|
+
* - Required Microdot source files
|
|
451
|
+
- | `login.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/auth.py>`_
|
|
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>`_
|
|
454
|
+
* - Required external dependencies
|
|
455
|
+
- | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
|
|
456
|
+
| MicroPython: `jwt.py <https://github.com/micropython/micropython-lib/blob/master/python-ecosys/pyjwt/jwt.py>`_,
|
|
457
|
+
`hmac.py <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_
|
|
458
|
+
* - Examples
|
|
459
|
+
- | `login.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/login/login.py>`_
|
|
460
|
+
|
|
461
|
+
The login extension provides user login functionality. The logged in state of
|
|
462
|
+
the user is stored in the user session cookie, and an optional "remember me"
|
|
463
|
+
cookie can also be added to keep the user logged in across browser sessions.
|
|
464
|
+
|
|
465
|
+
To use this extension, create instances of the
|
|
466
|
+
:class:`Session <microdot.session.Session>` and :class:`Login <microdot.login.Login>`
|
|
467
|
+
class::
|
|
468
|
+
|
|
469
|
+
Session(app, secret_key='top-secret!')
|
|
470
|
+
login = Login()
|
|
471
|
+
|
|
472
|
+
The ``Login`` class accept an optional argument with the URL of the login page.
|
|
473
|
+
The default for this URL is */login*.
|
|
474
|
+
|
|
475
|
+
The application must represent users as objects with an ``id`` attribute. A
|
|
476
|
+
function decorated with ``@login.user_loader`` is used to load a user object::
|
|
477
|
+
|
|
478
|
+
@login.user_loader
|
|
479
|
+
async def get_user(user_id):
|
|
480
|
+
return database.get_user(user_id)
|
|
481
|
+
|
|
482
|
+
The application must implement the login form. At the point in which the user
|
|
483
|
+
credentials have been received and verified, a call to the
|
|
484
|
+
:func:`login_user() <microdot.login.Login.login_user>` function must be made to
|
|
485
|
+
record the user in the user session::
|
|
486
|
+
|
|
487
|
+
@app.route('/login', methods=['GET', 'POST'])
|
|
488
|
+
async def login(request):
|
|
489
|
+
# ...
|
|
490
|
+
if user.check_password(password):
|
|
491
|
+
return await login.login_user(request, user, remember=remember_me)
|
|
492
|
+
return redirect('/login')
|
|
493
|
+
|
|
494
|
+
The optional ``remember`` argument is used to add a remember me cookie that
|
|
495
|
+
will log the user in automatically in future sessions. A value of ``True`` will
|
|
496
|
+
keep the log in active for 30 days. Alternatively, an integer number of days
|
|
497
|
+
can be passed in this argument.
|
|
498
|
+
|
|
499
|
+
Any routes that require the user to be logged in must be decorated with
|
|
500
|
+
:func:`@login <microdot.login.Login.__call__>`::
|
|
501
|
+
|
|
502
|
+
@app.route('/')
|
|
503
|
+
@login
|
|
504
|
+
async def index(request):
|
|
505
|
+
# ...
|
|
506
|
+
|
|
507
|
+
Routes that are of a sensitive nature can be decorated with
|
|
508
|
+
:func:`@login.fresh <microdot.login.Login.fresh>`
|
|
509
|
+
instead. This decorator requires that the user has logged in during the current
|
|
510
|
+
session, and will ask the user to logged in again if the session was
|
|
511
|
+
authenticated through a remember me cookie::
|
|
512
|
+
|
|
513
|
+
@app.get('/fresh')
|
|
514
|
+
@login.fresh
|
|
515
|
+
async def fresh(request):
|
|
516
|
+
# ...
|
|
517
|
+
|
|
518
|
+
To log out a user, the :func:`logout_user() <microdot.auth.Login.logout_user>`
|
|
519
|
+
is used::
|
|
520
|
+
|
|
521
|
+
@app.post('/logout')
|
|
522
|
+
@login
|
|
523
|
+
async def logout(request):
|
|
524
|
+
await login.logout_user(request)
|
|
525
|
+
return redirect('/')
|
|
526
|
+
|
|
273
527
|
Cross-Origin Resource Sharing (CORS)
|
|
274
528
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
275
529
|
|
|
@@ -305,8 +559,8 @@ Example::
|
|
|
305
559
|
cors = CORS(app, allowed_origins=['https://example.com'],
|
|
306
560
|
allow_credentials=True)
|
|
307
561
|
|
|
308
|
-
|
|
309
|
-
|
|
562
|
+
Test Client
|
|
563
|
+
~~~~~~~~~~~
|
|
310
564
|
|
|
311
565
|
.. list-table::
|
|
312
566
|
:align: left
|
|
@@ -342,8 +596,8 @@ Example::
|
|
|
342
596
|
See the documentation for the :class:`TestClient <microdot.test_client.TestClient>`
|
|
343
597
|
class for more details.
|
|
344
598
|
|
|
345
|
-
|
|
346
|
-
|
|
599
|
+
Production Deployments
|
|
600
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
347
601
|
|
|
348
602
|
The ``Microdot`` class creates its own simple web server. This is enough for an
|
|
349
603
|
application deployed with MicroPython, but when using CPython it may be useful
|
|
@@ -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
|
|
@@ -445,7 +482,7 @@ Mounting a Sub-Application
|
|
|
445
482
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
446
483
|
|
|
447
484
|
Small Microdot applications can be written as a single source file, but this
|
|
448
|
-
is not the best option for applications that
|
|
485
|
+
is not the best option for applications that pass a certain size. To make it
|
|
449
486
|
simpler to write large applications, Microdot supports the concept of
|
|
450
487
|
sub-applications that can be "mounted" on a larger application, possibly with
|
|
451
488
|
a common URL prefix applied to all of its routes. For developers familiar with
|
|
@@ -501,11 +538,25 @@ The resulting application will have the customer endpoints available at
|
|
|
501
538
|
*/customers/* and the order endpoints available at */orders/*.
|
|
502
539
|
|
|
503
540
|
.. note::
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
541
|
+
During the handling of a request, the
|
|
542
|
+
:attr:`Request.url_prefix <microdot.Microdot.url_prefix>` attribute is
|
|
543
|
+
set to the URL prefix under which the sub-application was mounted, or an
|
|
544
|
+
empty string if the endpoint did not come from a sub-application or the
|
|
545
|
+
sub-application was mounted without a URL prefix. It is possible to issue a
|
|
546
|
+
redirect that is relative to the sub-application as follows::
|
|
547
|
+
|
|
548
|
+
return redirect(request.url_prefix + '/relative-url')
|
|
549
|
+
|
|
550
|
+
When mounting an application as shown above, before-request, after-request and
|
|
551
|
+
error handlers defined in the sub-application are copied over to the main
|
|
552
|
+
application at mount time. Once installed in the main application, these
|
|
553
|
+
handlers will apply to the whole application and not just the sub-application
|
|
554
|
+
in which they were created.
|
|
555
|
+
|
|
556
|
+
The :func:`mount() <microdot.Microdot.mount>` method has a ``local`` argument
|
|
557
|
+
that defaults to ``False``. When this argument is set to ``True``, the
|
|
558
|
+
before-request, after-request and error handlers defined in the sub-application
|
|
559
|
+
will only apply to the sub-application.
|
|
509
560
|
|
|
510
561
|
Shutting Down the Server
|
|
511
562
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
@@ -881,18 +932,36 @@ Another option is to create a response object directly in the route function::
|
|
|
881
932
|
Concurrency
|
|
882
933
|
~~~~~~~~~~~
|
|
883
934
|
|
|
884
|
-
Microdot implements concurrency through the ``asyncio`` package
|
|
885
|
-
|
|
886
|
-
|
|
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.
|
|
887
953
|
|
|
888
|
-
When running
|
|
889
|
-
asyncio
|
|
890
|
-
|
|
891
|
-
|
|
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.
|
|
892
961
|
|
|
893
962
|
Under MicroPython the situation is different. Most microcontroller boards
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
complete
|
|
898
|
-
|
|
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
|