microdot 2.0.5__tar.gz → 2.0.7__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 (67) hide show
  1. {microdot-2.0.5/src/microdot.egg-info → microdot-2.0.7}/PKG-INFO +24 -6
  2. {microdot-2.0.5 → microdot-2.0.7}/README.md +20 -5
  3. {microdot-2.0.5 → microdot-2.0.7}/docs/extensions.rst +2 -2
  4. {microdot-2.0.5 → microdot-2.0.7}/docs/intro.rst +33 -6
  5. {microdot-2.0.5 → microdot-2.0.7}/pyproject.toml +7 -1
  6. microdot-2.0.7/src/microdot/helpers.py +8 -0
  7. {microdot-2.0.5 → microdot-2.0.7}/src/microdot/microdot.py +14 -6
  8. {microdot-2.0.5 → microdot-2.0.7}/src/microdot/session.py +28 -21
  9. {microdot-2.0.5 → microdot-2.0.7}/src/microdot/sse.py +7 -1
  10. {microdot-2.0.5 → microdot-2.0.7}/src/microdot/test_client.py +32 -9
  11. {microdot-2.0.5 → microdot-2.0.7}/src/microdot/websocket.py +2 -0
  12. {microdot-2.0.5 → microdot-2.0.7/src/microdot.egg-info}/PKG-INFO +24 -6
  13. {microdot-2.0.5 → microdot-2.0.7}/src/microdot.egg-info/SOURCES.txt +2 -0
  14. {microdot-2.0.5 → microdot-2.0.7}/src/microdot.egg-info/dependency_links.txt +0 -0
  15. {microdot-2.0.5 → microdot-2.0.7}/src/microdot.egg-info/not-zip-safe +0 -0
  16. microdot-2.0.7/src/microdot.egg-info/requires.txt +7 -0
  17. {microdot-2.0.5 → microdot-2.0.7}/src/microdot.egg-info/top_level.txt +0 -0
  18. microdot-2.0.7/tests/files/test.txt.gz +1 -0
  19. {microdot-2.0.5 → microdot-2.0.7}/tests/test_microdot.py +21 -0
  20. {microdot-2.0.5 → microdot-2.0.7}/tests/test_response.py +18 -3
  21. {microdot-2.0.5 → microdot-2.0.7}/tests/test_session.py +74 -0
  22. {microdot-2.0.5 → microdot-2.0.7}/tests/test_sse.py +4 -0
  23. microdot-2.0.5/src/microdot.egg-info/requires.txt +0 -3
  24. {microdot-2.0.5 → microdot-2.0.7}/LICENSE +0 -0
  25. {microdot-2.0.5 → microdot-2.0.7}/MANIFEST.in +0 -0
  26. {microdot-2.0.5 → microdot-2.0.7}/docs/Makefile +0 -0
  27. {microdot-2.0.5 → microdot-2.0.7}/docs/_static/css/custom.css +0 -0
  28. {microdot-2.0.5 → microdot-2.0.7}/docs/api.rst +0 -0
  29. {microdot-2.0.5 → microdot-2.0.7}/docs/conf.py +0 -0
  30. {microdot-2.0.5 → microdot-2.0.7}/docs/freezing.rst +0 -0
  31. {microdot-2.0.5 → microdot-2.0.7}/docs/index.rst +0 -0
  32. {microdot-2.0.5 → microdot-2.0.7}/docs/make.bat +0 -0
  33. {microdot-2.0.5 → microdot-2.0.7}/docs/migrating.rst +0 -0
  34. {microdot-2.0.5 → microdot-2.0.7}/setup.cfg +0 -0
  35. {microdot-2.0.5 → microdot-2.0.7}/src/microdot/__init__.py +0 -0
  36. {microdot-2.0.5 → microdot-2.0.7}/src/microdot/asgi.py +0 -0
  37. {microdot-2.0.5 → microdot-2.0.7}/src/microdot/cors.py +0 -0
  38. {microdot-2.0.5 → microdot-2.0.7}/src/microdot/jinja.py +0 -0
  39. {microdot-2.0.5 → microdot-2.0.7}/src/microdot/utemplate.py +0 -0
  40. {microdot-2.0.5 → microdot-2.0.7}/src/microdot/wsgi.py +0 -0
  41. {microdot-2.0.5 → microdot-2.0.7}/tests/__init__.py +0 -0
  42. {microdot-2.0.5 → microdot-2.0.7}/tests/files/test.bin +0 -0
  43. {microdot-2.0.5 → microdot-2.0.7}/tests/files/test.css +0 -0
  44. {microdot-2.0.5 → microdot-2.0.7}/tests/files/test.gif +0 -0
  45. {microdot-2.0.5 → microdot-2.0.7}/tests/files/test.gz +0 -0
  46. {microdot-2.0.5 → microdot-2.0.7}/tests/files/test.html +0 -0
  47. {microdot-2.0.5 → microdot-2.0.7}/tests/files/test.jpg +0 -0
  48. {microdot-2.0.5 → microdot-2.0.7}/tests/files/test.js +0 -0
  49. {microdot-2.0.5 → microdot-2.0.7}/tests/files/test.json +0 -0
  50. {microdot-2.0.5 → microdot-2.0.7}/tests/files/test.png +0 -0
  51. {microdot-2.0.5 → microdot-2.0.7}/tests/files/test.txt +0 -0
  52. {microdot-2.0.5 → microdot-2.0.7}/tests/mock_socket.py +0 -0
  53. {microdot-2.0.5 → microdot-2.0.7}/tests/templates/hello.jinja.txt +0 -0
  54. {microdot-2.0.5 → microdot-2.0.7}/tests/templates/hello.utemplate.txt +0 -0
  55. {microdot-2.0.5 → microdot-2.0.7}/tests/templates/hello_utemplate_txt.py +0 -0
  56. {microdot-2.0.5 → microdot-2.0.7}/tests/test_asgi.py +0 -0
  57. {microdot-2.0.5 → microdot-2.0.7}/tests/test_cors.py +0 -0
  58. {microdot-2.0.5 → microdot-2.0.7}/tests/test_end2end.py +0 -0
  59. {microdot-2.0.5 → microdot-2.0.7}/tests/test_jinja.py +0 -0
  60. {microdot-2.0.5 → microdot-2.0.7}/tests/test_multidict.py +0 -0
  61. {microdot-2.0.5 → microdot-2.0.7}/tests/test_request.py +0 -0
  62. {microdot-2.0.5 → microdot-2.0.7}/tests/test_url_pattern.py +0 -0
  63. {microdot-2.0.5 → microdot-2.0.7}/tests/test_urlencode.py +0 -0
  64. {microdot-2.0.5 → microdot-2.0.7}/tests/test_utemplate.py +0 -0
  65. {microdot-2.0.5 → microdot-2.0.7}/tests/test_websocket.py +0 -0
  66. {microdot-2.0.5 → microdot-2.0.7}/tests/test_wsgi.py +0 -0
  67. {microdot-2.0.5 → microdot-2.0.7}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: microdot
