microdot 2.2.0__tar.gz → 2.3.1__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 (72) hide show
  1. {microdot-2.2.0/src/microdot.egg-info → microdot-2.3.1}/PKG-INFO +1 -1
  2. {microdot-2.2.0 → microdot-2.3.1}/docs/extensions.rst +27 -2
  3. {microdot-2.2.0 → microdot-2.3.1}/pyproject.toml +1 -1
  4. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/auth.py +18 -0
  5. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/microdot.py +9 -5
  6. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/sse.py +7 -3
  7. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/test_client.py +9 -5
  8. {microdot-2.2.0 → microdot-2.3.1/src/microdot.egg-info}/PKG-INFO +1 -1
  9. {microdot-2.2.0 → microdot-2.3.1}/tests/test_auth.py +66 -1
  10. {microdot-2.2.0 → microdot-2.3.1}/tests/test_microdot.py +1 -1
  11. {microdot-2.2.0 → microdot-2.3.1}/tests/test_websocket.py +3 -3
  12. {microdot-2.2.0 → microdot-2.3.1}/LICENSE +0 -0
  13. {microdot-2.2.0 → microdot-2.3.1}/MANIFEST.in +0 -0
  14. {microdot-2.2.0 → microdot-2.3.1}/README.md +0 -0
  15. {microdot-2.2.0 → microdot-2.3.1}/docs/Makefile +0 -0
  16. {microdot-2.2.0 → microdot-2.3.1}/docs/_static/css/custom.css +0 -0
  17. {microdot-2.2.0 → microdot-2.3.1}/docs/api.rst +0 -0
  18. {microdot-2.2.0 → microdot-2.3.1}/docs/conf.py +0 -0
  19. {microdot-2.2.0 → microdot-2.3.1}/docs/freezing.rst +0 -0
  20. {microdot-2.2.0 → microdot-2.3.1}/docs/index.rst +0 -0
  21. {microdot-2.2.0 → microdot-2.3.1}/docs/intro.rst +0 -0
  22. {microdot-2.2.0 → microdot-2.3.1}/docs/make.bat +0 -0
  23. {microdot-2.2.0 → microdot-2.3.1}/docs/migrating.rst +0 -0
  24. {microdot-2.2.0 → microdot-2.3.1}/setup.cfg +0 -0
  25. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/__init__.py +0 -0
  26. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/asgi.py +0 -0
  27. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/cors.py +0 -0
  28. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/helpers.py +0 -0
  29. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/jinja.py +0 -0
  30. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/login.py +0 -0
  31. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/multipart.py +0 -0
  32. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/session.py +0 -0
  33. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/utemplate.py +0 -0
  34. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/websocket.py +0 -0
  35. {microdot-2.2.0 → microdot-2.3.1}/src/microdot/wsgi.py +0 -0
  36. {microdot-2.2.0 → microdot-2.3.1}/src/microdot.egg-info/SOURCES.txt +0 -0
  37. {microdot-2.2.0 → microdot-2.3.1}/src/microdot.egg-info/dependency_links.txt +0 -0
  38. {microdot-2.2.0 → microdot-2.3.1}/src/microdot.egg-info/not-zip-safe +0 -0
  39. {microdot-2.2.0 → microdot-2.3.1}/src/microdot.egg-info/requires.txt +0 -0
  40. {microdot-2.2.0 → microdot-2.3.1}/src/microdot.egg-info/top_level.txt +0 -0
  41. {microdot-2.2.0 → microdot-2.3.1}/tests/__init__.py +0 -0
  42. {microdot-2.2.0 → microdot-2.3.1}/tests/files/test.bin +0 -0
  43. {microdot-2.2.0 → microdot-2.3.1}/tests/files/test.css +0 -0
  44. {microdot-2.2.0 → microdot-2.3.1}/tests/files/test.gif +0 -0
  45. {microdot-2.2.0 → microdot-2.3.1}/tests/files/test.gz +0 -0
  46. {microdot-2.2.0 → microdot-2.3.1}/tests/files/test.html +0 -0
  47. {microdot-2.2.0 → microdot-2.3.1}/tests/files/test.jpg +0 -0
  48. {microdot-2.2.0 → microdot-2.3.1}/tests/files/test.js +0 -0
  49. {microdot-2.2.0 → microdot-2.3.1}/tests/files/test.json +0 -0
  50. {microdot-2.2.0 → microdot-2.3.1}/tests/files/test.png +0 -0
  51. {microdot-2.2.0 → microdot-2.3.1}/tests/files/test.txt +0 -0
  52. {microdot-2.2.0 → microdot-2.3.1}/tests/files/test.txt.gz +0 -0
  53. {microdot-2.2.0 → microdot-2.3.1}/tests/mock_socket.py +0 -0
  54. {microdot-2.2.0 → microdot-2.3.1}/tests/templates/hello.jinja.txt +0 -0
  55. {microdot-2.2.0 → microdot-2.3.1}/tests/templates/hello.utemplate.txt +0 -0
  56. {microdot-2.2.0 → microdot-2.3.1}/tests/templates/hello_utemplate_txt.py +0 -0
  57. {microdot-2.2.0 → microdot-2.3.1}/tests/test_asgi.py +0 -0
  58. {microdot-2.2.0 → microdot-2.3.1}/tests/test_cors.py +0 -0
  59. {microdot-2.2.0 → microdot-2.3.1}/tests/test_end2end.py +0 -0
  60. {microdot-2.2.0 → microdot-2.3.1}/tests/test_jinja.py +0 -0
  61. {microdot-2.2.0 → microdot-2.3.1}/tests/test_login.py +0 -0
  62. {microdot-2.2.0 → microdot-2.3.1}/tests/test_multidict.py +0 -0
  63. {microdot-2.2.0 → microdot-2.3.1}/tests/test_multipart.py +0 -0
  64. {microdot-2.2.0 → microdot-2.3.1}/tests/test_request.py +0 -0
  65. {microdot-2.2.0 → microdot-2.3.1}/tests/test_response.py +0 -0
  66. {microdot-2.2.0 → microdot-2.3.1}/tests/test_session.py +0 -0
  67. {microdot-2.2.0 → microdot-2.3.1}/tests/test_sse.py +0 -0
  68. {microdot-2.2.0 → microdot-2.3.1}/tests/test_url_pattern.py +0 -0
  69. {microdot-2.2.0 → microdot-2.3.1}/tests/test_urlencode.py +0 -0
  70. {microdot-2.2.0 → microdot-2.3.1}/tests/test_utemplate.py +0 -0
  71. {microdot-2.2.0 → microdot-2.3.1}/tests/test_wsgi.py +0 -0
  72. {microdot-2.2.0 → microdot-2.3.1}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdot
3
- Version: 2.2.0
3
+ Version: 2.3.1
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
@@ -414,10 +414,25 @@ decorator::
414
414
  While running an authenticated request, the user object returned by the
415
415
  authenticaction function is accessible as ``request.g.current_user``.
416
416
 
417
+ If an endpoint is intended to work with or without authentication, then it can
418
+ be protected with the ``auth.optional`` decorator::
419
+
420
+ @app.route('/')
421
+ @auth.optional
422
+ async def index(request):
423
+ if g.current_user:
424
+ return f'Hello, {request.g.current_user}!'
425
+ else:
426
+ return 'Hello, anonymous user!'
427
+
428
+ As shown in the example, a route can check ``g.current_user`` to determine if
429
+ the user is authenticated or not.
430
+
417
431
  Token Authentication
418
432
  ^^^^^^^^^^^^^^^^^^^^
419
433
 
420
- To set up token authentication, create an instance of :class:`TokenAuth <microdot.auth.TokenAuth>`::
434
+ To set up token authentication, create an instance of
435
+ :class:`TokenAuth <microdot.auth.TokenAuth>`::
421
436
 
422
437
  from microdot.auth import TokenAuth
423
438
 
@@ -437,7 +452,17 @@ protect your routes::
437
452
  @auth
438
453
  async def index(request):
439
454
  return f'Hello, {request.g.current_user}!'
440
-
455
+
456
+ Optional authentication can also be used with tokens::
457
+
458
+ @app.route('/')
459
+ @auth.optional
460
+ async def index(request):
461
+ if g.current_user:
462
+ return f'Hello, {request.g.current_user}!'
463
+ else:
464
+ return 'Hello, anonymous user!'
465
+
441
466
  User Logins
442
467
  ~~~~~~~~~~~
