microdot 2.0.4__tar.gz → 2.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {microdot-2.0.4/src/microdot.egg-info → microdot-2.0.6}/PKG-INFO +22 -6
- {microdot-2.0.4 → microdot-2.0.6}/README.md +20 -5
- {microdot-2.0.4 → microdot-2.0.6}/pyproject.toml +2 -1
- microdot-2.0.6/src/microdot/helpers.py +8 -0
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot/microdot.py +7 -7
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot/session.py +28 -21
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot/sse.py +7 -1
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot/test_client.py +32 -9
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot/websocket.py +2 -0
- {microdot-2.0.4 → microdot-2.0.6/src/microdot.egg-info}/PKG-INFO +22 -6
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot.egg-info/SOURCES.txt +1 -0
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot.egg-info/requires.txt +1 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_microdot.py +1 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_response.py +5 -1
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_session.py +74 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_sse.py +4 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_url_pattern.py +6 -0
- {microdot-2.0.4 → microdot-2.0.6}/LICENSE +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/MANIFEST.in +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/docs/Makefile +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/docs/_static/css/custom.css +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/docs/api.rst +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/docs/conf.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/docs/extensions.rst +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/docs/freezing.rst +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/docs/index.rst +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/docs/intro.rst +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/docs/make.bat +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/docs/migrating.rst +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/setup.cfg +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot/__init__.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot/asgi.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot/cors.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot/jinja.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot/utemplate.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot/wsgi.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot.egg-info/dependency_links.txt +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot.egg-info/not-zip-safe +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/src/microdot.egg-info/top_level.txt +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/__init__.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.bin +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.css +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.gif +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.gz +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.html +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.jpg +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.js +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.json +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.png +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.txt +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/mock_socket.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/templates/hello.jinja.txt +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/templates/hello.utemplate.txt +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/templates/hello_utemplate_txt.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_asgi.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_cors.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_end2end.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_jinja.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_multidict.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_request.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_urlencode.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_utemplate.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_websocket.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tests/test_wsgi.py +0 -0
- {microdot-2.0.4 → microdot-2.0.6}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: microdot
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.6
|
|
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
|
|
@@ -16,6 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Provides-Extra: docs
|
|
18
18
|
Requires-Dist: sphinx; extra == "docs"
|
|
19
|
+
Requires-Dist: pyjwt; extra == "docs"
|
|
19
20
|
|
|
20
21
|
# microdot
|
|
21
22
|
[](https://github.com/miguelgrinberg/microdot/actions) [](https://codecov.io/gh/miguelgrinberg/microdot)
|
|
@@ -51,10 +52,25 @@ describes the backwards incompatible changes that were made.
|
|
|
51
52
|
|
|
52
53
|
## Resources
|
|
53
54
|
|
|
55
|
+
- [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)
|
|
54
56
|
- Documentation
|
|
55
|
-
- [Stable](https://microdot.readthedocs.io/en/stable/)
|
|
56
57
|
- [Latest](https://microdot.readthedocs.io/en/latest/)
|
|
57
|
-
-
|
|
58
|
-
- [Code](https://github.com/miguelgrinberg/microdot/tree/v1)
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
- [Stable (v2)](https://microdot.readthedocs.io/en/stable/)
|
|
59
|
+
- [Legacy (v1)](https://microdot.readthedocs.io/en/v1/) ([Code](https://github.com/miguelgrinberg/microdot/tree/v1))
|
|
60
|
+
|
|
61
|
+
## Roadmap
|
|
62
|
+
|
|
63
|
+
The following features are planned for future releases of Microdot, both for
|
|
64
|
+
MicroPython and CPython:
|
|
65
|
+
|
|
66
|
+
- Support for forms encoded in `multipart/form-data` format
|
|
67
|
+
- Authentication support, similar to [Flask-Login](https://github.com/maxcountryman/flask-login) for Flask
|
|
68
|
+
- OpenAPI integration, similar to [APIFairy](https://github.com/miguelgrinberg/apifairy) for Flask
|
|
69
|
+
|
|
70
|
+
In addition to the above, the following extensions are also under consideration,
|
|
71
|
+
but only for CPython:
|
|
72
|
+
|
|
73
|
+
- Database integration through [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)
|
|
74
|
+
- Socket.IO support through [python-socketio](https://github.com/miguelgrinberg/python-socketio)
|
|
75
|
+
|
|
76
|
+
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
|
-
-
|
|
39
|
-
- [Code](https://github.com/miguelgrinberg/microdot/tree/v1)
|
|
40
|
-
|
|
41
|
-
|
|
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)!
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "microdot"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.6"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
|
|
6
6
|
]
|
|
@@ -26,6 +26,7 @@ Homepage = "https://github.com/miguelgrinberg/microdot"
|
|
|
26
26
|
[project.optional-dependencies]
|
|
27
27
|
docs = [
|
|
28
28
|
"sphinx",
|
|
29
|
+
"pyjwt"
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
[tool.setuptools]
|
|
@@ -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 \
|
|
@@ -862,8 +862,6 @@ class URLPattern():
|
|
|
862
862
|
if arg is None:
|
|
863
863
|
return
|
|
864
864
|
if 'name' in segment:
|
|
865
|
-
if not arg:
|
|
866
|
-
return
|
|
867
865
|
args[segment['name']] = arg
|
|
868
866
|
if path is not None:
|
|
869
867
|
return
|
|
@@ -879,6 +877,8 @@ class URLPattern():
|
|
|
879
877
|
|
|
880
878
|
def _string_segment(self, value):
|
|
881
879
|
s = value.split('/', 1)
|
|
880
|
+
if len(s[0]) == 0:
|
|
881
|
+
return None, None
|
|
882
882
|
return s[0], s[1] if len(s) > 1 else None
|
|
883
883
|
|
|
884
884
|
def _int_segment(self, value):
|
|
@@ -1191,7 +1191,7 @@ class Microdot:
|
|
|
1191
1191
|
Example::
|
|
1192
1192
|
|
|
1193
1193
|
import asyncio
|
|
1194
|
-
from
|
|
1194
|
+
from microdot import Microdot
|
|
1195
1195
|
|
|
1196
1196
|
app = Microdot()
|
|
1197
1197
|
|
|
@@ -1268,7 +1268,7 @@ class Microdot:
|
|
|
1268
1268
|
|
|
1269
1269
|
Example::
|
|
1270
1270
|
|
|
1271
|
-
from
|
|
1271
|
+
from microdot import Microdot
|
|
1272
1272
|
|
|
1273
1273
|
app = Microdot()
|
|
1274
1274
|
|
|
@@ -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
|
-
|
|
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 =
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
Version: 2.0.6
|
|
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
|
|
@@ -16,6 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Provides-Extra: docs
|
|
18
18
|
Requires-Dist: sphinx; extra == "docs"
|
|
19
|
+
Requires-Dist: pyjwt; extra == "docs"
|
|
19
20
|
|
|
20
21
|
# microdot
|
|
21
22
|
[](https://github.com/miguelgrinberg/microdot/actions) [](https://codecov.io/gh/miguelgrinberg/microdot)
|
|
@@ -51,10 +52,25 @@ describes the backwards incompatible changes that were made.
|
|
|
51
52
|
|
|
52
53
|
## Resources
|
|
53
54
|
|
|
55
|
+
- [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)
|
|
54
56
|
- Documentation
|
|
55
|
-
- [Stable](https://microdot.readthedocs.io/en/stable/)
|
|
56
57
|
- [Latest](https://microdot.readthedocs.io/en/latest/)
|
|
57
|
-
-
|
|
58
|
-
- [Code](https://github.com/miguelgrinberg/microdot/tree/v1)
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
- [Stable (v2)](https://microdot.readthedocs.io/en/stable/)
|
|
59
|
+
- [Legacy (v1)](https://microdot.readthedocs.io/en/v1/) ([Code](https://github.com/miguelgrinberg/microdot/tree/v1))
|
|
60
|
+
|
|
61
|
+
## Roadmap
|
|
62
|
+
|
|
63
|
+
The following features are planned for future releases of Microdot, both for
|
|
64
|
+
MicroPython and CPython:
|
|
65
|
+
|
|
66
|
+
- Support for forms encoded in `multipart/form-data` format
|
|
67
|
+
- Authentication support, similar to [Flask-Login](https://github.com/maxcountryman/flask-login) for Flask
|
|
68
|
+
- OpenAPI integration, similar to [APIFairy](https://github.com/miguelgrinberg/apifairy) for Flask
|
|
69
|
+
|
|
70
|
+
In addition to the above, the following extensions are also under consideration,
|
|
71
|
+
but only for CPython:
|
|
72
|
+
|
|
73
|
+
- Database integration through [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)
|
|
74
|
+
- Socket.IO support through [python-socketio](https://github.com/miguelgrinberg/python-socketio)
|
|
75
|
+
|
|
76
|
+
Do you have other ideas to propose? Let's [discuss them](https://github.com/miguelgrinberg/microdot/discussions/new?category=ideas)!
|
|
@@ -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'})
|
|
@@ -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;
|
|
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):
|
|
@@ -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'
|
|
@@ -26,10 +26,14 @@ class TestURLPattern(unittest.TestCase):
|
|
|
26
26
|
p = URLPattern('/<arg>')
|
|
27
27
|
self.assertEqual(p.match('/foo'), {'arg': 'foo'})
|
|
28
28
|
self.assertIsNone(p.match('/'))
|
|
29
|
+
self.assertIsNone(p.match('//'))
|
|
29
30
|
self.assertIsNone(p.match(''))
|
|
30
31
|
self.assertIsNone(p.match('foo/'))
|
|
31
32
|
self.assertIsNone(p.match('/foo/'))
|
|
33
|
+
self.assertIsNone(p.match('//foo/'))
|
|
34
|
+
self.assertIsNone(p.match('/foo//'))
|
|
32
35
|
self.assertIsNone(p.match('/foo/bar'))
|
|
36
|
+
self.assertIsNone(p.match('/foo//bar'))
|
|
33
37
|
|
|
34
38
|
p = URLPattern('/<arg>/')
|
|
35
39
|
self.assertEqual(p.match('/foo/'), {'arg': 'foo'})
|
|
@@ -64,6 +68,8 @@ class TestURLPattern(unittest.TestCase):
|
|
|
64
68
|
def test_int_argument(self):
|
|
65
69
|
p = URLPattern('/users/<int:id>')
|
|
66
70
|
self.assertEqual(p.match('/users/123'), {'id': 123})
|
|
71
|
+
self.assertEqual(p.match('/users/-123'), {'id': -123})
|
|
72
|
+
self.assertEqual(p.match('/users/0'), {'id': 0})
|
|
67
73
|
self.assertIsNone(p.match('/users/'))
|
|
68
74
|
self.assertIsNone(p.match('/users/abc'))
|
|
69
75
|
self.assertIsNone(p.match('/users/123abc'))
|
|
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
|
|
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
|