Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a6026006f | ||
|
|
6712c47400 | ||
|
|
c8c91e8345 | ||
|
|
5d188e8c0d | ||
|
|
b80b6b64d0 | ||
|
|
28007ea583 | ||
|
|
300f8563ed | ||
|
|
1fc11193da | ||
|
|
79452a4699 | ||
|
|
84842e39c3 | ||
|
|
2a3c889717 | ||
|
|
ad368be993 | ||
|
|
3df56c6ffe |
1
.github/workflows/tests.yml
vendored
1
.github/workflows/tests.yml
vendored
@@ -16,6 +16,7 @@ jobs:
|
|||||||
- run: python -m pip install --upgrade pip wheel
|
- run: python -m pip install --upgrade pip wheel
|
||||||
- run: pip install tox tox-gh-actions
|
- run: pip install tox tox-gh-actions
|
||||||
- run: tox -eflake8
|
- run: tox -eflake8
|
||||||
|
- run: tox -edocs
|
||||||
tests:
|
tests:
|
||||||
name: tests
|
name: tests
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
14
CHANGES.md
14
CHANGES.md
@@ -1,5 +1,19 @@
|
|||||||
# Microdot change log
|
# Microdot change log
|
||||||
|
|
||||||
|
**Release 2.0.3** - 2024-01-07
|
||||||
|
|
||||||
|
- Add a limit to WebSocket message size [#193](https://github.com/miguelgrinberg/microdot/issues/193) ([commit](https://github.com/miguelgrinberg/microdot/commit/5d188e8c0ddef6ce633ca702dbdd4a90f2799597))
|
||||||
|
- Pass keyword arguments to thread executor in the correct way [#195](https://github.com/miguelgrinberg/microdot/issues/195) ([commit](https://github.com/miguelgrinberg/microdot/commit/6712c47400d7c426c88032f65ab74466524eccab))
|
||||||
|
- Update uasyncio library used in tests to include new TLS support ([commit](https://github.com/miguelgrinberg/microdot/commit/c8c91e83457d24320f22c9a74e80b15e06b072ca))
|
||||||
|
- Documentation improvements ([commit](https://github.com/miguelgrinberg/microdot/commit/b80b6b64d02d21400ca8a5077f5ed1127cc202ae))
|
||||||
|
|
||||||
|
**Release 2.0.2** - 2023-12-28
|
||||||
|
|
||||||
|
- Support binary data in the SSE extension ([commit](https://github.com/miguelgrinberg/microdot/commit/1fc11193da0d298f5539e2ad218836910a13efb2))
|
||||||
|
- Upgrade micropython tests to use v1.22 + initial CircuitPython testing work ([commit](https://github.com/miguelgrinberg/microdot/commit/79452a46992351ccad2c0317c20bf50be0d76641))
|
||||||
|
- Improvements to migration guide ([commit](https://github.com/miguelgrinberg/microdot/commit/84842e39c360a8b3ddf36feac8af201fb19bbb0b))
|
||||||
|
- Remove spurious async in documentation example [#187](https://github.com/miguelgrinberg/microdot/issues/187) ([commit](https://github.com/miguelgrinberg/microdot/commit/ad368be993e2e3007579f1d3880e36d60c71da92)) (thanks **Tak Tran**!)
|
||||||
|
|
||||||
**Release 2.0.1** - 2023-12-23
|
**Release 2.0.1** - 2023-12-23
|
||||||
|
|
||||||
- Addressed some inadvertent mistakes in the template extensions ([commit](https://github.com/miguelgrinberg/microdot/commit/bd18ceb4424e9dfb52b1e6d498edd260aa24fc53))
|
- Addressed some inadvertent mistakes in the template extensions ([commit](https://github.com/miguelgrinberg/microdot/commit/bd18ceb4424e9dfb52b1e6d498edd260aa24fc53))
|
||||||
|
|||||||
BIN
bin/micropython
BIN
bin/micropython
Binary file not shown.
42
docs/api.rst
42
docs/api.rst
@@ -1,8 +1,8 @@
|
|||||||
API Reference
|
API Reference
|
||||||
=============
|
=============
|
||||||
|
|
||||||
``microdot`` module
|
Core API
|
||||||
-------------------
|
--------
|
||||||
|
|
||||||
.. autoclass:: microdot.Microdot
|
.. autoclass:: microdot.Microdot
|
||||||
:members:
|
:members:
|
||||||
@@ -14,51 +14,57 @@ API Reference
|
|||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
``websocket`` extension
|
WebSocket
|
||||||
-----------------------
|
---------
|
||||||
|
|
||||||
.. automodule:: microdot.websocket
|
.. automodule:: microdot.websocket
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
``utemplate`` templating extension
|
Server-Sent Events (SSE)
|
||||||
----------------------------------
|
------------------------
|
||||||
|
|
||||||
|
.. automodule:: microdot.sse
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Templates (uTemplate)
|
||||||
|
---------------------
|
||||||
|
|
||||||
.. automodule:: microdot.utemplate
|
.. automodule:: microdot.utemplate
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
``jinja`` templating extension
|
Templates (Jinja)
|
||||||
------------------------------
|
-----------------
|
||||||
|
|
||||||
.. automodule:: microdot.jinja
|
.. automodule:: microdot.jinja
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
``session`` extension
|
User Sessions
|
||||||
---------------------
|
-------------
|
||||||
|
|
||||||
.. automodule:: microdot.session
|
.. automodule:: microdot.session
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
``cors`` extension
|
Cross-Origin Resource Sharing (CORS)
|
||||||
------------------
|
------------------------------------
|
||||||
|
|
||||||
.. automodule:: microdot.cors
|
.. automodule:: microdot.cors
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
``test_client`` extension
|
Test Client
|
||||||
-------------------------
|
-----------
|
||||||
|
|
||||||
.. automodule:: microdot.test_client
|
.. automodule:: microdot.test_client
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
``asgi`` extension
|
ASGI
|
||||||
------------------
|
----
|
||||||
|
|
||||||
.. autoclass:: microdot.asgi.Microdot
|
.. autoclass:: microdot.asgi.Microdot
|
||||||
:members:
|
:members:
|
||||||
:exclude-members: shutdown, run
|
:exclude-members: shutdown, run
|
||||||
|
|
||||||
``wsgi`` extension
|
WSGI
|
||||||
-------------------
|
----
|
||||||
|
|
||||||
.. autoclass:: microdot.wsgi.Microdot
|
.. autoclass:: microdot.wsgi.Microdot
|
||||||
:members:
|
:members:
|
||||||
|
|||||||
@@ -25,14 +25,15 @@ and incorporated into a custom MicroPython firmware.
|
|||||||
|
|
||||||
Use the following guidelines to know what files to copy:
|
Use the following guidelines to know what files to copy:
|
||||||
|
|
||||||
- For a minimal setup with only the base web server functionality, copy
|
* For a minimal setup with only the base web server functionality, copy
|
||||||
`microdot.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/microdot.py>`_
|
`microdot.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/microdot.py>`_
|
||||||
into your project.
|
into your project.
|
||||||
- For a configuration that includes one or more optional extensions, create a
|
* For a configuration that includes one or more optional extensions, create a
|
||||||
*microdot* directory in your device and copy the following files:
|
*microdot* directory in your device and copy the following files:
|
||||||
- `__init__.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/__init__.py>`_
|
|
||||||
- `microdot.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/microdot.py>`_
|
* `__init__.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/__init__.py>`_
|
||||||
- any needed `extensions <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot>`_.
|
* `microdot.py <https://github.com/miguelgrinberg/microdot/blob/main/src/microdot/microdot.py>`_
|
||||||
|
* any needed `extensions <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot>`_.
|
||||||
|
|
||||||
|
|
||||||
Getting Started
|
Getting Started
|
||||||
@@ -171,10 +172,8 @@ configure the web server.
|
|||||||
app.run(port=4443, debug=True, ssl=sslctx)
|
app.run(port=4443, debug=True, ssl=sslctx)
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
The ``ssl`` argument can only be used with CPython at this time, because
|
When using CPython, the certificate and key files must be given in PEM
|
||||||
MicroPython's asyncio module does not currently support SSL certificates or
|
format. When using MicroPython, these files must be given in DER format.
|
||||||
TLS encryption. Work on this is
|
|
||||||
`in progress <https://github.com/micropython/micropython/pull/11897>`_.
|
|
||||||
|
|
||||||
Defining Routes
|
Defining Routes
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
@@ -297,7 +296,7 @@ match and the route will not be called.
|
|||||||
A special type ``path`` can be used to capture the remainder of the path as a
|
A special type ``path`` can be used to capture the remainder of the path as a
|
||||||
single argument. The difference between an argument of type ``path`` and one of
|
single argument. The difference between an argument of type ``path`` and one of
|
||||||
type ``string`` is that the latter stops capturing when a ``/`` appears in the
|
type ``string`` is that the latter stops capturing when a ``/`` appears in the
|
||||||
URL.
|
URL::
|
||||||
|
|
||||||
@app.get('/tests/<path:path>')
|
@app.get('/tests/<path:path>')
|
||||||
async def get_test(request, path):
|
async def get_test(request, path):
|
||||||
@@ -462,7 +461,7 @@ the sub-applications to build the larger combined application::
|
|||||||
from customers import customers_app
|
from customers import customers_app
|
||||||
from orders import orders_app
|
from orders import orders_app
|
||||||
|
|
||||||
async def create_app():
|
def create_app():
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
app.mount(customers_app, url_prefix='/customers')
|
app.mount(customers_app, url_prefix='/customers')
|
||||||
app.mount(orders_app, url_prefix='/orders')
|
app.mount(orders_app, url_prefix='/orders')
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ extension.
|
|||||||
Any applications built using the asyncio extension will need to update their
|
Any applications built using the asyncio extension will need to update their
|
||||||
imports from this::
|
imports from this::
|
||||||
|
|
||||||
from microdot.asyncio import Microdot
|
from microdot_asyncio import Microdot
|
||||||
|
|
||||||
to this::
|
to this::
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ as a single string::
|
|||||||
|
|
||||||
Streamed templates also have an asynchronous version::
|
Streamed templates also have an asynchronous version::
|
||||||
|
|
||||||
return await Template('index.html').generate_async(title='Home')
|
return Template('index.html').generate_async(title='Home')
|
||||||
|
|
||||||
Class-based user sessions
|
Class-based user sessions
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -138,5 +138,8 @@ deployed with standard WSGI servers such as Gunicorn.
|
|||||||
|
|
||||||
WebSocket support when using the WSGI extension is enabled when using a
|
WebSocket support when using the WSGI extension is enabled when using a
|
||||||
compatible web server. At this time only Gunicorn is supported for WebSocket.
|
compatible web server. At this time only Gunicorn is supported for WebSocket.
|
||||||
|
Given that WebSocket support is asynchronous, it would be better to switch to
|
||||||
|
the ASGI extension, which has full support for WebSocket as defined in the ASGI
|
||||||
|
specification.
|
||||||
|
|
||||||
As before, the WSGI extension is not available under MicroPython.
|
As before, the WSGI extension is not available under MicroPython.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ssl
|
import ssl
|
||||||
|
import sys
|
||||||
from microdot import Microdot
|
from microdot import Microdot
|
||||||
|
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
@@ -31,6 +32,7 @@ async def shutdown(request):
|
|||||||
return 'The server is shutting down...'
|
return 'The server is shutting down...'
|
||||||
|
|
||||||
|
|
||||||
|
ext = 'der' if sys.implementation.name == 'micropython' else 'pem'
|
||||||
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
sslctx.load_cert_chain('cert.pem', 'key.pem')
|
sslctx.load_cert_chain('cert.' + ext, 'key.' + ext)
|
||||||
app.run(port=4443, debug=True, ssl=sslctx)
|
app.run(port=4443, debug=True, ssl=sslctx)
|
||||||
|
|||||||
@@ -13,12 +13,6 @@ class Stream:
|
|||||||
def get_extra_info(self, v):
|
def get_extra_info(self, v):
|
||||||
return self.e[v]
|
return self.e[v]
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc, tb):
|
|
||||||
await self.close()
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -63,6 +57,8 @@ class Stream:
|
|||||||
while True:
|
while True:
|
||||||
yield core._io_queue.queue_read(self.s)
|
yield core._io_queue.queue_read(self.s)
|
||||||
l2 = self.s.readline() # may do multiple reads but won't block
|
l2 = self.s.readline() # may do multiple reads but won't block
|
||||||
|
if l2 is None:
|
||||||
|
continue
|
||||||
l += l2
|
l += l2
|
||||||
if not l2 or l[-1] == 10: # \n (check l in case l2 is str)
|
if not l2 or l[-1] == 10: # \n (check l in case l2 is str)
|
||||||
return l
|
return l
|
||||||
@@ -100,19 +96,29 @@ StreamWriter = Stream
|
|||||||
# Create a TCP stream connection to a remote host
|
# Create a TCP stream connection to a remote host
|
||||||
#
|
#
|
||||||
# async
|
# async
|
||||||
def open_connection(host, port):
|
def open_connection(host, port, ssl=None, server_hostname=None):
|
||||||
from errno import EINPROGRESS
|
from errno import EINPROGRESS
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0] # TODO this is blocking!
|
ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0] # TODO this is blocking!
|
||||||
s = socket.socket(ai[0], ai[1], ai[2])
|
s = socket.socket(ai[0], ai[1], ai[2])
|
||||||
s.setblocking(False)
|
s.setblocking(False)
|
||||||
ss = Stream(s)
|
|
||||||
try:
|
try:
|
||||||
s.connect(ai[-1])
|
s.connect(ai[-1])
|
||||||
except OSError as er:
|
except OSError as er:
|
||||||
if er.errno != EINPROGRESS:
|
if er.errno != EINPROGRESS:
|
||||||
raise er
|
raise er
|
||||||
|
# wrap with SSL, if requested
|
||||||
|
if ssl:
|
||||||
|
if ssl is True:
|
||||||
|
import ssl as _ssl
|
||||||
|
|
||||||
|
ssl = _ssl.SSLContext(_ssl.PROTOCOL_TLS_CLIENT)
|
||||||
|
if not server_hostname:
|
||||||
|
server_hostname = host
|
||||||
|
s = ssl.wrap_socket(s, server_hostname=server_hostname, do_handshake_on_connect=False)
|
||||||
|
s.setblocking(False)
|
||||||
|
ss = Stream(s)
|
||||||
yield core._io_queue.queue_write(s)
|
yield core._io_queue.queue_write(s)
|
||||||
return ss, ss
|
return ss, ss
|
||||||
|
|
||||||
@@ -135,7 +141,7 @@ class Server:
|
|||||||
async def wait_closed(self):
|
async def wait_closed(self):
|
||||||
await self.task
|
await self.task
|
||||||
|
|
||||||
async def _serve(self, s, cb):
|
async def _serve(self, s, cb, ssl):
|
||||||
self.state = False
|
self.state = False
|
||||||
# Accept incoming connections
|
# Accept incoming connections
|
||||||
while True:
|
while True:
|
||||||
@@ -156,6 +162,13 @@ class Server:
|
|||||||
except:
|
except:
|
||||||
# Ignore a failed accept
|
# Ignore a failed accept
|
||||||
continue
|
continue
|
||||||
|
if ssl:
|
||||||
|
try:
|
||||||
|
s2 = ssl.wrap_socket(s2, server_side=True, do_handshake_on_connect=False)
|
||||||
|
except OSError as e:
|
||||||
|
core.sys.print_exception(e)
|
||||||
|
s2.close()
|
||||||
|
continue
|
||||||
s2.setblocking(False)
|
s2.setblocking(False)
|
||||||
s2s = Stream(s2, {"peername": addr})
|
s2s = Stream(s2, {"peername": addr})
|
||||||
core.create_task(cb(s2s, s2s))
|
core.create_task(cb(s2s, s2s))
|
||||||
@@ -163,7 +176,7 @@ class Server:
|
|||||||
|
|
||||||
# Helper function to start a TCP stream server, running as a new task
|
# Helper function to start a TCP stream server, running as a new task
|
||||||
# TODO could use an accept-callback on socket read activity instead of creating a task
|
# TODO could use an accept-callback on socket read activity instead of creating a task
|
||||||
async def start_server(cb, host, port, backlog=5):
|
async def start_server(cb, host, port, backlog=5, ssl=None):
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
# Create and bind server socket.
|
# Create and bind server socket.
|
||||||
@@ -176,7 +189,7 @@ async def start_server(cb, host, port, backlog=5):
|
|||||||
|
|
||||||
# Create and return server object and task.
|
# Create and return server object and task.
|
||||||
srv = Server()
|
srv = Server()
|
||||||
srv.task = core.create_task(srv._serve(s, cb))
|
srv.task = core.create_task(srv._serve(s, cb, ssl))
|
||||||
try:
|
try:
|
||||||
# Ensure that the _serve task has been scheduled so that it gets to
|
# Ensure that the _serve task has been scheduled so that it gets to
|
||||||
# handle cancellation.
|
# handle cancellation.
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
from utime import *
|
|
||||||
from micropython import const
|
|
||||||
|
|
||||||
_TS_YEAR = const(0)
|
|
||||||
_TS_MON = const(1)
|
|
||||||
_TS_MDAY = const(2)
|
|
||||||
_TS_HOUR = const(3)
|
|
||||||
_TS_MIN = const(4)
|
|
||||||
_TS_SEC = const(5)
|
|
||||||
_TS_WDAY = const(6)
|
|
||||||
_TS_YDAY = const(7)
|
|
||||||
_TS_ISDST = const(8)
|
|
||||||
|
|
||||||
_WDAY = const(("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"))
|
|
||||||
_MDAY = const(
|
|
||||||
(
|
|
||||||
"January",
|
|
||||||
"February",
|
|
||||||
"March",
|
|
||||||
"April",
|
|
||||||
"May",
|
|
||||||
"June",
|
|
||||||
"July",
|
|
||||||
"August",
|
|
||||||
"September",
|
|
||||||
"October",
|
|
||||||
"November",
|
|
||||||
"December",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def strftime(datefmt, ts):
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
fmtsp = False
|
|
||||||
ftime = StringIO()
|
|
||||||
for k in datefmt:
|
|
||||||
if fmtsp:
|
|
||||||
if k == "a":
|
|
||||||
ftime.write(_WDAY[ts[_TS_WDAY]][0:3])
|
|
||||||
elif k == "A":
|
|
||||||
ftime.write(_WDAY[ts[_TS_WDAY]])
|
|
||||||
elif k == "b":
|
|
||||||
ftime.write(_MDAY[ts[_TS_MON] - 1][0:3])
|
|
||||||
elif k == "B":
|
|
||||||
ftime.write(_MDAY[ts[_TS_MON] - 1])
|
|
||||||
elif k == "d":
|
|
||||||
ftime.write("%02d" % ts[_TS_MDAY])
|
|
||||||
elif k == "H":
|
|
||||||
ftime.write("%02d" % ts[_TS_HOUR])
|
|
||||||
elif k == "I":
|
|
||||||
ftime.write("%02d" % (ts[_TS_HOUR] % 12))
|
|
||||||
elif k == "j":
|
|
||||||
ftime.write("%03d" % ts[_TS_YDAY])
|
|
||||||
elif k == "m":
|
|
||||||
ftime.write("%02d" % ts[_TS_MON])
|
|
||||||
elif k == "M":
|
|
||||||
ftime.write("%02d" % ts[_TS_MIN])
|
|
||||||
elif k == "P":
|
|
||||||
ftime.write("AM" if ts[_TS_HOUR] < 12 else "PM")
|
|
||||||
elif k == "S":
|
|
||||||
ftime.write("%02d" % ts[_TS_SEC])
|
|
||||||
elif k == "w":
|
|
||||||
ftime.write(str(ts[_TS_WDAY]))
|
|
||||||
elif k == "y":
|
|
||||||
ftime.write("%02d" % (ts[_TS_YEAR] % 100))
|
|
||||||
elif k == "Y":
|
|
||||||
ftime.write(str(ts[_TS_YEAR]))
|
|
||||||
else:
|
|
||||||
ftime.write(k)
|
|
||||||
fmtsp = False
|
|
||||||
elif k == "%":
|
|
||||||
fmtsp = True
|
|
||||||
else:
|
|
||||||
ftime.write(k)
|
|
||||||
val = ftime.getvalue()
|
|
||||||
ftime.close()
|
|
||||||
return val
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "microdot"
|
name = "microdot"
|
||||||
version = "2.0.1"
|
version = "2.0.3"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
|
{ name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ class _BodyStream: # pragma: no cover
|
|||||||
|
|
||||||
|
|
||||||
class Microdot(BaseMicrodot):
|
class Microdot(BaseMicrodot):
|
||||||
|
"""A subclass of the core :class:`Microdot <microdot.Microdot>` class that
|
||||||
|
implements the ASGI protocol.
|
||||||
|
|
||||||
|
This class must be used as the application instance when running under an
|
||||||
|
ASGI web server.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.embedded_server = False
|
self.embedded_server = False
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import time
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from inspect import iscoroutinefunction, iscoroutine
|
from inspect import iscoroutinefunction, iscoroutine
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
async def invoke_handler(handler, *args, **kwargs):
|
async def invoke_handler(handler, *args, **kwargs):
|
||||||
"""Invoke a handler and return the result.
|
"""Invoke a handler and return the result.
|
||||||
@@ -23,7 +24,7 @@ try:
|
|||||||
ret = await handler(*args, **kwargs)
|
ret = await handler(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
ret = await asyncio.get_running_loop().run_in_executor(
|
ret = await asyncio.get_running_loop().run_in_executor(
|
||||||
None, handler, *args, **kwargs)
|
None, partial(handler, *args, **kwargs))
|
||||||
return ret
|
return ret
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
def iscoroutine(coro):
|
def iscoroutine(coro):
|
||||||
@@ -595,7 +596,7 @@ class Response:
|
|||||||
if expires:
|
if expires:
|
||||||
if isinstance(expires, str):
|
if isinstance(expires, str):
|
||||||
http_cookie += '; Expires=' + expires
|
http_cookie += '; Expires=' + expires
|
||||||
else:
|
else: # pragma: no cover
|
||||||
http_cookie += '; Expires=' + time.strftime(
|
http_cookie += '; Expires=' + time.strftime(
|
||||||
'%a, %d %b %Y %H:%M:%S GMT', expires.timetuple())
|
'%a, %d %b %Y %H:%M:%S GMT', expires.timetuple())
|
||||||
if max_age:
|
if max_age:
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ def with_session(f):
|
|||||||
return 'Hello, World!'
|
return 'Hello, World!'
|
||||||
|
|
||||||
Note that the decorator does not save the session. To update the session,
|
Note that the decorator does not save the session. To update the session,
|
||||||
call the :func:`update_session <microdot.session.update_session>` function.
|
call the :func:`session.save() <microdot.session.SessionDict.save>` method.
|
||||||
"""
|
"""
|
||||||
async def wrapper(request, *args, **kwargs):
|
async def wrapper(request, *args, **kwargs):
|
||||||
return await invoke_handler(
|
return await invoke_handler(
|
||||||
|
|||||||
@@ -3,18 +3,33 @@ import json
|
|||||||
|
|
||||||
|
|
||||||
class SSE:
|
class SSE:
|
||||||
|
"""Server-Sent Events object.
|
||||||
|
|
||||||
|
An object of this class is sent to handler functions to manage the SSE
|
||||||
|
connection.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.event = asyncio.Event()
|
self.event = asyncio.Event()
|
||||||
self.queue = []
|
self.queue = []
|
||||||
|
|
||||||
async def send(self, data, event=None):
|
async def send(self, data, event=None):
|
||||||
|
"""Send an event to the client.
|
||||||
|
|
||||||
|
:param data: the data to send. It can be given as a string, bytes, dict
|
||||||
|
or list. Dictionaries and lists are serialized to JSON.
|
||||||
|
Any other types are converted to string before sending.
|
||||||
|
:param event: an optional event name, to send along with the data. If
|
||||||
|
given, it must be a string.
|
||||||
|
"""
|
||||||
if isinstance(data, (dict, list)):
|
if isinstance(data, (dict, list)):
|
||||||
data = json.dumps(data)
|
data = json.dumps(data).encode()
|
||||||
elif not isinstance(data, str):
|
elif isinstance(data, str):
|
||||||
data = str(data)
|
data = data.encode()
|
||||||
data = f'data: {data}\n\n'
|
elif not isinstance(data, bytes):
|
||||||
|
data = str(data).encode()
|
||||||
|
data = b'data: ' + data + b'\n\n'
|
||||||
if event:
|
if event:
|
||||||
data = f'event: {event}\n{data}'
|
data = b'event: ' + event.encode() + b'\n' + data
|
||||||
self.queue.append(data)
|
self.queue.append(data)
|
||||||
self.event.set()
|
self.event.set()
|
||||||
|
|
||||||
@@ -30,19 +45,9 @@ def sse_response(request, event_function, *args, **kwargs):
|
|||||||
:param args: additional positional arguments to be passed to the response.
|
:param args: additional positional arguments to be passed to the response.
|
||||||
:param kwargs: additional keyword arguments to be passed to the response.
|
:param kwargs: additional keyword arguments to be passed to the response.
|
||||||
|
|
||||||
Example::
|
This is a low-level function that can be used to implement a custom SSE
|
||||||
|
endpoint. In general the :func:`microdot.sse.with_sse` decorator should be
|
||||||
@app.route('/events')
|
used instead.
|
||||||
async def events_route(request):
|
|
||||||
async def events(request, sse):
|
|
||||||
# send an unnamed event with string data
|
|
||||||
await sse.send('hello')
|
|
||||||
# send an unnamed event with JSON data
|
|
||||||
await sse.send({'foo': 'bar'})
|
|
||||||
# send a named event
|
|
||||||
await sse.send('hello', event='greeting')
|
|
||||||
|
|
||||||
return sse_response(request, events)
|
|
||||||
"""
|
"""
|
||||||
sse = SSE()
|
sse = SSE()
|
||||||
|
|
||||||
@@ -85,9 +90,14 @@ def with_sse(f):
|
|||||||
@app.route('/events')
|
@app.route('/events')
|
||||||
@with_sse
|
@with_sse
|
||||||
async def events(request, sse):
|
async def events(request, sse):
|
||||||
for i in range(10):
|
# send an unnamed event with string data
|
||||||
await asyncio.sleep(1)
|
await sse.send('hello')
|
||||||
await sse.send(f'{i}')
|
|
||||||
|
# send an unnamed event with JSON data
|
||||||
|
await sse.send({'foo': 'bar'})
|
||||||
|
|
||||||
|
# send a named event
|
||||||
|
await sse.send('hello', event='greeting')
|
||||||
"""
|
"""
|
||||||
async def sse_handler(request, *args, **kwargs):
|
async def sse_handler(request, *args, **kwargs):
|
||||||
return sse_response(request, f, *args, **kwargs)
|
return sse_response(request, f, *args, **kwargs)
|
||||||
|
|||||||
@@ -292,6 +292,8 @@ class TestClient:
|
|||||||
async def awrite(self, data):
|
async def awrite(self, data):
|
||||||
if self.started:
|
if self.started:
|
||||||
h = WebSocket._parse_frame_header(data[0:2])
|
h = WebSocket._parse_frame_header(data[0:2])
|
||||||
|
if h[1] not in [WebSocket.TEXT, WebSocket.BINARY]:
|
||||||
|
return
|
||||||
if h[3] < 0:
|
if h[3] < 0:
|
||||||
data = data[2 - h[3]:]
|
data = data[2 - h[3]:]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import binascii
|
import binascii
|
||||||
import hashlib
|
import hashlib
|
||||||
from microdot import Response
|
from microdot import Request, Response
|
||||||
from microdot.microdot import MUTED_SOCKET_ERRORS
|
from microdot.microdot import MUTED_SOCKET_ERRORS, print_exception
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketError(Exception):
|
||||||
|
"""Exception raised when an error occurs in a WebSocket connection."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class WebSocket:
|
class WebSocket:
|
||||||
|
"""A WebSocket connection object.
|
||||||
|
|
||||||
|
An instance of this class is sent to handler functions to manage the
|
||||||
|
WebSocket connection.
|
||||||
|
"""
|
||||||
CONT = 0
|
CONT = 0
|
||||||
TEXT = 1
|
TEXT = 1
|
||||||
BINARY = 2
|
BINARY = 2
|
||||||
@@ -12,6 +22,18 @@ class WebSocket:
|
|||||||
PING = 9
|
PING = 9
|
||||||
PONG = 10
|
PONG = 10
|
||||||
|
|
||||||
|
#: Specify the maximum message size that can be received when calling the
|
||||||
|
#: ``receive()`` method. Messages with payloads that are larger than this
|
||||||
|
#: size will be rejected and the connection closed. Set to 0 to disable
|
||||||
|
#: the size check (be aware of potential security issues if you do this),
|
||||||
|
#: or to -1 to use the value set in
|
||||||
|
#: ``Request.max_body_length``. The default is -1.
|
||||||
|
#:
|
||||||
|
#: Example::
|
||||||
|
#:
|
||||||
|
#: WebSocket.max_message_length = 4 * 1024 # up to 4KB messages
|
||||||
|
max_message_length = -1
|
||||||
|
|
||||||
def __init__(self, request):
|
def __init__(self, request):
|
||||||
self.request = request
|
self.request = request
|
||||||
self.closed = False
|
self.closed = False
|
||||||
@@ -26,6 +48,7 @@ class WebSocket:
|
|||||||
b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
|
b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
|
||||||
|
|
||||||
async def receive(self):
|
async def receive(self):
|
||||||
|
"""Receive a message from the client."""
|
||||||
while True:
|
while True:
|
||||||
opcode, payload = await self._read_frame()
|
opcode, payload = await self._read_frame()
|
||||||
send_opcode, data = self._process_websocket_frame(opcode, payload)
|
send_opcode, data = self._process_websocket_frame(opcode, payload)
|
||||||
@@ -35,12 +58,20 @@ class WebSocket:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
async def send(self, data, opcode=None):
|
async def send(self, data, opcode=None):
|
||||||
|
"""Send a message to the client.
|
||||||
|
|
||||||
|
:param data: the data to send, given as a string or bytes.
|
||||||
|
:param opcode: a custom frame opcode to use. If not given, the opcode
|
||||||
|
is ``TEXT`` or ``BINARY`` depending on the type of the
|
||||||
|
data.
|
||||||
|
"""
|
||||||
frame = self._encode_websocket_frame(
|
frame = self._encode_websocket_frame(
|
||||||
opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
|
opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
|
||||||
data)
|
data)
|
||||||
await self.request.sock[1].awrite(frame)
|
await self.request.sock[1].awrite(frame)
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
|
"""Close the websocket connection."""
|
||||||
if not self.closed: # pragma: no cover
|
if not self.closed: # pragma: no cover
|
||||||
self.closed = True
|
self.closed = True
|
||||||
await self.send(b'', self.CLOSE)
|
await self.send(b'', self.CLOSE)
|
||||||
@@ -72,7 +103,7 @@ class WebSocket:
|
|||||||
fin = header[0] & 0x80
|
fin = header[0] & 0x80
|
||||||
opcode = header[0] & 0x0f
|
opcode = header[0] & 0x0f
|
||||||
if fin == 0 or opcode == cls.CONT: # pragma: no cover
|
if fin == 0 or opcode == cls.CONT: # pragma: no cover
|
||||||
raise OSError(32, 'Continuation frames not supported')
|
raise WebSocketError('Continuation frames not supported')
|
||||||
has_mask = header[1] & 0x80
|
has_mask = header[1] & 0x80
|
||||||
length = header[1] & 0x7f
|
length = header[1] & 0x7f
|
||||||
if length == 126:
|
if length == 126:
|
||||||
@@ -87,7 +118,7 @@ class WebSocket:
|
|||||||
elif opcode == self.BINARY:
|
elif opcode == self.BINARY:
|
||||||
pass
|
pass
|
||||||
elif opcode == self.CLOSE:
|
elif opcode == self.CLOSE:
|
||||||
raise OSError(32, 'Websocket connection closed')
|
raise WebSocketError('Websocket connection closed')
|
||||||
elif opcode == self.PING:
|
elif opcode == self.PING:
|
||||||
return self.PONG, payload
|
return self.PONG, payload
|
||||||
elif opcode == self.PONG: # pragma: no branch
|
elif opcode == self.PONG: # pragma: no branch
|
||||||
@@ -114,7 +145,7 @@ class WebSocket:
|
|||||||
async def _read_frame(self):
|
async def _read_frame(self):
|
||||||
header = await self.request.sock[0].read(2)
|
header = await self.request.sock[0].read(2)
|
||||||
if len(header) != 2: # pragma: no cover
|
if len(header) != 2: # pragma: no cover
|
||||||
raise OSError(32, 'Websocket connection closed')
|
raise WebSocketError('Websocket connection closed')
|
||||||
fin, opcode, has_mask, length = self._parse_frame_header(header)
|
fin, opcode, has_mask, length = self._parse_frame_header(header)
|
||||||
if length == -2:
|
if length == -2:
|
||||||
length = await self.request.sock[0].read(2)
|
length = await self.request.sock[0].read(2)
|
||||||
@@ -122,6 +153,10 @@ class WebSocket:
|
|||||||
elif length == -8:
|
elif length == -8:
|
||||||
length = await self.request.sock[0].read(8)
|
length = await self.request.sock[0].read(8)
|
||||||
length = int.from_bytes(length, 'big')
|
length = int.from_bytes(length, 'big')
|
||||||
|
max_allowed_length = Request.max_body_length \
|
||||||
|
if self.max_message_length == -1 else self.max_message_length
|
||||||
|
if length > max_allowed_length:
|
||||||
|
raise WebSocketError('Message too large')
|
||||||
if has_mask: # pragma: no cover
|
if has_mask: # pragma: no cover
|
||||||
mask = await self.request.sock[0].read(4)
|
mask = await self.request.sock[0].read(4)
|
||||||
payload = await self.request.sock[0].read(length)
|
payload = await self.request.sock[0].read(length)
|
||||||
@@ -161,11 +196,19 @@ def websocket_wrapper(f, upgrade_function):
|
|||||||
ws = await upgrade_function(request)
|
ws = await upgrade_function(request)
|
||||||
try:
|
try:
|
||||||
await f(request, ws, *args, **kwargs)
|
await f(request, ws, *args, **kwargs)
|
||||||
await ws.close() # pragma: no cover
|
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
if exc.errno not in MUTED_SOCKET_ERRORS: # pragma: no cover
|
if exc.errno not in MUTED_SOCKET_ERRORS: # pragma: no cover
|
||||||
raise
|
raise
|
||||||
return ''
|
except WebSocketError:
|
||||||
|
pass
|
||||||
|
except Exception as exc:
|
||||||
|
print_exception(exc)
|
||||||
|
finally: # pragma: no cover
|
||||||
|
try:
|
||||||
|
await ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return Response.already_handled
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ from microdot.websocket import WebSocket, websocket_upgrade, \
|
|||||||
|
|
||||||
|
|
||||||
class Microdot(BaseMicrodot):
|
class Microdot(BaseMicrodot):
|
||||||
|
"""A subclass of the core :class:`Microdot <microdot.Microdot>` class that
|
||||||
|
implements the WSGI protocol.
|
||||||
|
|
||||||
|
This class must be used as the application instance when running under a
|
||||||
|
WSGI web server.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.loop = asyncio.new_event_loop()
|
self.loop = asyncio.new_event_loop()
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ class TestMicrodot(unittest.TestCase):
|
|||||||
async def index2(req):
|
async def index2(req):
|
||||||
return 'foo-async'
|
return 'foo-async'
|
||||||
|
|
||||||
|
@app.route('/arg/<id>')
|
||||||
|
def index3(req, id):
|
||||||
|
return id
|
||||||
|
|
||||||
|
@app.route('/arg/async/<id>')
|
||||||
|
async def index4(req, id):
|
||||||
|
return f'async-{id}'
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
res = self._run(client.get('/'))
|
res = self._run(client.get('/'))
|
||||||
@@ -45,6 +53,24 @@ class TestMicrodot(unittest.TestCase):
|
|||||||
self.assertEqual(res.body, b'foo-async')
|
self.assertEqual(res.body, b'foo-async')
|
||||||
self.assertEqual(res.json, None)
|
self.assertEqual(res.json, None)
|
||||||
|
|
||||||
|
res = self._run(client.get('/arg/123'))
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
self.assertEqual(res.headers['Content-Type'],
|
||||||
|
'text/plain; charset=UTF-8')
|
||||||
|
self.assertEqual(res.headers['Content-Length'], '3')
|
||||||
|
self.assertEqual(res.text, '123')
|
||||||
|
self.assertEqual(res.body, b'123')
|
||||||
|
self.assertEqual(res.json, None)
|
||||||
|
|
||||||
|
res = self._run(client.get('/arg/async/123'))
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
self.assertEqual(res.headers['Content-Type'],
|
||||||
|
'text/plain; charset=UTF-8')
|
||||||
|
self.assertEqual(res.headers['Content-Length'], '9')
|
||||||
|
self.assertEqual(res.text, 'async-123')
|
||||||
|
self.assertEqual(res.body, b'async-123')
|
||||||
|
self.assertEqual(res.json, None)
|
||||||
|
|
||||||
def test_post_request(self):
|
def test_post_request(self):
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime
|
|
||||||
import unittest
|
import unittest
|
||||||
from microdot import Response
|
from microdot import Response
|
||||||
from tests.mock_socket import FakeStreamAsync
|
from tests.mock_socket import FakeStreamAsync
|
||||||
@@ -186,12 +185,12 @@ class TestResponse(unittest.TestCase):
|
|||||||
res.set_cookie('foo2', 'bar2', path='/', partitioned=True)
|
res.set_cookie('foo2', 'bar2', path='/', partitioned=True)
|
||||||
res.set_cookie('foo3', 'bar3', domain='example.com:1234')
|
res.set_cookie('foo3', 'bar3', domain='example.com:1234')
|
||||||
res.set_cookie('foo4', 'bar4',
|
res.set_cookie('foo4', 'bar4',
|
||||||
expires=datetime(2019, 11, 5, 2, 23, 54))
|
expires='Tue, 05 Nov 2019 02:23:54 GMT')
|
||||||
res.set_cookie('foo5', 'bar5', max_age=123,
|
res.set_cookie('foo5', 'bar5', max_age=123,
|
||||||
expires='Thu, 01 Jan 1970 00:00:00 GMT')
|
expires='Thu, 01 Jan 1970 00:00:00 GMT')
|
||||||
res.set_cookie('foo6', 'bar6', secure=True, http_only=True)
|
res.set_cookie('foo6', 'bar6', secure=True, http_only=True)
|
||||||
res.set_cookie('foo7', 'bar7', path='/foo', domain='example.com:1234',
|
res.set_cookie('foo7', 'bar7', path='/foo', domain='example.com:1234',
|
||||||
expires=datetime(2019, 11, 5, 2, 23, 54), max_age=123,
|
expires='Tue, 05 Nov 2019 02:23:54 GMT', max_age=123,
|
||||||
secure=True, http_only=True)
|
secure=True, http_only=True)
|
||||||
res.delete_cookie('foo8', http_only=True)
|
res.delete_cookie('foo8', http_only=True)
|
||||||
self.assertEqual(res.headers, {'Set-Cookie': [
|
self.assertEqual(res.headers, {'Set-Cookie': [
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class TestWebSocket(unittest.TestCase):
|
|||||||
await sse.send({'foo': 'bar'})
|
await sse.send({'foo': 'bar'})
|
||||||
await sse.send([42, 'foo', 'bar'])
|
await sse.send([42, 'foo', 'bar'])
|
||||||
await sse.send(ValueError('foo'))
|
await sse.send(ValueError('foo'))
|
||||||
|
await sse.send(b'foo')
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
response = self._run(client.get('/sse'))
|
response = self._run(client.get('/sse'))
|
||||||
@@ -35,4 +36,5 @@ class TestWebSocket(unittest.TestCase):
|
|||||||
'event: test\ndata: bar\n\n'
|
'event: test\ndata: bar\n\n'
|
||||||
'data: {"foo": "bar"}\n\n'
|
'data: {"foo": "bar"}\n\n'
|
||||||
'data: [42, "foo", "bar"]\n\n'
|
'data: [42, "foo", "bar"]\n\n'
|
||||||
|
'data: foo\n\n'
|
||||||
'data: foo\n\n'))
|
'data: foo\n\n'))
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from microdot import Microdot
|
from microdot import Microdot, Request
|
||||||
from microdot.websocket import with_websocket, WebSocket
|
from microdot.websocket import with_websocket, WebSocket, WebSocketError
|
||||||
from microdot.test_client import TestClient
|
from microdot.test_client import TestClient
|
||||||
|
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ class TestWebSocket(unittest.TestCase):
|
|||||||
return self.loop.run_until_complete(coro)
|
return self.loop.run_until_complete(coro)
|
||||||
|
|
||||||
def test_websocket_echo(self):
|
def test_websocket_echo(self):
|
||||||
|
WebSocket.max_message_length = 65537
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
|
|
||||||
@app.route('/echo')
|
@app.route('/echo')
|
||||||
@@ -26,34 +27,10 @@ class TestWebSocket(unittest.TestCase):
|
|||||||
data = await ws.receive()
|
data = await ws.receive()
|
||||||
await ws.send(data)
|
await ws.send(data)
|
||||||
|
|
||||||
results = []
|
@app.route('/divzero')
|
||||||
|
|
||||||
def ws():
|
|
||||||
data = yield 'hello'
|
|
||||||
results.append(data)
|
|
||||||
data = yield b'bye'
|
|
||||||
results.append(data)
|
|
||||||
data = yield b'*' * 300
|
|
||||||
results.append(data)
|
|
||||||
data = yield b'+' * 65537
|
|
||||||
results.append(data)
|
|
||||||
|
|
||||||
client = TestClient(app)
|
|
||||||
res = self._run(client.websocket('/echo', ws))
|
|
||||||
self.assertIsNone(res)
|
|
||||||
self.assertEqual(results, ['hello', b'bye', b'*' * 300, b'+' * 65537])
|
|
||||||
|
|
||||||
@unittest.skipIf(sys.implementation.name == 'micropython',
|
|
||||||
'no support for async generators in MicroPython')
|
|
||||||
def test_websocket_echo_async_client(self):
|
|
||||||
app = Microdot()
|
|
||||||
|
|
||||||
@app.route('/echo')
|
|
||||||
@with_websocket
|
@with_websocket
|
||||||
async def index(req, ws):
|
async def divzero(req, ws):
|
||||||
while True:
|
1 / 0
|
||||||
data = await ws.receive()
|
|
||||||
await ws.send(data)
|
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
@@ -72,6 +49,35 @@ class TestWebSocket(unittest.TestCase):
|
|||||||
self.assertIsNone(res)
|
self.assertIsNone(res)
|
||||||
self.assertEqual(results, ['hello', b'bye', b'*' * 300, b'+' * 65537])
|
self.assertEqual(results, ['hello', b'bye', b'*' * 300, b'+' * 65537])
|
||||||
|
|
||||||
|
res = self._run(client.websocket('/divzero', ws))
|
||||||
|
self.assertIsNone(res)
|
||||||
|
WebSocket.max_message_length = -1
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.implementation.name == 'micropython',
|
||||||
|
'no support for async generators in MicroPython')
|
||||||
|
def test_websocket_large_message(self):
|
||||||
|
saved_max_body_length = Request.max_body_length
|
||||||
|
Request.max_body_length = 10
|
||||||
|
app = Microdot()
|
||||||
|
|
||||||
|
@app.route('/echo')
|
||||||
|
@with_websocket
|
||||||
|
async def index(req, ws):
|
||||||
|
data = await ws.receive()
|
||||||
|
await ws.send(data)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
async def ws():
|
||||||
|
data = yield '0123456789abcdef'
|
||||||
|
results.append(data)
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
res = self._run(client.websocket('/echo', ws))
|
||||||
|
self.assertIsNone(res)
|
||||||
|
self.assertEqual(results, [])
|
||||||
|
Request.max_body_length = saved_max_body_length
|
||||||
|
|
||||||
def test_bad_websocket_request(self):
|
def test_bad_websocket_request(self):
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
|
|
||||||
@@ -106,7 +112,7 @@ class TestWebSocket(unittest.TestCase):
|
|||||||
(None, 'foo'))
|
(None, 'foo'))
|
||||||
self.assertEqual(ws._process_websocket_frame(WebSocket.BINARY, b'foo'),
|
self.assertEqual(ws._process_websocket_frame(WebSocket.BINARY, b'foo'),
|
||||||
(None, b'foo'))
|
(None, b'foo'))
|
||||||
self.assertRaises(OSError, ws._process_websocket_frame,
|
self.assertRaises(WebSocketError, ws._process_websocket_frame,
|
||||||
WebSocket.CLOSE, b'')
|
WebSocket.CLOSE, b'')
|
||||||
self.assertEqual(ws._process_websocket_frame(WebSocket.PING, b'foo'),
|
self.assertEqual(ws._process_websocket_frame(WebSocket.PING, b'foo'),
|
||||||
(WebSocket.PONG, b'foo'))
|
(WebSocket.PONG, b'foo'))
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
FROM ubuntu:22.04
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ARG VERSION=master
|
||||||
|
ENV VERSION=$VERSION
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y build-essential libffi-dev git pkg-config python3 && \
|
apt-get install -y build-essential libffi-dev git pkg-config python3 && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
git clone https://github.com/micropython/micropython.git && \
|
git clone https://github.com/micropython/micropython.git && \
|
||||||
cd micropython && \
|
cd micropython && \
|
||||||
|
git checkout $VERSION && \
|
||||||
git submodule update --init && \
|
git submodule update --init && \
|
||||||
cd mpy-cross && \
|
cd mpy-cross && \
|
||||||
make && \
|
make && \
|
||||||
cd .. && \
|
cd .. && \
|
||||||
cd ports/unix && \
|
cd ports/unix && \
|
||||||
make && \
|
make && \
|
||||||
make test && \
|
|
||||||
make install && \
|
make install && \
|
||||||
apt-get purge --auto-remove -y build-essential libffi-dev git pkg-config python3 && \
|
apt-get purge --auto-remove -y build-essential libffi-dev git pkg-config python3 && \
|
||||||
cd ../../.. && \
|
cd ../../.. && \
|
||||||
rm -rf micropython
|
rm -rf micropython
|
||||||
|
|
||||||
CMD ["/usr/local/bin/micropython"]
|
CMD ["/usr/local/bin/micropython"]
|
||||||
|
|
||||||
|
|||||||
24
tools/Dockerfile.circuitpython
Normal file
24
tools/Dockerfile.circuitpython
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ARG VERSION=main
|
||||||
|
ENV VERSION=$VERSION
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y build-essential libffi-dev git pkg-config python3 && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
git clone https://github.com/adafruit/circuitpython.git && \
|
||||||
|
cd circuitpython && \
|
||||||
|
git checkout $VERSION && \
|
||||||
|
git submodule update --init lib tools frozen && \
|
||||||
|
cd mpy-cross && \
|
||||||
|
make && \
|
||||||
|
cd .. && \
|
||||||
|
cd ports/unix && \
|
||||||
|
make && \
|
||||||
|
make install && \
|
||||||
|
apt-get purge --auto-remove -y build-essential libffi-dev git pkg-config python3 && \
|
||||||
|
cd ../../.. && \
|
||||||
|
rm -rf circuitpython
|
||||||
|
|
||||||
|
CMD ["/usr/local/bin/micropython"]
|
||||||
11
tools/update-circuitpython.sh
Executable file
11
tools/update-circuitpython.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# this script updates the micropython binary in the /bin directory that is
|
||||||
|
# used to run unit tests under GitHub Actions builds
|
||||||
|
|
||||||
|
DOCKER=${DOCKER:-docker}
|
||||||
|
VERSION=${1:-main}
|
||||||
|
|
||||||
|
$DOCKER build -f Dockerfile.circuitpython --build-arg VERSION=$VERSION -t circuitpython .
|
||||||
|
$DOCKER create -t --name dummy-circuitpython circuitpython
|
||||||
|
$DOCKER cp dummy-circuitpython:/usr/local/bin/micropython ../bin/circuitpython
|
||||||
|
$DOCKER rm dummy-circuitpython
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
# used to run unit tests under GitHub Actions builds
|
# used to run unit tests under GitHub Actions builds
|
||||||
|
|
||||||
DOCKER=${DOCKER:-docker}
|
DOCKER=${DOCKER:-docker}
|
||||||
|
VERSION=${1:-master}
|
||||||
|
|
||||||
$DOCKER build -t micropython .
|
$DOCKER build --build-arg VERSION=$VERSION -t micropython .
|
||||||
$DOCKER create -it --name dummy-micropython micropython
|
$DOCKER create -it --name dummy-micropython micropython
|
||||||
$DOCKER cp dummy-micropython:/usr/local/bin/micropython ../bin/micropython
|
$DOCKER cp dummy-micropython:/usr/local/bin/micropython ../bin/micropython
|
||||||
$DOCKER rm dummy-micropython
|
$DOCKER rm dummy-micropython
|
||||||
|
|||||||
17
tox.ini
17
tox.ini
@@ -1,5 +1,5 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist=flake8,py38,py39,py310,py311,py312,upy,benchmark
|
envlist=flake8,py38,py39,py310,py311,py312,upy,cpy,benchmark,docs
|
||||||
skipsdist=True
|
skipsdist=True
|
||||||
skip_missing_interpreters=True
|
skip_missing_interpreters=True
|
||||||
|
|
||||||
@@ -29,10 +29,13 @@ setenv=
|
|||||||
allowlist_externals=sh
|
allowlist_externals=sh
|
||||||
commands=sh -c "bin/micropython run_tests.py"
|
commands=sh -c "bin/micropython run_tests.py"
|
||||||
|
|
||||||
|
[testenv:cpy]
|
||||||
|
allowlist_externals=sh
|
||||||
|
commands=sh -c "bin/circuitpython run_tests.py"
|
||||||
|
|
||||||
[testenv:upy-mac]
|
[testenv:upy-mac]
|
||||||
allowlist_externals=micropython
|
allowlist_externals=micropython
|
||||||
commands=micropython run_tests.py
|
commands=micropython run_tests.py
|
||||||
deps=
|
|
||||||
|
|
||||||
[testenv:benchmark]
|
[testenv:benchmark]
|
||||||
deps=
|
deps=
|
||||||
@@ -55,3 +58,13 @@ deps=
|
|||||||
flake8
|
flake8
|
||||||
commands=
|
commands=
|
||||||
flake8 --ignore=W503 --exclude examples/templates/utemplate/templates src tests examples
|
flake8 --ignore=W503 --exclude examples/templates/utemplate/templates src tests examples
|
||||||
|
|
||||||
|
[testenv:docs]
|
||||||
|
changedir=docs
|
||||||
|
deps=
|
||||||
|
sphinx
|
||||||
|
pyjwt
|
||||||
|
allowlist_externals=
|
||||||
|
make
|
||||||
|
commands=
|
||||||
|
make html
|
||||||
|
|||||||
Reference in New Issue
Block a user