microdot 2.3.1__tar.gz → 2.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. {microdot-2.3.1/src/microdot.egg-info → microdot-2.3.3}/PKG-INFO +1 -1
  2. {microdot-2.3.1 → microdot-2.3.3}/docs/api.rst +2 -0
  3. {microdot-2.3.1 → microdot-2.3.3}/docs/extensions.rst +52 -5
  4. {microdot-2.3.1 → microdot-2.3.3}/docs/intro.rst +34 -15
  5. {microdot-2.3.1 → microdot-2.3.3}/pyproject.toml +1 -1
  6. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/auth.py +2 -2
  7. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/microdot.py +37 -5
  8. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/sse.py +13 -2
  9. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/test_client.py +40 -4
  10. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/websocket.py +4 -4
  11. {microdot-2.3.1 → microdot-2.3.3/src/microdot.egg-info}/PKG-INFO +1 -1
  12. {microdot-2.3.1 → microdot-2.3.3}/tests/test_sse.py +37 -0
  13. {microdot-2.3.1 → microdot-2.3.3}/LICENSE +0 -0
  14. {microdot-2.3.1 → microdot-2.3.3}/MANIFEST.in +0 -0
  15. {microdot-2.3.1 → microdot-2.3.3}/README.md +0 -0
  16. {microdot-2.3.1 → microdot-2.3.3}/docs/Makefile +0 -0
  17. {microdot-2.3.1 → microdot-2.3.3}/docs/_static/css/custom.css +0 -0
  18. {microdot-2.3.1 → microdot-2.3.3}/docs/conf.py +0 -0
  19. {microdot-2.3.1 → microdot-2.3.3}/docs/freezing.rst +0 -0
  20. {microdot-2.3.1 → microdot-2.3.3}/docs/index.rst +0 -0
  21. {microdot-2.3.1 → microdot-2.3.3}/docs/make.bat +0 -0
  22. {microdot-2.3.1 → microdot-2.3.3}/docs/migrating.rst +0 -0
  23. {microdot-2.3.1 → microdot-2.3.3}/setup.cfg +0 -0
  24. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/__init__.py +0 -0
  25. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/asgi.py +0 -0
  26. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/cors.py +0 -0
  27. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/helpers.py +0 -0
  28. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/jinja.py +0 -0
  29. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/login.py +0 -0
  30. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/multipart.py +0 -0
  31. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/session.py +0 -0
  32. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/utemplate.py +0 -0
  33. {microdot-2.3.1 → microdot-2.3.3}/src/microdot/wsgi.py +0 -0
  34. {microdot-2.3.1 → microdot-2.3.3}/src/microdot.egg-info/SOURCES.txt +0 -0
  35. {microdot-2.3.1 → microdot-2.3.3}/src/microdot.egg-info/dependency_links.txt +0 -0
  36. {microdot-2.3.1 → microdot-2.3.3}/src/microdot.egg-info/not-zip-safe +0 -0
  37. {microdot-2.3.1 → microdot-2.3.3}/src/microdot.egg-info/requires.txt +0 -0
  38. {microdot-2.3.1 → microdot-2.3.3}/src/microdot.egg-info/top_level.txt +0 -0
  39. {microdot-2.3.1 → microdot-2.3.3}/tests/__init__.py +0 -0
  40. {microdot-2.3.1 → microdot-2.3.3}/tests/files/test.bin +0 -0
  41. {microdot-2.3.1 → microdot-2.3.3}/tests/files/test.css +0 -0
  42. {microdot-2.3.1 → microdot-2.3.3}/tests/files/test.gif +0 -0
  43. {microdot-2.3.1 → microdot-2.3.3}/tests/files/test.gz +0 -0
  44. {microdot-2.3.1 → microdot-2.3.3}/tests/files/test.html +0 -0
  45. {microdot-2.3.1 → microdot-2.3.3}/tests/files/test.jpg +0 -0
  46. {microdot-2.3.1 → microdot-2.3.3}/tests/files/test.js +0 -0
  47. {microdot-2.3.1 → microdot-2.3.3}/tests/files/test.json +0 -0
  48. {microdot-2.3.1 → microdot-2.3.3}/tests/files/test.png +0 -0
  49. {microdot-2.3.1 → microdot-2.3.3}/tests/files/test.txt +0 -0
  50. {microdot-2.3.1 → microdot-2.3.3}/tests/files/test.txt.gz +0 -0
  51. {microdot-2.3.1 → microdot-2.3.3}/tests/mock_socket.py +0 -0
  52. {microdot-2.3.1 → microdot-2.3.3}/tests/templates/hello.jinja.txt +0 -0
  53. {microdot-2.3.1 → microdot-2.3.3}/tests/templates/hello.utemplate.txt +0 -0
  54. {microdot-2.3.1 → microdot-2.3.3}/tests/templates/hello_utemplate_txt.py +0 -0
  55. {microdot-2.3.1 → microdot-2.3.3}/tests/test_asgi.py +0 -0
  56. {microdot-2.3.1 → microdot-2.3.3}/tests/test_auth.py +0 -0
  57. {microdot-2.3.1 → microdot-2.3.3}/tests/test_cors.py +0 -0
  58. {microdot-2.3.1 → microdot-2.3.3}/tests/test_end2end.py +0 -0
  59. {microdot-2.3.1 → microdot-2.3.3}/tests/test_jinja.py +0 -0
  60. {microdot-2.3.1 → microdot-2.3.3}/tests/test_login.py +0 -0
  61. {microdot-2.3.1 → microdot-2.3.3}/tests/test_microdot.py +0 -0
  62. {microdot-2.3.1 → microdot-2.3.3}/tests/test_multidict.py +0 -0
  63. {microdot-2.3.1 → microdot-2.3.3}/tests/test_multipart.py +0 -0
  64. {microdot-2.3.1 → microdot-2.3.3}/tests/test_request.py +0 -0
  65. {microdot-2.3.1 → microdot-2.3.3}/tests/test_response.py +0 -0
  66. {microdot-2.3.1 → microdot-2.3.3}/tests/test_session.py +0 -0
  67. {microdot-2.3.1 → microdot-2.3.3}/tests/test_url_pattern.py +0 -0
  68. {microdot-2.3.1 → microdot-2.3.3}/tests/test_urlencode.py +0 -0
  69. {microdot-2.3.1 → microdot-2.3.3}/tests/test_utemplate.py +0 -0
  70. {microdot-2.3.1 → microdot-2.3.3}/tests/test_websocket.py +0 -0
  71. {microdot-2.3.1 → microdot-2.3.3}/tests/test_wsgi.py +0 -0
  72. {microdot-2.3.1 → microdot-2.3.3}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdot
