31 Commits
v2.0.5 ... main

Author SHA1 Message Date
Miguel Grinberg
d864b81b65 revert to default funding file #nolog 2025-01-06 17:49:09 +00:00
Miguel Grinberg
d7459f23b2 Version 2.0.8.dev0 2024-11-10 22:57:45 +00:00
Miguel Grinberg
32f5e415e7 Release 2.0.7 2024-11-10 22:57:31 +00:00
Miguel Grinberg
c46e429106 Accept responses with just a status code (Fixes #263) 2024-11-10 20:09:05 +00:00
Miguel Grinberg
4eac013087 Accept responses with just a status code (Fixes #263) 2024-11-10 00:35:21 +00:00
dependabot[bot]
496a288064 Bump werkzeug from 3.0.3 to 3.0.6 in /examples/benchmark (#260) #nolog
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.3 to 3.0.6.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.0.3...3.0.6)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-26 11:37:08 +01:00
dependabot[bot]
bcd876fcae Bump quart from 0.19.4 to 0.19.7 in /examples/benchmark (#259) #nolog
Bumps [quart](https://github.com/pallets/quart) from 0.19.4 to 0.19.7.
- [Release notes](https://github.com/pallets/quart/releases)
- [Changelog](https://github.com/pallets/quart/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/quart/compare/0.19.4...0.19.7)

---
updated-dependencies:
- dependency-name: quart
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-26 00:57:50 +01:00
Stanislav Garanzha
5e5fc5e93e Fix urls in docs (#253)
* Fix 404 external links in intro.rst

* Fix broken links in extensions.rst

`examples/cors/cors.py` does not exist

`uvicorn.org` - failed to resolve
2024-08-17 18:41:43 +01:00
Miguel Grinberg
8895af3737 add tox to dev dependencies 2024-08-15 20:40:54 +01:00
Miguel Grinberg
0a021462e0 Better documentation for start_server() method (Fixes #252) 2024-08-15 19:10:34 +01:00
Lukas Kremla
482ab6d5ca Fixed gzip automatic content-type assignment and added automatic compression header configuration (#251)
* Fixed gzip automatic content-type assignment and added automatic compression setting

This implements the fix for detecting the proper content-type even when the file has the ".gz" extension. It further makes sure the compression headers are set properly if a "gz." file is detected, but the compression headers weren't explicitly set by the user.

* Added a test for properly auto-determining mime types and setting content encoding header

* Modified the gzip file header assignments and following tests according to the feedback.

---------

Co-authored-by: Lukáš Kremla <lukas.kremla@bonnel.cz>
2024-08-14 23:02:23 +01:00
dependabot[bot]
5fe06f6bd5 Bump certifi from 2023.11.17 to 2024.7.4 in /examples/benchmark (#244)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.11.17 to 2024.7.4.
- [Commits](https://github.com/certifi/python-certifi/compare/2023.11.17...2024.07.04)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-06 11:17:41 +01:00
dependabot[bot]
c170e840ec Bump urllib3 from 2.1.0 to 2.2.2 in /examples/benchmark (#241) #nolog
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.1.0 to 2.2.2.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.1.0...2.2.2)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 00:12:28 +01:00
Miguel Grinberg
3a39b47ea8 Version 2.0.7.dev0 2024-06-18 23:14:36 +01:00
Miguel Grinberg
53287217ae Release 2.0.6 2024-06-18 23:14:14 +01:00
Miguel Grinberg
6ffb8a8fe9 Cookie path support in session and test client 2024-06-18 20:56:18 +01:00
Miguel Grinberg
0151611fc8 Configurable session cookie options (Fixes #242) 2024-06-18 00:09:44 +01:00
dependabot[bot]
4204db61e5 Bump jinja2 from 3.1.3 to 3.1.4 in /examples/benchmark (#230) #nolog
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-21 11:40:13 +01:00
dependabot[bot]
12438743a8 Bump werkzeug from 3.0.1 to 3.0.3 in /examples/benchmark (#229) #nolog
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.1 to 3.0.3.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.0.1...3.0.3)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-21 11:39:51 +01:00
dependabot[bot]
7cbb1edf59 Bump requests from 2.31.0 to 2.32.0 in /examples/benchmark (#232) #nolog
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-21 11:39:16 +01:00
Miguel Grinberg
dac6df7a7a use codecov token for coverage uploads #nolog 2024-04-28 00:31:53 +01:00
dependabot[bot]
5d6e838f3c Bump gunicorn from 21.2.0 to 22.0.0 in /examples/benchmark (#224) #nolog
Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 21.2.0 to 22.0.0.
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/21.2.0...22.0.0)

---
updated-dependencies:
- dependency-name: gunicorn
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-17 23:52:37 +01:00
dependabot[bot]
563bfdc8f5 Bump idna from 3.6 to 3.7 in /examples/benchmark (#223) #nolog
Bumps [idna](https://github.com/kjd/idna) from 3.6 to 3.7.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)
- [Commits](https://github.com/kjd/idna/compare/v3.6...v3.7)

---
updated-dependencies:
- dependency-name: idna
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-12 15:16:52 +01:00
Miguel Grinberg
679d8e63b8 Fix docs build #nolog 2024-03-24 19:56:43 +00:00
Miguel Grinberg
4cb155ee41 Improved cookie support in the test client 2024-03-24 19:45:22 +00:00
Miguel Grinberg
dea79c5ce2 Make Session class more reusable 2024-03-23 16:29:36 +00:00
Carlo Colombo
6b1fd61917 removed outdated import from documentation (Fixes #216) 2024-03-15 11:03:10 +00:00
Miguel Grinberg
f6876c0d15 Use @wraps on decorated functions 2024-03-14 00:16:35 +00:00
Hamsanger
904d5fcaa2 Add event ID to the SSE implementation (#213) 2024-03-10 23:52:43 +00:00
Miguel Grinberg
a0ea439def Add roadmap details to readme 2024-03-10 17:15:14 +00:00
Miguel Grinberg
a1801d9a53 Version 2.0.6.dev0 2024-03-09 10:48:18 +00:00
20 changed files with 303 additions and 66 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
github: miguelgrinberg
patreon: miguelgrinberg
custom: https://paypal.me/miguelgrinberg

View File

@@ -64,6 +64,7 @@ jobs:
with:
files: ./coverage.xml
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
benchmark:
name: benchmark
runs-on: ubuntu-latest

4
.gitignore vendored
View File

@@ -25,6 +25,8 @@ wheels/
.installed.cfg
*.egg
MANIFEST
requirements.txt
requirements-dev.txt
# PyInstaller
# Usually these files are written by a python script from a template
@@ -90,6 +92,8 @@ venv/
ENV/
env.bak/
venv.bak/
.direnv
.envrc
# Spyder project settings
.spyderproject

View File

@@ -1,5 +1,23 @@
# Microdot change log
**Release 2.0.7** - 2024-11-10
- Accept responses with just a status code [#263](https://github.com/miguelgrinberg/microdot/issues/263) ([commit #1](https://github.com/miguelgrinberg/microdot/commit/4eac013087f807cafa244b8a6b7b0ed4c82ff150) [commit #2](https://github.com/miguelgrinberg/microdot/commit/c46e4291061046f1be13f300dd08645b71c16635))
- Fixed compressed file content-type assignment [#251](https://github.com/miguelgrinberg/microdot/issues/251) ([commit](https://github.com/miguelgrinberg/microdot/commit/482ab6d5ca068d71ea6301f45918946161e9fcc1)) (thanks **Lukas Kremla**!)
- Better documentation for start_server[#252](https://github.com/miguelgrinberg/microdot/issues/252) ([commit](https://github.com/miguelgrinberg/microdot/commit/0a021462e0c42c249d587a2d600f5a21a408adfc))
- Fix URLs in documentation [#253](https://github.com/miguelgrinberg/microdot/issues/253) ([commit](https://github.com/miguelgrinberg/microdot/commit/5e5fc5e93e11cbf6e3dc8036494e8732d1815d3e)) (thanks **Stanislav Garanzha**!)
**Release 2.0.6** - 2024-06-18
- Add event ID to the SSE implementation [#213](https://github.com/miguelgrinberg/microdot/issues/213) ([commit](https://github.com/miguelgrinberg/microdot/commit/904d5fcaa2d19d939a719b8e68c4dee3eb470739)) (thanks **Hamsanger**!)
- Configurable session cookie options [#242](https://github.com/miguelgrinberg/microdot/issues/242) ([commit](https://github.com/miguelgrinberg/microdot/commit/0151611fc84fec450820d673f4c4d70c32c990a7))
- Improved cookie support in the test client ([commit](https://github.com/miguelgrinberg/microdot/commit/4cb155ee411dc2d9c9f15714cb32b25ba79b156a))
- Cookie path support in session extension and test client ([commit](https://github.com/miguelgrinberg/microdot/commit/6ffb8a8fe920111c4d8c16e98715a0d5ee2d1da3))
- Refactor `Session` class to make it more reusable ([commit](https://github.com/miguelgrinberg/microdot/commit/dea79c5ce224dec7858ffef45a42bed442fd3a5a))
- Use `@functools.wraps` on decorated functions ([commit](https://github.com/miguelgrinberg/microdot/commit/f6876c0d154adcae96098405fb6a1fdf1ea4ec28))
- Removed outdated import from documentation [#216](https://github.com/miguelgrinberg/microdot/issues/216) ([commit](https://github.com/miguelgrinberg/microdot/commit/6b1fd6191702e7a9ad934fddfcdd0a3cebea7c94)) (thanks **Carlo Colombo**!)
- Add roadmap details to readme ([commit](https://github.com/miguelgrinberg/microdot/commit/a0ea439def238084c4d68309c0992b66ffd28ad6))
**Release 2.0.5** - 2024-03-09
- Correct handling of 0 as an integer argument (regression from #207) [#212](https://github.com/miguelgrinberg/microdot/issues/212) ([commit](https://github.com/miguelgrinberg/microdot/commit/d0a4cf8fa7dfb1da7466157b18d3329a8cf9a5df))

View File

@@ -32,10 +32,25 @@ describes the backwards incompatible changes that were made.
## Resources
- Documentation
- [Stable](https://microdot.readthedocs.io/en/stable/)
- [Latest](https://microdot.readthedocs.io/en/latest/)
- Still using version 1?
- [Code](https://github.com/miguelgrinberg/microdot/tree/v1)
- [Documentation](https://microdot.readthedocs.io/en/v1/)
- [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)
- Documentation
- [Latest](https://microdot.readthedocs.io/en/latest/)
- [Stable (v2)](https://microdot.readthedocs.io/en/stable/)
- [Legacy (v1)](https://microdot.readthedocs.io/en/v1/) ([Code](https://github.com/miguelgrinberg/microdot/tree/v1))
## Roadmap
The following features are planned for future releases of Microdot, both for
MicroPython and CPython:
- Support for forms encoded in `multipart/form-data` format
- Authentication support, similar to [Flask-Login](https://github.com/maxcountryman/flask-login) for Flask
- OpenAPI integration, similar to [APIFairy](https://github.com/miguelgrinberg/apifairy) for Flask
In addition to the above, the following extensions are also under consideration,
but only for CPython:
- Database integration through [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)
- Socket.IO support through [python-socketio](https://github.com/miguelgrinberg/python-socketio)
Do you have other ideas to propose? Let's [discuss them](https://github.com/miguelgrinberg/microdot/discussions/new?category=ideas)!

View File

@@ -286,7 +286,7 @@ Cross-Origin Resource Sharing (CORS)
- | None
* - Examples
- | `cors.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/cors/cors.py>`_
- | `app.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/cors/app.py>`_
The CORS extension provides support for `Cross-Origin Resource Sharing
(CORS) <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_. CORS is a
@@ -363,7 +363,7 @@ Using an ASGI Web Server
- | `asgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/asgi.py>`_
* - Required external dependencies
- | An ASGI web server, such as `Uvicorn <https://uvicorn.org/>`_.
- | An ASGI web server, such as `Uvicorn <https://www.uvicorn.org/>`_.
* - Examples
- | `hello_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_asgi.py>`_

View File

@@ -82,8 +82,34 @@ handler functions can be defined as ``async def`` or ``def`` functions, but
``async def`` functions are recommended for performance.
The :func:`run() <microdot.Microdot.run>` method starts the application's web
server on port 5000 by default. This method blocks while it waits for
connections from clients.
server on port 5000 by default, and creates its own asynchronous loop. This
method blocks while it waits for connections from clients.
For some applications it may be necessary to run the web server alongside other
asynchronous tasks, on an already running loop. In that case, instead of
``app.run()`` the web server can be started by invoking the
:func:`start_server() <microdot.Microdot.start_server>` coroutine as shown in
the following example::
import asyncio
from microdot import Microdot
app = Microdot()
@app.route('/')
async def index(request):
return 'Hello, world!'
async def main():
# start the server in a background task
server = asyncio.create_task(app.start_server())
# ... do other asynchronous work here ...
# cleanup before ending the application
await server
asyncio.run(main())
Running with CPython
^^^^^^^^^^^^^^^^^^^^
@@ -92,7 +118,7 @@ Running with CPython
:align: left
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
- | `microdot.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/microdot.py>`_
* - Required external dependencies
- | None
@@ -118,7 +144,7 @@ Running with MicroPython
:align: left
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
- | `microdot.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/microdot.py>`_
* - Required external dependencies
- | None
@@ -145,8 +171,9 @@ changed by passing the ``port`` argument to the ``run()`` method.
Web Server Configuration
^^^^^^^^^^^^^^^^^^^^^^^^
The :func:`run() <microdot.Microdot.run>` method supports a few arguments to
configure the web server.
The :func:`run() <microdot.Microdot.run>` and
:func:`start_server() <microdot.Microdot.start_server>` methods support a few
arguments to configure the web server.
- ``port``: The port number to listen on. Pass the desired port number in this
argument to use a port different than the default of 5000. For example::

View File

@@ -16,7 +16,7 @@ blinker==1.7.0
# quart
build==1.0.3
# via pip-tools
certifi==2023.11.17
certifi==2024.7.4
# via requests
charset-normalizer==3.3.2
# via requests
@@ -32,7 +32,7 @@ flask==3.0.0
# via
# -r requirements.in
# quart
gunicorn==21.2.0
gunicorn==22.0.0
# via -r requirements.in
h11==0.14.0
# via
@@ -49,7 +49,7 @@ hypercorn==0.15.0
# via quart
hyperframe==6.0.1
# via h2
idna==3.6
idna==3.7
# via
# anyio
# requests
@@ -57,7 +57,7 @@ itsdangerous==2.1.2
# via
# flask
# quart
jinja2==3.1.3
jinja2==3.1.4
# via
# flask
# quart
@@ -82,9 +82,9 @@ pydantic-core==2.14.5
# via pydantic
pyproject-hooks==1.0.0
# via build
quart==0.19.4
quart==0.19.7
# via -r requirements.in
requests==2.31.0
requests==2.32.0
# via -r requirements.in
sniffio==1.3.0
# via anyio
@@ -95,11 +95,11 @@ typing-extensions==4.9.0
# fastapi
# pydantic
# pydantic-core
urllib3==2.1.0
urllib3==2.2.2
# via requests
uvicorn==0.24.0.post1
# via -r requirements.in
werkzeug==3.0.1
werkzeug==3.0.6
# via
# flask
# quart

View File

@@ -1,6 +1,6 @@
[project]
name = "microdot"
version = "2.0.5"
version = "2.0.8.dev0"
authors = [
{ name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
]
@@ -14,6 +14,8 @@ classifiers = [
"Operating System :: OS Independent",
]
requires-python = ">=3.8"
dependencies = [
]
[project.readme]
file = "README.md"
@@ -24,8 +26,12 @@ Homepage = "https://github.com/miguelgrinberg/microdot"
"Bug Tracker" = "https://github.com/miguelgrinberg/microdot/issues"
[project.optional-dependencies]
dev = [
"tox",
]
docs = [
"sphinx",
"pyjwt",
]
[tool.setuptools]

8
src/microdot/helpers.py Normal file
View File

@@ -0,0 +1,8 @@
try:
from functools import wraps
except ImportError: # pragma: no cover
# MicroPython does not currently implement functools.wraps
def wraps(wrapped):
def _(wrapper):
return wrapper
return _

View File

@@ -598,7 +598,7 @@ class Response:
else: # pragma: no cover
http_cookie += '; Expires=' + time.strftime(
'%a, %d %b %Y %H:%M:%S GMT', expires.timetuple())
if max_age:
if max_age is not None:
http_cookie += '; Max-Age=' + str(max_age)
if secure:
http_cookie += '; Secure'
@@ -616,10 +616,10 @@ class Response:
:param cookie: The cookie's name.
:param kwargs: Any cookie opens and flags supported by
``set_cookie()`` except ``expires``.
``set_cookie()`` except ``expires`` and ``max_age``.
"""
self.set_cookie(cookie, '', expires='Thu, 01 Jan 1970 00:00:01 GMT',
**kwargs)
max_age=0, **kwargs)
def complete(self):
if isinstance(self.body, bytes) and \
@@ -774,7 +774,10 @@ class Response:
first.
"""
if content_type is None:
ext = filename.split('.')[-1]
if compressed and filename.endswith('.gz'):
ext = filename[:-3].split('.')[-1]
else:
ext = filename.split('.')[-1]
if ext in Response.types_map:
content_type = Response.types_map[ext]
else:
@@ -1191,7 +1194,7 @@ class Microdot:
Example::
import asyncio
from microdot_asyncio import Microdot
from microdot import Microdot
app = Microdot()
@@ -1268,7 +1271,7 @@ class Microdot:
Example::
from microdot_asyncio import Microdot
from microdot import Microdot
app = Microdot()
@@ -1366,7 +1369,12 @@ class Microdot:
if res is None:
res = await invoke_handler(
f, req, **req.url_args)
if isinstance(res, int):
res = '', res
if isinstance(res, tuple):
if isinstance(res[0], int):
res = ('', res[0],
res[1] if len(res) > 1 else {})
body = res[0]
if isinstance(res[1], int):
status_code = res[1]

View File

@@ -1,7 +1,6 @@
import jwt
from microdot.microdot import invoke_handler
secret_key = None
from microdot.helpers import wraps
class SessionDict(dict):
@@ -30,14 +29,21 @@ class Session:
"""
secret_key = None
def __init__(self, app=None, secret_key=None):
def __init__(self, app=None, secret_key=None, cookie_options=None):
self.secret_key = secret_key
self.cookie_options = cookie_options or {}
if app is not None:
self.initialize(app)
def initialize(self, app, secret_key=None):
def initialize(self, app, secret_key=None, cookie_options=None):
if secret_key is not None:
self.secret_key = secret_key
if cookie_options is not None:
self.cookie_options = cookie_options
if 'path' not in self.cookie_options:
self.cookie_options['path'] = '/'
if 'http_only' not in self.cookie_options:
self.cookie_options['http_only'] = True
app._session = self
def get(self, request):
@@ -57,13 +63,7 @@ class Session:
if session is None:
request.g._session = SessionDict(request, {})
return request.g._session
try:
session = jwt.decode(session, self.secret_key,
algorithms=['HS256'])
except jwt.exceptions.PyJWTError: # pragma: no cover
request.g._session = SessionDict(request, {})
else:
request.g._session = SessionDict(request, session)
request.g._session = SessionDict(request, self.decode(session))
return request.g._session
def update(self, request, session):
@@ -89,12 +89,12 @@ class Session:
if not self.secret_key:
raise ValueError('The session secret key is not configured')
encoded_session = jwt.encode(session, self.secret_key,
algorithm='HS256')
encoded_session = self.encode(session)
@request.after_request
def _update_session(request, response):
response.set_cookie('session', encoded_session, http_only=True)
response.set_cookie('session', encoded_session,
**self.cookie_options)
return response
def delete(self, request):
@@ -117,10 +117,21 @@ class Session:
"""
@request.after_request
def _delete_session(request, response):
response.set_cookie('session', '', http_only=True,
expires='Thu, 01 Jan 1970 00:00:01 GMT')
response.delete_cookie('session', **self.cookie_options)
return response
def encode(self, payload, secret_key=None):
return jwt.encode(payload, secret_key or self.secret_key,
algorithm='HS256')
def decode(self, session, secret_key=None):
try:
payload = jwt.decode(session, secret_key or self.secret_key,
algorithms=['HS256'])
except jwt.exceptions.PyJWTError: # pragma: no cover
return {}
return payload
def with_session(f):
"""Decorator that passes the user session to the route handler.
@@ -136,13 +147,9 @@ def with_session(f):
Note that the decorator does not save the session. To update the session,
call the :func:`session.save() <microdot.session.SessionDict.save>` method.
"""
@wraps(f)
async def wrapper(request, *args, **kwargs):
return await invoke_handler(
f, request, request.app._session.get(request), *args, **kwargs)
for attr in ['__name__', '__doc__', '__module__', '__qualname__']:
try:
setattr(wrapper, attr, getattr(f, attr))
except AttributeError: # pragma: no cover
pass
return wrapper

View File

@@ -1,5 +1,6 @@
import asyncio
import json
from microdot.helpers import wraps
class SSE:
@@ -12,7 +13,7 @@ class SSE:
self.event = asyncio.Event()
self.queue = []
async def send(self, data, event=None):
async def send(self, data, event=None, event_id=None):
"""Send an event to the client.
:param data: the data to send. It can be given as a string, bytes, dict
@@ -20,6 +21,8 @@ class SSE:
Any other types are converted to string before sending.
:param event: an optional event name, to send along with the data. If
given, it must be a string.
:param event_id: an optional event id, to send along with the data. If
given, it must be a string.
"""
if isinstance(data, (dict, list)):
data = json.dumps(data).encode()
@@ -28,6 +31,8 @@ class SSE:
elif not isinstance(data, bytes):
data = str(data).encode()
data = b'data: ' + data + b'\n\n'
if event_id:
data = b'id: ' + event_id.encode() + b'\n' + data
if event:
data = b'event: ' + event.encode() + b'\n' + data
self.queue.append(data)
@@ -99,6 +104,7 @@ def with_sse(f):
# send a named event
await sse.send('hello', event='greeting')
"""
@wraps(f)
async def sse_handler(request, *args, **kwargs):
return sse_response(request, f, *args, **kwargs)

View File

@@ -77,7 +77,7 @@ class TestClient:
The following example shows how to create a test client for an application
and send a test request::
from microdot_asyncio import Microdot
from microdot import Microdot
app = Microdot()
@@ -112,9 +112,13 @@ class TestClient:
headers['Host'] = 'example.com:1234'
return body, headers
def _process_cookies(self, headers):
def _process_cookies(self, path, headers):
cookies = ''
for name, value in self.cookies.items():
if isinstance(value, tuple):
value, cookie_path = value
if not path.startswith(cookie_path):
continue
if cookies:
cookies += '; '
cookies += name + '=' + value
@@ -123,7 +127,7 @@ class TestClient:
headers['Cookie'] += '; ' + cookies
else:
headers['Cookie'] = cookies
return cookies, headers
return headers
def _render_request(self, method, path, headers, body):
request_bytes = '{method} {path} HTTP/1.0\n'.format(
@@ -139,26 +143,45 @@ class TestClient:
for cookie in cookies:
cookie_name, cookie_value = cookie.split('=', 1)
cookie_options = cookie_value.split(';')
path = '/'
delete = False
for option in cookie_options[1:]:
if option.strip().lower().startswith('expires='):
_, e = option.strip().split('=', 1)
option = option.strip().lower()
if option.startswith(
'max-age='): # pragma: no cover
_, age = option.split('=', 1)
try:
age = int(age)
except ValueError: # pragma: no cover
age = 0
if age <= 0:
delete = True
elif option.startswith('expires='):
_, e = option.split('=', 1)
# this is a very limited parser for cookie expiry
# that only detects a cookie deletion request when
# the date is 1/1/1970
if '1 jan 1970' in e.lower(): # pragma: no branch
delete = True
break
elif option.startswith('path='):
_, path = option.split('=', 1)
if delete:
if cookie_name in self.cookies: # pragma: no branch
del self.cookies[cookie_name]
cookie_path = self.cookies[cookie_name][1] \
if isinstance(self.cookies[cookie_name], tuple) \
else '/'
if path == cookie_path:
del self.cookies[cookie_name]
else:
self.cookies[cookie_name] = cookie_options[0]
if path == '/':
self.cookies[cookie_name] = cookie_options[0]
else:
self.cookies[cookie_name] = (cookie_options[0], path)
async def request(self, method, path, headers=None, body=None, sock=None):
headers = headers or {}
body, headers = self._process_body(body, headers)
cookies, headers = self._process_cookies(headers)
headers = self._process_cookies(path, headers)
request_bytes = self._render_request(method, path, headers, body)
if sock:
reader = sock[0]

View File

@@ -2,6 +2,7 @@ import binascii
import hashlib
from microdot import Request, Response
from microdot.microdot import MUTED_SOCKET_ERRORS, print_exception
from microdot.helpers import wraps
class WebSocketError(Exception):
@@ -192,6 +193,7 @@ async def websocket_upgrade(request):
def websocket_wrapper(f, upgrade_function):
@wraps(f)
async def wrapper(request, *args, **kwargs):
ws = await upgrade_function(request)
try:

1
tests/files/test.txt.gz Normal file
View File

@@ -0,0 +1 @@
foo

View File

@@ -203,6 +203,7 @@ class TestMicrodot(unittest.TestCase):
req.cookies['one'] + req.cookies['two'] + req.cookies['three'])
res.set_cookie('four', '4')
res.delete_cookie('two', path='/')
res.delete_cookie('one', path='/bad')
return res
client = TestClient(app, cookies={'one': '1', 'two': '2'})
@@ -273,6 +274,14 @@ class TestMicrodot(unittest.TestCase):
return '<p>four</p>', 202, \
{'Content-Type': 'text/html; charset=UTF-8'}
@app.route('/status')
def five(req):
return 202
@app.route('/status-headers')
def six(req):
return 202, {'Content-Type': 'text/html; charset=UTF-8'}
client = TestClient(app)
res = self._run(client.get('/body'))
@@ -298,6 +307,18 @@ class TestMicrodot(unittest.TestCase):
'text/html; charset=UTF-8')
self.assertEqual(res.text, '<p>four</p>')
res = self._run(client.get('/status'))
self.assertEqual(res.text, '')
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
res = self._run(client.get('/status-headers'))
self.assertEqual(res.text, '')
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'],
'text/html; charset=UTF-8')
def test_before_after_request(self):
app = Microdot()

View File

@@ -136,10 +136,10 @@ class TestResponse(unittest.TestCase):
self.assertTrue(fd.response.endswith(b'\r\n\r\nfoobar'))
def test_create_from_other(self):
res = Response(123)
res = Response(23.7)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers, {})
self.assertEqual(res.body, 123)
self.assertEqual(res.body, 23.7)
def test_create_with_status_code(self):
res = Response('not found', 404)
@@ -193,6 +193,7 @@ class TestResponse(unittest.TestCase):
expires='Tue, 05 Nov 2019 02:23:54 GMT', max_age=123,
secure=True, http_only=True)
res.delete_cookie('foo8', http_only=True)
res.delete_cookie('foo9', path='/s')
self.assertEqual(res.headers, {'Set-Cookie': [
'foo1=bar1',
'foo2=bar2; Path=/; Partitioned',
@@ -203,7 +204,10 @@ class TestResponse(unittest.TestCase):
'foo7=bar7; Path=/foo; Domain=example.com:1234; '
'Expires=Tue, 05 Nov 2019 02:23:54 GMT; Max-Age=123; Secure; '
'HttpOnly',
'foo8=; Expires=Thu, 01 Jan 1970 00:00:01 GMT; HttpOnly',
('foo8=; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; '
'HttpOnly'),
('foo9=; Path=/s; Expires=Thu, 01 Jan 1970 00:00:01 GMT; '
'Max-Age=0'),
]})
def test_redirect(self):
@@ -276,6 +280,17 @@ class TestResponse(unittest.TestCase):
'application/octet-stream')
self.assertEqual(res.headers['Content-Encoding'], 'gzip')
def test_send_file_gzip_handling(self):
res = Response.send_file('tests/files/test.txt.gz')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'application/octet-stream')
res = Response.send_file('tests/files/test.txt.gz', compressed=True)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Encoding'], 'gzip')
def test_default_content_type(self):
original_content_type = Response.default_content_type
res = Response('foo')

View File

@@ -82,3 +82,77 @@ class TestSession(unittest.TestCase):
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
def test_session_default_path(self):
app = Microdot()
Session(app, secret_key='some-other-secret')
client = TestClient(app)
@app.get('/')
@with_session
def index(req, session):
session['foo'] = 'bar'
session.save()
return ''
@app.get('/child')
@with_session
def child(req, session):
return str(session.get('foo'))
@app.get('/delete')
@with_session
def delete(req, session):
session.delete()
return ''
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
res = self._run(client.get('/child'))
self.assertEqual(res.text, 'bar')
res = self._run(client.get('/delete'))
res = self._run(client.get('/child'))
self.assertEqual(res.text, 'None')
def test_session_custom_path(self):
app = Microdot()
session_ext = Session()
session_ext.initialize(app, secret_key='some-other-secret',
cookie_options={'path': '/child',
'http_only': False})
client = TestClient(app)
@app.get('/')
@with_session
def index(req, session):
return str(session.get('foo'))
@app.get('/child')
@with_session
def child(req, session):
session['foo'] = 'bar'
session.save()
return ''
@app.get('/child/foo')
@with_session
def foo(req, session):
return str(session.get('foo'))
@app.get('/child/delete')
@with_session
def delete(req, session):
session.delete()
return ''
res = self._run(client.get('/child'))
self.assertEqual(res.status_code, 200)
res = self._run(client.get('/'))
self.assertEqual(res.text, 'None')
res = self._run(client.get('/child/foo'))
self.assertEqual(res.text, 'bar')
res = self._run(client.get('/child/delete'))
res = self._run(client.get('/'))
self.assertEqual(res.text, 'None')
res = self._run(client.get('/child/foo'))
self.assertEqual(res.text, 'None')

View File

@@ -23,6 +23,8 @@ class TestWebSocket(unittest.TestCase):
async def handle_sse(request, sse):
await sse.send('foo')
await sse.send('bar', event='test')
await sse.send('bar', event='test', event_id='id42')
await sse.send('bar', event_id='id42')
await sse.send({'foo': 'bar'})
await sse.send([42, 'foo', 'bar'])
await sse.send(ValueError('foo'))
@@ -34,6 +36,8 @@ class TestWebSocket(unittest.TestCase):
self.assertEqual(response.headers['Content-Type'], 'text/event-stream')
self.assertEqual(response.text, ('data: foo\n\n'
'event: test\ndata: bar\n\n'
'event: test\nid: id42\ndata: bar\n\n'
'id: id42\ndata: bar\n\n'
'data: {"foo": "bar"}\n\n'
'data: [42, "foo", "bar"]\n\n'
'data: foo\n\n'