/ 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