3
- Version: 2.3.1
3
+ Version: 2.3.3
4
4
  Summary: The impossibly small web framework for MicroPython
5
5
  Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/miguelgrinberg/microdot
@@ -13,6 +13,8 @@ Core API
13
13
  .. autoclass:: microdot.Response
14
14
  :members:
15
15
 
16
+ .. autoclass:: microdot.URLPattern
17
+ :members:
16
18
 
17
19
  Multipart Forms
18
20
  ---------------
@@ -116,6 +116,33 @@ Example::
116
116
  message = await ws.receive()
117
117
  await ws.send(message)
118
118
 
119
+ To end the WebSocket connection, the route handler can exit, without returning
120
+ anything::
121
+
122
+ @app.route('/echo')
123
+ @with_websocket
124
+ async def echo(request, ws):
125
+ while True:
126
+ message = await ws.receive()
127
+ if message == 'exit':
128
+ break
129
+ await ws.send(message)
130
+ await ws.send('goodbye')
131
+
132
+ If the client ends the WebSocket connection from their side, the route function
133
+ is cancelled. The route function can catch the ``CancelledError`` exception
134
+ from asyncio to perform cleanup tasks::
135
+
136
+ @app.route('/echo')
137
+ @with_websocket
138
+ async def echo(request, ws):
139
+ try:
140
+ while True:
141
+ message = await ws.receive()
142
+ await ws.send(message)
143
+ except asyncio.CancelledError:
144
+ print('Client disconnected!')
145
+
119
146
  Server-Sent Events
