Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b810346aa4 | ||
|
|
ae5d330b2d | ||
|
|
4c0afa2bec | ||
|
|
125af4b4a9 | ||
|
|
c5e1873523 | ||
|
|
dfbe2edd79 | ||
|
|
3e29af5775 | ||
|
|
1aacb3cf46 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tests/files/* binary
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
github: miguelgrinberg
|
||||
patreon: miguelgrinberg
|
||||
custom: https://paypal.me/miguelgrinberg
|
||||
53
.github/workflows/tests.yml
vendored
Normal file
53
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- run: python -m pip install --upgrade pip wheel
|
||||
- run: pip install tox tox-gh-actions
|
||||
- run: tox -eflake8
|
||||
tests:
|
||||
name: tests
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python: ['3.6', '3.7', '3.8', '3.9']
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- run: python -m pip install --upgrade pip wheel
|
||||
- run: pip install tox tox-gh-actions
|
||||
- run: tox
|
||||
tests-micropython:
|
||||
name: tests-micropython
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- run: python -m pip install --upgrade pip wheel
|
||||
- run: pip install tox tox-gh-actions
|
||||
- run: tox -eupy
|
||||
coverage:
|
||||
name: coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- run: python -m pip install --upgrade pip wheel
|
||||
- run: pip install tox tox-gh-actions codecov
|
||||
- run: tox
|
||||
- run: codecov
|
||||
18
.travis.yml
18
.travis.yml
@@ -1,18 +0,0 @@
|
||||
dist: xenial
|
||||
language: python
|
||||
matrix:
|
||||
include:
|
||||
- python: 3.7
|
||||
env: TOXENV=flake8
|
||||
- python: 3.5
|
||||
env: TOXENV=py35
|
||||
- python: 3.6
|
||||
env: TOXENV=py36
|
||||
- python: 3.7
|
||||
env: TOXENV=py37
|
||||
- python: 3.7
|
||||
env: TOXENV=upy
|
||||
install:
|
||||
- pip install tox
|
||||
script:
|
||||
- tox
|
||||
@@ -1,4 +1,8 @@
|
||||
# microdot
|
||||
[](https://travis-ci.org/miguelgrinberg/microdot)
|
||||
[](https://github.com/miguelgrinberg/microdot/actions) [](https://codecov.io/gh/miguelgrinberg/microdot)
|
||||
|
||||
A minimalistic Python web framework for microcontrollers inspired by Flask
|
||||
|
||||
## Documentation
|
||||
|
||||
Coming soon!
|
||||
|
||||
@@ -15,7 +15,7 @@ set -e
|
||||
for PKG in microdot*; do
|
||||
echo Building $PKG...
|
||||
cd $PKG
|
||||
sed -i "s/version.*$/version=\"$VERSION\",/" setup.py
|
||||
sed -i "" "s/version.*$/version=\"$VERSION\",/" setup.py
|
||||
git add setup.py
|
||||
rm -rf dist
|
||||
python setup.py sdist bdist_wheel --universal
|
||||
|
||||
26
examples/hello.py
Normal file
26
examples/hello.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from microdot import Microdot, Response
|
||||
|
||||
app = Microdot()
|
||||
|
||||
htmldoc = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Microdot Example Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1>Microdot Example Page</h1>
|
||||
<p>Hello from Microdot!</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
@app.route("", methods=["GET", "POST"])
|
||||
def serial_number(request):
|
||||
print(request.headers)
|
||||
return Response(body=htmldoc, headers={"Content-Type": "text/html"})
|
||||
|
||||
|
||||
app.run(debug=True)
|
||||
@@ -17,6 +17,8 @@ class Request(BaseRequest):
|
||||
async def create(stream, client_addr):
|
||||
# request line
|
||||
line = (await stream.readline()).strip().decode()
|
||||
if not line: # pragma: no cover
|
||||
return None
|
||||
method, url, http_version = line.split()
|
||||
http_version = http_version.split('/', 1)[1]
|
||||
|
||||
@@ -59,7 +61,17 @@ class Response(BaseResponse):
|
||||
|
||||
# body
|
||||
if self.body:
|
||||
await stream.awrite(self.body)
|
||||
if hasattr(self.body, 'read'):
|
||||
while True:
|
||||
buf = self.body.read(self.send_file_buffer_size)
|
||||
if len(buf):
|
||||
await stream.awrite(buf)
|
||||
if len(buf) < self.send_file_buffer_size:
|
||||
break
|
||||
if hasattr(self.body, 'close'):
|
||||
self.body.close()
|
||||
else:
|
||||
await stream.awrite(self.body)
|
||||
|
||||
|
||||
class Microdot(BaseMicrodot):
|
||||
@@ -93,48 +105,52 @@ class Microdot(BaseMicrodot):
|
||||
|
||||
async def dispatch_request(self, reader, writer):
|
||||
req = await Request.create(reader, writer.get_extra_info('peername'))
|
||||
f = self.find_route(req)
|
||||
try:
|
||||
res = None
|
||||
if f:
|
||||
for handler in self.before_request_handlers:
|
||||
res = await self._invoke_handler(handler, req)
|
||||
if res:
|
||||
break
|
||||
if res is None:
|
||||
res = await self._invoke_handler(f, req, **req.url_args)
|
||||
if isinstance(res, tuple):
|
||||
res = Response(*res)
|
||||
elif not isinstance(res, Response):
|
||||
res = Response(res)
|
||||
for handler in self.after_request_handlers:
|
||||
res = await self._invoke_handler(handler, req, res) or res
|
||||
elif 404 in self.error_handlers:
|
||||
res = await self._invoke_handler(self.error_handlers[404], req)
|
||||
else:
|
||||
res = 'Not found', 404
|
||||
except Exception as exc:
|
||||
print_exception(exc)
|
||||
res = None
|
||||
if exc.__class__ in self.error_handlers:
|
||||
try:
|
||||
if req:
|
||||
f = self.find_route(req)
|
||||
try:
|
||||
res = None
|
||||
if f:
|
||||
for handler in self.before_request_handlers:
|
||||
res = await self._invoke_handler(handler, req)
|
||||
if res:
|
||||
break
|
||||
if res is None:
|
||||
res = await self._invoke_handler(
|
||||
f, req, **req.url_args)
|
||||
if isinstance(res, tuple):
|
||||
res = Response(*res)
|
||||
elif not isinstance(res, Response):
|
||||
res = Response(res)
|
||||
for handler in self.after_request_handlers:
|
||||
res = await self._invoke_handler(
|
||||
handler, req, res) or res
|
||||
elif 404 in self.error_handlers:
|
||||
res = await self._invoke_handler(
|
||||
self.error_handlers[exc.__class__], req, exc)
|
||||
except Exception as exc2: # pragma: no cover
|
||||
print_exception(exc2)
|
||||
if res is None:
|
||||
if 500 in self.error_handlers:
|
||||
res = await self._invoke_handler(self.error_handlers[500],
|
||||
req)
|
||||
self.error_handlers[404], req)
|
||||
else:
|
||||
res = 'Internal server error', 500
|
||||
if isinstance(res, tuple):
|
||||
res = Response(*res)
|
||||
elif not isinstance(res, Response):
|
||||
res = Response(res)
|
||||
await res.write(writer)
|
||||
res = 'Not found', 404
|
||||
except Exception as exc:
|
||||
print_exception(exc)
|
||||
res = None
|
||||
if exc.__class__ in self.error_handlers:
|
||||
try:
|
||||
res = await self._invoke_handler(
|
||||
self.error_handlers[exc.__class__], req, exc)
|
||||
except Exception as exc2: # pragma: no cover
|
||||
print_exception(exc2)
|
||||
if res is None:
|
||||
if 500 in self.error_handlers:
|
||||
res = await self._invoke_handler(
|
||||
self.error_handlers[500], req)
|
||||
else:
|
||||
res = 'Internal server error', 500
|
||||
if isinstance(res, tuple):
|
||||
res = Response(*res)
|
||||
elif not isinstance(res, Response):
|
||||
res = Response(res)
|
||||
await res.write(writer)
|
||||
await writer.aclose()
|
||||
if self.debug: # pragma: no cover
|
||||
if self.debug and req: # pragma: no cover
|
||||
print('{method} {path} {status_code}'.format(
|
||||
method=req.method, path=req.path,
|
||||
status_code=res.status_code))
|
||||
|
||||
@@ -8,7 +8,7 @@ from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='microdot-asyncio',
|
||||
version="0.3.0",
|
||||
version="0.3.1",
|
||||
url='http://github.com/miguelgrinberg/microdot/',
|
||||
license='MIT',
|
||||
author='Miguel Grinberg',
|
||||
|
||||
@@ -43,7 +43,10 @@ except ImportError:
|
||||
try:
|
||||
import usocket as socket
|
||||
except ImportError:
|
||||
import socket
|
||||
try:
|
||||
import socket
|
||||
except ImportError: # pragma: no cover
|
||||
socket = None
|
||||
|
||||
|
||||
def urldecode(string):
|
||||
@@ -99,6 +102,8 @@ class Request():
|
||||
def create(client_stream, client_addr):
|
||||
# request line
|
||||
line = client_stream.readline().strip().decode()
|
||||
if not line: # pragma: no cover
|
||||
return None
|
||||
method, url, http_version = line.split()
|
||||
http_version = http_version.split('/', 1)[1]
|
||||
|
||||
@@ -154,6 +159,7 @@ class Response():
|
||||
'png': 'image/png',
|
||||
'txt': 'text/plain',
|
||||
}
|
||||
send_file_buffer_size = 1024
|
||||
|
||||
def __init__(self, body='', status_code=200, headers=None):
|
||||
self.status_code = status_code
|
||||
@@ -163,10 +169,9 @@ class Response():
|
||||
self.headers['Content-Type'] = 'application/json'
|
||||
elif isinstance(body, str):
|
||||
self.body = body.encode()
|
||||
elif isinstance(body, bytes):
|
||||
self.body = body
|
||||
else:
|
||||
self.body = str(body).encode()
|
||||
# this applies to bytes or file-like objects
|
||||
self.body = body
|
||||
|
||||
def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
|
||||
max_age=None, secure=False, http_only=False):
|
||||
@@ -190,7 +195,8 @@ class Response():
|
||||
self.headers['Set-Cookie'] = [http_cookie]
|
||||
|
||||
def complete(self):
|
||||
if 'Content-Length' not in self.headers:
|
||||
if isinstance(self.body, bytes) and \
|
||||
'Content-Length' not in self.headers:
|
||||
self.headers['Content-Length'] = str(len(self.body))
|
||||
if 'Content-Type' not in self.headers:
|
||||
self.headers['Content-Type'] = 'text/plain'
|
||||
@@ -213,7 +219,17 @@ class Response():
|
||||
|
||||
# body
|
||||
if self.body:
|
||||
stream.write(self.body)
|
||||
if hasattr(self.body, 'read'):
|
||||
while True:
|
||||
buf = self.body.read(self.send_file_buffer_size)
|
||||
if len(buf):
|
||||
stream.write(buf)
|
||||
if len(buf) < self.send_file_buffer_size:
|
||||
break
|
||||
if hasattr(self.body, 'close'):
|
||||
self.body.close()
|
||||
else:
|
||||
stream.write(self.body)
|
||||
|
||||
@classmethod
|
||||
def redirect(cls, location, status_code=302):
|
||||
@@ -227,11 +243,9 @@ class Response():
|
||||
content_type = Response.types_map[ext]
|
||||
else:
|
||||
content_type = 'application/octet-stream'
|
||||
with open(filename) as f:
|
||||
body = f.read()
|
||||
return cls(body=body, status_code=status_code,
|
||||
headers={'Content-Type': content_type,
|
||||
'Content-Length': str(len(body))})
|
||||
f = open(filename, 'rb')
|
||||
return cls(body=f, status_code=status_code,
|
||||
headers={'Content-Type': content_type})
|
||||
|
||||
|
||||
class URLPattern():
|
||||
@@ -350,48 +364,49 @@ class Microdot():
|
||||
stream = sock
|
||||
|
||||
req = Request.create(stream, addr)
|
||||
f = self.find_route(req)
|
||||
try:
|
||||
res = None
|
||||
if f:
|
||||
for handler in self.before_request_handlers:
|
||||
res = handler(req)
|
||||
if res:
|
||||
break
|
||||
if res is None:
|
||||
res = f(req, **req.url_args)
|
||||
if isinstance(res, tuple):
|
||||
res = Response(*res)
|
||||
elif not isinstance(res, Response):
|
||||
res = Response(res)
|
||||
for handler in self.after_request_handlers:
|
||||
res = handler(req, res) or res
|
||||
elif 404 in self.error_handlers:
|
||||
res = self.error_handlers[404](req)
|
||||
else:
|
||||
res = 'Not found', 404
|
||||
except Exception as exc:
|
||||
print_exception(exc)
|
||||
res = None
|
||||
if exc.__class__ in self.error_handlers:
|
||||
try:
|
||||
res = self.error_handlers[exc.__class__](req, exc)
|
||||
except Exception as exc2: # pragma: no cover
|
||||
print_exception(exc2)
|
||||
if res is None:
|
||||
if 500 in self.error_handlers:
|
||||
res = self.error_handlers[500](req)
|
||||
if req:
|
||||
f = self.find_route(req)
|
||||
try:
|
||||
res = None
|
||||
if f:
|
||||
for handler in self.before_request_handlers:
|
||||
res = handler(req)
|
||||
if res:
|
||||
break
|
||||
if res is None:
|
||||
res = f(req, **req.url_args)
|
||||
if isinstance(res, tuple):
|
||||
res = Response(*res)
|
||||
elif not isinstance(res, Response):
|
||||
res = Response(res)
|
||||
for handler in self.after_request_handlers:
|
||||
res = handler(req, res) or res
|
||||
elif 404 in self.error_handlers:
|
||||
res = self.error_handlers[404](req)
|
||||
else:
|
||||
res = 'Internal server error', 500
|
||||
if isinstance(res, tuple):
|
||||
res = Response(*res)
|
||||
elif not isinstance(res, Response):
|
||||
res = Response(res)
|
||||
res.write(stream)
|
||||
res = 'Not found', 404
|
||||
except Exception as exc:
|
||||
print_exception(exc)
|
||||
res = None
|
||||
if exc.__class__ in self.error_handlers:
|
||||
try:
|
||||
res = self.error_handlers[exc.__class__](req, exc)
|
||||
except Exception as exc2: # pragma: no cover
|
||||
print_exception(exc2)
|
||||
if res is None:
|
||||
if 500 in self.error_handlers:
|
||||
res = self.error_handlers[500](req)
|
||||
else:
|
||||
res = 'Internal server error', 500
|
||||
if isinstance(res, tuple):
|
||||
res = Response(*res)
|
||||
elif not isinstance(res, Response):
|
||||
res = Response(res)
|
||||
res.write(stream)
|
||||
stream.close()
|
||||
if stream != sock: # pragma: no cover
|
||||
sock.close()
|
||||
if self.debug: # pragma: no cover
|
||||
if self.debug and req: # pragma: no cover
|
||||
print('{method} {path} {status_code}'.format(
|
||||
method=req.method, path=req.path,
|
||||
status_code=res.status_code))
|
||||
|
||||
@@ -8,7 +8,7 @@ from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='microdot',
|
||||
version="0.3.0",
|
||||
version="0.3.1",
|
||||
url='http://github.com/miguelgrinberg/microdot/',
|
||||
license='MIT',
|
||||
author='Miguel Grinberg',
|
||||
|
||||
@@ -92,7 +92,7 @@ class TestResponse(unittest.TestCase):
|
||||
res = Response(123)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.headers, {})
|
||||
self.assertEqual(res.body, b'123')
|
||||
self.assertEqual(res.body, 123)
|
||||
|
||||
def test_create_with_status_code(self):
|
||||
res = Response('not found', 404)
|
||||
@@ -161,11 +161,9 @@ class TestResponse(unittest.TestCase):
|
||||
res = Response.send_file('tests/files/' + file)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.headers['Content-Type'], content_type)
|
||||
self.assertEqual(res.headers['Content-Length'], '4')
|
||||
self.assertEqual(res.body, b'foo\n')
|
||||
self.assertEqual(res.body.read(), b'foo\n')
|
||||
res = Response.send_file('tests/files/test.txt',
|
||||
content_type='text/html')
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.headers['Content-Type'], 'text/html')
|
||||
self.assertEqual(res.headers['Content-Length'], '4')
|
||||
self.assertEqual(res.body, b'foo\n')
|
||||
self.assertEqual(res.body.read(), b'foo\n')
|
||||
|
||||
17
tox.ini
17
tox.ini
@@ -1,22 +1,23 @@
|
||||
[tox]
|
||||
envlist=flake8,py35,py36,py37,upy
|
||||
envlist=flake8,py36,py37,py38,py39,upy
|
||||
skipsdist=True
|
||||
skip_missing_interpreters=True
|
||||
|
||||
[gh-actions]
|
||||
python =
|
||||
3.6: py36
|
||||
3.7: py37
|
||||
3.8: py38
|
||||
3.9: py39
|
||||
pypy3: pypy3
|
||||
|
||||
[testenv]
|
||||
commands=
|
||||
pip install -e microdot
|
||||
pip install -e microdot-asyncio
|
||||
coverage run --branch --include="microdot*.py" -m unittest tests
|
||||
coverage report --show-missing
|
||||
coverage erase
|
||||
deps=coverage
|
||||
basepython=
|
||||
flake8: python3.7
|
||||
py35: python3.5
|
||||
py36: python3.6
|
||||
py37: python3.7
|
||||
upy: python3.7
|
||||
|
||||
[testenv:flake8]
|
||||
deps=
|
||||
|
||||
Reference in New Issue
Block a user