12 Commits

Author SHA1 Message Date
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
10 changed files with 100 additions and 17 deletions

4
.gitignore vendored
View File

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

View File

@@ -1,5 +1,12 @@
# Microdot change log # 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 **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**!) - 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**!)

View File

@@ -286,7 +286,7 @@ Cross-Origin Resource Sharing (CORS)
- | None - | None
* - Examples * - 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 The CORS extension provides support for `Cross-Origin Resource Sharing
(CORS) <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_. CORS is a (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>`_ - | `asgi.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/asgi.py>`_
* - Required external dependencies * - 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 * - Examples
- | `hello_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_asgi.py>`_ - | `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. ``async def`` functions are recommended for performance.
The :func:`run() <microdot.Microdot.run>` method starts the application's web 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 server on port 5000 by default, and creates its own asynchronous loop. This
connections from clients. 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 Running with CPython
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
@@ -92,7 +118,7 @@ Running with CPython
:align: left :align: left
* - Required Microdot source files * - 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 * - Required external dependencies
- | None - | None
@@ -118,7 +144,7 @@ Running with MicroPython
:align: left :align: left
* - Required Microdot source files * - 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 * - Required external dependencies
- | None - | None
@@ -145,8 +171,9 @@ changed by passing the ``port`` argument to the ``run()`` method.
Web Server Configuration Web Server Configuration
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
The :func:`run() <microdot.Microdot.run>` method supports a few arguments to The :func:`run() <microdot.Microdot.run>` and
configure the web server. :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 - ``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:: argument to use a port different than the default of 5000. For example::

View File

@@ -16,7 +16,7 @@ blinker==1.7.0
# quart # quart
build==1.0.3 build==1.0.3
# via pip-tools # via pip-tools
certifi==2023.11.17 certifi==2024.7.4
# via requests # via requests
charset-normalizer==3.3.2 charset-normalizer==3.3.2
# via requests # via requests
@@ -82,7 +82,7 @@ pydantic-core==2.14.5
# via pydantic # via pydantic
pyproject-hooks==1.0.0 pyproject-hooks==1.0.0
# via build # via build
quart==0.19.4 quart==0.19.7
# via -r requirements.in # via -r requirements.in
requests==2.32.0 requests==2.32.0
# via -r requirements.in # via -r requirements.in
@@ -95,11 +95,11 @@ typing-extensions==4.9.0
# fastapi # fastapi
# pydantic # pydantic
# pydantic-core # pydantic-core
urllib3==2.1.0 urllib3==2.2.2
# via requests # via requests
uvicorn==0.24.0.post1 uvicorn==0.24.0.post1
# via -r requirements.in # via -r requirements.in
werkzeug==3.0.3 werkzeug==3.0.6
# via # via
# flask # flask
# quart # quart

View File

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

View File

@@ -774,7 +774,10 @@ class Response:
first. first.
""" """
if content_type is None: 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: if ext in Response.types_map:
content_type = Response.types_map[ext] content_type = Response.types_map[ext]
else: else:
@@ -1366,7 +1369,12 @@ class Microdot:
if res is None: if res is None:
res = await invoke_handler( res = await invoke_handler(
f, req, **req.url_args) f, req, **req.url_args)
if isinstance(res, int):
res = '', res
if isinstance(res, tuple): if isinstance(res, tuple):
if isinstance(res[0], int):
res = ('', res[0],
res[1] if len(res) > 1 else {})
body = res[0] body = res[0]
if isinstance(res[1], int): if isinstance(res[1], int):
status_code = res[1] status_code = res[1]

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

@@ -0,0 +1 @@
foo

View File

@@ -274,6 +274,14 @@ class TestMicrodot(unittest.TestCase):
return '<p>four</p>', 202, \ return '<p>four</p>', 202, \
{'Content-Type': 'text/html; charset=UTF-8'} {'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) client = TestClient(app)
res = self._run(client.get('/body')) res = self._run(client.get('/body'))
@@ -299,6 +307,18 @@ class TestMicrodot(unittest.TestCase):
'text/html; charset=UTF-8') 'text/html; charset=UTF-8')
self.assertEqual(res.text, '<p>four</p>') 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): def test_before_after_request(self):
app = Microdot() app = Microdot()

View File

@@ -136,10 +136,10 @@ class TestResponse(unittest.TestCase):
self.assertTrue(fd.response.endswith(b'\r\n\r\nfoobar')) self.assertTrue(fd.response.endswith(b'\r\n\r\nfoobar'))
def test_create_from_other(self): def test_create_from_other(self):
res = Response(123) res = Response(23.7)
self.assertEqual(res.status_code, 200) self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers, {}) self.assertEqual(res.headers, {})
self.assertEqual(res.body, 123) self.assertEqual(res.body, 23.7)
def test_create_with_status_code(self): def test_create_with_status_code(self):
res = Response('not found', 404) res = Response('not found', 404)
@@ -280,6 +280,17 @@ class TestResponse(unittest.TestCase):
'application/octet-stream') 'application/octet-stream')
self.assertEqual(res.headers['Content-Encoding'], 'gzip') 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): def test_default_content_type(self):
original_content_type = Response.default_content_type original_content_type = Response.default_content_type
res = Response('foo') res = Response('foo')