120
147
  ~~~~~~~~~~~~~~~~~~
121
148
 
@@ -153,6 +180,25 @@ Example::
153
180
  await sse.send({'counter': i}) # unnamed event
154
181
  await sse.send('end', event='comment') # named event
155
182
 
183
+ To end the SSE connection, the route handler can exit, without returning
184
+ anything, as shown in the above examples.
185
+
186
+ If the client ends the SSE connection from their side, the route function is
187
+ cancelled. The route function can catch the ``CancelledError`` exception from
188
+ asyncio to perform cleanup tasks::
189
+
190
+ @app.route('/events')
191
+ @with_sse
192
+ async def events(request, sse):
193
+ try:
194
+ i = 0
195
+ while True:
196
+ await asyncio.sleep(1)
197
+ await sse.send({'counter': i})
198
+ i += 1
199
+ except asyncio.CancelledError:
200
+ print('Client disconnected!')
201
+
156
202
  .. note::
157
203
  The SSE protocol is unidirectional, so there is no ``receive()`` method in
158
204
  the SSE object. For bidirectional communication with the client, use the
@@ -420,13 +466,13 @@ be protected with the ``auth.optional`` decorator::
420
466
  @app.route('/')
421
467
  @auth.optional
422
468
  async def index(request):
423
- if g.current_user:
469
+ if request.g.current_user:
424
470
  return f'Hello, {request.g.current_user}!'
425
471
  else:
426
472
  return 'Hello, anonymous user!'
427
473
 
428
- As shown in the example, a route can check ``g.current_user`` to determine if
429
- the user is authenticated or not.
474
+ As shown in the example, a route can check ``request.g.current_user`` to
475
+ determine if the user is authenticated or not.
430
476
 
431
477
  Token Authentication
432
478
  ^^^^^^^^^^^^^^^^^^^^
@@ -446,7 +492,8 @@ or ``None`` if the token is invalid or expired::
446
492
  return load_user_from_token(token)
447
493
 
448
494
  As with Basic authentication, the ``auth`` instance is used as a decorator to
449
- protect your routes::
495
+ protect your routes, and the authenticated user is accessible from the request
496
+ object as ``request.g.current_user``::
450
497
 
451
498
  @app.route('/')
452
499
  @auth
@@ -458,7 +505,7 @@ Optional authentication can also be used with tokens::
458
505
  @app.route('/')
459
506
  @auth.optional
460
507
  async def index(request):
461
- if g.current_user:
508
+ if request.g.current_user:
462
509
  return f'Hello, {request.g.current_user}!'
463
510
  else:
464
511
  return 'Hello, anonymous user!'
@@ -601,6 +601,13 @@ The request object provides access to the request attributes, including:
601
601
  specified by the client, or ``None`` if no content type was specified.
602
602
  - :attr:`content_length <microdot.Request.content_length>`: The content
603
603
  length of the request, or 0 if no content length was specified.
604
+ - :attr:`json <microdot.Request.json>`: The parsed JSON data in the request
605
+ body. See :ref:`below <JSON Payloads>` for additional details.
606
+ - :attr:`form <microdot.Request.form>`: The parsed form data in the request
607
+ body, as a dictionary. See :ref:`below <Form Data>` for additional details.
608
+ - :attr:`files <microdot.Request.files>`: A dictionary with the file uploads
609
+ included in the request body. Note that file uploads are only supported when
610
+ the :ref:`Multipart Forms` extension is used.
604
611
  - :attr:`client_addr <microdot.Request.client_addr>`: The network address of
