Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5061145f5c | ||
|
|
122c638bae | ||
|
|
bd74bcab74 | ||
|
|
5cd3ace516 | ||
|
|
da32f23e35 | ||
|
|
0641466faa | ||
|
|
dd3fc20507 | ||
|
|
46963ba464 | ||
|
|
1a8db51cb3 | ||
|
|
d903c42370 | ||
|
|
8b4ebbd953 | ||
|
|
a82ed55f56 | ||
|
|
ac87f0542f |
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -2,10 +2,10 @@ name: build
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: lint
|
name: lint
|
||||||
|
|||||||
12
CHANGES.md
12
CHANGES.md
@@ -1,5 +1,17 @@
|
|||||||
# Microdot change log
|
# Microdot change log
|
||||||
|
|
||||||
|
**Release 0.6.0** - 2021-08-11
|
||||||
|
|
||||||
|
- Better handling of content types in form and json methods [#24](https://github.com/miguelgrinberg/microdot/issues/24) ([commit](https://github.com/miguelgrinberg/microdot/commit/da32f23e35f871470a40638e7000e84b0ff6d17f))
|
||||||
|
- Accept a custom reason phrase for the HTTP response [#25](https://github.com/miguelgrinberg/microdot/issues/25) ([commit](https://github.com/miguelgrinberg/microdot/commit/bd74bcab74f283c89aadffc8f9c20d6ff0f771ce))
|
||||||
|
- Make mime type check for form submissions more robust ([commit](https://github.com/miguelgrinberg/microdot/commit/dd3fc20507715a23d0fa6fa3aae3715c8fbc0351))
|
||||||
|
- Copy client headers to avoid write back [#23](https://github.com/miguelgrinberg/microdot/issues/23) ([commit](https://github.com/miguelgrinberg/microdot/commit/0641466faa9dda0c54f78939ac05993c0812e84a)) (thanks **Mark Blakeney**!)
|
||||||
|
- Work around a bug in uasyncio's create_server() function ([commit](https://github.com/miguelgrinberg/microdot/commit/46963ba4644d7abc8dc653c99bc76222af526964))
|
||||||
|
- More unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/5cd3ace5166ec549579b0b1149ae3d7be195974a))
|
||||||
|
- Installation instructions ([commit](https://github.com/miguelgrinberg/microdot/commit/1a8db51cb3754308da6dcc227512dcdeb4ce4557))
|
||||||
|
- Run tests with pytest ([commit](https://github.com/miguelgrinberg/microdot/commit/8b4ebbd9535b3c083fb2a955284609acba07f05e))
|
||||||
|
- Deprecated the microdot-asyncio package ([commit](https://github.com/miguelgrinberg/microdot/commit/a82ed55f56e14fbcea93e8171af86ab42657fa96))
|
||||||
|
|
||||||
**Release 0.5.0** - 2021-06-06
|
**Release 0.5.0** - 2021-06-06
|
||||||
|
|
||||||
- [Documentation](https://microdot.readthedocs.io/en/latest/) site ([commit](https://github.com/miguelgrinberg/microdot/commit/12cd60305b7b48ab151da52661fc5988684dbcd8))
|
- [Documentation](https://microdot.readthedocs.io/en/latest/) site ([commit](https://github.com/miguelgrinberg/microdot/commit/12cd60305b7b48ab151da52661fc5988684dbcd8))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# microdot
|
# microdot
|
||||||
[](https://github.com/miguelgrinberg/microdot/actions) [](https://codecov.io/gh/miguelgrinberg/microdot)
|
[](https://github.com/miguelgrinberg/microdot/actions) [](https://codecov.io/gh/miguelgrinberg/microdot)
|
||||||
|
|
||||||
A minimalistic Python web framework for microcontrollers inspired by Flask
|
A minimalistic Python web framework for microcontrollers inspired by Flask
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,7 @@
|
|||||||
#
|
#
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.path.abspath('../microdot'))
|
sys.path.insert(0, os.path.abspath('../src'))
|
||||||
sys.path.insert(1, os.path.abspath('../microdot-asyncio'))
|
|
||||||
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
Microdot
|
Microdot
|
||||||
========
|
========
|
||||||
|
|
||||||
Microdot is a minimalistic Python web framework for microcontrollers inspired
|
Microdot is a minimalistic Python web framework inspired by
|
||||||
by `Flask <https://flask.palletsprojects.com/>`_, and designed to run on
|
`Flask <https://flask.palletsprojects.com/>`_, and designed to run on
|
||||||
systems with limited resources such as microcontrollers. It runs on standard
|
systems with limited resources such as microcontrollers. It runs on standard
|
||||||
Python and on `MicroPython <https://micropython.org>`_.
|
Python and on `MicroPython <https://micropython.org>`_.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Microdot can be installed with ``pip``::
|
||||||
|
|
||||||
|
pip install microdot
|
||||||
|
|
||||||
|
For platforms that do not support or cannot run ``pip``, you can also manually
|
||||||
|
copy and install the ``microdot.py`` and ``microdot_asyncio.py`` source files.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = microdot-asyncio
|
name = microdot-asyncio
|
||||||
version = 0.4.0
|
version = 0.5.0
|
||||||
author = Miguel Grinberg
|
author = Miguel Grinberg
|
||||||
author_email = miguel.grinberg@gmail.com
|
author_email = miguel.grinberg@gmail.com
|
||||||
description = AsyncIO support for the Microdot web framework'
|
description = AsyncIO support for the Microdot web framework'
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, 'microdot')
|
sys.path.insert(0, 'src')
|
||||||
sys.path.insert(1, 'microdot-asyncio')
|
|
||||||
sys.path.insert(2, 'tests/libs')
|
sys.path.insert(2, 'tests/libs')
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = microdot
|
name = microdot
|
||||||
version = 0.5.0
|
version = 0.6.0
|
||||||
author = Miguel Grinberg
|
author = Miguel Grinberg
|
||||||
author_email = miguel.grinberg@gmail.com
|
author_email = miguel.grinberg@gmail.com
|
||||||
description = The impossibly small web framework for MicroPython
|
description = The impossibly small web framework for MicroPython
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ class Request():
|
|||||||
"""
|
"""
|
||||||
# request line
|
# request line
|
||||||
line = client_stream.readline().strip().decode()
|
line = client_stream.readline().strip().decode()
|
||||||
if not line: # pragma: no cover
|
if not line:
|
||||||
return None
|
return None
|
||||||
method, url, http_version = line.split()
|
method, url, http_version = line.split()
|
||||||
http_version = http_version.split('/', 1)[1]
|
http_version = http_version.split('/', 1)[1]
|
||||||
@@ -267,17 +267,23 @@ class Request():
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
if self.content_type != 'application/json':
|
|
||||||
return None
|
|
||||||
if self._json is None:
|
if self._json is None:
|
||||||
|
if self.content_type is None:
|
||||||
|
return None
|
||||||
|
mime_type = self.content_type.split(';')[0]
|
||||||
|
if mime_type != 'application/json':
|
||||||
|
return None
|
||||||
self._json = json.loads(self.body.decode())
|
self._json = json.loads(self.body.decode())
|
||||||
return self._json
|
return self._json
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self):
|
def form(self):
|
||||||
if self.content_type != 'application/x-www-form-urlencoded':
|
|
||||||
return None
|
|
||||||
if self._form is None:
|
if self._form is None:
|
||||||
|
if self.content_type is None:
|
||||||
|
return None
|
||||||
|
mime_type = self.content_type.split(';')[0]
|
||||||
|
if mime_type != 'application/x-www-form-urlencoded':
|
||||||
|
return None
|
||||||
self._form = self._parse_urlencoded(self.body.decode())
|
self._form = self._parse_urlencoded(self.body.decode())
|
||||||
return self._form
|
return self._form
|
||||||
|
|
||||||
@@ -303,9 +309,10 @@ class Response():
|
|||||||
}
|
}
|
||||||
send_file_buffer_size = 1024
|
send_file_buffer_size = 1024
|
||||||
|
|
||||||
def __init__(self, body='', status_code=200, headers=None):
|
def __init__(self, body='', status_code=200, headers=None, reason=None):
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
self.headers = headers or {}
|
self.headers = headers.copy() if headers else {}
|
||||||
|
self.reason = reason
|
||||||
if isinstance(body, (dict, list)):
|
if isinstance(body, (dict, list)):
|
||||||
self.body = json.dumps(body).encode()
|
self.body = json.dumps(body).encode()
|
||||||
self.headers['Content-Type'] = 'application/json'
|
self.headers['Content-Type'] = 'application/json'
|
||||||
@@ -358,9 +365,10 @@ class Response():
|
|||||||
self.complete()
|
self.complete()
|
||||||
|
|
||||||
# status code
|
# status code
|
||||||
|
reason = self.reason if self.reason is not None else \
|
||||||
|
('OK' if self.status_code == 200 else 'N/A')
|
||||||
stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format(
|
stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format(
|
||||||
status_code=self.status_code,
|
status_code=self.status_code, reason=reason).encode())
|
||||||
reason='OK' if self.status_code == 200 else 'N/A').encode())
|
|
||||||
|
|
||||||
# headers
|
# headers
|
||||||
for header, value in self.headers.items():
|
for header, value in self.headers.items():
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ class Response(BaseResponse):
|
|||||||
self.complete()
|
self.complete()
|
||||||
|
|
||||||
# status code
|
# status code
|
||||||
|
reason = self.reason if self.reason is not None else \
|
||||||
|
('OK' if self.status_code == 200 else 'N/A')
|
||||||
await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
|
await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
|
||||||
status_code=self.status_code,
|
status_code=self.status_code, reason=reason).encode())
|
||||||
reason='OK' if self.status_code == 200 else 'N/A').encode())
|
|
||||||
|
|
||||||
# headers
|
# headers
|
||||||
for header, value in self.headers.items():
|
for header, value in self.headers.items():
|
||||||
@@ -95,7 +96,7 @@ class Response(BaseResponse):
|
|||||||
await stream.awrite(buf)
|
await stream.awrite(buf)
|
||||||
if len(buf) < self.send_file_buffer_size:
|
if len(buf) < self.send_file_buffer_size:
|
||||||
break
|
break
|
||||||
if hasattr(self.body, 'close'):
|
if hasattr(self.body, 'close'): # pragma: no cover
|
||||||
self.body.close()
|
self.body.close()
|
||||||
else:
|
else:
|
||||||
await stream.awrite(self.body)
|
await stream.awrite(self.body)
|
||||||
@@ -162,7 +163,14 @@ class Microdot(BaseMicrodot):
|
|||||||
host=host, port=port))
|
host=host, port=port))
|
||||||
|
|
||||||
self.server = await asyncio.start_server(serve, host, port)
|
self.server = await asyncio.start_server(serve, host, port)
|
||||||
await self.server.wait_closed()
|
while True:
|
||||||
|
try:
|
||||||
|
await self.server.wait_closed()
|
||||||
|
break
|
||||||
|
except AttributeError: # pragma: no cover
|
||||||
|
# the task hasn't been initialized in the server object yet
|
||||||
|
# wait a bit and try again
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
def run(self, host='0.0.0.0', port=5000, debug=False):
|
def run(self, host='0.0.0.0', port=5000, debug=False):
|
||||||
"""Start the web server. This function does not normally return, as
|
"""Start the web server. This function does not normally return, as
|
||||||
|
|||||||
@@ -65,6 +65,16 @@ class TestMicrodot(unittest.TestCase):
|
|||||||
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||||
self.assertTrue(fd.response.endswith(b'\r\n\r\nbar'))
|
self.assertTrue(fd.response.endswith(b'\r\n\r\nbar'))
|
||||||
|
|
||||||
|
def test_empty_request(self):
|
||||||
|
app = Microdot()
|
||||||
|
|
||||||
|
mock_socket.clear_requests()
|
||||||
|
fd = mock_socket.FakeStream(b'\n')
|
||||||
|
mock_socket._requests.append(fd)
|
||||||
|
self._add_shutdown(app)
|
||||||
|
app.run()
|
||||||
|
assert fd.response == b''
|
||||||
|
|
||||||
def test_method_decorators(self):
|
def test_method_decorators(self):
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,28 @@ class TestResponse(unittest.TestCase):
|
|||||||
self.assertEqual(res.headers, {'X-Test': 'Foo'})
|
self.assertEqual(res.headers, {'X-Test': 'Foo'})
|
||||||
self.assertEqual(res.body, b'foo')
|
self.assertEqual(res.body, b'foo')
|
||||||
|
|
||||||
|
def test_create_with_reason(self):
|
||||||
|
res = Response('foo', reason='ALL GOOD!')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
self.assertEqual(res.headers, {})
|
||||||
|
self.assertEqual(res.reason, 'ALL GOOD!')
|
||||||
|
self.assertEqual(res.body, b'foo')
|
||||||
|
fd = io.BytesIO()
|
||||||
|
res.write(fd)
|
||||||
|
response = fd.getvalue()
|
||||||
|
self.assertIn(b'HTTP/1.0 200 ALL GOOD!\r\n', response)
|
||||||
|
|
||||||
|
def test_create_with_status_and_reason(self):
|
||||||
|
res = Response('not found', 404, reason='NOT FOUND')
|
||||||
|
self.assertEqual(res.status_code, 404)
|
||||||
|
self.assertEqual(res.headers, {})
|
||||||
|
self.assertEqual(res.reason, 'NOT FOUND')
|
||||||
|
self.assertEqual(res.body, b'not found')
|
||||||
|
fd = io.BytesIO()
|
||||||
|
res.write(fd)
|
||||||
|
response = fd.getvalue()
|
||||||
|
self.assertIn(b'HTTP/1.0 404 NOT FOUND\r\n', response)
|
||||||
|
|
||||||
def test_cookies(self):
|
def test_cookies(self):
|
||||||
res = Response('ok')
|
res = Response('ok')
|
||||||
res.set_cookie('foo1', 'bar1')
|
res.set_cookie('foo1', 'bar1')
|
||||||
|
|||||||
@@ -76,6 +76,16 @@ class TestMicrodotAsync(unittest.TestCase):
|
|||||||
self.assertIn(b'Content-Type: text/plain\r\n', fd2.response)
|
self.assertIn(b'Content-Type: text/plain\r\n', fd2.response)
|
||||||
self.assertTrue(fd2.response.endswith(b'\r\n\r\nbar-async'))
|
self.assertTrue(fd2.response.endswith(b'\r\n\r\nbar-async'))
|
||||||
|
|
||||||
|
def test_empty_request(self):
|
||||||
|
app = Microdot()
|
||||||
|
|
||||||
|
mock_socket.clear_requests()
|
||||||
|
fd = mock_socket.FakeStream(b'\n')
|
||||||
|
mock_socket._requests.append(fd)
|
||||||
|
self._add_shutdown(app)
|
||||||
|
app.run()
|
||||||
|
assert fd.response == b''
|
||||||
|
|
||||||
def test_before_after_request(self):
|
def test_before_after_request(self):
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,26 @@ class TestResponseAsync(unittest.TestCase):
|
|||||||
self.assertIn(b'Content-Type: application/json\r\n', fd.response)
|
self.assertIn(b'Content-Type: application/json\r\n', fd.response)
|
||||||
self.assertTrue(fd.response.endswith(b'\r\n\r\n[1, "2"]'))
|
self.assertTrue(fd.response.endswith(b'\r\n\r\n[1, "2"]'))
|
||||||
|
|
||||||
|
def test_create_with_reason(self):
|
||||||
|
res = Response('foo', reason='ALL GOOD!')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
self.assertEqual(res.headers, {})
|
||||||
|
self.assertEqual(res.reason, 'ALL GOOD!')
|
||||||
|
self.assertEqual(res.body, b'foo')
|
||||||
|
fd = FakeStreamAsync()
|
||||||
|
_run(res.write(fd))
|
||||||
|
self.assertIn(b'HTTP/1.0 200 ALL GOOD!\r\n', fd.response)
|
||||||
|
|
||||||
|
def test_create_with_status_and_reason(self):
|
||||||
|
res = Response('not found', 404, reason='NOT FOUND')
|
||||||
|
self.assertEqual(res.status_code, 404)
|
||||||
|
self.assertEqual(res.headers, {})
|
||||||
|
self.assertEqual(res.reason, 'NOT FOUND')
|
||||||
|
self.assertEqual(res.body, b'not found')
|
||||||
|
fd = FakeStreamAsync()
|
||||||
|
_run(res.write(fd))
|
||||||
|
self.assertIn(b'HTTP/1.0 404 NOT FOUND\r\n', fd.response)
|
||||||
|
|
||||||
def test_send_file(self):
|
def test_send_file(self):
|
||||||
res = Response.send_file('tests/files/test.txt',
|
res = Response.send_file('tests/files/test.txt',
|
||||||
content_type='text/html')
|
content_type='text/html')
|
||||||
|
|||||||
12
tox.ini
12
tox.ini
@@ -13,17 +13,17 @@ python =
|
|||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands=
|
commands=
|
||||||
pip install -e microdot
|
pip install -e .
|
||||||
pip install -e microdot-asyncio
|
pytest -p no:logging --cov=src --cov-branch --cov-report=term-missing
|
||||||
coverage run --branch --include="microdot*.py" -m unittest tests
|
deps=
|
||||||
coverage report --show-missing
|
pytest
|
||||||
deps=coverage
|
pytest-cov
|
||||||
|
|
||||||
[testenv:flake8]
|
[testenv:flake8]
|
||||||
deps=
|
deps=
|
||||||
flake8
|
flake8
|
||||||
commands=
|
commands=
|
||||||
flake8 --ignore=W503 --exclude tests/libs microdot microdot-asyncio tests
|
flake8 --ignore=W503 --exclude tests/libs src tests
|
||||||
|
|
||||||
[testenv:upy]
|
[testenv:upy]
|
||||||
whitelist_externals=sh
|
whitelist_externals=sh
|
||||||
|
|||||||
Reference in New Issue
Block a user