microdot 2.0.6__tar.gz → 2.1.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.0.6/src/microdot.egg-info → microdot-2.1.0}/PKG-INFO +4 -2
- {microdot-2.0.6 → microdot-2.1.0}/docs/api.rst +16 -0
- {microdot-2.0.6 → microdot-2.1.0}/docs/extensions.rst +186 -14
- {microdot-2.0.6 → microdot-2.1.0}/docs/intro.rst +53 -12
- {microdot-2.0.6 → microdot-2.1.0}/pyproject.toml +7 -2
- microdot-2.1.0/src/microdot/auth.py +144 -0
- microdot-2.1.0/src/microdot/login.py +163 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/microdot.py +129 -52
- {microdot-2.0.6 → microdot-2.1.0/src/microdot.egg-info}/PKG-INFO +4 -2
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot.egg-info/SOURCES.txt +5 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot.egg-info/dependency_links.txt +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot.egg-info/not-zip-safe +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot.egg-info/requires.txt +3 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot.egg-info/top_level.txt +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/__init__.py +2 -0
- microdot-2.1.0/tests/files/test.txt.gz +1 -0
- microdot-2.1.0/tests/test_auth.py +125 -0
- microdot-2.1.0/tests/test_login.py +188 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_microdot.py +221 -2
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_response.py +13 -2
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_session.py +1 -1
- {microdot-2.0.6 → microdot-2.1.0}/LICENSE +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/MANIFEST.in +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/README.md +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/docs/Makefile +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/docs/_static/css/custom.css +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/docs/conf.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/docs/freezing.rst +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/docs/index.rst +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/docs/make.bat +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/docs/migrating.rst +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/setup.cfg +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/__init__.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/asgi.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/cors.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/helpers.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/jinja.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/session.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/sse.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/test_client.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/utemplate.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/websocket.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/src/microdot/wsgi.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/files/test.bin +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/files/test.css +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/files/test.gif +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/files/test.gz +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/files/test.html +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/files/test.jpg +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/files/test.js +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/files/test.json +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/files/test.png +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/files/test.txt +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/mock_socket.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/templates/hello.jinja.txt +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/templates/hello.utemplate.txt +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/templates/hello_utemplate_txt.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_asgi.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_cors.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_end2end.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_jinja.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_multidict.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_request.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_sse.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_url_pattern.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_urlencode.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_utemplate.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_websocket.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tests/test_wsgi.py +0 -0
- {microdot-2.0.6 → microdot-2.1.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: microdot
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
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,6 +14,8 @@ 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"
|
|
19
21
|
Requires-Dist: pyjwt; extra == "docs"
|
|
@@ -44,6 +44,22 @@ User Sessions
|
|
|
44
44
|
.. automodule:: microdot.session
|
|
45
45
|
:members:
|
|
46
46
|
|
|
47
|
+
Authentication
|
|
48
|
+
--------------
|
|
49
|
+
|
|
50
|
+
.. automodule:: microdot.auth
|
|
51
|
+
:inherited-members:
|
|
52
|
+
:special-members: __call__
|
|
53
|
+
:members:
|
|
54
|
+
|
|
55
|
+
User Logins
|
|
56
|
+
-----------
|
|
57
|
+
|
|
58
|
+
.. automodule:: microdot.login
|
|
59
|
+
:inherited-members:
|
|
60
|
+
:special-members: __call__
|
|
61
|
+
:members:
|
|
62
|
+
|
|
47
63
|
Cross-Origin Resource Sharing (CORS)
|
|
48
64
|
------------------------------------
|
|
49
65
|
|
|
@@ -5,8 +5,8 @@ Microdot is a highly extensible web application framework. The extensions
|
|
|
5
5
|
described in this section are maintained as part of the Microdot project in
|
|
6
6
|
the same source code repository.
|
|
7
7
|
|
|
8
|
-
WebSocket
|
|
9
|
-
|
|
8
|
+
WebSocket
|
|
9
|
+
~~~~~~~~-
|
|
10
10
|
|
|
11
11
|
.. list-table::
|
|
12
12
|
:align: left
|
|
@@ -39,8 +39,8 @@ Example::
|
|
|
39
39
|
message = await ws.receive()
|
|
40
40
|
await ws.send(message)
|
|
41
41
|
|
|
42
|
-
Server-Sent Events
|
|
43
|
-
|
|
42
|
+
Server-Sent Events
|
|
43
|
+
~~~~~~~~~~~~~~~~~~
|
|
44
44
|
|
|
45
45
|
.. list-table::
|
|
46
46
|
:align: left
|
|
@@ -78,8 +78,8 @@ Example::
|
|
|
78
78
|
the SSE object. For bidirectional communication with the client, use the
|
|
79
79
|
WebSocket extension.
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
Templates
|
|
82
|
+
~~~~~~~~~
|
|
83
83
|
|
|
84
84
|
Many web applications use HTML templates for rendering content to clients.
|
|
85
85
|
Microdot includes extensions to render templates with the
|
|
@@ -202,8 +202,8 @@ must be used.
|
|
|
202
202
|
.. note::
|
|
203
203
|
The Jinja extension is not compatible with MicroPython.
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
Secure User Sessions
|
|
206
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
207
207
|
|
|
208
208
|
.. list-table::
|
|
209
209
|
:align: left
|
|
@@ -270,6 +270,178 @@ The :func:`save() <microdot.session.SessionDict.save>` and
|
|
|
270
270
|
:func:`delete() <microdot.session.SessionDict.delete>` methods are used to update
|
|
271
271
|
and destroy the user session respectively.
|
|
272
272
|
|
|
273
|
+
Authentication
|
|
274
|
+
~~~~~~~~~~~~~~
|
|
275
|
+
|
|
276
|
+
.. list-table::
|
|
277
|
+
:align: left
|
|
278
|
+
|
|
279
|
+
* - Compatibility
|
|
280
|
+
- | CPython & MicroPython
|
|
281
|
+
|
|
282
|
+
* - Required Microdot source files
|
|
283
|
+
- | `auth.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/auth.py>`_
|
|
284
|
+
|
|
285
|
+
* - Required external dependencies
|
|
286
|
+
- | None
|
|
287
|
+
|
|
288
|
+
* - Examples
|
|
289
|
+
- | `basic_auth.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/auth/basic_auth.py>`_
|
|
290
|
+
| `token_auth.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/auth/token_auth.py>`_
|
|
291
|
+
|
|
292
|
+
The authentication extension provides helper classes for two commonly used
|
|
293
|
+
authentication patterns, described below.
|
|
294
|
+
|
|
295
|
+
Basic Authentication
|
|
296
|
+
^^^^^^^^^^^^^^^^^^^^
|
|
297
|
+
|
|
298
|
+
`Basic Authentication <https://en.wikipedia.org/wiki/Basic_access_authentication>`_
|
|
299
|
+
is a method of authentication that is part of the HTTP specification. It allows
|
|
300
|
+
clients to authenticate to a server using a username and a password. Web
|
|
301
|
+
browsers have native support for Basic Authentication and will automatically
|
|
302
|
+
prompt the user for a username and a password when a protected resource is
|
|
303
|
+
accessed.
|
|
304
|
+
|
|
305
|
+
To use Basic Authentication, create an instance of the :class:`BasicAuth <microdot.auth.BasicAuth>`
|
|
306
|
+
class::
|
|
307
|
+
|
|
308
|
+
from microdot.auth import BasicAuth
|
|
309
|
+
|
|
310
|
+
auth = BasicAuth(app)
|
|
311
|
+
|
|
312
|
+
Next, create an authentication function. The function must accept a request
|
|
313
|
+
object and a username and password pair provided by the user. If the
|
|
314
|
+
credentials are valid, the function must return an object that represents the
|
|
315
|
+
user. If the authentication function cannot validate the user provided
|
|
316
|
+
credentials it must return ``None``. Decorate the function with
|
|
317
|
+
``@auth.authenticate``::
|
|
318
|
+
|
|
319
|
+
@auth.authenticate
|
|
320
|
+
async def verify_user(request, username, password):
|
|
321
|
+
user = await load_user_from_database(username)
|
|
322
|
+
if user and user.verify_password(password):
|
|
323
|
+
return user
|
|
324
|
+
|
|
325
|
+
To protect a route with authentication, add the ``auth`` instance as a
|
|
326
|
+
decorator::
|
|
327
|
+
|
|
328
|
+
@app.route('/')
|
|
329
|
+
@auth
|
|
330
|
+
async def index(request):
|
|
331
|
+
return f'Hello, {request.g.current_user}!'
|
|
332
|
+
|
|
333
|
+
While running an authenticated request, the user object returned by the
|
|
334
|
+
authenticaction function is accessible as ``request.g.current_user``.
|
|
335
|
+
|
|
336
|
+
Token Authentication
|
|
337
|
+
^^^^^^^^^^^^^^^^^^^^
|
|
338
|
+
|
|
339
|
+
To set up token authentication, create an instance of :class:`TokenAuth <microdot.auth.TokenAuth>`::
|
|
340
|
+
|
|
341
|
+
from microdot.auth import TokenAuth
|
|
342
|
+
|
|
343
|
+
auth = TokenAuth()
|
|
344
|
+
|
|
345
|
+
Then add a function that verifies the token and returns the user it belongs to,
|
|
346
|
+
or ``None`` if the token is invalid or expired::
|
|
347
|
+
|
|
348
|
+
@auth.authenticate
|
|
349
|
+
async def verify_token(request, token):
|
|
350
|
+
return load_user_from_token(token)
|
|
351
|
+
|
|
352
|
+
As with Basic authentication, the ``auth`` instance is used as a decorator to
|
|
353
|
+
protect your routes::
|
|
354
|
+
|
|
355
|
+
@app.route('/')
|
|
356
|
+
@auth
|
|
357
|
+
async def index(request):
|
|
358
|
+
return f'Hello, {request.g.current_user}!'
|
|
359
|
+
|
|
360
|
+
User Logins
|
|
361
|
+
~~~~~~~~~~~
|
|
362
|
+
|
|
363
|
+
.. list-table::
|
|
364
|
+
:align: left
|
|
365
|
+
|
|
366
|
+
* - Compatibility
|
|
367
|
+
- | CPython & MicroPython
|
|
368
|
+
|
|
369
|
+
* - Required Microdot source files
|
|
370
|
+
- | `login.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/auth.py>`_
|
|
371
|
+
| `session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/session.py>`_
|
|
372
|
+
* - Required external dependencies
|
|
373
|
+
- | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
|
|
374
|
+
| MicroPython: `jwt.py <https://github.com/micropython/micropython-lib/blob/master/python-ecosys/pyjwt/jwt.py>`_,
|
|
375
|
+
`hmac.py <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_
|
|
376
|
+
* - Examples
|
|
377
|
+
- | `login.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/login/login.py>`_
|
|
378
|
+
|
|
379
|
+
The login extension provides user login functionality. The logged in state of
|
|
380
|
+
the user is stored in the user session cookie, and an optional "remember me"
|
|
381
|
+
cookie can also be added to keep the user logged in across browser sessions.
|
|
382
|
+
|
|
383
|
+
To use this extension, create instances of the
|
|
384
|
+
:class:`Session <microdot.session.Session>` and :class:`Login <microdot.login.Login>`
|
|
385
|
+
class::
|
|
386
|
+
|
|
387
|
+
Session(app, secret_key='top-secret!')
|
|
388
|
+
login = Login()
|
|
389
|
+
|
|
390
|
+
The ``Login`` class accept an optional argument with the URL of the login page.
|
|
391
|
+
The default for this URL is */login*.
|
|
392
|
+
|
|
393
|
+
The application must represent users as objects with an ``id`` attribute. A
|
|
394
|
+
function decorated with ``@login.user_loader`` is used to load a user object::
|
|
395
|
+
|
|
396
|
+
@login.user_loader
|
|
397
|
+
async def get_user(user_id):
|
|
398
|
+
return database.get_user(user_id)
|
|
399
|
+
|
|
400
|
+
The application must implement the login form. At the point in which the user
|
|
401
|
+
credentials have been received and verified, a call to the
|
|
402
|
+
:func:`login_user() <microdot.login.Login.login_user>` function must be made to
|
|
403
|
+
record the user in the user session::
|
|
404
|
+
|
|
405
|
+
@app.route('/login', methods=['GET', 'POST'])
|
|
406
|
+
async def login(request):
|
|
407
|
+
# ...
|
|
408
|
+
if user.check_password(password):
|
|
409
|
+
return await login.login_user(request, user, remember=remember_me)
|
|
410
|
+
return redirect('/login')
|
|
411
|
+
|
|
412
|
+
The optional ``remember`` argument is used to add a remember me cookie that
|
|
413
|
+
will log the user in automatically in future sessions. A value of ``True`` will
|
|
414
|
+
keep the log in active for 30 days. Alternatively, an integer number of days
|
|
415
|
+
can be passed in this argument.
|
|
416
|
+
|
|
417
|
+
Any routes that require the user to be logged in must be decorated with
|
|
418
|
+
:func:`@login <microdot.login.Login.__call__>`::
|
|
419
|
+
|
|
420
|
+
@app.route('/')
|
|
421
|
+
@login
|
|
422
|
+
async def index(request):
|
|
423
|
+
# ...
|
|
424
|
+
|
|
425
|
+
Routes that are of a sensitive nature can be decorated with
|
|
426
|
+
:func:`@login.fresh <microdot.login.Login.fresh>`
|
|
427
|
+
instead. This decorator requires that the user has logged in during the current
|
|
428
|
+
session, and will ask the user to logged in again if the session was
|
|
429
|
+
authenticated through a remember me cookie::
|
|
430
|
+
|
|
431
|
+
@app.get('/fresh')
|
|
432
|
+
@login.fresh
|
|
433
|
+
async def fresh(request):
|
|
434
|
+
# ...
|
|
435
|
+
|
|
436
|
+
To log out a user, the :func:`logout_user() <microdot.auth.Login.logout_user>`
|
|
437
|
+
is used::
|
|
438
|
+
|
|
439
|
+
@app.post('/logout')
|
|
440
|
+
@login
|
|
441
|
+
async def logout(request):
|
|
442
|
+
await login.logout_user(request)
|
|
443
|
+
return redirect('/')
|
|
444
|
+
|
|
273
445
|
Cross-Origin Resource Sharing (CORS)
|
|
274
446
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
275
447
|
|
|
@@ -286,7 +458,7 @@ Cross-Origin Resource Sharing (CORS)
|
|
|
286
458
|
- | None
|
|
287
459
|
|
|
288
460
|
* - Examples
|
|
289
|
-
- | `
|
|
461
|
+
- | `app.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/cors/app.py>`_
|
|
290
462
|
|
|
291
463
|
The CORS extension provides support for `Cross-Origin Resource Sharing
|
|
292
464
|
(CORS) <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_. CORS is a
|
|
@@ -305,8 +477,8 @@ Example::
|
|
|
305
477
|
cors = CORS(app, allowed_origins=['https://example.com'],
|
|
306
478
|
allow_credentials=True)
|
|
307
479
|
|
|
308
|
-
|
|
309
|
-
|
|
480
|
+
Test Client
|
|
481
|
+
~~~~~~~~~~~
|
|
310
482
|
|
|
311
483
|
.. list-table::
|
|
312
484
|
:align: left
|
|
@@ -342,8 +514,8 @@ Example::
|
|
|
342
514
|
See the documentation for the :class:`TestClient <microdot.test_client.TestClient>`
|
|
343
515
|
class for more details.
|
|
344
516
|
|
|
345
|
-
|
|
346
|
-
|
|
517
|
+
Production Deployments
|
|
518
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
347
519
|
|
|
348
520
|
The ``Microdot`` class creates its own simple web server. This is enough for an
|
|
349
521
|
application deployed with MicroPython, but when using CPython it may be useful
|
|
@@ -363,7 +535,7 @@ Using an ASGI Web Server
|
|
|
363
535
|
- | `asgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/asgi.py>`_
|
|
364
536
|
|
|
365
537
|
* - Required external dependencies
|
|
366
|
-
- | An ASGI web server, such as `Uvicorn <https://uvicorn.org/>`_.
|
|
538
|
+
- | An ASGI web server, such as `Uvicorn <https://www.uvicorn.org/>`_.
|
|
367
539
|
|
|
368
540
|
* - Examples
|
|
369
541
|
- | `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
|
|
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/
|
|
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/
|
|
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>`
|
|
149
|
-
|
|
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::
|
|
@@ -418,7 +445,7 @@ Mounting a Sub-Application
|
|
|
418
445
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
419
446
|
|
|
420
447
|
Small Microdot applications can be written as a single source file, but this
|
|
421
|
-
is not the best option for applications that
|
|
448
|
+
is not the best option for applications that pass a certain size. To make it
|
|
422
449
|
simpler to write large applications, Microdot supports the concept of
|
|
423
450
|
sub-applications that can be "mounted" on a larger application, possibly with
|
|
424
451
|
a common URL prefix applied to all of its routes. For developers familiar with
|
|
@@ -474,11 +501,25 @@ The resulting application will have the customer endpoints available at
|
|
|
474
501
|
*/customers/* and the order endpoints available at */orders/*.
|
|
475
502
|
|
|
476
503
|
.. note::
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
504
|
+
During the handling of a request, the
|
|
505
|
+
:attr:`Request.url_prefix <microdot.Microdot.url_prefix>` attribute is
|
|
506
|
+
set to the URL prefix under which the sub-application was mounted, or an
|
|
507
|
+
empty string if the endpoint did not come from a sub-application or the
|
|
508
|
+
sub-application was mounted without a URL prefix. It is possible to issue a
|
|
509
|
+
redirect that is relative to the sub-application as follows::
|
|
510
|
+
|
|
511
|
+
return redirect(request.url_prefix + '/relative-url')
|
|
512
|
+
|
|
513
|
+
When mounting an application as shown above, before-request, after-request and
|
|
514
|
+
error handlers defined in the sub-application are copied over to the main
|
|
515
|
+
application at mount time. Once installed in the main application, these
|
|
516
|
+
handlers will apply to the whole application and not just the sub-application
|
|
517
|
+
in which they were created.
|
|
518
|
+
|
|
519
|
+
The :func:`mount() <microdot.Microdot.mount>` method has a ``local`` argument
|
|
520
|
+
that defaults to ``False``. When this argument is set to ``True``, the
|
|
521
|
+
before-request, after-request and error handlers defined in the sub-application
|
|
522
|
+
will only apply to the sub-application.
|
|
482
523
|
|
|
483
524
|
Shutting Down the Server
|
|
484
525
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "microdot"
|
|
3
|
-
version = "2.0
|
|
3
|
+
version = "2.1.0"
|
|
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,9 +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",
|
|
29
|
-
"pyjwt"
|
|
34
|
+
"pyjwt",
|
|
30
35
|
]
|
|
31
36
|
|
|
32
37
|
[tool.setuptools]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from microdot import abort
|
|
2
|
+
from microdot.microdot import invoke_handler
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseAuth:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.auth_callback = None
|
|
8
|
+
self.error_callback = None
|
|
9
|
+
|
|
10
|
+
def __call__(self, f):
|
|
11
|
+
"""Decorator to protect a route with authentication.
|
|
12
|
+
|
|
13
|
+
An instance of this class must be used as a decorator on the routes
|
|
14
|
+
that need to be protected. Example::
|
|
15
|
+
|
|
16
|
+
auth = BasicAuth() # or TokenAuth()
|
|
17
|
+
|
|
18
|
+
@app.route('/protected')
|
|
19
|
+
@auth
|
|
20
|
+
def protected(request):
|
|
21
|
+
# ...
|
|
22
|
+
|
|
23
|
+
Routes that are decorated in this way will only be invoked if the
|
|
24
|
+
authentication callback returned a valid user object, otherwise the
|
|
25
|
+
error callback will be executed.
|
|
26
|
+
"""
|
|
27
|
+
async def wrapper(request, *args, **kwargs):
|
|
28
|
+
auth = self._get_auth(request)
|
|
29
|
+
if not auth:
|
|
30
|
+
return await invoke_handler(self.error_callback, request)
|
|
31
|
+
request.g.current_user = await invoke_handler(
|
|
32
|
+
self.auth_callback, request, *auth)
|
|
33
|
+
if not request.g.current_user:
|
|
34
|
+
return await invoke_handler(self.error_callback, request)
|
|
35
|
+
return await invoke_handler(f, request, *args, **kwargs)
|
|
36
|
+
|
|
37
|
+
return wrapper
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BasicAuth(BaseAuth):
|
|
41
|
+
"""Basic Authentication.
|
|
42
|
+
|
|
43
|
+
:param realm: The realm that is displayed when the user is prompted to
|
|
44
|
+
authenticate in the browser.
|
|
45
|
+
:param charset: The charset that is used to encode the realm.
|
|
46
|
+
:param scheme: The authentication scheme. Defaults to 'Basic'.
|
|
47
|
+
:param error_status: The error status code to return when authentication
|
|
48
|
+
fails. Defaults to 401.
|
|
49
|
+
"""
|
|
50
|
+
def __init__(self, realm='Please login', charset='UTF-8', scheme='Basic',
|
|
51
|
+
error_status=401):
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.realm = realm
|
|
54
|
+
self.charset = charset
|
|
55
|
+
self.scheme = scheme
|
|
56
|
+
self.error_status = error_status
|
|
57
|
+
self.error_callback = self.authentication_error
|
|
58
|
+
|
|
59
|
+
def _get_auth(self, request):
|
|
60
|
+
auth = request.headers.get('Authorization')
|
|
61
|
+
if auth and auth.startswith('Basic '):
|
|
62
|
+
import binascii
|
|
63
|
+
try:
|
|
64
|
+
username, password = binascii.a2b_base64(
|
|
65
|
+
auth[6:]).decode().split(':', 1)
|
|
66
|
+
except Exception: # pragma: no cover
|
|
67
|
+
return None
|
|
68
|
+
return username, password
|
|
69
|
+
|
|
70
|
+
def authentication_error(self, request):
|
|
71
|
+
return '', self.error_status, {
|
|
72
|
+
'WWW-Authenticate': '{} realm="{}", charset="{}"'.format(
|
|
73
|
+
self.scheme, self.realm, self.charset)}
|
|
74
|
+
|
|
75
|
+
def authenticate(self, f):
|
|
76
|
+
"""Decorator to configure the authentication callback.
|
|
77
|
+
|
|
78
|
+
This decorator must be used with a function that accepts the request
|
|
79
|
+
object, a username and a password and returns a user object if the
|
|
80
|
+
credentials are valid, or ``None`` if they are not. Example::
|
|
81
|
+
|
|
82
|
+
@auth.authenticate
|
|
83
|
+
async def check_credentials(request, username, password):
|
|
84
|
+
user = get_user(username)
|
|
85
|
+
if user and user.check_password(password):
|
|
86
|
+
return get_user(username)
|
|
87
|
+
"""
|
|
88
|
+
self.auth_callback = f
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TokenAuth(BaseAuth):
|
|
92
|
+
"""Token based authentication.
|
|
93
|
+
|
|
94
|
+
:param header: The name of the header that will contain the token. Defaults
|
|
95
|
+
to 'Authorization'.
|
|
96
|
+
:param scheme: The authentication scheme. Defaults to 'Bearer'.
|
|
97
|
+
:param error_status: The error status code to return when authentication
|
|
98
|
+
fails. Defaults to 401.
|
|
99
|
+
"""
|
|
100
|
+
def __init__(self, header='Authorization', scheme='Bearer',
|
|
101
|
+
error_status=401):
|
|
102
|
+
super().__init__()
|
|
103
|
+
self.header = header
|
|
104
|
+
self.scheme = scheme.lower()
|
|
105
|
+
self.error_status = error_status
|
|
106
|
+
self.error_callback = self.authentication_error
|
|
107
|
+
|
|
108
|
+
def _get_auth(self, request):
|
|
109
|
+
auth = request.headers.get(self.header)
|
|
110
|
+
if auth:
|
|
111
|
+
if self.header == 'Authorization':
|
|
112
|
+
try:
|
|
113
|
+
scheme, token = auth.split(' ', 1)
|
|
114
|
+
except Exception:
|
|
115
|
+
return None
|
|
116
|
+
if scheme.lower() == self.scheme:
|
|
117
|
+
return (token.strip(),)
|
|
118
|
+
else:
|
|
119
|
+
return (auth,)
|
|
120
|
+
|
|
121
|
+
def authenticate(self, f):
|
|
122
|
+
"""Decorator to configure the authentication callback.
|
|
123
|
+
|
|
124
|
+
This decorator must be used with a function that accepts the request
|
|
125
|
+
object, a username and a password and returns a user object if the
|
|
126
|
+
credentials are valid, or ``None`` if they are not. Example::
|
|
127
|
+
|
|
128
|
+
@auth.authenticate
|
|
129
|
+
async def check_credentials(request, token):
|
|
130
|
+
return get_user(token)
|
|
131
|
+
"""
|
|
132
|
+
self.auth_callback = f
|
|
133
|
+
|
|
134
|
+
def errorhandler(self, f):
|
|
135
|
+
"""Decorator to configure the error callback.
|
|
136
|
+
|
|
137
|
+
Microdot calls the error callback to allow the application to generate
|
|
138
|
+
a custom error response. The default error response is to call
|
|
139
|
+
``abort(401)``.
|
|
140
|
+
"""
|
|
141
|
+
self.error_callback = f
|
|
142
|
+
|
|
143
|
+
def authentication_error(self, request):
|
|
144
|
+
abort(self.error_status)
|