605
612
  the client, as a tuple (host, port).
606
613
  - :attr:`app <microdot.Request.app>`: The application instance that created the
@@ -627,8 +634,8 @@ to use this attribute::
627
634
  The client must set the ``Content-Type`` header to ``application/json`` for
628
635
  the ``json`` attribute of the request object to be populated.
629
636
 
630
- URLEncoded Form Data
631
- ^^^^^^^^^^^^^^^^^^^^
637
+ Form Data
638
+ ^^^^^^^^^
632
639
 
633
640
  The request object also supports standard HTML form submissions through the
634
641
  :attr:`form <microdot.Request.form>` attribute, which presents the form data
@@ -642,9 +649,10 @@ as a :class:`MultiDict <microdot.MultiDict>` object. Example::
642
649
  return f'Hello {name}'
643
650
 
644
651
  .. note::
645
- Form submissions are only parsed when the ``Content-Type`` header is set by
646
- the client to ``application/x-www-form-urlencoded``. Form submissions using
647
- the ``multipart/form-data`` content type are currently not supported.
652
+ Form submissions automatically parsed when the ``Content-Type`` header is
653
+ set by the client to ``application/x-www-form-urlencoded``. For form
654
+ submissions that use the ``multipart/form-data`` content type the
655
+ :ref:`Multipart Forms` extension must be used.
648
656
 
649
657
  Accessing the Raw Request Body
650
658
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -749,15 +757,18 @@ sections describe the different types of responses that are supported.
749
757
  The Three Parts of a Response
750
758
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
751
759
 
752
- Route functions can return one, two or three values. The first or only value is
753
- always returned to the client in the response body::
760
+ Route functions can return one, two or three values. The first and most
761
+ important value is the response body::
754
762
 
755
763
  @app.get('/')
756
764
  async def index(request):
757
765
  return 'Hello, World!'
758
766
 
759
- In the above example, Microdot issues a standard 200 status code response, and
760
- inserts default headers.
767
+ In the above example, Microdot issues a standard 200 status code response
768
+ indicating a successful request. The body of the response is the
769
+ ``'Hello, World!'`` string returned by the function. Microdot includes default
770
+ headers with this response, including the ``Content-Type`` header set to
771
+ ``text/plain`` to indicate a response in plain text.
761
772
 
762
773
  The application can provide its own status code as a second value returned from
763
774
  the route to override the 200 default. The example below returns a 202 status
@@ -769,22 +780,30 @@ code::
769
780
 
770
781
  The application can also return a third value, a dictionary with additional
771
782
  headers that are added to, or replace the default ones included by Microdot.
772
- The next example returns an HTML response, instead of a default text response::
783
+ The next example returns an HTML response, instead of the default plain text
784
+ response::
773
785
 
774
786
  @app.get('/')
775
787
  async def index(request):
776
788
  return '<h1>Hello, World!</h1>', 202, {'Content-Type': 'text/html'}
777
789
 
778
- If the application needs to return custom headers, but does not need to change
779
- the default status code, then it can return two values, omitting the status
780
- code::
790
+ If the application does not need to return a body, then it can omit it and
791
+ have the status code as the first or only returned value::
792
+
793
+ @app.get('/')
794
+ async def index(request):
795
+ return 204
796
+
797
+ Likewise, if the application needs to return a body and custom headers, but
798
+ does not need to change the default status code, then it can return two values,
799
+ omitting the status code::
781
800
 
782
801
  @app.get('/')
783
802
  async def index(request):
784
803
  return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'}
785
804
 
786
- The application can also return a :class:`Response <microdot.Response>` object
787
- containing all the details of the response as a single value.
805
+ Lastly, the application can also return a :class:`Response <microdot.Response>`
806
+ object containing all the details of the response as a single value.
788
807
 