3
- Version: 2.0.5
3
+ Version: 2.0.7
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
@@ -14,8 +14,11 @@ Classifier: Operating System :: OS Independent
14
14
  Requires-Python: >=3.8
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
+ Provides-Extra: dev
18
+ Requires-Dist: tox; extra == "dev"
17
19
  Provides-Extra: docs
18
20
  Requires-Dist: sphinx; extra == "docs"
21
+ Requires-Dist: pyjwt; extra == "docs"
19
22
 
20
23
  # microdot
21
24
  [![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)
@@ -51,10 +54,25 @@ describes the backwards incompatible changes that were made.
51
54
 
52
55
  ## Resources
53
56
 
57
+ - [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)
54
58
  - Documentation
55
- - [Stable](https://microdot.readthedocs.io/en/stable/)
56
59
  - [Latest](https://microdot.readthedocs.io/en/latest/)
57
- - Still using version 1?
58
- - [Code](https://github.com/miguelgrinberg/microdot/tree/v1)
59
- - [Documentation](https://microdot.readthedocs.io/en/v1/)
60
- - [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)
60
+ - [Stable (v2)](https://microdot.readthedocs.io/en/stable/)
61
+ - [Legacy (v1)](https://microdot.readthedocs.io/en/v1/) ([Code](https://github.com/miguelgrinberg/microdot/tree/v1))
62
+
63
+ ## Roadmap
64
+
65
+ The following features are planned for future releases of Microdot, both for
66
+ MicroPython and CPython:
67
+
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
70
+ - OpenAPI integration, similar to [APIFairy](https://github.com/miguelgrinberg/apifairy) for Flask
71
+
72
+ In addition to the above, the following extensions are also under consideration,
73
+ but only for CPython:
74
+
75
+ - Database integration through [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)
76
+ - Socket.IO support through [python-socketio](https://github.com/miguelgrinberg/python-socketio)
77
+
78
+ Do you have other ideas to propose? Let's [discuss them](https://github.com/miguelgrinberg/microdot/discussions/new?category=ideas)!
@@ -32,10 +32,25 @@ describes the backwards incompatible changes that were made.
32
32
 
33
33
  ## Resources
34
34
 
35
+ - [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)
35
36
  - Documentation
36
- - [Stable](https://microdot.readthedocs.io/en/stable/)
37
37
  - [Latest](https://microdot.readthedocs.io/en/latest/)
38
- - Still using version 1?
39
- - [Code](https://github.com/miguelgrinberg/microdot/tree/v1)
40
- - [Documentation](https://microdot.readthedocs.io/en/v1/)
41
- - [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)
38
+ - [Stable (v2)](https://microdot.readthedocs.io/en/stable/)
39
+ - [Legacy (v1)](https://microdot.readthedocs.io/en/v1/) ([Code](https://github.com/miguelgrinberg/microdot/tree/v1))
40
+
41
+ ## Roadmap
42
+
43
+ The following features are planned for future releases of Microdot, both for
44
+ MicroPython and CPython:
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
48
+ - OpenAPI integration, similar to [APIFairy](https://github.com/miguelgrinberg/apifairy) for Flask
49
+
50
+ In addition to the above, the following extensions are also under consideration,
51
+ but only for CPython:
52
+
53
+ - Database integration through [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)
54
+ - Socket.IO support through [python-socketio](https://github.com/miguelgrinberg/python-socketio)
55
+
56
+ Do you have other ideas to propose? Let's [discuss them](https://github.com/miguelgrinberg/microdot/discussions/new?category=ideas)!
@@ -286,7 +286,7 @@ Cross-Origin Resource Sharing (CORS)
286
286
  - | None
287
287
 
288
288
  * - Examples
289
- - | `cors.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/cors/cors.py>`_
289
+ - | `app.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/cors/app.py>`_
290
290
 
291
291
  The CORS extension provides support for `Cross-Origin Resource Sharing
292
292
  (CORS) <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_. CORS is a
@@ -363,7 +363,7 @@ Using an ASGI Web Server
363
363
  - | `asgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/asgi.py>`_
364
364
 
365
365
  * - Required external dependencies
366
- - | An ASGI web server, such as `Uvicorn <https://uvicorn.org/>`_.
366
+ - | An ASGI web server, such as `Uvicorn <https://www.uvicorn.org/>`_.
367
367
 
368
368
  * - Examples
369
369
  - | `hello_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_asgi.py>`_
@@ -82,8 +82,34 @@ handler functions can be defined as ``async def`` or ``def`` functions, but
82
82
  ``async def`` functions are recommended for performance.
83
83
 
84
84
  The :func:`run() <microdot.Microdot.run>` method starts the application's web
85
- server on port 5000 by default. This method blocks while it waits for
86
- connections from clients.
85
+ server on port 5000 by default, and creates its own asynchronous loop. This
86
+ method blocks while it waits for connections from clients.
87
+
88
+ For some applications it may be necessary to run the web server alongside other
89
+ asynchronous tasks, on an already running loop. In that case, instead of
90
+ ``app.run()`` the web server can be started by invoking the
91
+ :func:`start_server() <microdot.Microdot.start_server>` coroutine as shown in
92
+ the following example::
93
+
94
+ import asyncio
95
+ from microdot import Microdot
96
+
97
+ app = Microdot()
98
+
99
+ @app.route('/')
100
+ async def index(request):
101
+ return 'Hello, world!'
102
+
103
+ async def main():
104
+ # start the server in a background task
105
+ server = asyncio.create_task(app.start_server())
106
+
107
+ # ... do other asynchronous work here ...
108
+
109
+ # cleanup before ending the application
110
+ await server
111
+
112
+ asyncio.run(main())
87
113
 
88
114
  Running with CPython
89
115
  ^^^^^^^^^^^^^^^^^^^^
@@ -92,7 +118,7 @@ Running with CPython
92
118
  :align: left
93
119
 
94
120
  * - Required Microdot source files
95
- - | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
121
+ - | `microdot.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/microdot.py>`_
96
122
 
97
123
  * - Required external dependencies
98
124
  - | None
@@ -118,7 +144,7 @@ Running with MicroPython
118
144
  :align: left
119
145
 
120
146
  * - Required Microdot source files
121
- - | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
147
+ - | `microdot.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/microdot.py>`_
122
148
 
123
149
  * - Required external dependencies
124
150
  - | None
@@ -145,8 +171,9 @@ changed by passing the ``port`` argument to the ``run()`` method.
145
171
  Web Server Configuration
146
172
  ^^^^^^^^^^^^^^^^^^^^^^^^
147
173
 
148
- The :func:`run() <microdot.Microdot.run>` method supports a few arguments to
149
- configure the web server.
174
+ The :func:`run() <microdot.Microdot.run>` and
175
+ :func:`start_server() <microdot.Microdot.start_server>` methods support a few
176
+ arguments to configure the web server.
150
177
 
151
178
  - ``port``: The port number to listen on. Pass the desired port number in this
152
179
  argument to use a port different than the default of 5000. For example::
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "microdot"
3
- version = "2.0.5"
3
+ version = "2.0.7"
4
4
  authors = [
5
5
  { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
6
6
  ]
@@ -14,6 +14,8 @@ classifiers = [
14
14
  "Operating System :: OS Independent",
15
15
  ]
16
16
  requires-python = ">=3.8"
17
+ dependencies = [
18
+ ]
17
19
 
18
20
  [project.readme]
19
21
  file = "README.md"
@@ -24,8 +26,12 @@ Homepage = "https://github.com/miguelgrinberg/microdot"
24
26
  "Bug Tracker" = "https://github.com/miguelgrinberg/microdot/issues"
25
27
 
26
28
  [project.optional-dependencies]
29
+ dev = [
30
+ "tox",
31
+ ]
27
32
  docs = [
28
33
  "sphinx",
34
+ "pyjwt",
29
35
  ]
30
36
 
31
37
  [tool.setuptools]
@@ -0,0 +1,8 @@
1
+ try:
2
+ from functools import wraps
3
+ except ImportError: # pragma: no cover
4
+ # MicroPython does not currently implement functools.wraps
5
+ def wraps(wrapped):
6
+ def _(wrapper):
7
+ return wrapper
8
+ return _
@@ -598,7 +598,7 @@ class Response:
598
598
  else: # pragma: no cover
599
599
  http_cookie += '; Expires=' + time.strftime(
600
600
  '%a, %d %b %Y %H:%M:%S GMT', expires.timetuple())
601
- if max_age:
601
+ if max_age is not None:
602
602
  http_cookie += '; Max-Age=' + str(max_age)
603
603
  if secure:
604
604
  http_cookie += '; Secure'
@@ -616,10 +616,10 @@ class Response:
616
616
 
617
617
  :param cookie: The cookie's name.
618
618
  :param kwargs: Any cookie opens and flags supported by
619
- ``set_cookie()`` except ``expires``.
619
+ ``set_cookie()`` except ``expires`` and ``max_age``.
620
620
  """
621
621
  self.set_cookie(cookie, '', expires='Thu, 01 Jan 1970 00:00:01 GMT',
622
- **kwargs)
622
+ max_age=0, **kwargs)
623
623
 
624
624
  def complete(self):
625
625
  if isinstance(self.body, bytes) and \
@@ -774,7 +774,10 @@ class Response:
774
774
  first.
775
775
  """
776
776
  if content_type is None:
777
- ext = filename.split('.')[-1]
777
+ if compressed and filename.endswith('.gz'):
778
+ ext = filename[:-3].split('.')[-1]
779
+ else:
780
+ ext = filename.split('.')[-1]
778
781
  if ext in Response.types_map:
779
782
  content_type = Response.types_map[ext]
780
783
  else:
@@ -1191,7 +1194,7 @@ class Microdot:
1191
1194
  Example::
1192
1195
 
1193
1196
  import asyncio
1194
- from microdot_asyncio import Microdot
1197
+ from microdot import Microdot
1195
1198
 
1196
1199
  app = Microdot()
1197
1200
 
@@ -1268,7 +1271,7 @@ class Microdot:
1268
1271
 
1269
1272
  Example::
1270
1273
 
1271
- from microdot_asyncio import Microdot
1274
+ from microdot import Microdot
1272
1275
 
1273
1276
  app = Microdot()
1274
1277
 
@@ -1366,7 +1369,12 @@ class Microdot:
1366
1369
  if res is None:
1367
1370
  res = await invoke_handler(
1368
1371
  f, req, **req.url_args)
1372
+ if isinstance(res, int):
1373
+ res = '', res
1369
1374
  if isinstance(res, tuple):
1375
+ if isinstance(res[0], int):
1376
+ res = ('', res[0],
1377
+ res[1] if len(res) > 1 else {})
1370
1378
  body = res[0]
1371
1379
  if isinstance(res[1], int):
1372
1380
  status_code = res[1]
@@ -1,7 +1,6 @@
1
1
  import jwt
2
2
  from microdot.microdot import invoke_handler
3
-
4
- secret_key = None
3
+ from microdot.helpers import wraps
5
4
 
6
5
 
7
6
  class SessionDict(dict):
@@ -30,14 +29,21 @@ class Session:
30
29
  """
31
30
  secret_key = None
32
31
 
33
- def __init__(self, app=None, secret_key=None):
32
+ def __init__(self, app=None, secret_key=None, cookie_options=None):
34
33
  self.secret_key = secret_key
34
+ self.cookie_options = cookie_options or {}
35
35
  if app is not None:
36
36
  self.initialize(app)
37
37
 
38
- def initialize(self, app, secret_key=None):
38
+ def initialize(self, app, secret_key=None, cookie_options=None):
39
39
  if secret_key is not None:
40
40
  self.secret_key = secret_key
41
+ if cookie_options is not None:
42
+ self.cookie_options = cookie_options
43
+ if 'path' not in self.cookie_options:
44
+ self.cookie_options['path'] = '/'
45
+ if 'http_only' not in self.cookie_options:
46
+ self.cookie_options['http_only'] = True
41
47
  app._session = self
42
48
 
43
49
  def get(self, request):
@@ -57,13 +63,7 @@ class Session:
57
63
  if session is None:
58
64
  request.g._session = SessionDict(request, {})
59
65
  return request.g._session
60
- try:
61
- session = jwt.decode(session, self.secret_key,
62
- algorithms=['HS256'])
63
- except jwt.exceptions.PyJWTError: # pragma: no cover
64
- request.g._session = SessionDict(request, {})
65
- else:
66
- request.g._session = SessionDict(request, session)
66
+ request.g._session = SessionDict(request, self.decode(session))
67
67
  return request.g._session
68
68
 
69
69
  def update(self, request, session):
@@ -89,12 +89,12 @@ class Session:
89
89
  if not self.secret_key:
90
90
  raise ValueError('The session secret key is not configured')
91
91
 
92
- encoded_session = jwt.encode(session, self.secret_key,
93
- algorithm='HS256')
92
+ encoded_session = self.encode(session)
94
93
 
95
94
  @request.after_request
96
95
  def _update_session(request, response):
97
- response.set_cookie('session', encoded_session, http_only=True)
96
+ response.set_cookie('session', encoded_session,
97
+ **self.cookie_options)
98
98
  return response
99
99
 
100
100
  def delete(self, request):
@@ -117,10 +117,21 @@ class Session:
117
117
  """
118
118
  @request.after_request
119
119
  def _delete_session(request, response):
120
- response.set_cookie('session', '', http_only=True,
121
- expires='Thu, 01 Jan 1970 00:00:01 GMT')
120
+ response.delete_cookie('session', **self.cookie_options)
122
121
  return response
123
122
 
123
+ def encode(self, payload, secret_key=None):
124
+ return jwt.encode(payload, secret_key or self.secret_key,
125
+ algorithm='HS256')
126
+
127
+ def decode(self, session, secret_key=None):
128
+ try:
129
+ payload = jwt.decode(session, secret_key or self.secret_key,
130
+ algorithms=['HS256'])
131
+ except jwt.exceptions.PyJWTError: # pragma: no cover
132
+ return {}
133
+ return payload
134
+
124
135
 
125
136
  def with_session(f):
126
137
  """Decorator that passes the user session to the route handler.
@@ -136,13 +147,9 @@ def with_session(f):
136
147
  Note that the decorator does not save the session. To update the session,
137
148
  call the :func:`session.save() <microdot.session.SessionDict.save>` method.
138
149
  """
150
+ @wraps(f)
139
151
  async def wrapper(request, *args, **kwargs):
140
152
  return await invoke_handler(
141
153
  f, request, request.app._session.get(request), *args, **kwargs)
142
154
 
143
- for attr in ['__name__', '__doc__', '__module__', '__qualname__']:
144
- try:
145
- setattr(wrapper, attr, getattr(f, attr))
146
- except AttributeError: # pragma: no cover
147
- pass
148
155
  return wrapper
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import json
3
+ from microdot.helpers import wraps
3
4
 
4
5
 
5
6
  class SSE:
@@ -12,7 +13,7 @@ class SSE:
12
13
  self.event = asyncio.Event()
13
14
  self.queue = []
14
15
 
15
- async def send(self, data, event=None):
16
+ async def send(self, data, event=None, event_id=None):
16
17
  """Send an event to the client.
17
18
 
18
19
  :param data: the data to send. It can be given as a string, bytes, dict
@@ -20,6 +21,8 @@ class SSE:
20
21
  Any other types are converted to string before sending.
21
22
  :param event: an optional event name, to send along with the data. If
22
23
  given, it must be a string.
24
+ :param event_id: an optional event id, to send along with the data. If
25
+ given, it must be a string.
23
26
  """
24
27
  if isinstance(data, (dict, list)):
25
28
  data = json.dumps(data).encode()
@@ -28,6 +31,8 @@ class SSE:
28
31
  elif not isinstance(data, bytes):
29
32
  data = str(data).encode()
30
33
  data = b'data: ' + data + b'\n\n'
34
+ if event_id:
35
+ data = b'id: ' + event_id.encode() + b'\n' + data
31
36
  if event:
32
37
  data = b'event: ' + event.encode() + b'\n' + data
33
38
  self.queue.append(data)
@@ -99,6 +104,7 @@ def with_sse(f):
99
104
  # send a named event
100
105
  await sse.send('hello', event='greeting')
101
106
  """
107
+ @wraps(f)
102
108
  async def sse_handler(request, *args, **kwargs):
103
109
  return sse_response(request, f, *args, **kwargs)
104
110
 
@@ -77,7 +77,7 @@ class TestClient:
77
77
  The following example shows how to create a test client for an application
78
78
  and send a test request::
79
79
 
80
- from microdot_asyncio import Microdot
80
+ from microdot import Microdot
81
81
 
82
82
  app = Microdot()
83
83
 
@@ -112,9 +112,13 @@ class TestClient:
112
112
  headers['Host'] = 'example.com:1234'
113
113
  return body, headers
114
114
 
115
- def _process_cookies(self, headers):
115
+ def _process_cookies(self, path, headers):
116
116
  cookies = ''
117
117
  for name, value in self.cookies.items():
118
+ if isinstance(value, tuple):
119
+ value, cookie_path = value
120
+ if not path.startswith(cookie_path):
121
+ continue
118
122
  if cookies:
119
123
  cookies += '; '
120
124
  cookies += name + '=' + value
@@ -123,7 +127,7 @@ class TestClient:
123
127
  headers['Cookie'] += '; ' + cookies
124
128
  else:
125
129
  headers['Cookie'] = cookies
126
- return cookies, headers
130
+ return headers
127
131
 
128
132
  def _render_request(self, method, path, headers, body):
129
133
  request_bytes = '{method} {path} HTTP/1.0\n'.format(
@@ -139,26 +143,45 @@ class TestClient:
139
143
  for cookie in cookies:
140
144
  cookie_name, cookie_value = cookie.split('=', 1)
141
145
  cookie_options = cookie_value.split(';')
146
+ path = '/'
142
147
  delete = False
143
148
  for option in cookie_options[1:]:
144
- if option.strip().lower().startswith('expires='):
145
- _, e = option.strip().split('=', 1)
149
+ option = option.strip().lower()
150
+ if option.startswith(
151
+ 'max-age='): # pragma: no cover
152
+ _, age = option.split('=', 1)
153
+ try:
154
+ age = int(age)
155
+ except ValueError: # pragma: no cover
156
+ age = 0
157
+ if age <= 0:
158
+ delete = True
159
+ elif option.startswith('expires='):
160
+ _, e = option.split('=', 1)
146
161
  # this is a very limited parser for cookie expiry
147
162
  # that only detects a cookie deletion request when
148
163
  # the date is 1/1/1970
149
164
  if '1 jan 1970' in e.lower(): # pragma: no branch
150
165
  delete = True
151
- break
166
+ elif option.startswith('path='):
167
+ _, path = option.split('=', 1)
152
168
  if delete:
153
169
  if cookie_name in self.cookies: # pragma: no branch
154
- del self.cookies[cookie_name]
170
+ cookie_path = self.cookies[cookie_name][1] \
171
+ if isinstance(self.cookies[cookie_name], tuple) \
172
+ else '/'
173
+ if path == cookie_path:
174
+ del self.cookies[cookie_name]
155
175
  else:
156
- self.cookies[cookie_name] = cookie_options[0]
176
+ if path == '/':
177
+ self.cookies[cookie_name] = cookie_options[0]
178
+ else:
179
+ self.cookies[cookie_name] = (cookie_options[0], path)
157
180
 
158
181
  async def request(self, method, path, headers=None, body=None, sock=None):
159
182
  headers = headers or {}
160
183
  body, headers = self._process_body(body, headers)
161
- cookies, headers = self._process_cookies(headers)
184
+ headers = self._process_cookies(path, headers)
162
185
  request_bytes = self._render_request(method, path, headers, body)
163
186
  if sock:
164
187
  reader = sock[0]
@@ -2,6 +2,7 @@ import binascii
2
2
  import hashlib
3
3
  from microdot import Request, Response
4
4
  from microdot.microdot import MUTED_SOCKET_ERRORS, print_exception
5
+ from microdot.helpers import wraps
5
6
 
6
7
 
7
8
  class WebSocketError(Exception):
@@ -192,6 +193,7 @@ async def websocket_upgrade(request):
192
193
 
193
194
 
194
195
  def websocket_wrapper(f, upgrade_function):
196
+ @wraps(f)
195
197
  async def wrapper(request, *args, **kwargs):
196
198
  ws = await upgrade_function(request)
197
199
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: microdot
3
- Version: 2.0.5
3
+ Version: 2.0.7
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
@@ -14,8 +14,11 @@ Classifier: Operating System :: OS Independent
14
14
  Requires-Python: >=3.8
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
+ Provides-Extra: dev
18
+ Requires-Dist: tox; extra == "dev"
17
19
  Provides-Extra: docs
18
20
  Requires-Dist: sphinx; extra == "docs"
21
+ Requires-Dist: pyjwt; extra == "docs"
19
22
 
20
23
  # microdot
21
24
  [![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)
@@ -51,10 +54,25 @@ describes the backwards incompatible changes that were made.
51
54
 
52
55
  ## Resources
53
56
 
57
+ - [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)
54
58
  - Documentation
55
- - [Stable](https://microdot.readthedocs.io/en/stable/)
56
59
  - [Latest](https://microdot.readthedocs.io/en/latest/)
57
- - Still using version 1?
58
- - [Code](https://github.com/miguelgrinberg/microdot/tree/v1)
59
- - [Documentation](https://microdot.readthedocs.io/en/v1/)
60
- - [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)
60
+ - [Stable (v2)](https://microdot.readthedocs.io/en/stable/)
61
+ - [Legacy (v1)](https://microdot.readthedocs.io/en/v1/) ([Code](https://github.com/miguelgrinberg/microdot/tree/v1))
62
+
63
+ ## Roadmap
64
+
65
+ The following features are planned for future releases of Microdot, both for
66
+ MicroPython and CPython:
67
+
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
70
+ - OpenAPI integration, similar to [APIFairy](https://github.com/miguelgrinberg/apifairy) for Flask
71
+
72
+ In addition to the above, the following extensions are also under consideration,
73
+ but only for CPython:
74
+
75
+ - Database integration through [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)
76
+ - Socket.IO support through [python-socketio](https://github.com/miguelgrinberg/python-socketio)
77
+
78
+ Do you have other ideas to propose? Let's [discuss them](https://github.com/miguelgrinberg/microdot/discussions/new?category=ideas)!
@@ -16,6 +16,7 @@ docs/_static/css/custom.css
16
16
  src/microdot/__init__.py
17
17
  src/microdot/asgi.py
18
18
  src/microdot/cors.py
19
+ src/microdot/helpers.py
19
20
  src/microdot/jinja.py
20
21
  src/microdot/microdot.py
21
22
  src/microdot/session.py
@@ -57,6 +58,7 @@ tests/files/test.js
57
58
  tests/files/test.json
58
59
  tests/files/test.png
59
60
  tests/files/test.txt
61
+ tests/files/test.txt.gz
60
62
  tests/templates/hello.jinja.txt
61
63
  tests/templates/hello.utemplate.txt
62
64
  tests/templates/hello_utemplate_txt.py
@@ -0,0 +1,7 @@
1
+
2
+ [dev]
3
+ tox
4
+
5
+ [docs]
6
+ sphinx
7
+ pyjwt
@@ -0,0 +1 @@
1
+ foo
@@ -203,6 +203,7 @@ class TestMicrodot(unittest.TestCase):
203
203
  req.cookies['one'] + req.cookies['two'] + req.cookies['three'])
204
204
  res.set_cookie('four', '4')
205
205
  res.delete_cookie('two', path='/')
206
+ res.delete_cookie('one', path='/bad')
206
207
  return res
207
208
 
208
209
  client = TestClient(app, cookies={'one': '1', 'two': '2'})
@@ -273,6 +274,14 @@ class TestMicrodot(unittest.TestCase):
273
274
  return '<p>four</p>', 202, \
274
275
  {'Content-Type': 'text/html; charset=UTF-8'}
275
276
 
277
+ @app.route('/status')
278
+ def five(req):
279
+ return 202
280
+
281
+ @app.route('/status-headers')
282
+ def six(req):
283
+ return 202, {'Content-Type': 'text/html; charset=UTF-8'}
284
+
276
285
  client = TestClient(app)
277
286
 
278
287
  res = self._run(client.get('/body'))
@@ -298,6 +307,18 @@ class TestMicrodot(unittest.TestCase):
298
307
  'text/html; charset=UTF-8')
299
308
  self.assertEqual(res.text, '<p>four</p>')
300
309
 
310
+ res = self._run(client.get('/status'))
311
+ self.assertEqual(res.text, '')
312
+ self.assertEqual(res.status_code, 202)
313
+ self.assertEqual(res.headers['Content-Type'],
314
+ 'text/plain; charset=UTF-8')
315
+
316
+ res = self._run(client.get('/status-headers'))
317
+ self.assertEqual(res.text, '')
318
+ self.assertEqual(res.status_code, 202)
319
+ self.assertEqual(res.headers['Content-Type'],
320
+ 'text/html; charset=UTF-8')
321
+
301
322
  def test_before_after_request(self):
302
323
  app = Microdot()
303
324
 
@@ -136,10 +136,10 @@ class TestResponse(unittest.TestCase):
136
136
  self.assertTrue(fd.response.endswith(b'\r\n\r\nfoobar'))
137
137
 
138
138
  def test_create_from_other(self):
139
- res = Response(123)
139
+ res = Response(23.7)
140
140
  self.assertEqual(res.status_code, 200)
141
141
  self.assertEqual(res.headers, {})
142
- self.assertEqual(res.body, 123)
142
+ self.assertEqual(res.body, 23.7)
143
143
 
144
144
  def test_create_with_status_code(self):
145
145
  res = Response('not found', 404)
@@ -193,6 +193,7 @@ class TestResponse(unittest.TestCase):
193
193
  expires='Tue, 05 Nov 2019 02:23:54 GMT', max_age=123,
194
194
  secure=True, http_only=True)
195
195
  res.delete_cookie('foo8', http_only=True)
196
+ res.delete_cookie('foo9', path='/s')
196
197
  self.assertEqual(res.headers, {'Set-Cookie': [
197
198
  'foo1=bar1',
198
199
  'foo2=bar2; Path=/; Partitioned',
@@ -203,7 +204,10 @@ class TestResponse(unittest.TestCase):
203
204
  'foo7=bar7; Path=/foo; Domain=example.com:1234; '
204
205
  'Expires=Tue, 05 Nov 2019 02:23:54 GMT; Max-Age=123; Secure; '
205
206
  'HttpOnly',
206
- 'foo8=; Expires=Thu, 01 Jan 1970 00:00:01 GMT; HttpOnly',
207
+ ('foo8=; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; '
208
+ 'HttpOnly'),
209
+ ('foo9=; Path=/s; Expires=Thu, 01 Jan 1970 00:00:01 GMT; '
210
+ 'Max-Age=0'),
207
211
  ]})
208
212
 
209
213
  def test_redirect(self):
@@ -276,6 +280,17 @@ class TestResponse(unittest.TestCase):
276
280
  'application/octet-stream')
277
281
  self.assertEqual(res.headers['Content-Encoding'], 'gzip')
278
282
 
283
+ def test_send_file_gzip_handling(self):
284
+ res = Response.send_file('tests/files/test.txt.gz')
285
+ self.assertEqual(res.status_code, 200)
286
+ self.assertEqual(res.headers['Content-Type'],
287
+ 'application/octet-stream')
288
+
289
+ res = Response.send_file('tests/files/test.txt.gz', compressed=True)
290
+ self.assertEqual(res.status_code, 200)
291
+ self.assertEqual(res.headers['Content-Type'], 'text/plain')
292
+ self.assertEqual(res.headers['Content-Encoding'], 'gzip')
293
+
279
294
  def test_default_content_type(self):
280
295
  original_content_type = Response.default_content_type
281
296
  res = Response('foo')
@@ -82,3 +82,77 @@ class TestSession(unittest.TestCase):
82
82
 
83
83
  res = self._run(client.get('/'))
84
84
  self.assertEqual(res.status_code, 200)
85
+
86
+ def test_session_default_path(self):
87
+ app = Microdot()
88
+ Session(app, secret_key='some-other-secret')
89
+ client = TestClient(app)
90
+
91
+ @app.get('/')
92
+ @with_session
93
+ def index(req, session):
94
+ session['foo'] = 'bar'
95
+ session.save()
96
+ return ''
97
+
98
+ @app.get('/child')
99
+ @with_session
100
+ def child(req, session):
101
+ return str(session.get('foo'))
102
+
103
+ @app.get('/delete')
104
+ @with_session
105
+ def delete(req, session):
106
+ session.delete()
107
+ return ''
108
+
109
+ res = self._run(client.get('/'))
110
+ self.assertEqual(res.status_code, 200)
111
+ res = self._run(client.get('/child'))
112
+ self.assertEqual(res.text, 'bar')
113
+ res = self._run(client.get('/delete'))
114
+ res = self._run(client.get('/child'))
115
+ self.assertEqual(res.text, 'None')
116
+
117
+ def test_session_custom_path(self):
118
+ app = Microdot()
119
+ session_ext = Session()
120
+ session_ext.initialize(app, secret_key='some-other-secret',
121
+ cookie_options={'path': '/child',
122
+ 'http_only': False})
123
+ client = TestClient(app)
124
+
125
+ @app.get('/')
126
+ @with_session
127
+ def index(req, session):
128
+ return str(session.get('foo'))
129
+
130
+ @app.get('/child')
131
+ @with_session
132
+ def child(req, session):
133
+ session['foo'] = 'bar'
134
+ session.save()
135
+ return ''
136
+
137
+ @app.get('/child/foo')
138
+ @with_session
139
+ def foo(req, session):
140
+ return str(session.get('foo'))
141
+
142
+ @app.get('/child/delete')
143
+ @with_session
144
+ def delete(req, session):
145
+ session.delete()
146
+ return ''
147
+
148
+ res = self._run(client.get('/child'))
149
+ self.assertEqual(res.status_code, 200)
150
+ res = self._run(client.get('/'))
151
+ self.assertEqual(res.text, 'None')
152
+ res = self._run(client.get('/child/foo'))
153
+ self.assertEqual(res.text, 'bar')
154
+ res = self._run(client.get('/child/delete'))
155
+ res = self._run(client.get('/'))
156
+ self.assertEqual(res.text, 'None')
157
+ res = self._run(client.get('/child/foo'))
158
+ self.assertEqual(res.text, 'None')
@@ -23,6 +23,8 @@ class TestWebSocket(unittest.TestCase):
23
23
  async def handle_sse(request, sse):
24
24
  await sse.send('foo')
25
25
  await sse.send('bar', event='test')
26
+ await sse.send('bar', event='test', event_id='id42')
27
+ await sse.send('bar', event_id='id42')
26
28
  await sse.send({'foo': 'bar'})
27
29
  await sse.send([42, 'foo', 'bar'])
28
30
  await sse.send(ValueError('foo'))
@@ -34,6 +36,8 @@ class TestWebSocket(unittest.TestCase):
34
36
  self.assertEqual(response.headers['Content-Type'], 'text/event-stream')
35
37
  self.assertEqual(response.text, ('data: foo\n\n'
36
38
  'event: test\ndata: bar\n\n'
39
+ 'event: test\nid: id42\ndata: bar\n\n'
40
+ 'id: id42\ndata: bar\n\n'
37
41
  'data: {"foo": "bar"}\n\n'
38
42
  'data: [42, "foo", "bar"]\n\n'
39
43
  'data: foo\n\n'
@@ -1,3 +0,0 @@
1
-
2
- [docs]
3
- sphinx
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes