/ adafruit_wsgi / wsgi_app.py
wsgi_app.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2019 Matthew Costi for Adafruit Industries
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # The above copyright notice and this permission notice shall be included in
 13  # all copies or substantial portions of the Software.
 14  #
 15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 21  # THE SOFTWARE.
 22  """
 23  `wsgi_app`
 24  ================================================================================
 25  
 26  CircuitPython framework for creating WSGI server compatible web applications.
 27  This does *not* include server implementation, which is necessary in order
 28  to create a web application with this library.
 29  
 30  * Circuit Python implementation of an WSGI Server for ESP32 devices:
 31    https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI.git
 32  
 33  
 34  * Author(s): Matthew Costi
 35  
 36  Implementation Notes
 37  --------------------
 38  
 39  **Software and Dependencies:**
 40  
 41  * Adafruit CircuitPython firmware for the supported boards:
 42    https://github.com/adafruit/circuitpython/releases
 43  
 44  """
 45  
 46  import re
 47  
 48  from adafruit_wsgi.request import Request
 49  
 50  __version__ = "0.0.0-auto.0"
 51  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_WSGI.git"
 52  
 53  
 54  class WSGIApp:
 55      """
 56      The base WSGI Application class.
 57      """
 58  
 59      def __init__(self):
 60          self._routes = []
 61          self._variable_re = re.compile("^<([a-zA-Z]+)>$")
 62  
 63      def __call__(self, environ, start_response):
 64          """
 65          Called whenever the server gets a request.
 66          The environ dict has details about the request per wsgi specification.
 67          Call start_response with the response status string and headers as a list of tuples.
 68          Return a single item list with the item being your response data string.
 69          """
 70  
 71          status = ""
 72          headers = []
 73          resp_data = []
 74  
 75          request = Request(environ)
 76  
 77          match = self._match_route(request.path, request.method.upper())
 78  
 79          if match:
 80              args, route = match
 81              status, headers, resp_data = route["func"](request, *args)
 82  
 83          start_response(status, headers)
 84          return resp_data
 85  
 86      def on_request(self, methods, rule, request_handler):
 87          """
 88          Register a Request Handler for a particular HTTP method and path.
 89          request_handler will be called whenever a matching HTTP request is received.
 90  
 91          request_handler should accept the following args:
 92              (Dict environ)
 93          request_handler should return a tuple in the shape of:
 94              (status, header_list, data_iterable)
 95  
 96          :param list methods: the methods of the HTTP request to handle
 97          :param str rule: the path rule of the HTTP request
 98          :param func request_handler: the function to call
 99          """
100          regex = "^"
101          rule_parts = rule.split("/")
102          for part in rule_parts:
103              var = self._variable_re.match(part)
104              if var:
105                  # If named capture groups ever become a thing, use this regex instead
106                  # regex += "(?P<" + var.group("var") + r">[a-zA-Z0-9_-]*)\/"
107                  regex += r"([a-zA-Z0-9_-]+)\/"
108              else:
109                  regex += part + r"\/"
110          regex += "?$"  # make last slash optional and that we only allow full matches
111          self._routes.append(
112              (re.compile(regex), {"methods": methods, "func": request_handler})
113          )
114  
115      def route(self, rule, methods=None):
116          """
117          A decorator to register a route rule with an endpoint function.
118          if no methods are provided, default to GET
119          """
120          if not methods:
121              methods = ["GET"]
122          return lambda func: self.on_request(methods, rule, func)
123  
124      def _match_route(self, path, method):
125          for matcher, route in self._routes:
126              match = matcher.match(path)
127              if match and method in route["methods"]:
128                  return (match.groups(), route)
129          return None