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.
Files changed (65) hide show
  1. {microdot-2.0.4/src/microdot.egg-info → microdot-2.0.6}/PKG-INFO +22 -6
  2. {microdot-2.0.4 → microdot-2.0.6}/README.md +20 -5
  3. {microdot-2.0.4 → microdot-2.0.6}/pyproject.toml +2 -1
  4. microdot-2.0.6/src/microdot/helpers.py +8 -0
  5. {microdot-2.0.4 → microdot-2.0.6}/src/microdot/microdot.py +7 -7
  6. {microdot-2.0.4 → microdot-2.0.6}/src/microdot/session.py +28 -21
  7. {microdot-2.0.4 → microdot-2.0.6}/src/microdot/sse.py +7 -1
  8. {microdot-2.0.4 → microdot-2.0.6}/src/microdot/test_client.py +32 -9
  9. {microdot-2.0.4 → microdot-2.0.6}/src/microdot/websocket.py +2 -0
  10. {microdot-2.0.4 → microdot-2.0.6/src/microdot.egg-info}/PKG-INFO +22 -6
  11. {microdot-2.0.4 → microdot-2.0.6}/src/microdot.egg-info/SOURCES.txt +1 -0
  12. {microdot-2.0.4 → microdot-2.0.6}/src/microdot.egg-info/requires.txt +1 -0
  13. {microdot-2.0.4 → microdot-2.0.6}/tests/test_microdot.py +1 -0
  14. {microdot-2.0.4 → microdot-2.0.6}/tests/test_response.py +5 -1
  15. {microdot-2.0.4 → microdot-2.0.6}/tests/test_session.py +74 -0
  16. {microdot-2.0.4 → microdot-2.0.6}/tests/test_sse.py +4 -0
  17. {microdot-2.0.4 → microdot-2.0.6}/tests/test_url_pattern.py +6 -0
  18. {microdot-2.0.4 → microdot-2.0.6}/LICENSE +0 -0
  19. {microdot-2.0.4 → microdot-2.0.6}/MANIFEST.in +0 -0
  20. {microdot-2.0.4 → microdot-2.0.6}/docs/Makefile +0 -0
  21. {microdot-2.0.4 → microdot-2.0.6}/docs/_static/css/custom.css +0 -0
  22. {microdot-2.0.4 → microdot-2.0.6}/docs/api.rst +0 -0
  23. {microdot-2.0.4 → microdot-2.0.6}/docs/conf.py +0 -0
  24. {microdot-2.0.4 → microdot-2.0.6}/docs/extensions.rst +0 -0
  25. {microdot-2.0.4 → microdot-2.0.6}/docs/freezing.rst +0 -0
  26. {microdot-2.0.4 → microdot-2.0.6}/docs/index.rst +0 -0
  27. {microdot-2.0.4 → microdot-2.0.6}/docs/intro.rst +0 -0
  28. {microdot-2.0.4 → microdot-2.0.6}/docs/make.bat +0 -0
  29. {microdot-2.0.4 → microdot-2.0.6}/docs/migrating.rst +0 -0
  30. {microdot-2.0.4 → microdot-2.0.6}/setup.cfg +0 -0
  31. {microdot-2.0.4 → microdot-2.0.6}/src/microdot/__init__.py +0 -0
  32. {microdot-2.0.4 → microdot-2.0.6}/src/microdot/asgi.py +0 -0
  33. {microdot-2.0.4 → microdot-2.0.6}/src/microdot/cors.py +0 -0
  34. {microdot-2.0.4 → microdot-2.0.6}/src/microdot/jinja.py +0 -0
  35. {microdot-2.0.4 → microdot-2.0.6}/src/microdot/utemplate.py +0 -0
  36. {microdot-2.0.4 → microdot-2.0.6}/src/microdot/wsgi.py +0 -0
  37. {microdot-2.0.4 → microdot-2.0.6}/src/microdot.egg-info/dependency_links.txt +0 -0
  38. {microdot-2.0.4 → microdot-2.0.6}/src/microdot.egg-info/not-zip-safe +0 -0
  39. {microdot-2.0.4 → microdot-2.0.6}/src/microdot.egg-info/top_level.txt +0 -0
  40. {microdot-2.0.4 → microdot-2.0.6}/tests/__init__.py +0 -0
  41. {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.bin +0 -0
  42. {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.css +0 -0
  43. {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.gif +0 -0
  44. {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.gz +0 -0
  45. {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.html +0 -0
  46. {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.jpg +0 -0
  47. {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.js +0 -0
  48. {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.json +0 -0
  49. {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.png +0 -0
  50. {microdot-2.0.4 → microdot-2.0.6}/tests/files/test.txt +0 -0
  51. {microdot-2.0.4 → microdot-2.0.6}/tests/mock_socket.py +0 -0
  52. {microdot-2.0.4 → microdot-2.0.6}/tests/templates/hello.jinja.txt +0 -0
  53. {microdot-2.0.4 → microdot-2.0.6}/tests/templates/hello.utemplate.txt +0 -0
  54. {microdot-2.0.4 → microdot-2.0.6}/tests/templates/hello_utemplate_txt.py +0 -0
  55. {microdot-2.0.4 → microdot-2.0.6}/tests/test_asgi.py +0 -0
  56. {microdot-2.0.4 → microdot-2.0.6}/tests/test_cors.py +0 -0
  57. {microdot-2.0.4 → microdot-2.0.6}/tests/test_end2end.py +0 -0
  58. {microdot-2.0.4 → microdot-2.0.6}/tests/test_jinja.py +0 -0
  59. {microdot-2.0.4 → microdot-2.0.6}/tests/test_multidict.py +0 -0
  60. {microdot-2.0.4 → microdot-2.0.6}/tests/test_request.py +0 -0
  61. {microdot-2.0.4 → microdot-2.0.6}/tests/test_urlencode.py +0 -0
  62. {microdot-2.0.4 → microdot-2.0.6}/tests/test_utemplate.py +0 -0
  63. {microdot-2.0.4 → microdot-2.0.6}/tests/test_websocket.py +0 -0
  64. {microdot-2.0.4 → microdot-2.0.6}/tests/test_wsgi.py +0 -0
  65. {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.4
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
  [![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 +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
- - 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)
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
- - 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)!
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "microdot"
3
- version = "2.0.4"
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]
@@ -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 \
@@ -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 microdot_asyncio import Microdot
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 microdot_asyncio import Microdot
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
- 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.4
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
  [![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 +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
- - 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)
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)!
@@ -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
@@ -1,3 +1,4 @@
1
1
 
2
2
  [docs]
3
3
  sphinx
4
+ pyjwt
@@ -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; 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):
@@ -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