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 # 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 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 for PKG in microdot*; do
echo Building $PKG... echo Building $PKG...
cd $PKG cd $PKG
sed -i "s/version.*$/version=\"$VERSION\",/" setup.py sed -i "" "s/version.*$/version=\"$VERSION\",/" setup.py
git add setup.py git add setup.py
rm -rf dist rm -rf dist
python setup.py sdist bdist_wheel --universal 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): async def create(stream, client_addr):
# request line # request line
line = (await stream.readline()).strip().decode() line = (await stream.readline()).strip().decode()
if not line: # pragma: no cover
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]
@@ -59,7 +61,17 @@ class Response(BaseResponse):
# body # body
if self.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): class Microdot(BaseMicrodot):
@@ -93,48 +105,52 @@ class Microdot(BaseMicrodot):
async def dispatch_request(self, reader, writer): async def dispatch_request(self, reader, writer):
req = await Request.create(reader, writer.get_extra_info('peername')) req = await Request.create(reader, writer.get_extra_info('peername'))
f = self.find_route(req) if req:
try: f = self.find_route(req)
res = None try:
if f: res = None
for handler in self.before_request_handlers: if f:
res = await self._invoke_handler(handler, req) for handler in self.before_request_handlers:
if res: res = await self._invoke_handler(handler, req)
break if res:
if res is None: break
res = await self._invoke_handler(f, req, **req.url_args) if res is None:
if isinstance(res, tuple): res = await self._invoke_handler(
res = Response(*res) f, req, **req.url_args)
elif not isinstance(res, Response): if isinstance(res, tuple):
res = Response(res) res = Response(*res)
for handler in self.after_request_handlers: elif not isinstance(res, Response):
res = await self._invoke_handler(handler, req, res) or res res = Response(res)
elif 404 in self.error_handlers: for handler in self.after_request_handlers:
res = await self._invoke_handler(self.error_handlers[404], req) res = await self._invoke_handler(
else: handler, req, res) or res
res = 'Not found', 404 elif 404 in self.error_handlers:
except Exception as exc:
print_exception(exc)
res = None
if exc.__class__ in self.error_handlers:
try:
res = await self._invoke_handler( res = await self._invoke_handler(
self.error_handlers[exc.__class__], req, exc) self.error_handlers[404], req)
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: else:
res = 'Internal server error', 500 res = 'Not found', 404
if isinstance(res, tuple): except Exception as exc:
res = Response(*res) print_exception(exc)
elif not isinstance(res, Response): res = None
res = Response(res) if exc.__class__ in self.error_handlers:
await res.write(writer) 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() await writer.aclose()
if self.debug: # pragma: no cover if self.debug and req: # pragma: no cover
print('{method} {path} {status_code}'.format( print('{method} {path} {status_code}'.format(
method=req.method, path=req.path, method=req.method, path=req.path,
status_code=res.status_code)) status_code=res.status_code))

View File

@@ -8,7 +8,7 @@ from setuptools import setup
setup( setup(
name='microdot-asyncio', name='microdot-asyncio',
version="0.3.0", version="0.3.1",
url='http://github.com/miguelgrinberg/microdot/', url='http://github.com/miguelgrinberg/microdot/',
license='MIT', license='MIT',
author='Miguel Grinberg', author='Miguel Grinberg',

View File

@@ -43,7 +43,10 @@ except ImportError:
try: try:
import usocket as socket import usocket as socket
except ImportError: except ImportError:
import socket try:
import socket
except ImportError: # pragma: no cover
socket = None
def urldecode(string): def urldecode(string):
@@ -99,6 +102,8 @@ class Request():
def create(client_stream, client_addr): def create(client_stream, client_addr):
# request line # request line
line = client_stream.readline().strip().decode() line = client_stream.readline().strip().decode()
if not line: # pragma: no cover
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]
@@ -154,6 +159,7 @@ class Response():
'png': 'image/png', 'png': 'image/png',
'txt': 'text/plain', 'txt': 'text/plain',
} }
send_file_buffer_size = 1024
def __init__(self, body='', status_code=200, headers=None): def __init__(self, body='', status_code=200, headers=None):
self.status_code = status_code self.status_code = status_code
@@ -163,10 +169,9 @@ class Response():
self.headers['Content-Type'] = 'application/json' self.headers['Content-Type'] = 'application/json'
elif isinstance(body, str): elif isinstance(body, str):
self.body = body.encode() self.body = body.encode()
elif isinstance(body, bytes):
self.body = body
else: 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, def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
max_age=None, secure=False, http_only=False): max_age=None, secure=False, http_only=False):
@@ -190,7 +195,8 @@ class Response():
self.headers['Set-Cookie'] = [http_cookie] self.headers['Set-Cookie'] = [http_cookie]
def complete(self): 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)) self.headers['Content-Length'] = str(len(self.body))
if 'Content-Type' not in self.headers: if 'Content-Type' not in self.headers:
self.headers['Content-Type'] = 'text/plain' self.headers['Content-Type'] = 'text/plain'
@@ -213,7 +219,17 @@ class Response():
# body # body
if self.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 @classmethod
def redirect(cls, location, status_code=302): def redirect(cls, location, status_code=302):
@@ -227,11 +243,9 @@ class Response():
content_type = Response.types_map[ext] content_type = Response.types_map[ext]
else: else:
content_type = 'application/octet-stream' content_type = 'application/octet-stream'
with open(filename) as f: f = open(filename, 'rb')
body = f.read() return cls(body=f, status_code=status_code,
return cls(body=body, status_code=status_code, headers={'Content-Type': content_type})
headers={'Content-Type': content_type,
'Content-Length': str(len(body))})
class URLPattern(): class URLPattern():
@@ -350,48 +364,49 @@ class Microdot():
stream = sock stream = sock
req = Request.create(stream, addr) req = Request.create(stream, addr)
f = self.find_route(req) if req:
try: f = self.find_route(req)
res = None try:
if f: res = None
for handler in self.before_request_handlers: if f:
res = handler(req) for handler in self.before_request_handlers:
if res: res = handler(req)
break if res:
if res is None: break
res = f(req, **req.url_args) if res is None:
if isinstance(res, tuple): res = f(req, **req.url_args)
res = Response(*res) if isinstance(res, tuple):
elif not isinstance(res, Response): res = Response(*res)
res = Response(res) elif not isinstance(res, Response):
for handler in self.after_request_handlers: res = Response(res)
res = handler(req, res) or res for handler in self.after_request_handlers:
elif 404 in self.error_handlers: res = handler(req, res) or res
res = self.error_handlers[404](req) elif 404 in self.error_handlers:
else: res = self.error_handlers[404](req)
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: else:
res = 'Internal server error', 500 res = 'Not found', 404
if isinstance(res, tuple): except Exception as exc:
res = Response(*res) print_exception(exc)
elif not isinstance(res, Response): res = None
res = Response(res) if exc.__class__ in self.error_handlers:
res.write(stream) 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() stream.close()
if stream != sock: # pragma: no cover if stream != sock: # pragma: no cover
sock.close() sock.close()
if self.debug: # pragma: no cover if self.debug and req: # pragma: no cover
print('{method} {path} {status_code}'.format( print('{method} {path} {status_code}'.format(
method=req.method, path=req.path, method=req.method, path=req.path,
status_code=res.status_code)) status_code=res.status_code))

View File

@@ -8,7 +8,7 @@ from setuptools import setup
setup( setup(
name='microdot', name='microdot',
version="0.3.0", version="0.3.1",
url='http://github.com/miguelgrinberg/microdot/', url='http://github.com/miguelgrinberg/microdot/',
license='MIT', license='MIT',
author='Miguel Grinberg', author='Miguel Grinberg',

View File

@@ -92,7 +92,7 @@ class TestResponse(unittest.TestCase):
res = Response(123) res = Response(123)
self.assertEqual(res.status_code, 200) self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers, {}) self.assertEqual(res.headers, {})
self.assertEqual(res.body, b'123') self.assertEqual(res.body, 123)
def test_create_with_status_code(self): def test_create_with_status_code(self):
res = Response('not found', 404) res = Response('not found', 404)
@@ -161,11 +161,9 @@ class TestResponse(unittest.TestCase):
res = Response.send_file('tests/files/' + file) res = Response.send_file('tests/files/' + file)
self.assertEqual(res.status_code, 200) self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], content_type) self.assertEqual(res.headers['Content-Type'], content_type)
self.assertEqual(res.headers['Content-Length'], '4') self.assertEqual(res.body.read(), b'foo\n')
self.assertEqual(res.body, b'foo\n')
res = Response.send_file('tests/files/test.txt', res = Response.send_file('tests/files/test.txt',
content_type='text/html') content_type='text/html')
self.assertEqual(res.status_code, 200) self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/html') self.assertEqual(res.headers['Content-Type'], 'text/html')
self.assertEqual(res.headers['Content-Length'], '4') self.assertEqual(res.body.read(), b'foo\n')
self.assertEqual(res.body, b'foo\n')

17
tox.ini
View File

@@ -1,22 +1,23 @@
[tox] [tox]
envlist=flake8,py35,py36,py37,upy envlist=flake8,py36,py37,py38,py39,upy
skipsdist=True skipsdist=True
skip_missing_interpreters=True skip_missing_interpreters=True
[gh-actions]
python =
3.6: py36
3.7: py37
3.8: py38
3.9: py39
pypy3: pypy3
[testenv] [testenv]
commands= commands=
pip install -e microdot pip install -e microdot
pip install -e microdot-asyncio pip install -e microdot-asyncio
coverage run --branch --include="microdot*.py" -m unittest tests coverage run --branch --include="microdot*.py" -m unittest tests
coverage report --show-missing coverage report --show-missing
coverage erase
deps=coverage deps=coverage
basepython=
flake8: python3.7
py35: python3.5
py36: python3.6
py37: python3.7
upy: python3.7
[testenv:flake8] [testenv:flake8]
deps= deps=