24 Commits
v1.3.1 ... v1

Author SHA1 Message Date
Miguel Grinberg
7a329d98a8 Version 1.3.5.dev0 2023-11-08 00:15:07 +00:00
Miguel Grinberg
93411c6a9f Release 1.3.4 2023-11-08 00:14:21 +00:00
Miguel Grinberg
5550b20cdd Handle change in wait_closed() behavior in python 3.12 (Fixes #177) 2023-11-08 00:11:14 +00:00
dependabot[bot]
d8d2667053 Bump urllib3 from 1.26.17 to 1.26.18 in /examples/benchmark (#173) #nolog
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.17 to 1.26.18.
- [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/1.26.17...1.26.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-18 09:38:45 +01:00
Miguel Grinberg
3943a69374 Migrate Python package metadata to pyproject.toml 2023-10-15 12:51:27 +01:00
dependabot[bot]
a2f6985d01 Bump urllib3 from 1.26.11 to 1.26.17 in /examples/benchmark (#172) #nolog
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.11 to 1.26.17.
- [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/1.26.11...1.26.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 12:10:54 +01:00
dependabot[bot]
4238aa4cd4 Bump flask from 2.2.1 to 2.3.2 in /examples/benchmark (#131) #nolog
Bumps [flask](https://github.com/pallets/flask) from 2.2.1 to 2.3.2.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/2.2.1...2.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-14 10:20:02 +01:00
Miguel Grinberg
744548f8dc Added missing request argument in some documentation examples (Fixes #163) 2023-09-01 10:33:58 +01:00
dependabot[bot]
d46d2950c8 Bump certifi from 2022.12.7 to 2023.7.22 in /examples/benchmark (#158) #nolog
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-20 11:41:24 +01:00
Andy Piper
2e4911d108 Docs: fix minor typos (#161) 2023-08-03 10:40:55 +01:00
Miguel Grinberg
3eb57d0fcf Version 1.3.4.dev0 2023-07-16 11:42:54 +01:00
Miguel Grinberg
42406cef42 Release 1.3.3 2023-07-16 11:41:02 +01:00
Miguel Grinberg
e09e9830f4 Support empty responses with ASGI adapter 2023-07-16 11:36:48 +01:00
Miguel Grinberg
304ca2ef68 Added CORS extension to Python package 2023-06-29 00:36:06 +01:00
Miguel Grinberg
d99df2c401 Document access to WSGI and ASGI attributes (Fixes #153) 2023-06-24 10:34:55 +01:00
Miguel Grinberg
3554bc91cb Handle query string arguments without value (Fixes #149) 2023-06-21 20:20:53 +01:00
Miguel Grinberg
51f910087a Add readthedocs config file 2023-06-20 12:34:59 +01:00
Miguel Grinberg
e0f0565551 Upgrade micropython tests to use v1.20 2023-06-16 16:53:03 +01:00
Miguel Grinberg
2a6e76c685 Version 1.3.3.dev0 2023-06-13 14:45:20 +01:00
Miguel Grinberg
42c88b6b20 Release 1.3.2 2023-06-13 14:45:10 +01:00
Miguel Grinberg
c07a539435 Incorrect import in static_async.py 2023-06-08 00:33:58 +01:00
Miguel Grinberg
e92310fa55 In ASGI, return headers as strings and not binary (Fixes #144) 2023-06-07 23:50:44 +01:00
dependabot[bot]
9b9b7aa76d Bump requests from 2.28.1 to 2.31.0 in /examples/benchmark (#138) #nolog
Bumps [requests](https://github.com/psf/requests) from 2.28.1 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.28.1...v2.31.0)

---
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>
2023-05-23 09:40:38 +01:00
Miguel Grinberg
696f2e3e18 Version 1.3.2.dev0 2023-05-21 23:37:55 +01:00
17 changed files with 150 additions and 85 deletions

16
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,16 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: docs/conf.py
python:
install:
- method: pip
path: .
extra_requirements:
- docs

View File

@@ -1,5 +1,24 @@
# Microdot change log # Microdot change log
**Release 1.3.4** - 2023-11-08
- Handle change in `wait_closed()` behavior in Python 3.12 [#177](https://github.com/miguelgrinberg/microdot/issues/177) ([commit](https://github.com/miguelgrinberg/microdot/commit/5550b20cdd347d59e2aa68f6ebf9e9abffaff9fc))
- Added missing request argument in some documentation examples [#163](https://github.com/miguelgrinberg/microdot/issues/163) ([commit](https://github.com/miguelgrinberg/microdot/commit/744548f8dc33a72512b34c4001ee9c6c1edd22ee))
- Fix minor documentation typos [#161](https://github.com/miguelgrinberg/microdot/issues/161) ([commit](https://github.com/miguelgrinberg/microdot/commit/2e4911d10826cbb3914de4a45e495c3be36543fa)) (thanks **Andy Piper**!)
**Release 1.3.3** - 2023-07-16
- Handle query string arguments without value [#149](https://github.com/miguelgrinberg/microdot/issues/149) ([commit](https://github.com/miguelgrinberg/microdot/commit/3554bc91cb1523efa5b66fe3ef173f8e86e8c2a0))
- Support empty responses with ASGI adapter ([commit](https://github.com/miguelgrinberg/microdot/commit/e09e9830f43af41d38775547637558494151a385))
- Added CORS extension to Python package ([commit](https://github.com/miguelgrinberg/microdot/commit/304ca2ef6881fe718126b3e308211e760109d519))
- Document access to WSGI and ASGI attributes [#153](https://github.com/miguelgrinberg/microdot/issues/153) ([commit](https://github.com/miguelgrinberg/microdot/commit/d99df2c4010ab70c60b86ab334d656903e04eb26))
- Upgrade micropython tests to use v1.20 ([commit](https://github.com/miguelgrinberg/microdot/commit/e0f0565551966ee0238a5a1819c78a13639ad704))
**Release 1.3.2** - 2023-06-13
- In ASGI, return headers as strings and not binary [#144](https://github.com/miguelgrinberg/microdot/issues/144) ([commit](https://github.com/miguelgrinberg/microdot/commit/e92310fa55bbffcdcbb33f560e27c3579d7ac451))
- Incorrect import in `static_async.py` example ([commit](https://github.com/miguelgrinberg/microdot/commit/c07a53943508e64baea160748e67efc92e75b036))
**Release 1.3.1** - 2023-05-21 **Release 1.3.1** - 2023-05-21
- Support negative numbers for int path components [#137](https://github.com/miguelgrinberg/microdot/issues/137) ([commit](https://github.com/miguelgrinberg/microdot/commit/a0dd7c8ab6d681932324e56ed101aba861a105a0)) - Support negative numbers for int path components [#137](https://github.com/miguelgrinberg/microdot/issues/137) ([commit](https://github.com/miguelgrinberg/microdot/commit/a0dd7c8ab6d681932324e56ed101aba861a105a0))

5
MANIFEST.in Normal file
View File

@@ -0,0 +1,5 @@
include README.md LICENSE tox.ini
recursive-include docs *
recursive-exclude docs/_build *
recursive-include tests *
exclude **/*.pyc

Binary file not shown.

View File

@@ -137,8 +137,8 @@ subdirectory. This location can be changed with the
.. note:: .. note::
The Jinja extension is not compatible with MicroPython. The Jinja extension is not compatible with MicroPython.
Maintaing Secure User Sessions Maintaining Secure User Sessions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: .. list-table::
:align: left :align: left
@@ -486,6 +486,9 @@ web application using the Gunicorn web server::
gunicorn test:app gunicorn test:app
When using this WSGI adapter, the ``environ`` dictionary provided by the web
server is available to request handlers as ``request.environ``.
Using an ASGI Web Server Using an ASGI Web Server
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
@@ -529,3 +532,5 @@ web application using the Uvicorn web server::
uvicorn test:app uvicorn test:app
When using this ASGI adapter, the ``scope`` dictionary provided by the web
server is available to request handlers as ``request.asgi_scope``.

View File

@@ -301,7 +301,7 @@ expected to return an updated response object.
.. note:: .. note::
The :ref:`request.g <The "g" Object>` object is a special object that allows The :ref:`request.g <The "g" Object>` object is a special object that allows
the before and after request handlers, as well sa the route function to the before and after request handlers, as well as the route function to
share data during the life of the request. share data during the life of the request.
Error Handlers Error Handlers
@@ -500,7 +500,7 @@ contents as a file-like object.
Cookies Cookies
^^^^^^^ ^^^^^^^
Cookies that are sent by the client are made available throught the Cookies that are sent by the client are made available through the
:attr:`cookies <microdot.Request.cookies>` attribute of the request object in :attr:`cookies <microdot.Request.cookies>` attribute of the request object in
dictionary form. dictionary form.
@@ -595,7 +595,7 @@ always returned to the client in the response body::
In the above example, Microdot issues a standard 200 status code response, and In the above example, Microdot issues a standard 200 status code response, and
inserts the necessary headers. inserts the necessary headers.
The applicaton can provide its own status code as a second value returned from The application can provide its own status code as a second value returned from
the route. The example below returns a 202 status code:: the route. The example below returns a 202 status code::
@app.get('/') @app.get('/')
@@ -611,7 +611,7 @@ The next example returns an HTML response, instead of a default text response::
return '<h1>Hello, World!</h1>', 202, {'Content-Type': 'text/html'} return '<h1>Hello, World!</h1>', 202, {'Content-Type': 'text/html'}
If the application needs to return custom headers, but does not need to change If the application needs to return custom headers, but does not need to change
the default status code, then it can return two values, omitting the stauts the default status code, then it can return two values, omitting the status
code:: code::
@app.get('/') @app.get('/')
@@ -753,7 +753,7 @@ Another option is to create a response object directly in the route function::
Standard cookies do not offer sufficient privacy and security controls, so Standard cookies do not offer sufficient privacy and security controls, so
never store sensitive information in them unless you are adding additional never store sensitive information in them unless you are adding additional
protection mechanisms such as encryption or cryptographic signing. The protection mechanisms such as encryption or cryptographic signing. The
:ref:`session <Maintaing Secure User Sessions>` extension implements signed :ref:`session <Maintaining Secure User Sessions>` extension implements signed
cookies that prevent tampering by malicious actors. cookies that prevent tampering by malicious actors.
Concurrency Concurrency

View File

@@ -1,11 +1,11 @@
aiofiles==0.8.0 aiofiles==0.8.0
anyio==3.6.1 anyio==3.6.1
blinker==1.5 blinker==1.5
certifi==2022.12.7 certifi==2023.7.22
charset-normalizer==2.1.0 charset-normalizer==2.1.0
click==8.1.3 click==8.1.3
fastapi==0.79.0 fastapi==0.79.0
Flask==2.2.1 Flask==2.3.2
gunicorn==20.1.0 gunicorn==20.1.0
h11==0.13.0 h11==0.13.0
h2==4.1.0 h2==4.1.0
@@ -22,12 +22,12 @@ priority==2.0.0
psutil==5.9.1 psutil==5.9.1
pydantic==1.9.1 pydantic==1.9.1
quart==0.18.0 quart==0.18.0
requests==2.28.1 requests==2.31.0
sniffio==1.2.0 sniffio==1.2.0
starlette==0.27.0 starlette==0.27.0
toml==0.10.2 toml==0.10.2
typing_extensions==4.3.0 typing_extensions==4.3.0
urllib3==1.26.11 urllib3==1.26.18
uvicorn==0.18.2 uvicorn==0.18.2
Werkzeug==2.2.3 Werkzeug==2.2.3
wsproto==1.1.0 wsproto==1.1.0

View File

@@ -1,5 +1,4 @@
from microdot_asyncio import Microdot from microdot_asyncio import Microdot, send_file
from microdot import send_file
app = Microdot() app = Microdot()

View File

@@ -1,6 +1,57 @@
[project]
name = "microdot"
version = "1.3.5.dev0"
authors = [
{ name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
]
description = "The impossibly small web framework for MicroPython"
classifiers = [
"Environment :: Web Environment",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: Implementation :: MicroPython",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
[project.readme]
file = "README.md"
content-type = "text/markdown"
[project.urls]
Homepage = "https://github.com/miguelgrinberg/microdot"
"Bug Tracker" = "https://github.com/miguelgrinberg/microdot/issues"
[project.optional-dependencies]
docs = [
"sphinx",
]
[tool.setuptools]
zip-safe = false
include-package-data = true
py-modules = [
"microdot",
"microdot_asyncio",
"microdot_utemplate",
"microdot_jinja",
"microdot_session",
"microdot_cors",
"microdot_websocket",
"microdot_websocket_alt",
"microdot_asyncio_websocket",
"microdot_test_client",
"microdot_asyncio_test_client",
"microdot_wsgi",
"microdot_asgi",
"microdot_asgi_websocket",
]
[tool.setuptools.package-dir]
"" = "src"
[build-system] [build-system]
requires = [ requires = [
"setuptools>=42", "setuptools>=61.2",
"wheel"
] ]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"

View File

@@ -1,38 +0,0 @@
[metadata]
name = microdot
version = 1.3.1
author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com
description = The impossibly small web framework for MicroPython
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/miguelgrinberg/microdot
project_urls =
Bug Tracker = https://github.com/miguelgrinberg/microdot/issues
classifiers =
Environment :: Web Environment
Intended Audience :: Developers
Programming Language :: Python :: 3
Programming Language :: Python :: Implementation :: MicroPython
License :: OSI Approved :: MIT License
Operating System :: OS Independent
[options]
zip_safe = False
include_package_data = True
package_dir =
= src
py_modules =
microdot
microdot_asyncio
microdot_utemplate
microdot_jinja
microdot_session
microdot_websocket
microdot_websocket_alt
microdot_asyncio_websocket
microdot_test_client
microdot_asyncio_test_client
microdot_wsgi
microdot_asgi
microdot_asgi_websocket

View File

@@ -1,3 +0,0 @@
import setuptools
setuptools.setup()

View File

@@ -404,13 +404,15 @@ class Request():
data = MultiDict() data = MultiDict()
if len(urlencoded) > 0: if len(urlencoded) > 0:
if isinstance(urlencoded, str): if isinstance(urlencoded, str):
for k, v in [pair.split('=', 1) for kv in [pair.split('=', 1)
for pair in urlencoded.split('&') if pair]: for pair in urlencoded.split('&') if pair]:
data[urldecode_str(k)] = urldecode_str(v) data[urldecode_str(kv[0])] = urldecode_str(kv[1]) \
if len(kv) > 1 else ''
elif isinstance(urlencoded, bytes): # pragma: no branch elif isinstance(urlencoded, bytes): # pragma: no branch
for k, v in [pair.split(b'=', 1) for kv in [pair.split(b'=', 1)
for pair in urlencoded.split(b'&') if pair]: for pair in urlencoded.split(b'&') if pair]:
data[urldecode_bytes(k)] = urldecode_bytes(v) data[urldecode_bytes(kv[0])] = urldecode_bytes(kv[1]) \
if len(kv) > 1 else b''
return data return data
@property @property
@@ -1072,7 +1074,7 @@ class Microdot():
app = Microdot() app = Microdot()
@app.route('/') @app.route('/')
def index(): def index(request):
return 'Hello, world!' return 'Hello, world!'
app.run(debug=True) app.run(debug=True)

View File

@@ -59,8 +59,9 @@ class Microdot(BaseMicrodot):
headers = NoCaseDict() headers = NoCaseDict()
content_length = 0 content_length = 0
for key, value in scope.get('headers', []): for key, value in scope.get('headers', []):
headers[key] = value key = key.decode().title()
if key.lower() == 'content-length': headers[key] = value.decode()
if key == 'Content-Length':
content_length = int(value) content_length = int(value)
if content_length and content_length <= Request.max_body_length: if content_length and content_length <= Request.max_body_length:
@@ -119,17 +120,18 @@ class Microdot(BaseMicrodot):
asyncio.ensure_future(cancel_monitor()) asyncio.ensure_future(cancel_monitor())
body_iter = res.body_iter().__aiter__() body_iter = res.body_iter().__aiter__()
res_body = b''
try: try:
body = await body_iter.__anext__() res_body = await body_iter.__anext__()
while not cancelled: # pragma: no branch while not cancelled: # pragma: no branch
next_body = await body_iter.__anext__() next_body = await body_iter.__anext__()
await send({'type': 'http.response.body', await send({'type': 'http.response.body',
'body': body, 'body': res_body,
'more_body': True}) 'more_body': True})
body = next_body res_body = next_body
except StopAsyncIteration: except StopAsyncIteration:
await send({'type': 'http.response.body', await send({'type': 'http.response.body',
'body': body, 'body': res_body,
'more_body': False}) 'more_body': False})
async def __call__(self, scope, receive, send): async def __call__(self, scope, receive, send):

View File

@@ -241,7 +241,7 @@ class Microdot(BaseMicrodot):
app = Microdot() app = Microdot()
@app.route('/') @app.route('/')
async def index(): async def index(request):
return 'Hello, world!' return 'Hello, world!'
async def main(): async def main():
@@ -280,6 +280,11 @@ class Microdot(BaseMicrodot):
while True: while True:
try: try:
if hasattr(self.server, 'serve_forever'): # pragma: no cover
try:
await self.server.serve_forever()
except asyncio.CancelledError:
pass
await self.server.wait_closed() await self.server.wait_closed()
break break
except AttributeError: # pragma: no cover except AttributeError: # pragma: no cover
@@ -313,7 +318,7 @@ class Microdot(BaseMicrodot):
app = Microdot() app = Microdot()
@app.route('/') @app.route('/')
async def index(): async def index(request):
return 'Hello, world!' return 'Hello, world!'
app.run(debug=True) app.run(debug=True)

View File

@@ -53,9 +53,9 @@ class TestMicrodotASGI(unittest.TestCase):
'type': 'http', 'type': 'http',
'path': '/foo/bar', 'path': '/foo/bar',
'query_string': b'baz=1', 'query_string': b'baz=1',
'headers': [('Authorization', 'Bearer 123'), 'headers': [(b'Authorization', b'Bearer 123'),
('Cookie', 'session=xyz'), (b'Cookie', b'session=xyz'),
('Content-Length', 4)], (b'Content-Length', b'4')],
'client': ['1.2.3.4', 1234], 'client': ['1.2.3.4', 1234],
'method': 'POST', 'method': 'POST',
'http_version': '1.1', 'http_version': '1.1',
@@ -114,9 +114,9 @@ class TestMicrodotASGI(unittest.TestCase):
scope = { scope = {
'type': 'http', 'type': 'http',
'path': '/foo/bar', 'path': '/foo/bar',
'headers': [('Authorization', 'Bearer 123'), 'headers': [(b'Authorization', b'Bearer 123'),
('Cookie', 'session=xyz'), (b'Cookie', b'session=xyz'),
('Content-Length', 4)], (b'Content-Length', b'4')],
'client': ['1.2.3.4', 1234], 'client': ['1.2.3.4', 1234],
'method': 'POST', 'method': 'POST',
'http_version': '1.1', 'http_version': '1.1',

View File

@@ -39,11 +39,12 @@ class TestRequest(unittest.TestCase):
self.assertEqual(req.body, b'aaa') self.assertEqual(req.body, b'aaa')
def test_args(self): def test_args(self):
fd = get_request_fd('GET', '/?foo=bar&abc=def&x=%2f%%') fd = get_request_fd('GET', '/?foo=bar&abc=def&foo&x=%2f%%')
req = Request.create('app', fd, 'addr') req = Request.create('app', fd, 'addr')
self.assertEqual(req.query_string, 'foo=bar&abc=def&x=%2f%%') self.assertEqual(req.query_string, 'foo=bar&abc=def&foo&x=%2f%%')
self.assertEqual(req.args, MultiDict( md = MultiDict({'foo': 'bar', 'abc': 'def', 'x': '/%%'})
{'foo': 'bar', 'abc': 'def', 'x': '/%%'})) md['foo'] = ''
self.assertEqual(req.args, md)
def test_badly_formatted_args(self): def test_badly_formatted_args(self):
fd = get_request_fd('GET', '/?&foo=bar&abc=def&&&x=%2f%%') fd = get_request_fd('GET', '/?&foo=bar&abc=def&&&x=%2f%%')

View File

@@ -49,11 +49,12 @@ class TestRequestAsync(unittest.TestCase):
self.assertEqual(req.body, b'aaa') self.assertEqual(req.body, b'aaa')
def test_args(self): def test_args(self):
fd = get_async_request_fd('GET', '/?foo=bar&abc=def&x=%2f%%') fd = get_async_request_fd('GET', '/?foo=bar&abc=def&foo&x=%2f%%')
req = _run(Request.create('app', fd, 'writer', 'addr')) req = _run(Request.create('app', fd, 'writer', 'addr'))
self.assertEqual(req.query_string, 'foo=bar&abc=def&x=%2f%%') self.assertEqual(req.query_string, 'foo=bar&abc=def&foo&x=%2f%%')
self.assertEqual(req.args, MultiDict( md = MultiDict({'foo': 'bar', 'abc': 'def', 'x': '/%%'})
{'foo': 'bar', 'abc': 'def', 'x': '/%%'})) md['foo'] = ''
self.assertEqual(req.args, md)
def test_json(self): def test_json(self):
fd = get_async_request_fd('GET', '/foo', headers={ fd = get_async_request_fd('GET', '/foo', headers={