443
468
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "microdot"
3
- version = "2.2.0"
3
+ version = "2.3.1"
4
4
  authors = [
5
5
  { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
6
6
  ]
@@ -36,6 +36,24 @@ class BaseAuth:
36
36
 
37
37
  return wrapper
38
38
 
39
+ def optional(self, f):
40
+ """Decorator to protect a route with optional authentication.
41
+
42
+ This decorator makes authentication for the decorated route optional,
43
+ meaning that the route is allowed to run with or with
44
+ authentication given in the request.
45
+ """
46
+ async def wrapper(request, *args, **kwargs):
47
+ auth = self._get_auth(request)
48
+ if not auth:
49
+ request.g.current_user = None
50
+ else:
51
+ request.g.current_user = await invoke_handler(
52
+ self.auth_callback, request, *auth)
53
+ return await invoke_handler(f, request, *args, **kwargs)
54
+
55
+ return wrapper
56
+
39
57
 
40
58
  class BasicAuth(BaseAuth):
41
59
  """Basic Authentication.
@@ -7,10 +7,14 @@ servers for MicroPython and standard Python.
7
7
  """
8
8
  import asyncio
9
9
  import io
10
- import json
11
10
  import re
12
11
  import time
13
12
 
13
+ try:
14
+ import orjson as json
15
+ except ImportError:
16
+ import json
17
+
14
18
  try:
15
19
  from inspect import iscoroutinefunction, iscoroutine
16
20
  from functools import partial
@@ -574,9 +578,9 @@ class Response:
574
578
  self.headers = NoCaseDict(headers or {})
575
579
  self.reason = reason
576
580
  if isinstance(body, (dict, list)):
577
- self.body = json.dumps(body).encode()
581
+ body = json.dumps(body)
578
582
  self.headers['Content-Type'] = 'application/json; charset=UTF-8'
579
- elif isinstance(body, str):
583
+ if isinstance(body, str):
580
584
  self.body = body.encode()
581
585
  else:
582
586
  # this applies to bytes, file-like objects or generators
@@ -1336,9 +1340,9 @@ class Microdot:
1336
1340
  print_exception(exc)
1337
1341
 
1338
1342
  res = await self.dispatch_request(req)
1339
- if res != Response.already_handled: # pragma: no branch
1340
- await res.write(writer)
1341
1343
  try:
1344
+ if res != Response.already_handled: # pragma: no branch
1345
+ await res.write(writer)
1342
1346
  await writer.aclose()
1343
1347
  except OSError as exc: # pragma: no cover
1344
1348
  if exc.errno in MUTED_SOCKET_ERRORS:
@@ -1,7 +1,11 @@
1
1
  import asyncio
2
- import json
3
2
  from microdot.helpers import wraps
4
3
 
4
+ try:
5
+ import orjson as json
6
+ except ImportError:
7
+ import json
8
+
5
9
 
6
10
  class SSE:
7
11
  """Server-Sent Events object.
@@ -25,8 +29,8 @@ class SSE:
25
29
  given, it must be a string.
26
30
  """
27
31
  if isinstance(data, (dict, list)):
28
- data = json.dumps(data).encode()
29
- elif isinstance(data, str):
32
+ data = json.dumps(data)
33
+ if isinstance(data, str):
30
34
  data = data.encode()
31
35
  elif not isinstance(data, bytes):
32
36
  data = str(data).encode()
@@ -1,4 +1,3 @@
1
- import json
2
1
  from microdot.microdot import Request, Response, AsyncBytesIO
3
2
 
4
3
  try:
@@ -6,6 +5,11 @@ try:
6
5
  except: # pragma: no cover # noqa: E722
7
6
  WebSocket = None
8
7
 
8
+ try:
9
+ import orjson as json
10
+ except ImportError:
11
+ import json
12
+
9
13
  __all__ = ['TestClient', 'TestResponse']
10
14
 
11
15
 
@@ -19,7 +23,7 @@ class TestResponse:
19
23
  #: explicitly sets it on the response object.
20
24
  self.reason = None
21
25
  #: A dictionary with the response headers.
22
- self.headers = None
26
+ self.headers = {}
23
27
  #: The body of the response, as a bytes object.
24
28
  self.body = None
25
29
  #: The body of the response, decoded to a UTF-8 string. Set to
@@ -101,10 +105,10 @@ class TestClient:
101
105
  if body is None:
102
106
  body = b''
103
107
  elif isinstance(body, (dict, list)):
104
- body = json.dumps(body).encode()
108
+ body = json.dumps(body)
105
109
  if 'Content-Type' not in headers: # pragma: no cover
106
110
  headers['Content-Type'] = 'application/json'
107
- elif isinstance(body, str):
111
+ if isinstance(body, str):
108
112
  body = body.encode()
109
113
  if body and 'Content-Length' not in headers:
110
114
  headers['Content-Length'] = str(len(body))
@@ -195,7 +199,7 @@ class TestClient:
195
199
  ('127.0.0.1', 1234))
196
200
  res = await self.app.dispatch_request(req)
197
201
  if res == Response.already_handled:
198
- return None
202
+ return TestResponse()
199
203
  res.complete()
200
204
 
201
205
  self._update_cookies(res)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdot
3
- Version: 2.2.0
3
+ Version: 2.3.1
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
@@ -45,6 +45,38 @@ class TestAuth(unittest.TestCase):
45
45
  b'foo:baz').decode()}))
46
46
  self.assertEqual(res.status_code, 401)
47
47
 
