microdot 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {microdot-2.1.0/src/microdot.egg-info → microdot-2.2.0}/PKG-INFO +6 -5
  2. {microdot-2.1.0 → microdot-2.2.0}/README.md +3 -3
  3. {microdot-2.1.0 → microdot-2.2.0}/docs/api.rst +6 -0
  4. {microdot-2.1.0 → microdot-2.2.0}/docs/extensions.rst +89 -7
  5. {microdot-2.1.0 → microdot-2.2.0}/docs/freezing.rst +8 -5
  6. {microdot-2.1.0 → microdot-2.2.0}/docs/intro.rst +71 -16
  7. {microdot-2.1.0 → microdot-2.2.0}/pyproject.toml +1 -1
  8. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/__init__.py +1 -1
  9. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/jinja.py +15 -8
  10. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/microdot.py +62 -85
  11. microdot-2.2.0/src/microdot/multipart.py +291 -0
  12. {microdot-2.1.0 → microdot-2.2.0/src/microdot.egg-info}/PKG-INFO +6 -5
  13. {microdot-2.1.0 → microdot-2.2.0}/src/microdot.egg-info/SOURCES.txt +2 -0
  14. {microdot-2.1.0 → microdot-2.2.0}/tests/__init__.py +1 -0
  15. microdot-2.2.0/tests/test_multipart.py +192 -0
  16. {microdot-2.1.0 → microdot-2.2.0}/tests/test_url_pattern.py +27 -2
  17. microdot-2.2.0/tests/test_urlencode.py +13 -0
  18. microdot-2.1.0/tests/test_urlencode.py +0 -11
  19. {microdot-2.1.0 → microdot-2.2.0}/LICENSE +0 -0
  20. {microdot-2.1.0 → microdot-2.2.0}/MANIFEST.in +0 -0
  21. {microdot-2.1.0 → microdot-2.2.0}/docs/Makefile +0 -0
  22. {microdot-2.1.0 → microdot-2.2.0}/docs/_static/css/custom.css +0 -0
  23. {microdot-2.1.0 → microdot-2.2.0}/docs/conf.py +0 -0
  24. {microdot-2.1.0 → microdot-2.2.0}/docs/index.rst +0 -0
  25. {microdot-2.1.0 → microdot-2.2.0}/docs/make.bat +0 -0
  26. {microdot-2.1.0 → microdot-2.2.0}/docs/migrating.rst +0 -0
  27. {microdot-2.1.0 → microdot-2.2.0}/setup.cfg +0 -0
  28. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/asgi.py +0 -0
  29. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/auth.py +0 -0
  30. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/cors.py +0 -0
  31. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/helpers.py +0 -0
  32. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/login.py +0 -0
  33. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/session.py +0 -0
  34. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/sse.py +0 -0
  35. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/test_client.py +0 -0
  36. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/utemplate.py +0 -0
  37. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/websocket.py +0 -0
  38. {microdot-2.1.0 → microdot-2.2.0}/src/microdot/wsgi.py +0 -0
  39. {microdot-2.1.0 → microdot-2.2.0}/src/microdot.egg-info/dependency_links.txt +0 -0
  40. {microdot-2.1.0 → microdot-2.2.0}/src/microdot.egg-info/not-zip-safe +0 -0
  41. {microdot-2.1.0 → microdot-2.2.0}/src/microdot.egg-info/requires.txt +0 -0
  42. {microdot-2.1.0 → microdot-2.2.0}/src/microdot.egg-info/top_level.txt +0 -0
  43. {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.bin +0 -0
  44. {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.css +0 -0
  45. {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.gif +0 -0
  46. {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.gz +0 -0
  47. {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.html +0 -0
  48. {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.jpg +0 -0
  49. {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.js +0 -0
  50. {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.json +0 -0
  51. {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.png +0 -0
  52. {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.txt +0 -0
  53. {microdot-2.1.0 → microdot-2.2.0}/tests/files/test.txt.gz +0 -0
  54. {microdot-2.1.0 → microdot-2.2.0}/tests/mock_socket.py +0 -0
  55. {microdot-2.1.0 → microdot-2.2.0}/tests/templates/hello.jinja.txt +0 -0
  56. {microdot-2.1.0 → microdot-2.2.0}/tests/templates/hello.utemplate.txt +0 -0
  57. {microdot-2.1.0 → microdot-2.2.0}/tests/templates/hello_utemplate_txt.py +0 -0
  58. {microdot-2.1.0 → microdot-2.2.0}/tests/test_asgi.py +0 -0
  59. {microdot-2.1.0 → microdot-2.2.0}/tests/test_auth.py +0 -0
  60. {microdot-2.1.0 → microdot-2.2.0}/tests/test_cors.py +0 -0
  61. {microdot-2.1.0 → microdot-2.2.0}/tests/test_end2end.py +0 -0
  62. {microdot-2.1.0 → microdot-2.2.0}/tests/test_jinja.py +0 -0
  63. {microdot-2.1.0 → microdot-2.2.0}/tests/test_login.py +0 -0
  64. {microdot-2.1.0 → microdot-2.2.0}/tests/test_microdot.py +0 -0
  65. {microdot-2.1.0 → microdot-2.2.0}/tests/test_multidict.py +0 -0
  66. {microdot-2.1.0 → microdot-2.2.0}/tests/test_request.py +0 -0
  67. {microdot-2.1.0 → microdot-2.2.0}/tests/test_response.py +0 -0
  68. {microdot-2.1.0 → microdot-2.2.0}/tests/test_session.py +0 -0
  69. {microdot-2.1.0 → microdot-2.2.0}/tests/test_sse.py +0 -0
  70. {microdot-2.1.0 → microdot-2.2.0}/tests/test_utemplate.py +0 -0
  71. {microdot-2.1.0 → microdot-2.2.0}/tests/test_websocket.py +0 -0
  72. {microdot-2.1.0 → microdot-2.2.0}/tests/test_wsgi.py +0 -0
  73. {microdot-2.1.0 → microdot-2.2.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: microdot
3
- Version: 2.1.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
  [![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
 
@@ -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
- @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
111
+
112
+ @app.route('/echo')
113
+ @with_websocket
114
+ async def echo(request, ws):
115
+ while True:
116
+ message = await ws.receive()
117
+ await ws.send(message)
41
118
 
42
119
  Server-Sent Events
43
120
  ~~~~~~~~~~~~~~~~~~
@@ -50,6 +127,7 @@ Server-Sent Events
50
127
 
51
128
  * - Required Microdot source files
52
129
  - | `sse.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/sse.py>`_
130
+ | `helpers.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/helpers.py>`_
53
131
 
54
132
  * - Required external dependencies
55
133
  - | None
@@ -65,6 +143,8 @@ asynchronous method to send an event to the client.
65
143
 
66
144
  Example::
67
145
 
146
+ from microdot.sse import with_sse
147
+
68
148
  @app.route('/events')
69
149
  @with_sse
70
150
  async def events(request, sse):
@@ -213,6 +293,7 @@ Secure User Sessions
213
293
 
214
294
  * - Required Microdot source files
215
295
  - | `session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/session.py>`_
296
+ | `helpers.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/helpers.py>`_
216
297
 
217
298
  * - Required external dependencies
218
299
  - | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
@@ -369,6 +450,7 @@ User Logins
369
450
  * - Required Microdot source files
370
451
  - | `login.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/auth.py>`_
371
452
  | `session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/session.py>`_
453
+ | `helpers.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/helpers.py>`_
372
454
  * - Required external dependencies
373
455
  - | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
374
456
  | MicroPython: `jwt.py <https://github.com/micropython/micropython-lib/blob/master/python-ecosys/pyjwt/jwt.py>`_,
@@ -1,5 +1,8 @@
1
- Cross-Compiling and Freezing Microdot (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
@@ -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. Applications
899
- must ensure their handlers do not block, as this will prevent other concurrent
900
- 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.
901
953
 
902
- When running under CPython, ``async def`` handler functions run as native
903
- asyncio tasks, while ``def`` handler functions are executed in a
904
- `thread executor <https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor>`_
905
- 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.
906
961
 
907
962
  Under MicroPython the situation is different. Most microcontroller boards
908
- implementing MicroPython do not have threading support or executors, so ``def``
909
- handler functions in this platform can only run in the main and only thread.
910
- These functions will block the asynchronous loop when they take too long to
911
- complete so ``async def`` handlers properly written to allow other handlers to
912
- 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.1.0"
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
@@ -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
- global _jinja_env
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 _jinja_env is None: # pragma: no cover
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 = _jinja_env.get_template(template)
46
+ self.template = self.jinja_env.get_template(template, **kwargs)
40
47
 
41
48
  def generate(self, *args, **kwargs):
42
49
  """Return a generator that renders the template in chunks, with the
@@ -8,6 +8,7 @@ servers for MicroPython and standard Python.
8
8
  import asyncio
9
9
  import io
10
10
  import json
11
+ import re
11
12
  import time
12
13
 
13
14
  try:
@@ -56,23 +57,9 @@ MUTED_SOCKET_ERRORS = [
56
57
  ]
57
58
 
58
59
 
59
- def urldecode_str(s):
60
- s = s.replace('+', ' ')
61
- parts = s.split('%')
62
- if len(parts) == 1:
63
- return s
64
- result = [parts[0]]
65
- for item in parts[1:]:
66
- if item == '':
67
- result.append('%')
68
- else:
69
- code = item[:2]
70
- result.append(chr(int(code, 16)))
71
- result.append(item[2:])
72
- return ''.join(result)
73
-
74
-
75
- def urldecode_bytes(s):
60
+ def urldecode(s):
61
+ if isinstance(s, str):
62
+ s = s.encode()
76
63
  s = s.replace(b'+', b' ')
77
64
  parts = s.split(b'%')
78
65
  if len(parts) == 1:
@@ -384,6 +371,7 @@ class Request:
384
371
  self.sock = sock
385
372
  self._json = None
386
373
  self._form = None
374
+ self._files = None
387
375
  self.after_request_handlers = []
388
376
 
389
377
  @staticmethod
@@ -440,12 +428,12 @@ class Request:
440
428
  if isinstance(urlencoded, str):
441
429
  for kv in [pair.split('=', 1)
442
430
  for pair in urlencoded.split('&') if pair]:
443
- data[urldecode_str(kv[0])] = urldecode_str(kv[1]) \
431
+ data[urldecode(kv[0])] = urldecode(kv[1]) \
444
432
  if len(kv) > 1 else ''
445
433
  elif isinstance(urlencoded, bytes): # pragma: no branch
446
434
  for kv in [pair.split(b'=', 1)
447
435
  for pair in urlencoded.split(b'&') if pair]:
448
- data[urldecode_bytes(kv[0])] = urldecode_bytes(kv[1]) \
436
+ data[urldecode(kv[0])] = urldecode(kv[1]) \
449
437
  if len(kv) > 1 else b''
450
438
  return data
451
439
 
@@ -478,7 +466,13 @@ class Request:
478
466
  def form(self):
479
467
  """The parsed form submission body, as a
480
468
  :class:`MultiDict <microdot.MultiDict>` object, or ``None`` if the
481
- request does not have a form submission."""
469
+ request does not have a form submission.
470
+
471
+ Forms that are URL encoded are processed by default. For multipart
472
+ forms to be processed, the
473
+ :func:`with_form_data <microdot.multipart.with_form_data>`
474
+ decorator must be added to the route.
475
+ """
482
476
  if self._form is None:
483
477
  if self.content_type is None:
484
478
  return None
@@ -488,6 +482,17 @@ class Request:
488
482
  self._form = self._parse_urlencoded(self.body)
489
483
  return self._form
490
484
 
485
+ @property
486
+ def files(self):
487
+ """The files uploaded in the request as a dictionary, or ``None`` if
488
+ the request does not have any files.
489
+
490
+ The :func:`with_form_data <microdot.multipart.with_form_data>`
491
+ decorator must be added to the route that receives file uploads for
492
+ this property to be set.
493
+ """
494
+ return self._files
495
+
491
496
  def after_request(self, f):
492
497
  """Register a request-specific function to run after the request is
493
498
  handled. Request-specific after request handlers run at the very end,
@@ -805,13 +810,23 @@ class Response:
805
810
 
806
811
 
807
812
  class URLPattern():
813
+ segment_patterns = {
814
+ 'string': '/([^/]+)',
815
+ 'int': '/(-?\\d+)',
816
+ 'path': '/(.+)',
817
+ }
818
+ segment_parsers = {
819
+ 'int': lambda value: int(value),
820
+ }
821
+
808
822
  def __init__(self, url_pattern):
809
823
  self.url_pattern = url_pattern
810
824
  self.segments = []
811
825
  self.regex = None
826
+
827
+ def compile(self):
812
828
  pattern = ''
813
- use_regex = False
814
- for segment in url_pattern.lstrip('/').split('/'):
829
+ for segment in self.url_pattern.lstrip('/').split('/'):
815
830
  if segment and segment[0] == '<':
816
831
  if segment[-1] != '>':
817
832
  raise ValueError('invalid URL pattern')
@@ -822,82 +837,44 @@ class URLPattern():
822
837
  type_ = 'string'
823
838
  name = segment
824
839
  parser = None
825
- if type_ == 'string':
826
- parser = self._string_segment
827
- pattern += '/([^/]+)'
828
- elif type_ == 'int':
829
- parser = self._int_segment
830
- pattern += '/(-?\\d+)'
831
- elif type_ == 'path':
832
- use_regex = True
833
- pattern += '/(.+)'
834
- elif type_.startswith('re:'):
835
- use_regex = True
840
+ if type_.startswith('re:'):
836
841
  pattern += '/({pattern})'.format(pattern=type_[3:])
837
842
  else:
838
- raise ValueError('invalid URL segment type')
843
+ if type_ not in self.segment_patterns:
844
+ raise ValueError('invalid URL segment type')
845
+ pattern += self.segment_patterns[type_]
846
+ parser = self.segment_parsers.get(type_)
839
847
  self.segments.append({'parser': parser, 'name': name,
840
848
  'type': type_})
841
849
  else:
842
850
  pattern += '/' + segment
843
- self.segments.append({'parser': self._static_segment(segment)})
844
- if use_regex:
845
- import re
846
- self.regex = re.compile('^' + pattern + '$')
851
+ self.segments.append({'parser': None})
852
+ self.regex = re.compile('^' + pattern + '$')
853
+ return self.regex
854
+
855
+ @classmethod
856
+ def register_type(cls, type_name, pattern='[^/]+', parser=None):
857
+ cls.segment_patterns[type_name] = '/({})'.format(pattern)
858
+ cls.segment_parsers[type_name] = parser
847
859
 
848
860
  def match(self, path):
849
861
  args = {}
850
- if self.regex:
851
- g = self.regex.match(path)
852
- if not g:
853
- return
854
- i = 1
855
- for segment in self.segments:
856
- if 'name' not in segment:
857
- continue
858
- value = g.group(i)
859
- if segment['type'] == 'int':
860
- value = int(value)
861
- args[segment['name']] = value
862
- i += 1
863
- else:
864
- if len(path) == 0 or path[0] != '/':
865
- return
866
- path = path[1:]
867
- args = {}
868
- for segment in self.segments:
869
- if path is None:
870
- return
871
- arg, path = segment['parser'](path)
862
+ g = (self.regex or self.compile()).match(path)
863
+ if not g:
864
+ return
865
+ i = 1
866
+ for segment in self.segments:
867
+ if 'name' not in segment:
868
+ continue
869
+ arg = g.group(i)
870
+ if segment['parser']:
871
+ arg = self.segment_parsers[segment['type']](arg)
872
872
  if arg is None:
873
873
  return
874
- if 'name' in segment:
875
- args[segment['name']] = arg
876
- if path is not None:
877
- return
874
+ args[segment['name']] = arg
875
+ i += 1
878
876
  return args
879
877
 
880
- def _static_segment(self, segment):
881
- def _static(value):
882
- s = value.split('/', 1)
883
- if s[0] == segment:
884
- return '', s[1] if len(s) > 1 else None
885
- return None, None
886
- return _static
887
-
888
- def _string_segment(self, value):
889
- s = value.split('/', 1)
890
- if len(s[0]) == 0:
891
- return None, None
892
- return s[0], s[1] if len(s) > 1 else None
893
-
894
- def _int_segment(self, value):
895
- s = value.split('/', 1)
896
- try:
897
- return int(s[0]), s[1] if len(s) > 1 else None
898
- except ValueError:
899
- return None, None
900
-
901
878
  def __repr__(self): # pragma: no cover
902
879
  return 'URLPattern: {}'.format(self.url_pattern)
903
880