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.
Files changed (73) hide show
  1. {microdot-2.0.7/src/microdot.egg-info → microdot-2.2.0}/PKG-INFO +6 -5
  2. {microdot-2.0.7 → microdot-2.2.0}/README.md +3 -3
  3. {microdot-2.0.7 → microdot-2.2.0}/docs/api.rst +22 -0
  4. {microdot-2.0.7 → microdot-2.2.0}/docs/extensions.rst +272 -18
  5. {microdot-2.0.7 → microdot-2.2.0}/docs/freezing.rst +8 -5
  6. {microdot-2.0.7 → microdot-2.2.0}/docs/intro.rst +91 -22
  7. {microdot-2.0.7 → microdot-2.2.0}/pyproject.toml +1 -1
  8. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/__init__.py +1 -1
  9. microdot-2.2.0/src/microdot/auth.py +144 -0
  10. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/jinja.py +15 -8
  11. microdot-2.2.0/src/microdot/login.py +163 -0
  12. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/microdot.py +181 -135
  13. microdot-2.2.0/src/microdot/multipart.py +291 -0
  14. {microdot-2.0.7 → microdot-2.2.0/src/microdot.egg-info}/PKG-INFO +6 -5
  15. {microdot-2.0.7 → microdot-2.2.0}/src/microdot.egg-info/SOURCES.txt +6 -0
  16. {microdot-2.0.7 → microdot-2.2.0}/tests/__init__.py +3 -0
  17. microdot-2.2.0/tests/test_auth.py +125 -0
  18. microdot-2.2.0/tests/test_login.py +188 -0
  19. {microdot-2.0.7 → microdot-2.2.0}/tests/test_microdot.py +201 -2
  20. microdot-2.2.0/tests/test_multipart.py +192 -0
  21. {microdot-2.0.7 → microdot-2.2.0}/tests/test_session.py +1 -1
  22. {microdot-2.0.7 → microdot-2.2.0}/tests/test_url_pattern.py +27 -2
  23. microdot-2.2.0/tests/test_urlencode.py +13 -0
  24. microdot-2.0.7/tests/test_urlencode.py +0 -11
  25. {microdot-2.0.7 → microdot-2.2.0}/LICENSE +0 -0
  26. {microdot-2.0.7 → microdot-2.2.0}/MANIFEST.in +0 -0
  27. {microdot-2.0.7 → microdot-2.2.0}/docs/Makefile +0 -0
  28. {microdot-2.0.7 → microdot-2.2.0}/docs/_static/css/custom.css +0 -0
  29. {microdot-2.0.7 → microdot-2.2.0}/docs/conf.py +0 -0
  30. {microdot-2.0.7 → microdot-2.2.0}/docs/index.rst +0 -0
  31. {microdot-2.0.7 → microdot-2.2.0}/docs/make.bat +0 -0
  32. {microdot-2.0.7 → microdot-2.2.0}/docs/migrating.rst +0 -0
  33. {microdot-2.0.7 → microdot-2.2.0}/setup.cfg +0 -0
  34. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/asgi.py +0 -0
  35. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/cors.py +0 -0
  36. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/helpers.py +0 -0
  37. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/session.py +0 -0
  38. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/sse.py +0 -0
  39. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/test_client.py +0 -0
  40. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/utemplate.py +0 -0
  41. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/websocket.py +0 -0
  42. {microdot-2.0.7 → microdot-2.2.0}/src/microdot/wsgi.py +0 -0
  43. {microdot-2.0.7 → microdot-2.2.0}/src/microdot.egg-info/dependency_links.txt +0 -0
  44. {microdot-2.0.7 → microdot-2.2.0}/src/microdot.egg-info/not-zip-safe +0 -0
  45. {microdot-2.0.7 → microdot-2.2.0}/src/microdot.egg-info/requires.txt +0 -0
  46. {microdot-2.0.7 → microdot-2.2.0}/src/microdot.egg-info/top_level.txt +0 -0
  47. {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.bin +0 -0
  48. {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.css +0 -0
  49. {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.gif +0 -0
  50. {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.gz +0 -0
  51. {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.html +0 -0
  52. {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.jpg +0 -0
  53. {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.js +0 -0
  54. {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.json +0 -0
  55. {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.png +0 -0
  56. {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.txt +0 -0
  57. {microdot-2.0.7 → microdot-2.2.0}/tests/files/test.txt.gz +0 -0
  58. {microdot-2.0.7 → microdot-2.2.0}/tests/mock_socket.py +0 -0
  59. {microdot-2.0.7 → microdot-2.2.0}/tests/templates/hello.jinja.txt +0 -0
  60. {microdot-2.0.7 → microdot-2.2.0}/tests/templates/hello.utemplate.txt +0 -0
  61. {microdot-2.0.7 → microdot-2.2.0}/tests/templates/hello_utemplate_txt.py +0 -0
  62. {microdot-2.0.7 → microdot-2.2.0}/tests/test_asgi.py +0 -0
  63. {microdot-2.0.7 → microdot-2.2.0}/tests/test_cors.py +0 -0
  64. {microdot-2.0.7 → microdot-2.2.0}/tests/test_end2end.py +0 -0
  65. {microdot-2.0.7 → microdot-2.2.0}/tests/test_jinja.py +0 -0
  66. {microdot-2.0.7 → microdot-2.2.0}/tests/test_multidict.py +0 -0
  67. {microdot-2.0.7 → microdot-2.2.0}/tests/test_request.py +0 -0
  68. {microdot-2.0.7 → microdot-2.2.0}/tests/test_response.py +0 -0
  69. {microdot-2.0.7 → microdot-2.2.0}/tests/test_sse.py +0 -0
  70. {microdot-2.0.7 → microdot-2.2.0}/tests/test_utemplate.py +0 -0
  71. {microdot-2.0.7 → microdot-2.2.0}/tests/test_websocket.py +0 -0
  72. {microdot-2.0.7 → microdot-2.2.0}/tests/test_wsgi.py +0 -0
  73. {microdot-2.0.7 → microdot-2.2.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: microdot
3
- Version: 2.0.7
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
  [![Build status](https://github.com/miguelgrinberg/microdot/workflows/build/badge.svg)](https://github.com/miguelgrinberg/microdot/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/microdot/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/microdot)
@@ -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
- - Support for forms encoded in `multipart/form-data` format
69
- - Authentication support, similar to [Flask-Login](https://github.com/maxcountryman/flask-login) for Flask
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/miguelgrinberg/microdot/discussions/new?category=ideas)!
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
- - Support for forms encoded in `multipart/form-data` format
47
- - Authentication support, similar to [Flask-Login](https://github.com/maxcountryman/flask-login) for Flask
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/miguelgrinberg/microdot/discussions/new?category=ideas)!
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
- WebSocket Support
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
- @app.route('/echo')
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
- Server-Sent Events Support
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
- Rendering Templates
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
- Maintaining Secure User Sessions
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
- Testing with the Test Client
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
- Deploying on a Production Web Server
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 (MicroPython Only)
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 without problems.
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 depending on the device, so you will need to consult the MicroPython
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
- For the most control, the ``re`` type allows the application to provide a
333
- custom regular expression for the dynamic component. The next example defines
334
- a route that only matches usernames that begin with an upper or lower case
335
- letter, followed by a sequence of letters or numbers::
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 past a certain size. To make it
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
- Before-request, after-request and error handlers defined in the
505
- sub-application are also copied over to the main application at mount time.
506
- Once installed in the main application, these handlers will apply to the
507
- whole application and not just the sub-application in which they were
508
- created.
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. Applications
885
- must ensure their handlers do not block, as this will prevent other concurrent
886
- requests from being handled.
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 under CPython, ``async def`` handler functions run as native
889
- asyncio tasks, while ``def`` handler functions are executed in a
890
- `thread executor <https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor>`_
891
- to prevent them from blocking the asynchronous loop.
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
- implementing MicroPython do not have threading support or executors, so ``def``
895
- handler functions in this platform can only run in the main and only thread.
896
- These functions will block the asynchronous loop when they take too long to
897
- complete so ``async def`` handlers properly written to allow other handlers to
898
- run in parallel should be preferred.
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "microdot"
3
- version = "2.0.7"
3
+ version = "2.2.0"
4
4
  authors = [
5
5
  { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
6
6
  ]
@@ -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