microdot 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {microdot-2.2.0/src/microdot.egg-info → microdot-2.3.0}/PKG-INFO +1 -1
- {microdot-2.2.0 → microdot-2.3.0}/docs/extensions.rst +27 -2
- {microdot-2.2.0 → microdot-2.3.0}/pyproject.toml +1 -1
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/auth.py +18 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/microdot.py +7 -3
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/sse.py +5 -1
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/test_client.py +7 -3
- {microdot-2.2.0 → microdot-2.3.0/src/microdot.egg-info}/PKG-INFO +1 -1
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_auth.py +66 -1
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_microdot.py +1 -1
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_websocket.py +3 -3
- {microdot-2.2.0 → microdot-2.3.0}/LICENSE +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/MANIFEST.in +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/README.md +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/docs/Makefile +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/docs/_static/css/custom.css +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/docs/api.rst +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/docs/conf.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/docs/freezing.rst +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/docs/index.rst +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/docs/intro.rst +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/docs/make.bat +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/docs/migrating.rst +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/setup.cfg +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/__init__.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/asgi.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/cors.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/helpers.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/jinja.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/login.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/multipart.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/session.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/utemplate.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/websocket.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot/wsgi.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot.egg-info/SOURCES.txt +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot.egg-info/dependency_links.txt +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot.egg-info/not-zip-safe +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot.egg-info/requires.txt +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/src/microdot.egg-info/top_level.txt +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/__init__.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/files/test.bin +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/files/test.css +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/files/test.gif +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/files/test.gz +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/files/test.html +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/files/test.jpg +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/files/test.js +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/files/test.json +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/files/test.png +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/files/test.txt +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/files/test.txt.gz +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/mock_socket.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/templates/hello.jinja.txt +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/templates/hello.utemplate.txt +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/templates/hello_utemplate_txt.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_asgi.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_cors.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_end2end.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_jinja.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_login.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_multidict.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_multipart.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_request.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_response.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_session.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_sse.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_url_pattern.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_urlencode.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_utemplate.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tests/test_wsgi.py +0 -0
- {microdot-2.2.0 → microdot-2.3.0}/tox.ini +0 -0
|
@@ -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
|
|
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
|
|
|
@@ -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
|
|
@@ -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,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 =
|
|
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
|
|
@@ -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
|
|
202
|
+
return TestResponse()
|
|
199
203
|
res.complete()
|
|
200
204
|
|
|
201
205
|
self._update_cookies(res)
|
|
@@ -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': '
|
|
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')
|
|
@@ -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
|
|
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
|