8 Commits

Author SHA1 Message Date
Miguel Grinberg
2de57498a8 Release 0.5.0 2021-06-06 11:47:09 +01:00
Miguel Grinberg
b7b881e3c7 merge microdot-asyncio package with microdot 2021-06-06 11:15:32 +01:00
Miguel Grinberg
9955ac99a6 change log 2021-06-06 00:38:46 +01:00
Miguel Grinberg
4b101d1597 Improve project structure 2021-06-06 00:29:52 +01:00
Miguel Grinberg
097cd9cf02 Documentation updates 2021-06-05 15:42:37 +01:00
Miguel Grinberg
b0c25a1a72 Support duplicate arguments in query string and form submissions
Fixes #21
2021-06-05 12:26:37 +01:00
Miguel Grinberg
b7b8e58d6a added documentation link 2021-06-05 00:56:16 +01:00
Miguel Grinberg
12cd60305b Documentation 2021-06-05 00:52:41 +01:00
25 changed files with 908 additions and 86 deletions

54
CHANGES.md Normal file
View File

@@ -0,0 +1,54 @@
# Microdot change log
**Release 0.5.0** - 2021-06-06
- [Documentation](https://microdot.readthedocs.io/en/latest/) site ([commit](https://github.com/miguelgrinberg/microdot/commit/12cd60305b7b48ab151da52661fc5988684dbcd8))
- Support duplicate arguments in query string and form submissions [#21](https://github.com/miguelgrinberg/microdot/issues/21) ([commit](https://github.com/miguelgrinberg/microdot/commit/b0c25a1a7298189373be5df1668e0afb5532cdaf))
- Merge `microdot-asyncio` package with `microdot` ([commit](https://github.com/miguelgrinberg/microdot/commit/b7b881e3c7f1c6ede6546e498737e93928425c30))
- Added a change log ([commit](https://github.com/miguelgrinberg/microdot/commit/9955ac99a6ac20308644f02d6e6e32847d28b70c))
- Improve project structure ([commit](https://github.com/miguelgrinberg/microdot/commit/4b101d15971fa2883d187f0bab0be999ae30b583))
**Release v0.4.0** - 2021-06-04
- Add HTTP method-specific route decorators ([commit](https://github.com/miguelgrinberg/microdot/commit/a3288a63ed45f700f79b67d0b57fc4dd20e844c1))
- Server shutdown [#19](https://github.com/miguelgrinberg/microdot/issues/19) ([commit](https://github.com/miguelgrinberg/microdot/commit/0ad538df91f8b6b8a3885aa602c014ee7fe4526b))
- Update microypthon binary for tests to 1.15 ([commit](https://github.com/miguelgrinberg/microdot/commit/3bd7fe8cea4598a7dbd0efcb9c6ce57ec2b79f9c))
**Release v0.3.1** - 2021-02-06
- Support large downloads in send_file [#3](https://github.com/miguelgrinberg/microdot/issues/3) ([commit](https://github.com/miguelgrinberg/microdot/commit/3e29af57753dbb7961ff98719a4fc4f71c0b4e3e))
- Move socket import and add simple hello example [#12](https://github.com/miguelgrinberg/microdot/issues/12) ([commit](https://github.com/miguelgrinberg/microdot/commit/c5e1873523b609680ff67d7abfada72568272250)) (thanks **Damien George**!)
- Update python versions to build ([commit](https://github.com/miguelgrinberg/microdot/commit/dfbe2edd797153fc9be40bc1928d93bdee7e7be5))
- Handle Chrome preconnect [#8](https://github.com/miguelgrinberg/microdot/issues/8) ([commit](https://github.com/miguelgrinberg/microdot/commit/125af4b4a92b1d78acfa9d57ad2f507e759b6938)) (thanks **Ricardo Mendonça Ferreira**!)
- Readme update ([commit](https://github.com/miguelgrinberg/microdot/commit/1aacb3cf46bd0b634ec3bc852ff9439f3c5dd773))
- Switch to GitHub actions for builds ([commit](https://github.com/miguelgrinberg/microdot/commit/4c0afa2beca0c3b0f167fd25c6849d6937c412ba))
**Release v0.3.0** - 2019-05-05
- g, before_request and after_request ([commit](https://github.com/miguelgrinberg/microdot/commit/8aa50f171d2d04bc15c472ab1d9b3288518f7a21))
- Threaded mode ([commit](https://github.com/miguelgrinberg/microdot/commit/494800ff9ff474c38644979086057e3584573969))
- Optional asyncio support ([commit](https://github.com/miguelgrinberg/microdot/commit/3d9b5d7084d52e749553ca79206ed7060f963f9d))
- Debug mode ([commit](https://github.com/miguelgrinberg/microdot/commit/4c83cb75636572066958ef2cc0802909deafe542))
- Print exceptions ([commit](https://github.com/miguelgrinberg/microdot/commit/491202de1fce232b9629b7f1db63594fd13f84a3))
- Flake8 ([commit](https://github.com/miguelgrinberg/microdot/commit/92edc17522d7490544c7186d62a2964caf35c861))
- Unit testing framework ([commit](https://github.com/miguelgrinberg/microdot/commit/f741ed7cf83320d25ce16a1a29796af6fdfb91e9))
- More robust header checking in tests ([commit](https://github.com/miguelgrinberg/microdot/commit/03efe46a26e7074f960dd4c9a062c53d6f72bfa0))
- Response unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/cd71986a5042dcc308617a3db89476f28dd13ecf))
- Request unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/0b95feafc96dc91d7d34528ff2d8931a8aa3d612))
- More unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/76ab1fa6d72dd9deaa24aeaf4895a0c6fc883bcb))
- Async request and response unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/89f7f09b9a2d0dfccefabebbe9b83307133bd97c))
- More asyncio unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/ba986a89ff72ebbd9a65307b81ee769879961594))
- Improve code structure ([commit](https://github.com/miguelgrinberg/microdot/commit/b16466f1a9432a608eb23769907e8952fe304a9a))
- URL pattern matching unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/0a373775d54df571ceddaac090094bb62dbe6c72))
- Rename microdot_async to microdot_asyncio ([commit](https://github.com/miguelgrinberg/microdot/commit/e5525c5c485ae8901c9602da7e4582b58fb2da40))
**Release 0.2.0** - 2019-04-19
- Error handlers ([commit](https://github.com/miguelgrinberg/microdot/commit/0f2c749f6d1b9edbf124523160e10449c932ea45))
- Fleshed out example GPIO application ([commit](https://github.com/miguelgrinberg/microdot/commit/52f2d0c4918d00d1a7e46cc7fd9a909ef6d259c1))
- More robust parsing of cookie header ([commit](https://github.com/miguelgrinberg/microdot/commit/2f58c41cc89946d51646df83d4f9ae0e24e447b9))
**Release 0.1.1** - 2019-04-17
- Minor fixes for micropython ([commit](https://github.com/miguelgrinberg/microdot/commit/e4ff70cf8fe839f5b5297157bf028569188b9031))
- Initial commit ([commit](https://github.com/miguelgrinberg/microdot/commit/311a82a44430d427948866b09cb6136e60a5b1c9))

View File

@@ -3,6 +3,7 @@
A minimalistic Python web framework for microcontrollers inspired by Flask A minimalistic Python web framework for microcontrollers inspired by Flask
## Documentation ## Resources
Coming soon! - [Documentation](https://microdot.readthedocs.io/en/latest/)
- [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

3
docs/_static/css/custom.css vendored Normal file
View File

@@ -0,0 +1,3 @@
.py .class, .py .method, .py .property {
margin-top: 20px;
}

62
docs/api.rst Normal file
View File

@@ -0,0 +1,62 @@
API Reference
=============
``microdot`` module
-------------------
The ``microdot`` module defines a few classes that help implement HTTP-based
servers for MicroPython and standard Python, with multithreading support for
Python interpreters that support it.
``Microdot`` class
~~~~~~~~~~~~~~~~~~
.. autoclass:: microdot.Microdot
:members:
``Request`` class
~~~~~~~~~~~~~~~~~
.. autoclass:: microdot.Request
:members:
``Response`` class
~~~~~~~~~~~~~~~~~~
.. autoclass:: microdot.Response
:members:
``MultiDict`` class
~~~~~~~~~~~~~~~~~~~
.. autoclass:: microdot.MultiDict
:members:
``microdot_asyncio`` module
---------------------------
The ``microdot_asyncio`` module defines a few classes that help implement
HTTP-based servers for MicroPython and standard Python that use ``asyncio``
and coroutines.
``Microdot`` class
~~~~~~~~~~~~~~~~~~
.. autoclass:: microdot_asyncio.Microdot
:inherited-members:
:members:
``Request`` class
~~~~~~~~~~~~~~~~~
.. autoclass:: microdot_asyncio.Request
:inherited-members:
:members:
``Response`` class
~~~~~~~~~~~~~~~~~~
.. autoclass:: microdot_asyncio.Response
:inherited-members:
:members:

71
docs/conf.py Normal file
View File

@@ -0,0 +1,71 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('../microdot'))
sys.path.insert(1, os.path.abspath('../microdot-asyncio'))
# -- Project information -----------------------------------------------------
project = 'Microdot'
copyright = '2021, Miguel Grinberg'
author = 'Miguel Grinberg'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_css_files = [
'css/custom.css',
]
html_theme_options = {
'github_user': 'miguelgrinberg',
'github_repo': 'microdot',
'github_banner': True,
'github_button': True,
'github_type': 'star',
'fixed_sidebar': True,
}
autodoc_default_options = {
'member-order': 'bysource',
}

21
docs/index.rst Normal file
View File

@@ -0,0 +1,21 @@
.. Microdot documentation master file, created by
sphinx-quickstart on Fri Jun 4 17:40:19 2021.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Microdot
========
Microdot is a minimalistic Python web framework for microcontrollers inspired
by `Flask <https://flask.palletsprojects.com/>`_, and designed to run on
systems with limited resources such as microcontrollers. It runs on standard
Python and on `MicroPython <https://micropython.org>`_.
.. toctree::
:maxdepth: 3
intro
api
* :ref:`genindex`
* :ref:`search`

29
docs/intro.rst Normal file
View File

@@ -0,0 +1,29 @@
Examples
--------
The following is an example of a standard single or multi-threaded web
server::
from microdot import Microdot
app = Microdot()
@app.route('/')
def hello(request):
return 'Hello, world!'
app.run()
Microdot also supports the asynchronous model and can be used under
``asyncio``. The example that follows is equivalent to the one above, but uses
coroutines for concurrency::
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
async def hello(request):
return 'Hello, world!'
app.run()

35
docs/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

5
legacy/README.md Normal file
View File

@@ -0,0 +1,5 @@
microdot-asyncio
================
This package has been merged with the ``microdot`` package. It currently
installs as an empty package that depends on it.

6
legacy/pyproject.toml Normal file
View File

@@ -0,0 +1,6 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"

24
legacy/setup.cfg Normal file
View File

@@ -0,0 +1,24 @@
[metadata]
name = microdot-asyncio
version = 0.4.0
author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com
description = AsyncIO support for the Microdot web framework'
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/miguelgrinberg/microdot
project_urls =
Bug Tracker = https://github.com/miguelgrinberg/microdot/issues
classifiers =
Environment :: Web Environment
Intended Audience :: Developers
Programming Language :: Python :: 3
Programming Language :: Python :: Implementation :: MicroPython
License :: OSI Approved :: MIT License
Operating System :: OS Independent
[options]
zip_safe = False
include_package_data = True
py_modules =
install_requires =
microdot

3
legacy/setup.py Normal file
View File

@@ -0,0 +1,3 @@
import setuptools
setuptools.setup()

View File

@@ -1,35 +0,0 @@
"""
Microdot-AsyncIO
----------------
AsyncIO support for the Microdot web framework.
"""
from setuptools import setup
setup(
name='microdot-asyncio',
version="0.4.0",
url='http://github.com/miguelgrinberg/microdot/',
license='MIT',
author='Miguel Grinberg',
author_email='miguel.grinberg@gmail.com',
description='AsyncIO support for the Microdot web framework',
long_description=__doc__,
py_modules=['microdot_asyncio'],
platforms='any',
install_requires=[
'microdot',
'micropython-uasyncio;implementation_name=="micropython"'
],
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'
]
)

View File

@@ -1,31 +0,0 @@
"""
Microdot
--------
The impossibly small web framework for MicroPython.
"""
from setuptools import setup
setup(
name='microdot',
version="0.4.0",
url='http://github.com/miguelgrinberg/microdot/',
license='MIT',
author='Miguel Grinberg',
author_email='miguel.grinberg@gmail.com',
description='The impossibly small web framework for MicroPython',
long_description=__doc__,
py_modules=['microdot'],
platforms='any',
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'
]
)

6
pyproject.toml Normal file
View File

@@ -0,0 +1,6 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"

27
setup.cfg Normal file
View File

@@ -0,0 +1,27 @@
[metadata]
name = microdot
version = 0.5.0
author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com
description = The impossibly small web framework for MicroPython
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/miguelgrinberg/microdot
project_urls =
Bug Tracker = https://github.com/miguelgrinberg/microdot/issues
classifiers =
Environment :: Web Environment
Intended Audience :: Developers
Programming Language :: Python :: 3
Programming Language :: Python :: Implementation :: MicroPython
License :: OSI Approved :: MIT License
Operating System :: OS Independent
[options]
zip_safe = False
include_package_data = True
package_dir =
= src
py_modules =
microdot
microdot_asyncio

3
setup.py Executable file
View File

@@ -0,0 +1,3 @@
import setuptools
setuptools.setup()

View File

@@ -1,3 +1,11 @@
"""
microdot
--------
The ``microdot`` module defines a few classes that help implement HTTP-based
servers for MicroPython and standard Python, with multithreading support for
Python interpreters that support it.
"""
try: try:
from sys import print_exception from sys import print_exception
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
@@ -16,21 +24,21 @@ try: # pragma: no cover
import threading import threading
def create_thread(f, *args, **kwargs): def create_thread(f, *args, **kwargs):
"""Use the threading module.""" # use the threading module
threading.Thread(target=f, args=args, kwargs=kwargs).start() threading.Thread(target=f, args=args, kwargs=kwargs).start()
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
try: try:
import _thread import _thread
def create_thread(f, *args, **kwargs): def create_thread(f, *args, **kwargs):
"""Use MicroPython's _thread module.""" # use MicroPython's _thread module
def run(): def run():
f(*args, **kwargs) f(*args, **kwargs)
_thread.start_new_thread(run, ()) _thread.start_new_thread(run, ())
except ImportError: except ImportError:
def create_thread(f, *args, **kwargs): def create_thread(f, *args, **kwargs):
"""No threads available, call function synchronously.""" # no threads available, call function synchronously
f(*args, **kwargs) f(*args, **kwargs)
concurrency_mode = 'sync' concurrency_mode = 'sync'
@@ -69,7 +77,117 @@ def urldecode(string):
return ''.join(result) return ''.join(result)
class MultiDict(dict):
"""A subclass of dictionary that can hold multiple values for the same
key. It is used to hold key/value pairs decoded from query strings and
form submissions.
:param initial_dict: an initial dictionary of key/value pairs to
initialize this object with.
Example::
>>> d = MultiDict()
>>> d['sort'] = 'name'
>>> d['sort'] = 'email'
>>> print(d['sort'])
'name'
>>> print(d.getlist('sort'))
['name', 'email']
"""
def __init__(self, initial_dict=None):
super().__init__()
if initial_dict:
for key, value in initial_dict.items():
self[key] = value
def __setitem__(self, key, value):
if key not in self:
super().__setitem__(key, [])
super().__getitem__(key).append(value)
def __getitem__(self, key):
return super().__getitem__(key)[0]
def get(self, key, default=None, type=None):
"""Return the value for a given key.
:param key: The key to retrieve.
:param default: A default value to use if the key does not exist.
:param type: A type conversion callable to apply to the value.
If the multidict contains more than one value for the requested key,
this method returns the first value only.
Example::
>>> d = MultiDict()
>>> d['age'] = '42'
>>> d.get('age')
'42'
>>> d.get('age', type=int)
42
>>> d.get('name', default='noname')
'noname'
"""
if key not in self:
return default
value = self[key]
if type is not None:
value = type(value)
return value
def getlist(self, key, type=None):
"""Return all the values for a given key.
:param key: The key to retrieve.
:param type: A type conversion callable to apply to the values.
If the requested key does not exist in the dictionary, this method
returns an empty list.
Example::
>>> d = MultiDict()
>>> d.getlist('items')
[]
>>> d['items'] = '3'
>>> d.getlist('items')
['3']
>>> d['items'] = '56'
>>> d.getlist('items')
['3', '56']
>>> d.getlist('items', type=int)
[3, 56]
"""
if key not in self:
return []
values = super().__getitem__(key)
if type is not None:
values = [type(value) for value in values]
return values
class Request(): class Request():
"""An HTTP request class.
:var app: The application instance to which this request belongs.
:var method: The HTTP method of the request.
:var path: The path portion of the URL.
:var query_string: The query string portion of the URL.
:var args: The parsed query string, as a :class:`MultiDict` object.
:var headers: A dictionary with the headers included in the request.
:var cookies: A dictionary with the cookies included in the request.
:var content_length: The parsed ``Content-Length`` header.
:var content_type: The parsed ``Content-Type`` header.
:var body: A stream from where the body can be read.
:var json: The parsed JSON body, as a dictionary or list, or ``None`` if
the request does not have a JSON body.
:var form: The parsed form submission body, as a :class:`MultiDict` object,
or ``None`` if the request does not have a form submission.
:var g: A general purpose container for applications to store data during
the life of the request.
"""
class G: class G:
pass pass
@@ -106,6 +224,15 @@ class Request():
@staticmethod @staticmethod
def create(app, client_stream, client_addr): def create(app, client_stream, client_addr):
"""Create a request object.
:param app: The Microdot application instance.
:param client_stream: An input stream from where the request data can
be read.
:param client_addr: The address of the client, as a tuple.
This method returns a newly created ``Request`` object.
"""
# 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: # pragma: no cover
@@ -133,10 +260,10 @@ class Request():
body) body)
def _parse_urlencoded(self, urlencoded): def _parse_urlencoded(self, urlencoded):
return { data = MultiDict()
urldecode(key): urldecode(value) for key, value in [ for k, v in [pair.split('=', 1) for pair in urlencoded.split('&')]:
pair.split('=', 1) for pair in data[urldecode(k)] = urldecode(v)
urlencoded.split('&')]} return data
@property @property
def json(self): def json(self):
@@ -156,6 +283,14 @@ class Request():
class Response(): class Response():
"""An HTTP response class.
:param body: The body of the response. If a dictionary or list is given,
a JSON formatter is used to generate the body.
:param status_code: The numeric HTTP status code of the response. The
default is 200.
:param headers: A dictionary of headers to include in the response.
"""
types_map = { types_map = {
'css': 'text/css', 'css': 'text/css',
'gif': 'image/gif', 'gif': 'image/gif',
@@ -182,6 +317,17 @@ class Response():
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):
"""Add a cookie to the response.
:param cookie: The cookie's name.
:param value: The cookie's value.
:param path: The cookie's path.
:param domain: The cookie's domain.
:param expires: The cookie expiration time, as a ``datetime`` object.
:param max_age: The cookie's ``Max-Age`` value.
:param secure: The cookie's ``secure`` flag.
:param http_only: The cookie's ``HttpOnly`` flag.
"""
http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value) http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value)
if path: if path:
http_cookie += '; Path=' + path http_cookie += '; Path=' + path
@@ -233,17 +379,32 @@ class Response():
stream.write(buf) stream.write(buf)
if len(buf) < self.send_file_buffer_size: if len(buf) < self.send_file_buffer_size:
break break
if hasattr(self.body, 'close'): # pragma: no close if hasattr(self.body, 'close'): # pragma: no cover
self.body.close() self.body.close()
else: else:
stream.write(self.body) stream.write(self.body)
@classmethod @classmethod
def redirect(cls, location, status_code=302): def redirect(cls, location, status_code=302):
"""Return a redirect response.
:param location: The URL to redirect to.
:param status_code: The 3xx status code to use for the redirect. The
default is 302.
"""
return cls(status_code=status_code, headers={'Location': location}) return cls(status_code=status_code, headers={'Location': location})
@classmethod @classmethod
def send_file(cls, filename, status_code=200, content_type=None): def send_file(cls, filename, status_code=200, content_type=None):
"""Send file contents in a response.
:param filename: The filename of the file.
:param status_code: The 3xx status code to use for the redirect. The
default is 302.
:param content_type: The ``Content-Type`` header to use in the
response. If omitted, it is generated
automatically from the file extension.
"""
if content_type is None: if content_type is None:
ext = filename.split('.')[-1] ext = filename.split('.')[-1]
if ext in Response.types_map: if ext in Response.types_map:
@@ -308,6 +469,19 @@ class URLPattern():
class Microdot(): class Microdot():
"""An HTTP application class.
This class implements an HTTP application instance and is heavily
influenced by the ``Flask`` class of the Flask framework. It is typically
declared near the start of the main application script.
Example::
from microdot import Microdot
app = Microdot()
"""
def __init__(self): def __init__(self):
self.url_map = [] self.url_map = []
self.before_request_handlers = [] self.before_request_handlers = []
@@ -318,6 +492,35 @@ class Microdot():
self.server = None self.server = None
def route(self, url_pattern, methods=None): def route(self, url_pattern, methods=None):
"""Decorator that is used to register a function as a request handler
for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
:param methods: The list of HTTP methods to be handled by the
decorated function. If omitted, only ``GET`` requests
are handled.
The URL pattern can be a static path (for example, ``/users`` or
``/api/invoices/search``) or a path with dynamic components enclosed
in ``<`` and ``>`` (for example, ``/users/<id>`` or
``/invoices/<number>/products``). Dynamic path components can also
include a type prefix, separated from the name with a colon (for
example, ``/users/<int:id>``). The type can be ``string`` (the
default), ``int``, ``path`` or ``re:[regular-expression]``.
The first argument of the decorated function must be
the request object. Any path arguments that are specified in the URL
pattern are passed as keyword arguments. The return value of the
function must be a :class:`Response` instance, or the arguments to
be passed to this class.
Example::
@app.route('/')
def index(request):
return 'Hello, world!'
"""
def decorated(f): def decorated(f):
self.url_map.append( self.url_map.append(
(methods or ['GET'], URLPattern(url_pattern), f)) (methods or ['GET'], URLPattern(url_pattern), f))
@@ -325,35 +528,179 @@ class Microdot():
return decorated return decorated
def get(self, url_pattern): def get(self, url_pattern):
"""Decorator that is used to register a function as a ``GET`` request
handler for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
This decorator can be used as an alias to the ``route`` decorator with
``methods=['GET']``.
Example::
@app.get('/users/<int:id>')
def get_user(request, id):
# ...
"""
return self.route(url_pattern, methods=['GET']) return self.route(url_pattern, methods=['GET'])
def post(self, url_pattern): def post(self, url_pattern):
"""Decorator that is used to register a function as a ``POST`` request
handler for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
This decorator can be used as an alias to the``route`` decorator with
``methods=['POST']``.
Example::
@app.post('/users')
def create_user(request):
# ...
"""
return self.route(url_pattern, methods=['POST']) return self.route(url_pattern, methods=['POST'])
def put(self, url_pattern): def put(self, url_pattern):
"""Decorator that is used to register a function as a ``PUT`` request
handler for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
This decorator can be used as an alias to the ``route`` decorator with
``methods=['PUT']``.
Example::
@app.put('/users/<int:id>')
def edit_user(request, id):
# ...
"""
return self.route(url_pattern, methods=['PUT']) return self.route(url_pattern, methods=['PUT'])
def patch(self, url_pattern): def patch(self, url_pattern):
"""Decorator that is used to register a function as a ``PATCH`` request
handler for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
This decorator can be used as an alias to the ``route`` decorator with
``methods=['PATCH']``.
Example::
@app.patch('/users/<int:id>')
def edit_user(request, id):
# ...
"""
return self.route(url_pattern, methods=['PATCH']) return self.route(url_pattern, methods=['PATCH'])
def delete(self, url_pattern): def delete(self, url_pattern):
"""Decorator that is used to register a function as a ``DELETE``
request handler for a given URL.
:param url_pattern: The URL pattern that will be compared against
incoming requests.
This decorator can be used as an alias to the ``route`` decorator with
``methods=['DELETE']``.
Example::
@app.delete('/users/<int:id>')
def delete_user(request, id):
# ...
"""
return self.route(url_pattern, methods=['DELETE']) return self.route(url_pattern, methods=['DELETE'])
def before_request(self, f): def before_request(self, f):
"""Decorator to register a function to run before each request is
handled. The decorated function must take a single argument, the
request object.
Example::
@app.before_request
def func(request):
# ...
"""
self.before_request_handlers.append(f) self.before_request_handlers.append(f)
return f return f
def after_request(self, f): def after_request(self, f):
"""Decorator to register a function to run after each request is
handled. The decorated function must take two arguments, the request
and response objects. The return value of the function must be an
updated response object.
Example::
@app.before_request
def func(request, response):
# ...
"""
self.after_request_handlers.append(f) self.after_request_handlers.append(f)
return f return f
def errorhandler(self, status_code_or_exception_class): def errorhandler(self, status_code_or_exception_class):
"""Decorator to register a function as an error handler. Error handler
functions for numeric HTTP status codes must accept a single argument,
the request object. Error handler functions for Python exceptions
must accept two arguments, the request object and the exception
object.
:param status_code_or_exception_class: The numeric HTTP status code or
Python exception class to
handle.
Examples::
@app.errorhandler(404)
def not_found(request):
return 'Not found'
@app.errorhandler(RuntimeError)
def runtime_error(request, exception):
return 'Runtime error'
"""
def decorated(f): def decorated(f):
self.error_handlers[status_code_or_exception_class] = f self.error_handlers[status_code_or_exception_class] = f
return f return f
return decorated return decorated
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
the server enters an endless listening loop. The :func:`shutdown`
function provides a method for terminating the server gracefully.
:param host: The hostname or IP address of the network interface that
will be listening for requests. A value of ``'0.0.0.0'``
(the default) indicates that the server should listen for
requests on all the available interfaces, and a value of
``127.0.0.1`` indicates that the server should listen
for requests only on the internal networking interface of
the host.
:param port: The port number to listen for requests. The default is
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
Example::
from microdot import Microdot
app = Microdot()
@app.route('/')
def index():
return 'Hello, world!'
app.run(debug=True)
"""
self.debug = debug self.debug = debug
self.shutdown_requested = False self.shutdown_requested = False
@@ -371,7 +718,7 @@ class Microdot():
while not self.shutdown_requested: while not self.shutdown_requested:
try: try:
sock, addr = self.server.accept() sock, addr = self.server.accept()
except OSError as exc: except OSError as exc: # pragma: no cover
if exc.args[0] == errno.ECONNABORTED: if exc.args[0] == errno.ECONNABORTED:
break break
else: else:
@@ -379,6 +726,18 @@ class Microdot():
create_thread(self.dispatch_request, sock, addr) create_thread(self.dispatch_request, sock, addr)
def shutdown(self): def shutdown(self):
"""Request a server shutdown. The server will then exit its request
listening loop and the :func:`run` function will return. This function
can be safely called from a route handler, as it only schedules the
server to terminate as soon as the request completes.
Example::
@app.route('/shutdown')
def shutdown(request):
request.app.shutdown()
return 'The server is shutting down...'
"""
self.shutdown_requested = True self.shutdown_requested = True
def find_route(self, req): def find_route(self, req):

View File

@@ -1,3 +1,11 @@
"""
microdot_asyncio
----------------
The ``microdot_asyncio`` module defines a few classes that help implement
HTTP-based servers for MicroPython and standard Python that use ``asyncio``
and coroutines.
"""
try: try:
import uasyncio as asyncio import uasyncio as asyncio
except ImportError: except ImportError:
@@ -14,9 +22,19 @@ def _iscoroutine(coro):
class Request(BaseRequest): class Request(BaseRequest):
@staticmethod @staticmethod
async def create(app, stream, client_addr): async def create(app, client_stream, client_addr):
"""Create a request object.
:param app: The Microdot application instance.
:param client_stream: An input stream from where the request data can
be read.
:param client_addr: The address of the client, as a tuple.
This method is a coroutine. It returns a newly created ``Request``
object.
"""
# request line # request line
line = (await stream.readline()).strip().decode() line = (await client_stream.readline()).strip().decode()
if not line: # pragma: no cover if not line: # pragma: no cover
return None return None
method, url, http_version = line.split() method, url, http_version = line.split()
@@ -26,7 +44,7 @@ class Request(BaseRequest):
headers = {} headers = {}
content_length = 0 content_length = 0
while True: while True:
line = (await stream.readline()).strip().decode() line = (await client_stream.readline()).strip().decode()
if line == '': if line == '':
break break
header, value = line.split(':', 1) header, value = line.split(':', 1)
@@ -36,7 +54,7 @@ class Request(BaseRequest):
content_length = int(value) content_length = int(value)
# body # body
body = await stream.read(content_length) \ body = await client_stream.read(content_length) \
if content_length else b'' if content_length else b''
return Request(app, client_addr, method, url, http_version, headers, return Request(app, client_addr, method, url, http_version, headers,
@@ -44,6 +62,14 @@ class Request(BaseRequest):
class Response(BaseResponse): class Response(BaseResponse):
"""An HTTP response class.
:param body: The body of the response. If a dictionary or list is given,
a JSON formatter is used to generate the body.
:param status_code: The numeric HTTP status code of the response. The
default is 200.
:param headers: A dictionary of headers to include in the response.
"""
async def write(self, stream): async def write(self, stream):
self.complete() self.complete()
@@ -77,6 +103,41 @@ class Response(BaseResponse):
class Microdot(BaseMicrodot): class Microdot(BaseMicrodot):
async def start_server(self, host='0.0.0.0', port=5000, debug=False): async def start_server(self, host='0.0.0.0', port=5000, debug=False):
"""Start the Microdot web server as a coroutine. This coroutine does
not normally return, as the server enters an endless listening loop.
The :func:`shutdown` function provides a method for terminating the
server gracefully.
:param host: The hostname or IP address of the network interface that
will be listening for requests. A value of ``'0.0.0.0'``
(the default) indicates that the server should listen for
requests on all the available interfaces, and a value of
``127.0.0.1`` indicates that the server should listen
for requests only on the internal networking interface of
the host.
:param port: The port number to listen for requests. The default is
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
This method is a coroutine.
Example::
import asyncio
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
async def index():
return 'Hello, world!'
async def main():
await app.start_server(debug=True)
asyncio.run(main())
"""
self.debug = debug self.debug = debug
async def serve(reader, writer): async def serve(reader, writer):
@@ -104,6 +165,34 @@ class Microdot(BaseMicrodot):
await self.server.wait_closed() await self.server.wait_closed()
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
the server enters an endless listening loop. The :func:`shutdown`
function provides a method for terminating the server gracefully.
:param host: The hostname or IP address of the network interface that
will be listening for requests. A value of ``'0.0.0.0'``
(the default) indicates that the server should listen for
requests on all the available interfaces, and a value of
``127.0.0.1`` indicates that the server should listen
for requests only on the internal networking interface of
the host.
:param port: The port number to listen for requests. The default is
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
Example::
from microdot_asyncio import Microdot
app = Microdot()
@app.route('/')
async def index():
return 'Hello, world!'
app.run(debug=True)
"""
asyncio.run(self.start_server(host=host, port=port, debug=debug)) asyncio.run(self.start_server(host=host, port=port, debug=debug))
def shutdown(self): def shutdown(self):

View File

@@ -1,3 +1,4 @@
from tests.microdot.test_multidict import TestMultiDict
from tests.microdot.test_request import TestRequest from tests.microdot.test_request import TestRequest
from tests.microdot.test_response import TestResponse from tests.microdot.test_response import TestResponse
from tests.microdot.test_url_pattern import TestURLPattern from tests.microdot.test_url_pattern import TestURLPattern

View File

@@ -65,6 +65,39 @@ 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_method_decorators(self):
app = Microdot()
@app.get('/get')
def get(req):
return 'GET'
@app.post('/post')
def post(req):
return 'POST'
@app.put('/put')
def put(req):
return 'PUT'
@app.patch('/patch')
def patch(req):
return 'PATCH'
@app.delete('/delete')
def delete(req):
return 'DELETE'
methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
mock_socket.clear_requests()
fds = [mock_socket.add_request(method, '/' + method.lower())
for method in methods]
self._add_shutdown(app)
app.run()
for fd, method in zip(fds, methods):
self.assertTrue(fd.response.endswith(
b'\r\n\r\n' + method.encode()))
def test_before_after_request(self): def test_before_after_request(self):
app = Microdot() app = Microdot()

View File

@@ -0,0 +1,31 @@
import unittest
from microdot import MultiDict
class TestMultiDict(unittest.TestCase):
def test_multidict(self):
d = MultiDict()
assert dict(d) == {}
assert d.get('zero') is None
assert d.get('zero', default=0) == 0
assert d.getlist('zero') == []
assert d.getlist('zero', type=int) == []
d['one'] = 1
assert d['one'] == 1
assert d.get('one') == 1
assert d.get('one', default=2) == 1
assert d.get('one', type=int) == 1
assert d.get('one', type=str) == '1'
d['two'] = 1
d['two'] = 2
assert d['two'] == 1
assert d.get('two') == 1
assert d.get('two', default=2) == 1
assert d.get('two', type=int) == 1
assert d.get('two', type=str) == '1'
assert d.getlist('two') == [1, 2]
assert d.getlist('two', type=int) == [1, 2]
assert d.getlist('two', type=str) == ['1', '2']

View File

@@ -1,5 +1,5 @@
import unittest import unittest
from microdot import Request from microdot import Request, MultiDict
from tests.mock_socket import get_request_fd from tests.mock_socket import get_request_fd
@@ -42,7 +42,8 @@ class TestRequest(unittest.TestCase):
fd = get_request_fd('GET', '/?foo=bar&abc=def&x=%2f%%') fd = get_request_fd('GET', '/?foo=bar&abc=def&x=%2f%%')
req = Request.create('app', fd, 'addr') req = Request.create('app', fd, 'addr')
self.assertEqual(req.query_string, 'foo=bar&abc=def&x=%2f%%') self.assertEqual(req.query_string, 'foo=bar&abc=def&x=%2f%%')
self.assertEqual(req.args, {'foo': 'bar', 'abc': 'def', 'x': '/%%'}) self.assertEqual(req.args, MultiDict(
{'foo': 'bar', 'abc': 'def', 'x': '/%%'}))
def test_json(self): def test_json(self):
fd = get_request_fd('GET', '/foo', headers={ fd = get_request_fd('GET', '/foo', headers={
@@ -68,7 +69,8 @@ class TestRequest(unittest.TestCase):
body='foo=bar&abc=def&x=%2f%%') body='foo=bar&abc=def&x=%2f%%')
req = Request.create('app', fd, 'addr') req = Request.create('app', fd, 'addr')
form = req.form form = req.form
self.assertEqual(form, {'foo': 'bar', 'abc': 'def', 'x': '/%%'}) self.assertEqual(form, MultiDict(
{'foo': 'bar', 'abc': 'def', 'x': '/%%'}))
self.assertTrue(req.form is form) self.assertTrue(req.form is form)
fd = get_request_fd('GET', '/foo', headers={ fd = get_request_fd('GET', '/foo', headers={

View File

@@ -4,6 +4,7 @@ except ImportError:
import asyncio import asyncio
import unittest import unittest
from microdot import MultiDict
from microdot_asyncio import Request from microdot_asyncio import Request
from tests.mock_socket import get_async_request_fd from tests.mock_socket import get_async_request_fd
@@ -51,7 +52,8 @@ class TestRequestAsync(unittest.TestCase):
fd = get_async_request_fd('GET', '/?foo=bar&abc=def&x=%2f%%') fd = get_async_request_fd('GET', '/?foo=bar&abc=def&x=%2f%%')
req = _run(Request.create('app', fd, 'addr')) req = _run(Request.create('app', fd, 'addr'))
self.assertEqual(req.query_string, 'foo=bar&abc=def&x=%2f%%') self.assertEqual(req.query_string, 'foo=bar&abc=def&x=%2f%%')
self.assertEqual(req.args, {'foo': 'bar', 'abc': 'def', 'x': '/%%'}) self.assertEqual(req.args, MultiDict(
{'foo': 'bar', 'abc': 'def', 'x': '/%%'}))
def test_json(self): def test_json(self):
fd = get_async_request_fd('GET', '/foo', headers={ fd = get_async_request_fd('GET', '/foo', headers={
@@ -77,7 +79,8 @@ class TestRequestAsync(unittest.TestCase):
body='foo=bar&abc=def&x=%2f%%') body='foo=bar&abc=def&x=%2f%%')
req = _run(Request.create('app', fd, 'addr')) req = _run(Request.create('app', fd, 'addr'))
form = req.form form = req.form
self.assertEqual(form, {'foo': 'bar', 'abc': 'def', 'x': '/%%'}) self.assertEqual(form, MultiDict(
{'foo': 'bar', 'abc': 'def', 'x': '/%%'}))
self.assertTrue(req.form is form) self.assertTrue(req.form is form)
fd = get_async_request_fd('GET', '/foo', headers={ fd = get_async_request_fd('GET', '/foo', headers={