Initial commit
This commit is contained in:
104
.gitignore
vendored
Normal file
104
.gitignore
vendored
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Miguel Grinberg
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
2
README.md
Normal file
2
README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# microdot
|
||||||
|
A minimalistic Python web framework for microcontrollers inspired by Flask
|
||||||
15
examples/gpio.html
Normal file
15
examples/gpio.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Microdot GPIO Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Microdot GPIO Example</h1>
|
||||||
|
<form method="POST" action="">
|
||||||
|
<p>GPIO Pin: <input type="text" name="pin" size="3"></p>
|
||||||
|
<input type="submit" name="read" value="Read">
|
||||||
|
<input type="submit" name="set-low" value="Set Low">
|
||||||
|
<input type="submit" name="set-high" value="Set high">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
examples/gpio.py
Normal file
19
examples/gpio.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import machine
|
||||||
|
from microdot import Microdot, redirect, send_file
|
||||||
|
|
||||||
|
app = Microdot()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
|
def index(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
if 'set-read' in request.form:
|
||||||
|
pin = machine.Pin(int(request.form['pin']), machine.Pin.IN)
|
||||||
|
else:
|
||||||
|
pin = machine.Pin(int(request.form['pin']), machine.Pin.OUT)
|
||||||
|
pin.value(0 if 'set-low' in request.form else 1)
|
||||||
|
return redirect('/')
|
||||||
|
return send_file('gpio.html')
|
||||||
|
|
||||||
|
|
||||||
|
app.run()
|
||||||
281
microdot.py
Normal file
281
microdot.py
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
try:
|
||||||
|
import usocket as socket
|
||||||
|
except ImportError:
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
def urldecode(string):
|
||||||
|
string = string.replace('+', ' ')
|
||||||
|
parts = string.split('%')
|
||||||
|
if len(parts) == 1:
|
||||||
|
return string
|
||||||
|
result = [parts[0]]
|
||||||
|
for item in parts[1:]:
|
||||||
|
if item == '':
|
||||||
|
result.append('%')
|
||||||
|
else:
|
||||||
|
code = item[:2]
|
||||||
|
result.append(chr(int(code, 16)))
|
||||||
|
result.append(item[2:])
|
||||||
|
return ''.join(result)
|
||||||
|
|
||||||
|
|
||||||
|
class Request():
|
||||||
|
def __init__(self, client_sock, client_addr):
|
||||||
|
self.client_sock = client_sock
|
||||||
|
self.client_addr = client_addr
|
||||||
|
|
||||||
|
if not hasattr(client_sock, 'readline'):
|
||||||
|
self.client_stream = client_sock.makefile("rwb")
|
||||||
|
else:
|
||||||
|
self.client_stream = client_sock
|
||||||
|
|
||||||
|
# request line
|
||||||
|
line = self.client_stream.readline().strip().decode('utf-8')
|
||||||
|
self.method, self.path, self.http_version = line.split()
|
||||||
|
if '?' in self.path:
|
||||||
|
self.path, self.query_string = self.path.split('?', 1)
|
||||||
|
else:
|
||||||
|
self.query_string = None
|
||||||
|
|
||||||
|
# headers
|
||||||
|
self.headers = {}
|
||||||
|
self.cookies = {}
|
||||||
|
self.content_length = 0
|
||||||
|
while True:
|
||||||
|
line = self.client_stream.readline().strip().decode('utf-8')
|
||||||
|
if line == '':
|
||||||
|
break
|
||||||
|
header, value = line.split(':', 1)
|
||||||
|
header = header.title()
|
||||||
|
value = value.strip()
|
||||||
|
self.headers[header] = value
|
||||||
|
if header == 'Content-Length':
|
||||||
|
self.content_length = int(value)
|
||||||
|
elif header == 'Content-Type':
|
||||||
|
self.content_type = value
|
||||||
|
elif header == 'Cookie':
|
||||||
|
for cookie in self.headers['Cookie'].split(';'):
|
||||||
|
name, value = cookie.split('=', 1)
|
||||||
|
self.cookies[name] = value
|
||||||
|
|
||||||
|
# body
|
||||||
|
self.body = self.client_stream.read(self.content_length)
|
||||||
|
self._json = None
|
||||||
|
self._form = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def json(self):
|
||||||
|
if self.content_type != 'application/json':
|
||||||
|
return None
|
||||||
|
if self._json is None:
|
||||||
|
self._json = json.loads(self.body)
|
||||||
|
return self._json
|
||||||
|
|
||||||
|
@property
|
||||||
|
def form(self):
|
||||||
|
if self.content_type != 'application/x-www-form-urlencoded':
|
||||||
|
return None
|
||||||
|
if self._form is None:
|
||||||
|
self._form = {urldecode(key): urldecode(value) for key, value in
|
||||||
|
[pair.split('=', 1) for pair in self.body.decode().split('&')]}
|
||||||
|
return self._form
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.client_stream.close()
|
||||||
|
if self.client_stream != self.client_sock:
|
||||||
|
self.client_sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
class Response():
|
||||||
|
types_map = {
|
||||||
|
'css': 'text/css',
|
||||||
|
'gif': 'image/gif',
|
||||||
|
'html': 'text/html',
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'js': 'application/javascript',
|
||||||
|
'json': 'application/json',
|
||||||
|
'png': 'image/png',
|
||||||
|
'txt': 'text/plain',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, body='', status_code=200, headers=None):
|
||||||
|
self.status_code = status_code
|
||||||
|
self.headers = headers or {}
|
||||||
|
self.cookies = []
|
||||||
|
if isinstance(body, (dict, list)):
|
||||||
|
self.body = json.dumps(body).encode()
|
||||||
|
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()
|
||||||
|
|
||||||
|
def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
|
||||||
|
max_age=None, secure=False, http_only=False):
|
||||||
|
http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value)
|
||||||
|
if path:
|
||||||
|
http_cookie += '; Path=' + path
|
||||||
|
if domain:
|
||||||
|
http_cookie += '; Domain=' + domain
|
||||||
|
if expires:
|
||||||
|
http_cookie += '; Expires=' + expires.strftime(
|
||||||
|
"%a, %d %b %Y %H:%M:%S GMT")
|
||||||
|
if max_age:
|
||||||
|
http_cookie += '; Max-Age=' + str(max_age)
|
||||||
|
if secure:
|
||||||
|
http_cookie += '; Secure'
|
||||||
|
if http_only:
|
||||||
|
http_cookie += '; httpOnly'
|
||||||
|
if 'Set-Cookie' in self.headers:
|
||||||
|
self.headers['Set-Cookie'].append(http_cookie)
|
||||||
|
else:
|
||||||
|
self.headers['Set-Cookie'] = [http_cookie]
|
||||||
|
|
||||||
|
def write(self, client_stream):
|
||||||
|
# status code
|
||||||
|
client_stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format(
|
||||||
|
status_code=self.status_code,
|
||||||
|
reason='OK' if self.status_code == 200 else 'N/A').encode())
|
||||||
|
|
||||||
|
# headers
|
||||||
|
content_length_found = False
|
||||||
|
content_type_found = False
|
||||||
|
for header, value in self.headers.items():
|
||||||
|
header = header.title()
|
||||||
|
values = value if isinstance(value, list) else [value]
|
||||||
|
for value in values:
|
||||||
|
client_stream.write('{header}: {value}\r\n'.format(
|
||||||
|
header=header, value=value).encode())
|
||||||
|
if header == 'Content-Length':
|
||||||
|
content_length_found = True
|
||||||
|
elif header == 'Content-Type':
|
||||||
|
content_type_found = True
|
||||||
|
if not content_length_found:
|
||||||
|
client_stream.write('Content-Length: {length}\r\n'.format(
|
||||||
|
length=len(self.body)).encode())
|
||||||
|
if not content_type_found:
|
||||||
|
client_stream.write(b'Content-Type: text/plain\r\n')
|
||||||
|
client_stream.write(b'\r\n')
|
||||||
|
|
||||||
|
# body
|
||||||
|
if self.body:
|
||||||
|
client_stream.write(self.body)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def redirect(location, status_code=302):
|
||||||
|
return Response(status_code=status_code,
|
||||||
|
headers={'Location': location})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_file(filename, status_code=200, content_type=None):
|
||||||
|
if content_type is None:
|
||||||
|
ext = filename.split('.')[-1]
|
||||||
|
if ext in Response.types_map:
|
||||||
|
content_type = Response.types_map[ext]
|
||||||
|
else:
|
||||||
|
content_type = 'application/octet-stream'
|
||||||
|
with open(filename) as f:
|
||||||
|
return Response(body=f.read(), status_code=status_code,
|
||||||
|
headers={'Content-Type': content_type})
|
||||||
|
|
||||||
|
|
||||||
|
class URLPattern():
|
||||||
|
def __init__(self, url_pattern):
|
||||||
|
self.pattern = ''
|
||||||
|
self.args = []
|
||||||
|
use_regex = False
|
||||||
|
for segment in url_pattern.lstrip('/').split('/'):
|
||||||
|
if segment and segment[0] == '<':
|
||||||
|
if segment[-1] != '>':
|
||||||
|
raise ValueError('invalid URL pattern')
|
||||||
|
segment = segment[1:-1]
|
||||||
|
if ':' in segment:
|
||||||
|
type_, name = segment.split(':', 1)
|
||||||
|
else:
|
||||||
|
type_ = 'string'
|
||||||
|
name = segment
|
||||||
|
if type_ == 'string':
|
||||||
|
pattern = '[^/]*'
|
||||||
|
elif type_ == 'int':
|
||||||
|
pattern = '\\d+'
|
||||||
|
elif type_ == 'path':
|
||||||
|
pattern = '.*'
|
||||||
|
elif type_.startswith('regex'):
|
||||||
|
pattern = eval(type_[5:])
|
||||||
|
else:
|
||||||
|
raise ValueError('invalid URL segment type')
|
||||||
|
use_regex = True
|
||||||
|
self.pattern += '/({pattern})'.format(pattern=pattern)
|
||||||
|
self.args.append({'type': type_, 'name': name})
|
||||||
|
else:
|
||||||
|
self.pattern += '/{segment}'.format(segment=segment)
|
||||||
|
if use_regex:
|
||||||
|
self.pattern = re.compile(self.pattern)
|
||||||
|
|
||||||
|
def match(self, path):
|
||||||
|
if isinstance(self.pattern, str):
|
||||||
|
if path != self.pattern:
|
||||||
|
return
|
||||||
|
return {}
|
||||||
|
g = self.pattern.match(path)
|
||||||
|
if not g:
|
||||||
|
return
|
||||||
|
args = {}
|
||||||
|
i = 1
|
||||||
|
for arg in self.args:
|
||||||
|
value = g.group(i)
|
||||||
|
if arg['type'] == 'int':
|
||||||
|
value = int(value)
|
||||||
|
args[arg['name']] = value
|
||||||
|
i += 1
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
class Microdot():
|
||||||
|
def __init__(self) :
|
||||||
|
self.url_map = []
|
||||||
|
|
||||||
|
def route(self, url_pattern, methods=None):
|
||||||
|
def decorated(f):
|
||||||
|
self.url_map.append(
|
||||||
|
(methods or ['GET'], URLPattern(url_pattern), f))
|
||||||
|
return f
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
def run(self, host='0.0.0.0', port=5000):
|
||||||
|
s = socket.socket()
|
||||||
|
ai = socket.getaddrinfo(host, port)
|
||||||
|
addr = ai[0][-1]
|
||||||
|
|
||||||
|
print('Listening on {host}:{port}...'.format(host=host, port=port))
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
s.bind(addr)
|
||||||
|
s.listen(5)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
req = Request(*s.accept())
|
||||||
|
f = None
|
||||||
|
args = None
|
||||||
|
for route_methods, route_pattern, route_handler in self.url_map:
|
||||||
|
if req.method in route_methods:
|
||||||
|
args = route_pattern.match(req.path)
|
||||||
|
if args is not None:
|
||||||
|
f = route_handler
|
||||||
|
break
|
||||||
|
if f:
|
||||||
|
resp = f(req, **args)
|
||||||
|
if isinstance(resp, tuple):
|
||||||
|
resp = Response(*resp)
|
||||||
|
elif not isinstance(resp, Response):
|
||||||
|
resp = Response(resp)
|
||||||
|
resp.write(req.client_stream)
|
||||||
|
req.close()
|
||||||
|
|
||||||
|
|
||||||
|
redirect = Response.redirect
|
||||||
|
send_file = Response.send_file
|
||||||
36
setup.py
Executable file
36
setup.py
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
"""
|
||||||
|
Microdot
|
||||||
|
--------
|
||||||
|
|
||||||
|
Impossibly small web framework for MicroPython.
|
||||||
|
"""
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='microdot',
|
||||||
|
version='0.1.0',
|
||||||
|
url='http://github.com/miguelgrinberg/microdot/',
|
||||||
|
license='MIT',
|
||||||
|
author='Miguel Grinberg',
|
||||||
|
author_email='miguel.grinberg@gmail.com',
|
||||||
|
description='Impossibly small web framework for MicroPython',
|
||||||
|
long_description=__doc__,
|
||||||
|
packages=['microdot'],
|
||||||
|
zip_safe=False,
|
||||||
|
include_package_data=True,
|
||||||
|
platforms='any',
|
||||||
|
tests_require=[
|
||||||
|
'coverage'
|
||||||
|
],
|
||||||
|
classifiers=[
|
||||||
|
'Environment :: Web Environment',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Programming Language :: Python',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: Implementation :: MicroPython',
|
||||||
|
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
||||||
|
'Topic :: Software Development :: Libraries :: Python Modules'
|
||||||
|
]
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user