microdot
To provide HTTP services, you can use the open-source microdot
library. Refer microdot.
Note
This library is not integrated by default in the gateway and must be manually included in your script for use.
For larger static pages, consider using cloud-hosted static resources.
Example
Description
The example uses Microdot to provide an HTTP service on port 60000 and registers three routes.
/
: Serve static page responses/hello
: Return JSON Responses/api/app/config/meta
: Return JSON responses for RESTful API requests from static pages.
Code
1# ===========================
2# microdot
3# ===========================
4
5"""
6microdot
7--------
8https://microdot.readthedocs.io/en/stable/intro.html#running-with-micropython
9
10The ``microdot`` module defines a few classes that help implement HTTP-based
11servers for MicroPython and standard Python.
12"""
13import asyncio
14import io
15import re
16import time
17
18try:
19 import orjson as json
20except ImportError:
21 import json
22
23try:
24 from inspect import iscoroutinefunction, iscoroutine
25 from functools import partial
26
27 async def invoke_handler(handler, *args, **kwargs):
28 """Invoke a handler and return the result.
29
30 This method runs sync handlers in a thread pool executor.
31 """
32 if iscoroutinefunction(handler):
33 ret = await handler(*args, **kwargs)
34 else:
35 ret = await asyncio.get_running_loop().run_in_executor(
36 None, partial(handler, *args, **kwargs)
37 )
38 return ret
39
40except ImportError: # pragma: no cover
41
42 def iscoroutine(coro):
43 return hasattr(coro, "send") and hasattr(coro, "throw")
44
45 async def invoke_handler(handler, *args, **kwargs):
46 """Invoke a handler and return the result.
47
48 This method runs sync handlers in the asyncio thread, which can
49 potentially cause blocking and performance issues.
50 """
51 ret = handler(*args, **kwargs)
52 if iscoroutine(ret):
53 ret = await ret
54 return ret
55
56
57try:
58 from sys import print_exception
59except ImportError: # pragma: no cover
60 import traceback
61
62 def print_exception(exc):
63 traceback.print_exc()
64
65
66MUTED_SOCKET_ERRORS = [
67 32, # Broken pipe
68 54, # Connection reset by peer
69 104, # Connection reset by peer
70 128, # Operation on closed socket
71]
72
73
74def urldecode(s):
75 if isinstance(s, str):
76 s = s.encode()
77 s = s.replace(b"+", b" ")
78 parts = s.split(b"%")
79 if len(parts) == 1:
80 return s.decode()
81 result = [parts[0]]
82 for item in parts[1:]:
83 if item == b"":
84 result.append(b"%")
85 else:
86 code = item[:2]
87 result.append(bytes([int(code, 16)]))
88 result.append(item[2:])
89 return b"".join(result).decode()
90
91
92def urlencode(s):
93 return (
94 s.replace("+", "%2B")
95 .replace(" ", "+")
96 .replace("%", "%25")
97 .replace("?", "%3F")
98 .replace("#", "%23")
99 .replace("&", "%26")
100 .replace("=", "%3D")
101 )
102
103
104class NoCaseDict(dict):
105 """A subclass of dictionary that holds case-insensitive keys.
106
107 :param initial_dict: an initial dictionary of key/value pairs to
108 initialize this object with.
109
110 Example::
111
112 >>> d = NoCaseDict()
113 >>> d['Content-Type'] = 'text/html'
114 >>> print(d['Content-Type'])
115 text/html
116 >>> print(d['content-type'])
117 text/html
118 >>> print(d['CONTENT-TYPE'])
119 text/html
120 >>> del d['cOnTeNt-TyPe']
121 >>> print(d)
122 {}
123 """
124
125 def __init__(self, initial_dict=None):
126 super().__init__(initial_dict or {})
127 self.keymap = {k.lower(): k for k in self.keys() if k.lower() != k}
128
129 def __setitem__(self, key, value):
130 kl = key.lower()
131 key = self.keymap.get(kl, key)
132 if kl != key:
133 self.keymap[kl] = key
134 super().__setitem__(key, value)
135
136 def __getitem__(self, key):
137 kl = key.lower()
138 return super().__getitem__(self.keymap.get(kl, kl))
139
140 def __delitem__(self, key):
141 kl = key.lower()
142 super().__delitem__(self.keymap.get(kl, kl))
143
144 def __contains__(self, key):
145 kl = key.lower()
146 return self.keymap.get(kl, kl) in self.keys()
147
148 def get(self, key, default=None):
149 kl = key.lower()
150 return super().get(self.keymap.get(kl, kl), default)
151
152 def update(self, other_dict):
153 for key, value in other_dict.items():
154 self[key] = value
155
156
157def mro(cls): # pragma: no cover
158 """Return the method resolution order of a class.
159
160 This is a helper function that returns the method resolution order of a
161 class. It is used by Microdot to find the best error handler to invoke for
162 the raised exception.
163
164 In CPython, this function returns the ``__mro__`` attribute of the class.
165 In MicroPython, this function implements a recursive depth-first scanning
166 of the class hierarchy.
167 """
168 if hasattr(cls, "mro"):
169 return cls.__mro__
170
171 def _mro(cls):
172 m = [cls]
173 for base in cls.__bases__:
174 m += _mro(base)
175 return m
176
177 mro_list = _mro(cls)
178
179 # If a class appears multiple times (due to multiple inheritance) remove
180 # all but the last occurence. This matches the method resolution order
181 # of MicroPython, but not CPython.
182 mro_pruned = []
183 for i in range(len(mro_list)):
184 base = mro_list.pop(0)
185 if base not in mro_list:
186 mro_pruned.append(base)
187 return mro_pruned
188
189
190class MultiDict(dict):
191 """A subclass of dictionary that can hold multiple values for the same
192 key. It is used to hold key/value pairs decoded from query strings and
193 form submissions.
194
195 :param initial_dict: an initial dictionary of key/value pairs to
196 initialize this object with.
197
198 Example::
199
200 >>> d = MultiDict()
201 >>> d['sort'] = 'name'
202 >>> d['sort'] = 'email'
203 >>> print(d['sort'])
204 'name'
205 >>> print(d.getlist('sort'))
206 ['name', 'email']
207 """
208
209 def __init__(self, initial_dict=None):
210 super().__init__()
211 if initial_dict:
212 for key, value in initial_dict.items():
213 self[key] = value
214
215 def __setitem__(self, key, value):
216 if key not in self:
217 super().__setitem__(key, [])
218 super().__getitem__(key).append(value)
219
220 def __getitem__(self, key):
221 return super().__getitem__(key)[0]
222
223 def get(self, key, default=None, type=None):
224 """Return the value for a given key.
225
226 :param key: The key to retrieve.
227 :param default: A default value to use if the key does not exist.
228 :param type: A type conversion callable to apply to the value.
229
230 If the multidict contains more than one value for the requested key,
231 this method returns the first value only.
232
233 Example::
234
235 >>> d = MultiDict()
236 >>> d['age'] = '42'
237 >>> d.get('age')
238 '42'
239 >>> d.get('age', type=int)
240 42
241 >>> d.get('name', default='noname')
242 'noname'
243 """
244 if key not in self:
245 return default
246 value = self[key]
247 if type is not None:
248 value = type(value)
249 return value
250
251 def getlist(self, key, type=None):
252 """Return all the values for a given key.
253
254 :param key: The key to retrieve.
255 :param type: A type conversion callable to apply to the values.
256
257 If the requested key does not exist in the dictionary, this method
258 returns an empty list.
259
260 Example::
261
262 >>> d = MultiDict()
263 >>> d.getlist('items')
264 []
265 >>> d['items'] = '3'
266 >>> d.getlist('items')
267 ['3']
268 >>> d['items'] = '56'
269 >>> d.getlist('items')
270 ['3', '56']
271 >>> d.getlist('items', type=int)
272 [3, 56]
273 """
274 if key not in self:
275 return []
276 values = super().__getitem__(key)
277 if type is not None:
278 values = [type(value) for value in values]
279 return values
280
281
282class AsyncBytesIO:
283 """An async wrapper for BytesIO."""
284
285 def __init__(self, data):
286 self.stream = io.BytesIO(data)
287
288 async def read(self, n=-1):
289 return self.stream.read(n)
290
291 async def readline(self): # pragma: no cover
292 return self.stream.readline()
293
294 async def readexactly(self, n): # pragma: no cover
295 return self.stream.read(n)
296
297 async def readuntil(self, separator=b"\n"): # pragma: no cover
298 return self.stream.readuntil(separator=separator)
299
300 async def awrite(self, data): # pragma: no cover
301 return self.stream.write(data)
302
303 async def aclose(self): # pragma: no cover
304 pass
305
306
307class Request:
308 """An HTTP request."""
309
310 #: Specify the maximum payload size that is accepted. Requests with larger
311 #: payloads will be rejected with a 413 status code. Applications can
312 #: change this maximum as necessary.
313 #:
314 #: Example::
315 #:
316 #: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed
317 max_content_length = 16 * 1024
318
319 #: Specify the maximum payload size that can be stored in ``body``.
320 #: Requests with payloads that are larger than this size and up to
321 #: ``max_content_length`` bytes will be accepted, but the application will
322 #: only be able to access the body of the request by reading from
323 #: ``stream``. Set to 0 if you always access the body as a stream.
324 #:
325 #: Example::
326 #:
327 #: Request.max_body_length = 4 * 1024 # up to 4KB bodies read
328 max_body_length = 16 * 1024
329
330 #: Specify the maximum length allowed for a line in the request. Requests
331 #: with longer lines will not be correctly interpreted. Applications can
332 #: change this maximum as necessary.
333 #:
334 #: Example::
335 #:
336 #: Request.max_readline = 16 * 1024 # 16KB lines allowed
337 max_readline = 2 * 1024
338
339 class G:
340 pass
341
342 def __init__(
343 self,
344 app,
345 client_addr,
346 method,
347 url,
348 http_version,
349 headers,
350 body=None,
351 stream=None,
352 sock=None,
353 url_prefix="",
354 subapp=None,
355 ):
356 #: The application instance to which this request belongs.
357 self.app = app
358 #: The address of the client, as a tuple (host, port).
359 self.client_addr = client_addr
360 #: The HTTP method of the request.
361 self.method = method
362 #: The request URL, including the path and query string.
363 self.url = url
364 #: The URL prefix, if the endpoint comes from a mounted
365 #: sub-application, or else ''.
366 self.url_prefix = url_prefix
367 #: The sub-application instance, or `None` if this isn't a mounted
368 #: endpoint.
369 self.subapp = subapp
370 #: The path portion of the URL.
371 self.path = url
372 #: The query string portion of the URL.
373 self.query_string = None
374 #: The parsed query string, as a
375 #: :class:`MultiDict <microdot.MultiDict>` object.
376 self.args = {}
377 #: A dictionary with the headers included in the request.
378 self.headers = headers
379 #: A dictionary with the cookies included in the request.
380 self.cookies = {}
381 #: The parsed ``Content-Length`` header.
382 self.content_length = 0
383 #: The parsed ``Content-Type`` header.
384 self.content_type = None
385 #: A general purpose container for applications to store data during
386 #: the life of the request.
387 self.g = Request.G()
388
389 self.http_version = http_version
390 if "?" in self.path:
391 self.path, self.query_string = self.path.split("?", 1)
392 self.args = self._parse_urlencoded(self.query_string)
393
394 if "Content-Length" in self.headers:
395 self.content_length = int(self.headers["Content-Length"])
396 if "Content-Type" in self.headers:
397 self.content_type = self.headers["Content-Type"]
398 if "Cookie" in self.headers:
399 for cookie in self.headers["Cookie"].split(";"):
400 name, value = cookie.strip().split("=", 1)
401 self.cookies[name] = value
402
403 self._body = body
404 self.body_used = False
405 self._stream = stream
406 self.sock = sock
407 self._json = None
408 self._form = None
409 self._files = None
410 self.after_request_handlers = []
411
412 @staticmethod
413 async def create(app, client_reader, client_writer, client_addr):
414 """Create a request object.
415
416 :param app: The Microdot application instance.
417 :param client_reader: An input stream from where the request data can
418 be read.
419 :param client_writer: An output stream where the response data can be
420 written.
421 :param client_addr: The address of the client, as a tuple.
422
423 This method is a coroutine. It returns a newly created ``Request``
424 object.
425 """
426 # request line
427 line = (await Request._safe_readline(client_reader)).strip().decode()
428 if not line: # pragma: no cover
429 return None
430 method, url, http_version = line.split()
431 http_version = http_version.split("/", 1)[1]
432
433 # headers
434 headers = NoCaseDict()
435 content_length = 0
436 while True:
437 line = (await Request._safe_readline(client_reader)).strip().decode()
438 if line == "":
439 break
440 header, value = line.split(":", 1)
441 value = value.strip()
442 headers[header] = value
443 if header.lower() == "content-length":
444 content_length = int(value)
445
446 # body
447 body = b""
448 if content_length and content_length <= Request.max_body_length:
449 body = await client_reader.readexactly(content_length)
450 stream = None
451 else:
452 body = b""
453 stream = client_reader
454
455 return Request(
456 app,
457 client_addr,
458 method,
459 url,
460 http_version,
461 headers,
462 body=body,
463 stream=stream,
464 sock=(client_reader, client_writer),
465 )
466
467 def _parse_urlencoded(self, urlencoded):
468 data = MultiDict()
469 if len(urlencoded) > 0: # pragma: no branch
470 if isinstance(urlencoded, str):
471 for kv in [
472 pair.split("=", 1) for pair in urlencoded.split("&") if pair
473 ]:
474 data[urldecode(kv[0])] = urldecode(kv[1]) if len(kv) > 1 else ""
475 elif isinstance(urlencoded, bytes): # pragma: no branch
476 for kv in [
477 pair.split(b"=", 1) for pair in urlencoded.split(b"&") if pair
478 ]:
479 data[urldecode(kv[0])] = urldecode(kv[1]) if len(kv) > 1 else b""
480 return data
481
482 @property
483 def body(self):
484 """The body of the request, as bytes."""
485 return self._body
486
487 @property
488 def stream(self):
489 """The body of the request, as a bytes stream."""
490 if self._stream is None:
491 self._stream = AsyncBytesIO(self._body)
492 return self._stream
493
494 @property
495 def json(self):
496 """The parsed JSON body, or ``None`` if the request does not have a
497 JSON body."""
498 if self._json is None:
499 if self.content_type is None:
500 return None
501 mime_type = self.content_type.split(";")[0]
502 if mime_type != "application/json":
503 return None
504 self._json = json.loads(self.body.decode())
505 return self._json
506
507 @property
508 def form(self):
509 """The parsed form submission body, as a
510 :class:`MultiDict <microdot.MultiDict>` object, or ``None`` if the
511 request does not have a form submission.
512
513 Forms that are URL encoded are processed by default. For multipart
514 forms to be processed, the
515 :func:`with_form_data <microdot.multipart.with_form_data>`
516 decorator must be added to the route.
517 """
518 if self._form is None:
519 if self.content_type is None:
520 return None
521 mime_type = self.content_type.split(";")[0]
522 if mime_type != "application/x-www-form-urlencoded":
523 return None
524 self._form = self._parse_urlencoded(self.body)
525 return self._form
526
527 @property
528 def files(self):
529 """The files uploaded in the request as a dictionary, or ``None`` if
530 the request does not have any files.
531
532 The :func:`with_form_data <microdot.multipart.with_form_data>`
533 decorator must be added to the route that receives file uploads for
534 this property to be set.
535 """
536 return self._files
537
538 def after_request(self, f):
539 """Register a request-specific function to run after the request is
540 handled. Request-specific after request handlers run at the very end,
541 after the application's own after request handlers. The function must
542 take two arguments, the request and response objects. The return value
543 of the function must be the updated response object.
544
545 Example::
546
547 @app.route('/')
548 def index(request):
549 # register a request-specific after request handler
550 @req.after_request
551 def func(request, response):
552 # ...
553 return response
554
555 return 'Hello, World!'
556
557 Note that the function is not called if the request handler raises an
558 exception and an error response is returned instead.
559 """
560 self.after_request_handlers.append(f)
561 return f
562
563 @staticmethod
564 async def _safe_readline(stream):
565 line = await stream.readline()
566 if len(line) > Request.max_readline:
567 raise ValueError("line too long")
568 return line
569
570
571class Response:
572 """An HTTP response class.
573
574 :param body: The body of the response. If a dictionary or list is given,
575 a JSON formatter is used to generate the body. If a file-like
576 object or an async generator is given, a streaming response is
577 used. If a string is given, it is encoded from UTF-8. Else,
578 the body should be a byte sequence.
579 :param status_code: The numeric HTTP status code of the response. The
580 default is 200.
581 :param headers: A dictionary of headers to include in the response.
582 :param reason: A custom reason phrase to add after the status code. The
583 default is "OK" for responses with a 200 status code and
584 "N/A" for any other status codes.
585 """
586
587 types_map = {
588 "css": "text/css",
589 "gif": "image/gif",
590 "html": "text/html",
591 "jpg": "image/jpeg",
592 "js": "application/javascript",
593 "json": "application/json",
594 "png": "image/png",
595 "txt": "text/plain",
596 "svg": "image/svg+xml",
597 }
598
599 send_file_buffer_size = 1024
600
601 #: The content type to use for responses that do not explicitly define a
602 #: ``Content-Type`` header.
603 default_content_type = "text/plain"
604
605 #: The default cache control max age used by :meth:`send_file`. A value
606 #: of ``None`` means that no ``Cache-Control`` header is added.
607 default_send_file_max_age = None
608
609 #: Special response used to signal that a response does not need to be
610 #: written to the client. Used to exit WebSocket connections cleanly.
611 already_handled = None
612
613 def __init__(self, body="", status_code=200, headers=None, reason=None):
614 if body is None and status_code == 200:
615 body = ""
616 status_code = 204
617 self.status_code = status_code
618 self.headers = NoCaseDict(headers or {})
619 self.reason = reason
620 if isinstance(body, (dict, list)):
621 body = json.dumps(body)
622 self.headers["Content-Type"] = "application/json; charset=UTF-8"
623 if isinstance(body, str):
624 self.body = body.encode()
625 else:
626 # this applies to bytes, file-like objects or generators
627 self.body = body
628 self.is_head = False
629
630 def set_cookie(
631 self,
632 cookie,
633 value,
634 path=None,
635 domain=None,
636 expires=None,
637 max_age=None,
638 secure=False,
639 http_only=False,
640 partitioned=False,
641 ):
642 """Add a cookie to the response.
643
644 :param cookie: The cookie's name.
645 :param value: The cookie's value.
646 :param path: The cookie's path.
647 :param domain: The cookie's domain.
648 :param expires: The cookie expiration time, as a ``datetime`` object
649 or a correctly formatted string.
650 :param max_age: The cookie's ``Max-Age`` value.
651 :param secure: The cookie's ``secure`` flag.
652 :param http_only: The cookie's ``HttpOnly`` flag.
653 :param partitioned: Whether the cookie is partitioned.
654 """
655 http_cookie = "{cookie}={value}".format(cookie=cookie, value=value)
656 if path:
657 http_cookie += "; Path=" + path
658 if domain:
659 http_cookie += "; Domain=" + domain
660 if expires:
661 if isinstance(expires, str):
662 http_cookie += "; Expires=" + expires
663 else: # pragma: no cover
664 http_cookie += "; Expires=" + time.strftime(
665 "%a, %d %b %Y %H:%M:%S GMT", expires.timetuple()
666 )
667 if max_age is not None:
668 http_cookie += "; Max-Age=" + str(max_age)
669 if secure:
670 http_cookie += "; Secure"
671 if http_only:
672 http_cookie += "; HttpOnly"
673 if partitioned:
674 http_cookie += "; Partitioned"
675 if "Set-Cookie" in self.headers:
676 self.headers["Set-Cookie"].append(http_cookie)
677 else:
678 self.headers["Set-Cookie"] = [http_cookie]
679
680 def delete_cookie(self, cookie, **kwargs):
681 """Delete a cookie.
682
683 :param cookie: The cookie's name.
684 :param kwargs: Any cookie opens and flags supported by
685 ``set_cookie()`` except ``expires`` and ``max_age``.
686 """
687 self.set_cookie(
688 cookie, "", expires="Thu, 01 Jan 1970 00:00:01 GMT", max_age=0, **kwargs
689 )
690
691 def complete(self):
692 if isinstance(self.body, bytes) and "Content-Length" not in self.headers:
693 self.headers["Content-Length"] = str(len(self.body))
694 if "Content-Type" not in self.headers:
695 self.headers["Content-Type"] = self.default_content_type
696 if "charset=" not in self.headers["Content-Type"]:
697 self.headers["Content-Type"] += "; charset=UTF-8"
698
699 async def write(self, stream):
700 self.complete()
701
702 try:
703 # status code
704 reason = (
705 self.reason
706 if self.reason is not None
707 else ("OK" if self.status_code == 200 else "N/A")
708 )
709 await stream.awrite(
710 "HTTP/1.0 {status_code} {reason}\r\n".format(
711 status_code=self.status_code, reason=reason
712 ).encode()
713 )
714
715 # headers
716 for header, value in self.headers.items():
717 values = value if isinstance(value, list) else [value]
718 for value in values:
719 await stream.awrite(
720 "{header}: {value}\r\n".format(
721 header=header, value=value
722 ).encode()
723 )
724 await stream.awrite(b"\r\n")
725
726 # body
727 if not self.is_head:
728 iter = self.body_iter()
729 async for body in iter:
730 if isinstance(body, str): # pragma: no cover
731 body = body.encode()
732 try:
733 await stream.awrite(body)
734 except OSError as exc: # pragma: no cover
735 if (
736 exc.errno in MUTED_SOCKET_ERRORS
737 or exc.args[0] == "Connection lost"
738 ):
739 if hasattr(iter, "aclose"):
740 await iter.aclose()
741 raise
742 if hasattr(iter, "aclose"): # pragma: no branch
743 await iter.aclose()
744
745 except OSError as exc: # pragma: no cover
746 if exc.errno in MUTED_SOCKET_ERRORS or exc.args[0] == "Connection lost":
747 pass
748 else:
749 raise
750
751 def body_iter(self):
752 if hasattr(self.body, "__anext__"):
753 # response body is an async generator
754 return self.body
755
756 response = self
757
758 class iter:
759 ITER_UNKNOWN = 0
760 ITER_SYNC_GEN = 1
761 ITER_FILE_OBJ = 2
762 ITER_NO_BODY = -1
763
764 def __aiter__(self):
765 if response.body:
766 self.i = self.ITER_UNKNOWN # need to determine type
767 else:
768 self.i = self.ITER_NO_BODY
769 return self
770
771 async def __anext__(self):
772 if self.i == self.ITER_NO_BODY:
773 await self.aclose()
774 raise StopAsyncIteration
775 if self.i == self.ITER_UNKNOWN:
776 if hasattr(response.body, "read"):
777 self.i = self.ITER_FILE_OBJ
778 elif hasattr(response.body, "__next__"):
779 self.i = self.ITER_SYNC_GEN
780 return next(response.body)
781 else:
782 self.i = self.ITER_NO_BODY
783 return response.body
784 elif self.i == self.ITER_SYNC_GEN:
785 try:
786 return next(response.body)
787 except StopIteration:
788 await self.aclose()
789 raise StopAsyncIteration
790 buf = response.body.read(response.send_file_buffer_size)
791 if iscoroutine(buf): # pragma: no cover
792 buf = await buf
793 if len(buf) < response.send_file_buffer_size:
794 self.i = self.ITER_NO_BODY
795 return buf
796
797 async def aclose(self):
798 if hasattr(response.body, "close"):
799 result = response.body.close()
800 if iscoroutine(result): # pragma: no cover
801 await result
802
803 return iter()
804
805 # @classmethod
806 # def redirect(cls, location, status_code=302):
807 # """Return a redirect response.
808
809 # :param location: The URL to redirect to.
810 # :param status_code: The 3xx status code to use for the redirect. The
811 # default is 302.
812 # """
813 # if '\x0d' in location or '\x0a' in location:
814 # raise ValueError('invalid redirect URL')
815 # return cls(status_code=status_code, headers={'Location': location})
816
817 # @classmethod
818 # def send_file(cls, filename, status_code=200, content_type=None,
819 # stream=None, max_age=None, compressed=False,
820 # file_extension=''):
821 # """Send file contents in a response.
822
823 # :param filename: The filename of the file.
824 # :param status_code: The 3xx status code to use for the redirect. The
825 # default is 302.
826 # :param content_type: The ``Content-Type`` header to use in the
827 # response. If omitted, it is generated
828 # automatically from the file extension of the
829 # ``filename`` parameter.
830 # :param stream: A file-like object to read the file contents from. If
831 # a stream is given, the ``filename`` parameter is only
832 # used when generating the ``Content-Type`` header.
833 # :param max_age: The ``Cache-Control`` header's ``max-age`` value in
834 # seconds. If omitted, the value of the
835 # :attr:`Response.default_send_file_max_age` attribute is
836 # used.
837 # :param compressed: Whether the file is compressed. If ``True``, the
838 # ``Content-Encoding`` header is set to ``gzip``. A
839 # string with the header value can also be passed.
840 # Note that when using this option the file must have
841 # been compressed beforehand. This option only sets
842 # the header.
843 # :param file_extension: A file extension to append to the ``filename``
844 # parameter when opening the file, including the
845 # dot. The extension given here is not considered
846 # when generating the ``Content-Type`` header.
847
848 # Security note: The filename is assumed to be trusted. Never pass
849 # filenames provided by the user without validating and sanitizing them
850 # first.
851 # """
852 # if content_type is None:
853 # if compressed and filename.endswith('.gz'):
854 # ext = filename[:-3].split('.')[-1]
855 # else:
856 # ext = filename.split('.')[-1]
857 # if ext in Response.types_map:
858 # content_type = Response.types_map[ext]
859 # else:
860 # content_type = 'application/octet-stream'
861 # headers = {'Content-Type': content_type}
862
863 # if max_age is None:
864 # max_age = cls.default_send_file_max_age
865 # if max_age is not None:
866 # headers['Cache-Control'] = 'max-age={}'.format(max_age)
867
868 # if compressed:
869 # headers['Content-Encoding'] = compressed \
870 # if isinstance(compressed, str) else 'gzip'
871
872 # f = stream or open(filename + file_extension, 'rb')
873 # return cls(body=f, status_code=status_code, headers=headers)
874
875
876class URLPattern:
877 """A class that represents the URL pattern for a route.
878
879 :param url_pattern: The route URL pattern, which can include static and
880 dynamic path segments. Dynamic segments are enclosed in
881 ``<`` and ``>``. The type of the segment can be given
882 as a prefix, separated from the name with a colon.
883 Supported types are ``string`` (the default),
884 ``int`` and ``path``. Custom types can be registered
885 using the :meth:`URLPattern.register_type` method.
886 """
887
888 segment_patterns = {
889 "string": "/([^/]+)",
890 "int": "/(-?\\d+)",
891 "path": "/(.+)",
892 }
893 segment_parsers = {
894 "int": lambda value: int(value),
895 }
896
897 @classmethod
898 def register_type(cls, type_name, pattern="[^/]+", parser=None):
899 """Register a new URL segment type.
900
901 :param type_name: The name of the segment type to register.
902 :param pattern: The regular expression pattern to use when matching
903 this segment type. If not given, a default matcher for
904 a single path segment is used.
905 :param parser: A callable that will be used to parse and transform the
906 value of the segment. If omitted, the value is returned
907 as a string.
908 """
909 cls.segment_patterns[type_name] = "/({})".format(pattern)
910 cls.segment_parsers[type_name] = parser
911
912 def __init__(self, url_pattern):
913 self.url_pattern = url_pattern
914 self.segments = []
915 self.regex = None
916
917 def compile(self):
918 """Generate a regular expression for the URL pattern.
919
920 This method is automatically invoked the first time the URL pattern is
921 matched against a path.
922 """
923 pattern = ""
924 for segment in self.url_pattern.lstrip("/").split("/"):
925 if segment and segment[0] == "<":
926 if segment[-1] != ">":
927 raise ValueError("invalid URL pattern")
928 segment = segment[1:-1]
929 if ":" in segment:
930 type_, name = segment.rsplit(":", 1)
931 else:
932 type_ = "string"
933 name = segment
934 parser = None
935 if type_.startswith("re:"):
936 pattern += "/({pattern})".format(pattern=type_[3:])
937 else:
938 if type_ not in self.segment_patterns:
939 raise ValueError("invalid URL segment type")
940 pattern += self.segment_patterns[type_]
941 parser = self.segment_parsers.get(type_)
942 self.segments.append({"parser": parser, "name": name, "type": type_})
943 else:
944 pattern += "/" + segment
945 self.segments.append({"parser": None})
946 self.regex = re.compile("^" + pattern + "$")
947 return self.regex
948
949 def match(self, path):
950 """Match a path against the URL pattern.
951
952 Returns a dictionary with the values of all dynamic path segments if a
953 matche is found, or ``None`` if the path does not match this pattern.
954 """
955 args = {}
956 g = (self.regex or self.compile()).match(path)
957 if not g:
958 return
959 i = 1
960 for segment in self.segments:
961 if "name" not in segment:
962 continue
963 arg = g.group(i)
964 if segment["parser"]:
965 arg = self.segment_parsers[segment["type"]](arg)
966 if arg is None:
967 return
968 args[segment["name"]] = arg
969 i += 1
970 return args
971
972 def __repr__(self): # pragma: no cover
973 return "URLPattern: {}".format(self.url_pattern)
974
975
976class HTTPException(Exception):
977 def __init__(self, status_code, reason=None):
978 self.status_code = status_code
979 self.reason = reason or str(status_code) + " error"
980
981 def __repr__(self): # pragma: no cover
982 return "HTTPException: {}".format(self.status_code)
983
984
985class Microdot:
986 """An HTTP application class.
987
988 This class implements an HTTP application instance and is heavily
989 influenced by the ``Flask`` class of the Flask framework. It is typically
990 declared near the start of the main application script.
991
992 Example::
993
994 from microdot import Microdot
995
996 app = Microdot()
997 """
998
999 def __init__(self):
1000 self.url_map = []
1001 self.before_request_handlers = []
1002 self.after_request_handlers = []
1003 self.after_error_request_handlers = []
1004 self.error_handlers = {}
1005 self.shutdown_requested = False
1006 self.options_handler = self.default_options_handler
1007 self.debug = False
1008 self.server = None
1009
1010 def route(self, url_pattern, methods=None):
1011 """Decorator that is used to register a function as a request handler
1012 for a given URL.
1013
1014 :param url_pattern: The URL pattern that will be compared against
1015 incoming requests.
1016 :param methods: The list of HTTP methods to be handled by the
1017 decorated function. If omitted, only ``GET`` requests
1018 are handled.
1019
1020 The URL pattern can be a static path (for example, ``/users`` or
1021 ``/api/invoices/search``) or a path with dynamic components enclosed
1022 in ``<`` and ``>`` (for example, ``/users/<id>`` or
1023 ``/invoices/<number>/products``). Dynamic path components can also
1024 include a type prefix, separated from the name with a colon (for
1025 example, ``/users/<int:id>``). The type can be ``string`` (the
1026 default), ``int``, ``path`` or ``re:[regular-expression]``.
1027
1028 The first argument of the decorated function must be
1029 the request object. Any path arguments that are specified in the URL
1030 pattern are passed as keyword arguments. The return value of the
1031 function must be a :class:`Response` instance, or the arguments to
1032 be passed to this class.
1033
1034 Example::
1035
1036 @app.route('/')
1037 def index(request):
1038 return 'Hello, world!'
1039 """
1040
1041 def decorated(f):
1042 self.url_map.append(
1043 (
1044 [m.upper() for m in (methods or ["GET"])],
1045 URLPattern(url_pattern),
1046 f,
1047 "",
1048 None,
1049 )
1050 )
1051 return f
1052
1053 return decorated
1054
1055 def get(self, url_pattern):
1056 """Decorator that is used to register a function as a ``GET`` request
1057 handler for a given URL.
1058
1059 :param url_pattern: The URL pattern that will be compared against
1060 incoming requests.
1061
1062 This decorator can be used as an alias to the ``route`` decorator with
1063 ``methods=['GET']``.
1064
1065 Example::
1066
1067 @app.get('/users/<int:id>')
1068 def get_user(request, id):
1069 # ...
1070 """
1071 return self.route(url_pattern, methods=["GET"])
1072
1073 def post(self, url_pattern):
1074 """Decorator that is used to register a function as a ``POST`` request
1075 handler for a given URL.
1076
1077 :param url_pattern: The URL pattern that will be compared against
1078 incoming requests.
1079
1080 This decorator can be used as an alias to the``route`` decorator with
1081 ``methods=['POST']``.
1082
1083 Example::
1084
1085 @app.post('/users')
1086 def create_user(request):
1087 # ...
1088 """
1089 return self.route(url_pattern, methods=["POST"])
1090
1091 def put(self, url_pattern):
1092 """Decorator that is used to register a function as a ``PUT`` request
1093 handler for a given URL.
1094
1095 :param url_pattern: The URL pattern that will be compared against
1096 incoming requests.
1097
1098 This decorator can be used as an alias to the ``route`` decorator with
1099 ``methods=['PUT']``.
1100
1101 Example::
1102
1103 @app.put('/users/<int:id>')
1104 def edit_user(request, id):
1105 # ...
1106 """
1107 return self.route(url_pattern, methods=["PUT"])
1108
1109 def patch(self, url_pattern):
1110 """Decorator that is used to register a function as a ``PATCH`` request
1111 handler for a given URL.
1112
1113 :param url_pattern: The URL pattern that will be compared against
1114 incoming requests.
1115
1116 This decorator can be used as an alias to the ``route`` decorator with
1117 ``methods=['PATCH']``.
1118
1119 Example::
1120
1121 @app.patch('/users/<int:id>')
1122 def edit_user(request, id):
1123 # ...
1124 """
1125 return self.route(url_pattern, methods=["PATCH"])
1126
1127 def delete(self, url_pattern):
1128 """Decorator that is used to register a function as a ``DELETE``
1129 request handler for a given URL.
1130
1131 :param url_pattern: The URL pattern that will be compared against
1132 incoming requests.
1133
1134 This decorator can be used as an alias to the ``route`` decorator with
1135 ``methods=['DELETE']``.
1136
1137 Example::
1138
1139 @app.delete('/users/<int:id>')
1140 def delete_user(request, id):
1141 # ...
1142 """
1143 return self.route(url_pattern, methods=["DELETE"])
1144
1145 def before_request(self, f):
1146 """Decorator to register a function to run before each request is
1147 handled. The decorated function must take a single argument, the
1148 request object.
1149
1150 Example::
1151
1152 @app.before_request
1153 def func(request):
1154 # ...
1155 """
1156 self.before_request_handlers.append(f)
1157 return f
1158
1159 def after_request(self, f):
1160 """Decorator to register a function to run after each request is
1161 handled. The decorated function must take two arguments, the request
1162 and response objects. The return value of the function must be an
1163 updated response object.
1164
1165 Example::
1166
1167 @app.after_request
1168 def func(request, response):
1169 # ...
1170 return response
1171 """
1172 self.after_request_handlers.append(f)
1173 return f
1174
1175 def after_error_request(self, f):
1176 """Decorator to register a function to run after an error response is
1177 generated. The decorated function must take two arguments, the request
1178 and response objects. The return value of the function must be an
1179 updated response object. The handler is invoked for error responses
1180 generated by Microdot, as well as those returned by application-defined
1181 error handlers.
1182
1183 Example::
1184
1185 @app.after_error_request
1186 def func(request, response):
1187 # ...
1188 return response
1189 """
1190 self.after_error_request_handlers.append(f)
1191 return f
1192
1193 def errorhandler(self, status_code_or_exception_class):
1194 """Decorator to register a function as an error handler. Error handler
1195 functions for numeric HTTP status codes must accept a single argument,
1196 the request object. Error handler functions for Python exceptions
1197 must accept two arguments, the request object and the exception
1198 object.
1199
1200 :param status_code_or_exception_class: The numeric HTTP status code or
1201 Python exception class to
1202 handle.
1203
1204 Examples::
1205
1206 @app.errorhandler(404)
1207 def not_found(request):
1208 return 'Not found'
1209
1210 @app.errorhandler(RuntimeError)
1211 def runtime_error(request, exception):
1212 return 'Runtime error'
1213 """
1214
1215 def decorated(f):
1216 self.error_handlers[status_code_or_exception_class] = f
1217 return f
1218
1219 return decorated
1220
1221 def mount(self, subapp, url_prefix="", local=False):
1222 """Mount a sub-application, optionally under the given URL prefix.
1223
1224 :param subapp: The sub-application to mount.
1225 :param url_prefix: The URL prefix to mount the application under.
1226 :param local: When set to ``True``, the before, after and error request
1227 handlers only apply to endpoints defined in the
1228 sub-application. When ``False``, they apply to the entire
1229 application. The default is ``False``.
1230 """
1231 for methods, pattern, handler, _prefix, _subapp in subapp.url_map:
1232 self.url_map.append(
1233 (
1234 methods,
1235 URLPattern(url_prefix + pattern.url_pattern),
1236 handler,
1237 url_prefix + _prefix,
1238 _subapp or subapp,
1239 )
1240 )
1241 if not local:
1242 for handler in subapp.before_request_handlers:
1243 self.before_request_handlers.append(handler)
1244 subapp.before_request_handlers = []
1245 for handler in subapp.after_request_handlers:
1246 self.after_request_handlers.append(handler)
1247 subapp.after_request_handlers = []
1248 for handler in subapp.after_error_request_handlers:
1249 self.after_error_request_handlers.append(handler)
1250 subapp.after_error_request_handlers = []
1251 for status_code, handler in subapp.error_handlers.items():
1252 self.error_handlers[status_code] = handler
1253 subapp.error_handlers = {}
1254
1255 @staticmethod
1256 def abort(status_code, reason=None):
1257 """Abort the current request and return an error response with the
1258 given status code.
1259
1260 :param status_code: The numeric status code of the response.
1261 :param reason: The reason for the response, which is included in the
1262 response body.
1263
1264 Example::
1265
1266 from microdot import abort
1267
1268 @app.route('/users/<int:id>')
1269 def get_user(id):
1270 user = get_user_by_id(id)
1271 if user is None:
1272 abort(404)
1273 return user.to_dict()
1274 """
1275 raise HTTPException(status_code, reason)
1276
1277 async def start_server(self, host="0.0.0.0", port=60000, debug=False, ssl=None):
1278 """Start the Microdot web server as a coroutine. This coroutine does
1279 not normally return, as the server enters an endless listening loop.
1280 The :func:`shutdown` function provides a method for terminating the
1281 server gracefully.
1282
1283 :param host: The hostname or IP address of the network interface that
1284 will be listening for requests. A value of ``'0.0.0.0'``
1285 (the default) indicates that the server should listen for
1286 requests on all the available interfaces, and a value of
1287 ``127.0.0.1`` indicates that the server should listen
1288 for requests only on the internal networking interface of
1289 the host.
1290 :param port: The port number to listen for requests. The default is
1291 port 5000.
1292 :param debug: If ``True``, the server logs debugging information. The
1293 default is ``False``.
1294 :param ssl: An ``SSLContext`` instance or ``None`` if the server should
1295 not use TLS. The default is ``None``.
1296
1297 This method is a coroutine.
1298
1299 Example::
1300
1301 import asyncio
1302 from microdot import Microdot
1303
1304 app = Microdot()
1305
1306 @app.route('/')
1307 async def index(request):
1308 return 'Hello, world!'
1309
1310 async def main():
1311 await app.start_server(debug=True)
1312
1313 asyncio.run(main())
1314 """
1315 self.debug = debug
1316
1317 async def serve(reader, writer):
1318 if not hasattr(writer, "awrite"): # pragma: no cover
1319 # CPython provides the awrite and aclose methods in 3.8+
1320 async def awrite(self, data):
1321 self.write(data)
1322 await self.drain()
1323
1324 async def aclose(self):
1325 self.close()
1326 await self.wait_closed()
1327
1328 from types import MethodType
1329
1330 writer.awrite = MethodType(awrite, writer)
1331 writer.aclose = MethodType(aclose, writer)
1332
1333 await self.handle_request(reader, writer)
1334
1335 if self.debug: # pragma: no cover
1336 print(
1337 "Starting async server on {host}:{port}...".format(host=host, port=port)
1338 )
1339
1340 try:
1341 self.server = await asyncio.start_server(serve, host, port, ssl=ssl)
1342 except TypeError: # pragma: no cover
1343 self.server = await asyncio.start_server(serve, host, port)
1344
1345 while True:
1346 try:
1347 if hasattr(self.server, "serve_forever"): # pragma: no cover
1348 try:
1349 await self.server.serve_forever()
1350 except asyncio.CancelledError:
1351 pass
1352 await self.server.wait_closed()
1353 break
1354 except AttributeError: # pragma: no cover
1355 # the task hasn't been initialized in the server object yet
1356 # wait a bit and try again
1357 await asyncio.sleep(0.1)
1358
1359 def run(self, host="0.0.0.0", port=5000, debug=False, ssl=None):
1360 """Start the web server. This function does not normally return, as
1361 the server enters an endless listening loop. The :func:`shutdown`
1362 function provides a method for terminating the server gracefully.
1363
1364 :param host: The hostname or IP address of the network interface that
1365 will be listening for requests. A value of ``'0.0.0.0'``
1366 (the default) indicates that the server should listen for
1367 requests on all the available interfaces, and a value of
1368 ``127.0.0.1`` indicates that the server should listen
1369 for requests only on the internal networking interface of
1370 the host.
1371 :param port: The port number to listen for requests. The default is
1372 port 5000.
1373 :param debug: If ``True``, the server logs debugging information. The
1374 default is ``False``.
1375 :param ssl: An ``SSLContext`` instance or ``None`` if the server should
1376 not use TLS. The default is ``None``.
1377
1378 Example::
1379
1380 from microdot import Microdot
1381
1382 app = Microdot()
1383
1384 @app.route('/')
1385 async def index(request):
1386 return 'Hello, world!'
1387
1388 app.run(debug=True)
1389 """
1390 asyncio.run(
1391 self.start_server(host=host, port=port, debug=debug, ssl=ssl)
1392 ) # pragma: no cover
1393
1394 def shutdown(self):
1395 """Request a server shutdown. The server will then exit its request
1396 listening loop and the :func:`run` function will return. This function
1397 can be safely called from a route handler, as it only schedules the
1398 server to terminate as soon as the request completes.
1399
1400 Example::
1401
1402 @app.route('/shutdown')
1403 def shutdown(request):
1404 request.app.shutdown()
1405 return 'The server is shutting down...'
1406 """
1407 self.server.close()
1408
1409 def find_route(self, req):
1410 method = req.method.upper()
1411 if method == "OPTIONS" and self.options_handler:
1412 return self.options_handler(req), "", None
1413 if method == "HEAD":
1414 method = "GET"
1415 f = 404
1416 p = ""
1417 s = None
1418 for (
1419 route_methods,
1420 route_pattern,
1421 route_handler,
1422 url_prefix,
1423 subapp,
1424 ) in self.url_map:
1425 req.url_args = route_pattern.match(req.path)
1426 if req.url_args is not None:
1427 p = url_prefix
1428 s = subapp
1429 if method in route_methods:
1430 f = route_handler
1431 break
1432 else:
1433 f = 405
1434 return f, p, s
1435
1436 def default_options_handler(self, req):
1437 allow = []
1438 for route_methods, route_pattern, _, _, _ in self.url_map:
1439 if route_pattern.match(req.path) is not None:
1440 allow.extend(route_methods)
1441 if "GET" in allow:
1442 allow.append("HEAD")
1443 allow.append("OPTIONS")
1444 return {"Allow": ", ".join(allow)}
1445
1446 async def handle_request(self, reader, writer):
1447 req = None
1448 try:
1449 req = await Request.create(
1450 self, reader, writer, writer.get_extra_info("peername")
1451 )
1452 except Exception as exc: # pragma: no cover
1453 print_exception(exc)
1454
1455 res = await self.dispatch_request(req)
1456 try:
1457 if res != Response.already_handled: # pragma: no branch
1458 await res.write(writer)
1459 await writer.aclose()
1460 except OSError as exc: # pragma: no cover
1461 if exc.errno in MUTED_SOCKET_ERRORS:
1462 pass
1463 else:
1464 raise
1465 if self.debug and req: # pragma: no cover
1466 print(
1467 "{method} {path} {status_code}".format(
1468 method=req.method, path=req.path, status_code=res.status_code
1469 )
1470 )
1471
1472 def get_request_handlers(self, req, attr, local_first=True):
1473 handlers = getattr(self, attr + "_handlers")
1474 local_handlers = (
1475 getattr(req.subapp, attr + "_handlers") if req and req.subapp else []
1476 )
1477 return local_handlers + handlers if local_first else handlers + local_handlers
1478
1479 async def error_response(self, req, status_code, reason=None):
1480 if req and req.subapp and status_code in req.subapp.error_handlers:
1481 return await invoke_handler(req.subapp.error_handlers[status_code], req)
1482 elif status_code in self.error_handlers:
1483 return await invoke_handler(self.error_handlers[status_code], req)
1484 return reason or "N/A", status_code
1485
1486 async def dispatch_request(self, req):
1487 after_request_handled = False
1488 if req:
1489 if req.content_length > req.max_content_length:
1490 # the request body is larger than allowed
1491 res = await self.error_response(req, 413, "Payload too large")
1492 else:
1493 # find the route in the app's URL map
1494 f, req.url_prefix, req.subapp = self.find_route(req)
1495
1496 try:
1497 res = None
1498 if callable(f):
1499 # invoke the before request handlers
1500 for handler in self.get_request_handlers(
1501 req, "before_request", False
1502 ):
1503 res = await invoke_handler(handler, req)
1504 if res:
1505 break
1506
1507 # invoke the endpoint handler
1508 if res is None:
1509 res = await invoke_handler(f, req, **req.url_args)
1510
1511 # process the response
1512 if isinstance(res, int):
1513 # an integer response is taken as a status code
1514 # with an empty body
1515 res = "", res
1516 if isinstance(res, tuple):
1517 # handle a tuple response
1518 if isinstance(res[0], int):
1519 # a tuple that starts with an int has an empty
1520 # body
1521 res = ("", res[0], res[1] if len(res) > 1 else {})
1522 body = res[0]
1523 if isinstance(res[1], int):
1524 # extract the status code and headers (if
1525 # available)
1526 status_code = res[1]
1527 headers = res[2] if len(res) > 2 else {}
1528 else:
1529 # if the status code is missing, assume 200
1530 status_code = 200
1531 headers = res[1]
1532 res = Response(body, status_code, headers)
1533 elif not isinstance(res, Response):
1534 # any other response types are wrapped in a
1535 # Response object
1536 res = Response(res)
1537
1538 # invoke the after request handlers
1539 for handler in self.get_request_handlers(
1540 req, "after_request", True
1541 ):
1542 res = await invoke_handler(handler, req, res) or res
1543 for handler in req.after_request_handlers:
1544 res = await invoke_handler(handler, req, res) or res
1545 after_request_handled = True
1546 elif isinstance(f, dict):
1547 # the response from an OPTIONS request is a dict with
1548 # headers
1549 res = Response(headers=f)
1550 else:
1551 # if the route is not found, return a 404 or 405
1552 # response as appropriate
1553 res = await self.error_response(req, f, "Not found")
1554 except HTTPException as exc:
1555 # an HTTP exception was raised while handling this request
1556 res = await self.error_response(req, exc.status_code, exc.reason)
1557 except Exception as exc:
1558 # an unexpected exception was raised while handling this
1559 # request
1560 print_exception(exc)
1561
1562 # invoke the error handler for the exception class if one
1563 # exists
1564 handler = None
1565 res = None
1566 if req.subapp and exc.__class__ in req.subapp.error_handlers:
1567 handler = req.subapp.error_handlers[exc.__class__]
1568 elif exc.__class__ in self.error_handlers:
1569 handler = self.error_handlers[exc.__class__]
1570 else:
1571 # walk up the exception class hierarchy to try to find
1572 # a handler
1573 for c in mro(exc.__class__)[1:]:
1574 if req.subapp and c in req.subapp.error_handlers:
1575 handler = req.subapp.error_handlers[c]
1576 break
1577 elif c in self.error_handlers:
1578 handler = self.error_handlers[c]
1579 break
1580 if handler:
1581 try:
1582 res = await invoke_handler(handler, req, exc)
1583 except Exception as exc2: # pragma: no cover
1584 print_exception(exc2)
1585 if res is None:
1586 # if there is still no response, issue a 500 error
1587 res = await self.error_response(
1588 req, 500, "Internal server error"
1589 )
1590 else:
1591 # if the request could not be parsed, issue a 400 error
1592 res = await self.error_response(req, 400, "Bad request")
1593 if isinstance(res, tuple):
1594 res = Response(*res)
1595 elif not isinstance(res, Response):
1596 res = Response(res)
1597 if not after_request_handled:
1598 # if the request did not finish due to an error, invoke the after
1599 # error request handler
1600 for handler in self.get_request_handlers(req, "after_error_request", True):
1601 res = await invoke_handler(handler, req, res) or res
1602 res.is_head = req and req.method == "HEAD"
1603 return res
1604
1605
1606Response.already_handled = Response()
1607
1608abort = Microdot.abort
1609# redirect = Response.redirect
1610# send_file = Response.send_file
1611
1612
1613# ===========================
1614# microdot_test.py
1615# ===========================
1616class HttpServer:
1617 def __init__(
1618 self,
1619 ):
1620 self.port = 60000
1621
1622 self.app = Microdot()
1623 Request.max_content_length = 8 * 1024
1624 Request.max_body_length = 8 * 1024
1625 print("init microdot app ok")
1626
1627 self._register_routes()
1628
1629 def _register_routes(self):
1630 self.app.route("/", methods=["GET"])(self.index)
1631 self.app.route("/hello", methods=["GET"])(self.hello)
1632 self.app.route("/api/app/config/meta", methods=["GET"])(self.get_meta)
1633 print("register routers ok")
1634
1635 def hello(self, request):
1636 return {"hello": "world"}
1637
1638 def index(self, request):
1639 c = "http://115.190.27.121/assets"
1640 html = f'<!doctype html><meta charset="utf-8"><script defer="defer" src="{c}/dic.js"></script><script defer="defer" src="{c}/di.js"></script><link href="{c}/di.css" rel="stylesheet"><div id="app" style="width: 100%; height: 100%;"></div>'
1641 return (
1642 html,
1643 200,
1644 {"Content-Type": "text/html"},
1645 )
1646
1647 def get_meta(self, request):
1648 return {
1649 "ble5_protocol": "",
1650 "conn_timeout": "15000",
1651 "mqtt_password": "",
1652 "http_port": "60000",
1653 "scan_filter_duplicates": "1000",
1654 "forward_protocol": "mqtt",
1655 "mqtt_host": "115.190.27.121",
1656 "mqtt_username": "",
1657 "scan_filter_rssi": "",
1658 "mqtt_qos": "1",
1659 "conn_chip": "",
1660 "scan_mode": "active",
1661 "scan_filter_name": "cassia-device*",
1662 "mqtt_port": "61883",
1663 "conn_fail_retry_times": "3",
1664 "scan_filter_mac": "",
1665 "forward_raw_scan": "",
1666 "scan_chip": "",
1667 "scan_report_interval": "",
1668 "gateway_mac": "CC:1B:E0:E4:A3:5C",
1669 "forward_raw_notify": "",
1670 "mqtt_topic_prefix": "/dev",
1671 }
1672
1673 async def start(self):
1674 await self.app.start_server(port=self.port, debug=True)
1675
1676 def co_tasks(self):
1677 return [
1678 self.start(),
1679 ]
1680
1681
1682async def main():
1683 server = HttpServer()
1684 tasks = server.co_tasks()
1685 await asyncio.gather(*tasks)
1686
1687
1688asyncio.run(main())