microdot 2.1.0__tar.gz → 2.3.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.3.0}/PKG-INFO +6 -5
- {microdot-2.1.0 → microdot-2.3.0}/README.md +3 -3
- {microdot-2.1.0 → microdot-2.3.0}/docs/api.rst +6 -0
- {microdot-2.1.0 → microdot-2.3.0}/docs/extensions.rst +116 -9
- {microdot-2.1.0 → microdot-2.3.0}/docs/freezing.rst +8 -5
- {microdot-2.1.0 → microdot-2.3.0}/docs/intro.rst +71 -16
- {microdot-2.1.0 → microdot-2.3.0}/pyproject.toml +1 -1
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/__init__.py +1 -1
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/auth.py +18 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/jinja.py +15 -8
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/microdot.py +69 -88
- microdot-2.3.0/src/microdot/multipart.py +291 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/sse.py +5 -1
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/test_client.py +7 -3
- {microdot-2.1.0 → microdot-2.3.0/src/microdot.egg-info}/PKG-INFO +6 -5
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot.egg-info/SOURCES.txt +2 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/__init__.py +1 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_auth.py +66 -1
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_microdot.py +1 -1
- microdot-2.3.0/tests/test_multipart.py +192 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_url_pattern.py +27 -2
- microdot-2.3.0/tests/test_urlencode.py +13 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_websocket.py +3 -3
- microdot-2.1.0/tests/test_urlencode.py +0 -11
- {microdot-2.1.0 → microdot-2.3.0}/LICENSE +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/MANIFEST.in +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/docs/Makefile +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/docs/_static/css/custom.css +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/docs/conf.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/docs/index.rst +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/docs/make.bat +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/docs/migrating.rst +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/setup.cfg +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/asgi.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/cors.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/helpers.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/login.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/session.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/utemplate.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/websocket.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot/wsgi.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot.egg-info/dependency_links.txt +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot.egg-info/not-zip-safe +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot.egg-info/requires.txt +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/src/microdot.egg-info/top_level.txt +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/files/test.bin +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/files/test.css +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/files/test.gif +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/files/test.gz +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/files/test.html +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/files/test.jpg +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/files/test.js +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/files/test.json +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/files/test.png +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/files/test.txt +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/files/test.txt.gz +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/mock_socket.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/templates/hello.jinja.txt +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/templates/hello.utemplate.txt +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/templates/hello_utemplate_txt.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_asgi.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_cors.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_end2end.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_jinja.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_login.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_multidict.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_request.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_response.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_session.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_sse.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_utemplate.py +0 -0
- {microdot-2.1.0 → microdot-2.3.0}/tests/test_wsgi.py +0 -0
- {microdot-2.1.0 → microdot-2.3.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.3.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/>`_
|
|
@@ -333,10 +414,25 @@ decorator::
|
|
|
333
414
|
While running an authenticated request, the user object returned by the
|
|
334
415
|
authenticaction function is accessible as ``request.g.current_user``.
|
|
335
416
|
|
|
417
|
+
If an endpoint is intended to work with or without authentication, then it can
|
|
418
|
+
be protected with the ``auth.optional`` decorator::
|
|
419
|
+
|
|
420
|
+
@app.route('/')
|
|
421
|
+
@auth.optional
|
|
422
|
+
async def index(request):
|
|
423
|
+
if g.current_user:
|
|
424
|
+
return f'Hello, {request.g.current_user}!'
|
|
425
|
+
else:
|
|
426
|
+
return 'Hello, anonymous user!'
|
|
427
|
+
|
|
428
|
+
As shown in the example, a route can check ``g.current_user`` to determine if
|
|
429
|
+
the user is authenticated or not.
|
|
430
|
+
|
|
336
431
|
Token Authentication
|
|
337
432
|
^^^^^^^^^^^^^^^^^^^^
|
|
338
433
|
|
|
339
|
-
To set up token authentication, create an instance of
|
|
434
|
+
To set up token authentication, create an instance of
|
|
435
|
+
:class:`TokenAuth <microdot.auth.TokenAuth>`::
|
|
340
436
|
|
|
341
437
|
from microdot.auth import TokenAuth
|
|
342
438
|
|
|
@@ -356,7 +452,17 @@ protect your routes::
|
|
|
356
452
|
@auth
|
|
357
453
|
async def index(request):
|
|
358
454
|
return f'Hello, {request.g.current_user}!'
|
|
359
|
-
|
|
455
|
+
|
|
456
|
+
Optional authentication can also be used with tokens::
|
|
457
|
+
|
|
458
|
+
@app.route('/')
|
|
459
|
+
@auth.optional
|
|
460
|
+
async def index(request):
|
|
461
|
+
if g.current_user:
|
|
462
|
+
return f'Hello, {request.g.current_user}!'
|
|
463
|
+
else:
|
|
464
|
+
return 'Hello, anonymous user!'
|
|
465
|
+
|
|
360
466
|
User Logins
|
|
361
467
|
~~~~~~~~~~~
|
|
362
468
|
|
|
@@ -369,6 +475,7 @@ User Logins
|
|
|
369
475
|
* - Required Microdot source files
|
|
370
476
|
- | `login.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/auth.py>`_
|
|
371
477
|
| `session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/session.py>`_
|
|
478
|
+
| `helpers.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/helpers.py>`_
|
|
372
479
|
* - Required external dependencies
|
|
373
480
|
- | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
|
|
374
481
|
| 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
|
|
@@ -36,6 +36,24 @@ class BaseAuth:
|
|
|
36
36
|
|
|
37
37
|
return wrapper
|
|
38
38
|
|
|
39
|
+
def optional(self, f):
|
|
40
|
+
"""Decorator to protect a route with optional authentication.
|
|
41
|
+
|
|
42
|
+
This decorator makes authentication for the decorated route optional,
|
|
43
|
+
meaning that the route is allowed to run with or with
|
|
44
|
+
authentication given in the request.
|
|
45
|
+
"""
|
|
46
|
+
async def wrapper(request, *args, **kwargs):
|
|
47
|
+
auth = self._get_auth(request)
|
|
48
|
+
if not auth:
|
|
49
|
+
request.g.current_user = None
|
|
50
|
+
else:
|
|
51
|
+
request.g.current_user = await invoke_handler(
|
|
52
|
+
self.auth_callback, request, *auth)
|
|
53
|
+
return await invoke_handler(f, request, *args, **kwargs)
|
|
54
|
+
|
|
55
|
+
return wrapper
|
|
56
|
+
|
|
39
57
|
|
|
40
58
|
class BasicAuth(BaseAuth):
|
|
41
59
|
"""Basic Authentication.
|
|
@@ -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
|