8 Commits

Author SHA1 Message Date
Miguel Grinberg
b810346aa4 Release v0.3.1 2021-02-06 12:12:08 +00:00
Miguel Grinberg
ae5d330b2d fix release script 2021-02-06 12:10:37 +00:00
Miguel Grinberg
4c0afa2bec switch to GitHub actions for builds 2021-02-06 12:01:11 +00:00
Ricardo Mendonça Ferreira
125af4b4a9 Handle Chrome preconnect (Fixes #8) 2021-02-06 00:04:21 +00:00
Damien George
c5e1873523 Move socket import, remove Request.G, and add simple hello example (#12)
* Further guard import of socket to make it optional

This is so that systems without a (u)socket module can still use Microdot.
For example if the transport layer is provided by a serial link.

* Add simple hello.py example that serves a static HTML page
2020-06-30 23:23:17 +01:00
Miguel Grinberg
dfbe2edd79 Update python versions to build 2020-02-19 00:08:07 +00:00
Miguel Grinberg
3e29af5775 Support large downloads in send_file (fixes #3) 2020-02-19 00:08:07 +00:00
Miguel Grinberg
1aacb3cf46 readme update 2019-06-09 17:47:20 +01:00
13 changed files with 223 additions and 124 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
tests/files/* binary

3
.github/FUNDING.yml vendored Normal file
View File

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

53
.github/workflows/tests.yml vendored Normal file
View 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

View File

@@ -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

View File

@@ -1,4 +1,8 @@
# microdot
[![Build Status](https://travis-ci.org/miguelgrinberg/microdot.svg?branch=master)](https://travis-ci.org/miguelgrinberg/microdot)
[![Build status](https://github.com/miguelgrinberg/microdot/workflows/build/badge.svg)](https://github.com/miguelgrinberg/microdot/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/microdot/branch/master/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/microdot)
A minimalistic Python web framework for microcontrollers inspired by Flask
## Documentation
Coming soon!

View File

@@ -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
View 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)

View File

@@ -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))

View File

@@ -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',

View File

@@ -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))

View File

@@ -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',

View File

@@ -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
View File

@@ -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=