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())