48
+ def test_basic_optional_auth(self):
49
+ app = Microdot()
50
+ basic_auth = BasicAuth()
51
+
52
+ @basic_auth.authenticate
53
+ def authenticate(request, username, password):
54
+ if username == 'foo' and password == 'bar':
55
+ return {'username': username}
56
+
57
+ @app.route('/')
58
+ @basic_auth.optional
59
+ def index(request):
60
+ return request.g.current_user['username'] \
61
+ if request.g.current_user else ''
62
+
63
+ client = TestClient(app)
64
+ res = self._run(client.get('/'))
65
+ self.assertEqual(res.status_code, 200)
66
+ self.assertEqual(res.text, '')
67
+
68
+ res = self._run(client.get('/', headers={
69
+ 'Authorization': 'Basic ' + binascii.b2a_base64(
70
+ b'foo:bar').decode()}))
71
+ self.assertEqual(res.status_code, 200)
72
+ self.assertEqual(res.text, 'foo')
73
+
74
+ res = self._run(client.get('/', headers={
75
+ 'Authorization': 'Basic ' + binascii.b2a_base64(
76
+ b'foo:baz').decode()}))
77
+ self.assertEqual(res.status_code, 200)
78
+ self.assertEqual(res.text, '')
79
+
48
80
  def test_token_auth(self):
49
81
  app = Microdot()
50
82
  token_auth = TokenAuth()
@@ -67,7 +99,7 @@ class TestAuth(unittest.TestCase):
67
99
  'Authorization': 'Basic foo'}))
68
100
  self.assertEqual(res.status_code, 401)
69
101
 
70
- res = self._run(client.get('/', headers={'Authorization': 'foo'}))
102
+ res = self._run(client.get('/', headers={'Authorization': 'invalid'}))
71
103
  self.assertEqual(res.status_code, 401)
72
104
 
73
105
  res = self._run(client.get('/', headers={
@@ -75,6 +107,39 @@ class TestAuth(unittest.TestCase):
75
107
  self.assertEqual(res.status_code, 200)
76
108
  self.assertEqual(res.text, 'user')
77
109
 
110
+ def test_token_optional_auth(self):
111
+ app = Microdot()
112
+ token_auth = TokenAuth()
113
+
114
+ @token_auth.authenticate
115
+ def authenticate(request, token):
116
+ if token == 'foo':
117
+ return 'user'
118
+
119
+ @app.route('/')
120
+ @token_auth.optional
121
+ def index(request):
122
+ return request.g.current_user or ''
123
+
124
+ client = TestClient(app)
125
+ res = self._run(client.get('/'))
126
+ self.assertEqual(res.status_code, 200)
127
+ self.assertEqual(res.text, '')
128
+
129
+ res = self._run(client.get('/', headers={
130
+ 'Authorization': 'Basic foo'}))
131
+ self.assertEqual(res.status_code, 200)
132
+ self.assertEqual(res.text, '')
133
+
134
+ res = self._run(client.get('/', headers={'Authorization': 'foo'}))
135
+ self.assertEqual(res.status_code, 200)
136
+ self.assertEqual(res.text, '')
137
+
138
+ res = self._run(client.get('/', headers={
139
+ 'Authorization': 'Bearer foo'}))
140
+ self.assertEqual(res.status_code, 200)
141
+ self.assertEqual(res.text, 'user')
142
+
78
143
  def test_token_auth_custom_header(self):
79
144
  app = Microdot()
80
145
  token_auth = TokenAuth(header='X-Auth-Token')
@@ -771,7 +771,7 @@ class TestMicrodot(unittest.TestCase):
771
771
 
772
772
  client = TestClient(app)
773
773
  res = self._run(client.get('/'))
774
- self.assertEqual(res, None)
774
+ self.assertEqual(res.body, None)
775
775
 
776
776
  def test_mount(self):
777
777
  subapp = Microdot()
@@ -46,11 +46,11 @@ class TestWebSocket(unittest.TestCase):
46
46
 
47
47
  client = TestClient(app)
48
48
  res = self._run(client.websocket('/echo', ws))
49
- self.assertIsNone(res)
49
+ self.assertIsNone(res.body)
50
50
  self.assertEqual(results, ['hello', b'bye', b'*' * 300, b'+' * 65537])
51
51
 
52
52
  res = self._run(client.websocket('/divzero', ws))
53
- self.assertIsNone(res)
53
+ self.assertIsNone(res.body)
54
54
  WebSocket.max_message_length = -1
55
55
 
56
56
  @unittest.skipIf(sys.implementation.name == 'micropython',
@@ -74,7 +74,7 @@ class TestWebSocket(unittest.TestCase):
74
74
 
75
75
  client = TestClient(app)
76
76
  res = self._run(client.websocket('/echo', ws))
77
- self.assertIsNone(res)
77
+ self.assertIsNone(res.body)
78
78
  self.assertEqual(results, [])
79
79
  Request.max_body_length = saved_max_body_length
80
80
 
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