789
808
  JSON Responses
790
809
  ^^^^^^^^^^^^^^
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "microdot"
3
- version = "2.3.1"
3
+ version = "2.3.3"
4
4
  authors = [
5
5
  { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
6
6
  ]
@@ -85,7 +85,7 @@ class BasicAuth(BaseAuth):
85
85
  return None
86
86
  return username, password
87
87
 
88
- def authentication_error(self, request):
88
+ async def authentication_error(self, request):
89
89
  return '', self.error_status, {
90
90
  'WWW-Authenticate': '{} realm="{}", charset="{}"'.format(
91
91
  self.scheme, self.realm, self.charset)}
@@ -158,5 +158,5 @@ class TokenAuth(BaseAuth):
158
158
  """
159
159
  self.error_callback = f
160
160
 
161
- def authentication_error(self, request):
161
+ async def authentication_error(self, request):
162
162
  abort(self.error_status)
@@ -554,6 +554,7 @@ class Response:
554
554
  'json': 'application/json',
555
555
  'png': 'image/png',
556
556
  'txt': 'text/plain',
557
+ 'svg': 'image/svg+xml',
557
558
  }
558
559
 
559
560
  send_file_buffer_size = 1024
@@ -814,6 +815,17 @@ class Response:
814
815
 
815
816
 
816
817
  class URLPattern():
818
+ """A class that represents the URL pattern for a route.
819
+
820
+ :param url_pattern: The route URL pattern, which can include static and
821
+ dynamic path segments. Dynamic segments are enclosed in
822
+ ``<`` and ``>``. The type of the segment can be given
823
+ as a prefix, separated from the name with a colon.
824
+ Supported types are ``string`` (the default),
825
+ ``int`` and ``path``. Custom types can be registered
826
+ using the :meth:`URLPattern.register_type` method.
827
+ """
828
+
817
829
  segment_patterns = {
818
830
  'string': '/([^/]+)',
819
831
  'int': '/(-?\\d+)',
@@ -823,12 +835,32 @@ class URLPattern():
823
835
  'int': lambda value: int(value),
824
836
  }
825
837
 
838
+ @classmethod
839
+ def register_type(cls, type_name, pattern='[^/]+', parser=None):
840
+ """Register a new URL segment type.
841
+
842
+ :param type_name: The name of the segment type to register.
843
+ :param pattern: The regular expression pattern to use when matching
844
+ this segment type. If not given, a default matcher for
845
+ a single path segment is used.
846
+ :param parser: A callable that will be used to parse and transform the
847
+ value of the segment. If omitted, the value is returned
848
+ as a string.
849
+ """
850
+ cls.segment_patterns[type_name] = '/({})'.format(pattern)
851
+ cls.segment_parsers[type_name] = parser
852
+
826
853
  def __init__(self, url_pattern):
827
854
  self.url_pattern = url_pattern
828
855
  self.segments = []
829
856
  self.regex = None
830
857
 
831
858
  def compile(self):
859
+ """Generate a regular expression for the URL pattern.
860
+
861
+ This method is automatically invoked the first time the URL pattern is
862
+ matched against a path.
863
+ """
832
864
  pattern = ''
833
865
  for segment in self.url_pattern.lstrip('/').split('/'):
834
866
  if segment and segment[0] == '<':
@@ -856,12 +888,12 @@ class URLPattern():
856
888
  self.regex = re.compile('^' + pattern + '$')
857
889
  return self.regex
858
890
 
859
- @classmethod
860
- def register_type(cls, type_name, pattern='[^/]+', parser=None):
861
- cls.segment_patterns[type_name] = '/({})'.format(pattern)
862
- cls.segment_parsers[type_name] = parser
863
-
864
891
  def match(self, path):
892
+ """Match a path against the URL pattern.
893
+
894
+ Returns a dictionary with the values of all dynamic path segments if a
895
+ matche is found, or ``None`` if the path does not match this pattern.
896
+ """
865
897
  args = {}
866
898
  g = (self.regex or self.compile()).match(path)
867
899
  if not g:
@@ -61,7 +61,14 @@ def sse_response(request, event_function, *args, **kwargs):
61
61
  sse = SSE()
62
62
 
63
63
  async def sse_task_wrapper():
64
- await event_function(request, sse, *args, **kwargs)
64
+ try:
65
+ await event_function(request, sse, *args, **kwargs)
66
+ except asyncio.CancelledError: # pragma: no cover
67
+ pass
68
+ except Exception as exc:
69
+ # the SSE task raised an exception so we need to pass it to the
70
+ # main route so that it is re-raised there
71
+ sse.queue.append(exc)
65
72
  sse.event.set()
66
73
 
67
74
  task = asyncio.create_task(sse_task_wrapper())
@@ -79,7 +86,11 @@ def sse_response(request, event_function, *args, **kwargs):
79
86
  except IndexError:
80
87
  await sse.event.wait()
81
88
  sse.event.clear()
82
- if event is None:
89
+ if isinstance(event, Exception):
90
+ # if the event is an exception we re-raise it here so that it
91
+ # can be handled appropriately
92
+ raise event
93
+ elif event is None:
83
94
  raise StopAsyncIteration
84
95
  return event
85
96
 
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from microdot.microdot import Request, Response, AsyncBytesIO
2
3
 
3
4
  try:
@@ -32,6 +33,11 @@ class TestResponse:
32
33
  #: The body of the JSON response, decoded to a dictionary or list. Set
33
34
  #: ``Note`` if the response does not have a JSON payload.
34
35
  self.json = None
36
+ #: The body of the SSE response, decoded to a list of events, each
37
+ #: given as a dictionary with a ``data`` key and optionally also
38
+ #: ``event`` and ``id`` keys. Set to ``None`` if the response does not
39
+ #: have an SSE payload.
40
+ self.events = None
35
41
 
36
42
  def _initialize_response(self, res):
37
43
  self.status_code = res.status_code
@@ -41,10 +47,13 @@ class TestResponse:
41
47
  async def _initialize_body(self, res):
42
48
  self.body = b''
43
49
  iter = res.body_iter()
44
- async for body in iter: # pragma: no branch
45
- if isinstance(body, str):
46
- body = body.encode()
47
- self.body += body
50
+ try:
51
+ async for body in iter: # pragma: no branch
52
+ if isinstance(body, str):
53
+ body = body.encode()
54
+ self.body += body
55
+ except asyncio.CancelledError: # pragma: no cover
56
+ pass
48
57
  if hasattr(iter, 'aclose'): # pragma: no branch
49
58
  await iter.aclose()
50
59
 
@@ -60,6 +69,32 @@ class TestResponse:
60
69
  if content_type.split(';')[0] == 'application/json':
61
70
  self.json = json.loads(self.text)
62
71
 
72
+ def _process_sse_body(self):
73
+ if 'Content-Type' in self.headers: # pragma: no branch
74
+ content_type = self.headers['Content-Type']
75
+ if content_type.split(';')[0] == 'text/event-stream':
76
+ self.events = []
77
+ for sse_event in self.body.split(b'\n\n'):
78
+ data = None
79
+ event = None
80
+ event_id = None
81
+ for line in sse_event.split(b'\n'):
82
+ if line.startswith(b'data:'):
83
+ data = line[5:].strip()
84
+ elif line.startswith(b'event:'):
85
+ event = line[6:].strip().decode()
86
+ elif line.startswith(b'id:'):
87
+ event_id = line[3:].strip().decode()
88
+ if data:
89
+ data_json = None
90
+ try:
91
+ data_json = json.loads(data)
92
+ except ValueError:
93
+ pass
94
+ self.events.append({
95
+ "data": data, "data_json": data_json,
96
+ "event": event, "event_id": event_id})
97
+
63
98
  @classmethod
64
99
  async def create(cls, res):
65
100
  test_res = cls()
@@ -68,6 +103,7 @@ class TestResponse:
68
103
  await test_res._initialize_body(res)
69
104
  test_res._process_text_body()
70
105
  test_res._process_json_body()
106
+ test_res._process_sse_body()
71
107
  return test_res
72
108
 
73
109
 
@@ -149,18 +149,18 @@ class WebSocket:
149
149
  raise WebSocketError('Websocket connection closed')
150
150
  fin, opcode, has_mask, length = self._parse_frame_header(header)
151
151
  if length == -2:
152
- length = await self.request.sock[0].read(2)
152
+ length = await self.request.sock[0].readexactly(2)
153
153
  length = int.from_bytes(length, 'big')
154
154
  elif length == -8:
155
- length = await self.request.sock[0].read(8)
155
+ length = await self.request.sock[0].readexactly(8)
156
156
  length = int.from_bytes(length, 'big')
157
157
  max_allowed_length = Request.max_body_length \
158
158
  if self.max_message_length == -1 else self.max_message_length
159
159
  if length > max_allowed_length:
160
160
  raise WebSocketError('Message too large')
161
161
  if has_mask: # pragma: no cover
162
- mask = await self.request.sock[0].read(4)
163
- payload = await self.request.sock[0].read(length)
162
+ mask = await self.request.sock[0].readexactly(4)
163
+ payload = await self.request.sock[0].readexactly(length)
164
164
  if has_mask: # pragma: no cover
165
165
  payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
166
166
  return opcode, payload
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdot
3
- Version: 2.3.1
3
+ Version: 2.3.3
4
4
  Summary: The impossibly small web framework for MicroPython
5
5
  Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/miguelgrinberg/microdot
@@ -42,3 +42,40 @@ class TestWebSocket(unittest.TestCase):
42
42
  'data: [42, "foo", "bar"]\n\n'
43
43
  'data: foo\n\n'
44
44
  'data: foo\n\n'))
45
+ self.assertEqual(len(response.events), 8)
46
+ self.assertEqual(response.events[0], {
47
+ 'data': b'foo', 'data_json': None, 'event': None,
48
+ 'event_id': None})
49
+ self.assertEqual(response.events[1], {
50
+ 'data': b'bar', 'data_json': None, 'event': 'test',
51
+ 'event_id': None})
52
+ self.assertEqual(response.events[2], {
53
+ 'data': b'bar', 'data_json': None, 'event': 'test',
54
+ 'event_id': 'id42'})
55
+ self.assertEqual(response.events[3], {
56
+ 'data': b'bar', 'data_json': None, 'event': None,
57
+ 'event_id': 'id42'})
58
+ self.assertEqual(response.events[4], {
59
+ 'data': b'{"foo": "bar"}', 'data_json': {'foo': 'bar'},
60
+ 'event': None, 'event_id': None})
61
+ self.assertEqual(response.events[5], {
62
+ 'data': b'[42, "foo", "bar"]', 'data_json': [42, 'foo', 'bar'],
63
+ 'event': None, 'event_id': None})
64
+ self.assertEqual(response.events[6], {
65
+ 'data': b'foo', 'data_json': None, 'event': None,
66
+ 'event_id': None})
67
+ self.assertEqual(response.events[7], {
68
+ 'data': b'foo', 'data_json': None, 'event': None,
69
+ 'event_id': None})
70
+
71
+ def test_sse_exception(self):
72
+ app = Microdot()
73
+
74
+ @app.route('/sse')
75
+ @with_sse
76
+ async def handle_sse(request, sse):
77
+ await sse.send('foo')
78
+ await sse.send(1 / 0)
79
+
80
+ client = TestClient(app)
81
+ self.assertRaises(ZeroDivisionError, self._run, client.get('/sse'))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes