diff --git a/example/app.py b/example/app.py index 229dc96..8e47e64 100644 --- a/example/app.py +++ b/example/app.py @@ -6,7 +6,7 @@ from lambda_proxy.proxy import API -APP = API(name="app") +APP = API(name="app", debug=True) @APP.route("/", methods=["GET"], cors=True) @@ -15,16 +15,16 @@ def main() -> Tuple[str, str, str]: return ("OK", "text/plain", "Yo") -@APP.route("/", methods=["GET"], cors=True) +@APP.route("/", methods=["GET"], cors=True) def _re_one(regex1: str) -> Tuple[str, str, str]: """Return JSON Object.""" - return ("OK", "text/plain", input) + return ("OK", "text/plain", regex1) @APP.route("/", methods=["GET"], cors=True) def _re_two(regex2: str) -> Tuple[str, str, str]: """Return JSON Object.""" - return ("OK", "text/plain", input) + return ("OK", "text/plain", regex2) @APP.route("/add", methods=["GET", "POST"], cors=True) @@ -33,8 +33,8 @@ def post(body) -> Tuple[str, str, str]: return ("OK", "text/plain", body) -@APP.route("/", methods=["GET"], cors=True) -@APP.route("//", methods=["GET"], cors=True) +@APP.route("/user/", methods=["GET"], cors=True) +@APP.route("/user//", methods=["GET"], cors=True) def double(user: str, num: int = 0) -> Tuple[str, str, str]: """Return JSON Object.""" return ("OK", "text/plain", f"{user}-{num}") @@ -60,7 +60,7 @@ def json_handler() -> Tuple[str, str, str]: return ("OK", "application/json", json.dumps({"app": "it works"})) -@APP.route("/binary", methods=["GET"], cors=True, payload_compression_method="gzip") +@APP.route("/bin/binary", methods=["GET"], cors=True, payload_compression_method="gzip") def bin() -> Tuple[str, str, io.BinaryIO]: """Return image.""" with open("./rpix.png", "rb") as f: @@ -68,7 +68,7 @@ def bin() -> Tuple[str, str, io.BinaryIO]: @APP.route( - "/b64binary", + "/bin/b64binary", methods=["GET"], cors=True, payload_compression_method="gzip", diff --git a/example/cli.py b/example/cli.py index 302349c..15d52f6 100644 --- a/example/cli.py +++ b/example/cli.py @@ -1,16 +1,39 @@ """Launch server""" -import click +import base64 from urllib.parse import urlparse, parse_qsl - from http.server import HTTPServer, BaseHTTPRequestHandler +import click + from app import APP class HTTPRequestHandler(BaseHTTPRequestHandler): """Requests handler.""" + def do_OPTIONS(self): + """OPTIONS requests.""" + q = urlparse(self.path) + request = { + "headers": dict(self.headers), + "path": q.path, + "queryStringParameters": dict(parse_qsl(q.query)), + "httpMethod": self.command, + } + response = APP(request, None) + + self.send_response(int(response["statusCode"])) + for r in response["headers"]: + self.send_header(r, response["headers"][r]) + self.end_headers() + + body = response.get("body", None) + if isinstance(body, str): + self.wfile.write(bytes(response["body"], "utf-8")) + elif body is not None: + self.wfile.write(response["body"]) + def do_GET(self): """Get requests.""" q = urlparse(self.path) @@ -32,6 +55,40 @@ def do_GET(self): else: self.wfile.write(response["body"]) + def do_POST(self): + """POST requests.""" + q = urlparse(self.path) + q = urlparse(self.path) + + body = self.rfile.read(int(dict(self.headers).get("Content-Length"))) + body = base64.b64encode(body).decode() + request = { + "headers": dict(self.headers), + "path": q.path, + "queryStringParameters": dict(parse_qsl(q.query)), + "httpMethod": self.command, + } + + request = { + "headers": dict(self.headers), + "path": q.path, + "queryStringParameters": dict(parse_qsl(q.query)), + "httpMethod": self.command, + "body": body, + "isBase64Encoded": True, + } + response = APP(request, None) + + self.send_response(int(response["statusCode"])) + for r in response["headers"]: + self.send_header(r, response["headers"][r]) + self.end_headers() + + if isinstance(response["body"], str): + self.wfile.write(bytes(response["body"], "utf-8")) + else: + self.wfile.write(response["body"]) + @click.command(short_help="Local Server") @click.option("--port", type=int, default=8000, help="port") diff --git a/lambda_proxy/proxy.py b/lambda_proxy/proxy.py index 91ce9c7..0251c3d 100644 --- a/lambda_proxy/proxy.py +++ b/lambda_proxy/proxy.py @@ -377,6 +377,9 @@ def _add_route(self, path: str, endpoint: callable, **kwargs) -> None: "URL paths must be unique.".format(path) ) + if "OPTIONS" not in methods: + methods.append("OPTIONS") + self.routes[path] = RouteEntry( endpoint, path, @@ -533,10 +536,9 @@ def response( "image/jp2", ] - messageData = { - "statusCode": statusCode[status], - "headers": {"Content-Type": content_type}, - } + messageData = {"statusCode": statusCode[status], "headers": {}} + if content_type is not None: + messageData["headers"].update({"Content-Type": content_type}) if cors: messageData["headers"]["Access-Control-Allow-Origin"] = "*" @@ -545,7 +547,11 @@ def response( ) messageData["headers"]["Access-Control-Allow-Credentials"] = "true" - if compression and compression in accepted_compression: + if ( + compression + and compression in accepted_compression + and response_body is not None + ): messageData["headers"]["Content-Encoding"] = compression if isinstance(response_body, str): response_body = bytes(response_body, "utf-8") @@ -576,7 +582,7 @@ def response( if ttl: messageData["headers"]["Cache-Control"] = ( - f"max-age={ttl}" if status == "OK" else "no-cache" + f"max-age={ttl}" if status not in ["ERROR", "CONFLICT"] else "no-cache" ) if ( @@ -584,9 +590,12 @@ def response( ) and b64encode: messageData["isBase64Encoded"] = True messageData["body"] = base64.b64encode(response_body).decode() - else: + elif response_body is not None: messageData["body"] = response_body + if messageData.get("body"): + messageData["headers"]["Content-Length"] = len(messageData["body"]) + return messageData def __call__(self, event, context): @@ -644,6 +653,15 @@ def __call__(self, event, context): {"errorMessage": "Unsupported method: {}".format(http_method)} ), ) + if http_method == "OPTIONS": + return self.response( + "EMPTY", + None, + None, + cors=route_entry.cors, + accepted_methods=route_entry.methods, + ttl=604800, + ) # remove access_token from kwargs request_params.pop("access_token", False) diff --git a/tox.ini b/tox.ini index 2b6ca16..85c6e7b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py36,py37 [flake8] ignore = D203 exclude = .git,__pycache__,docs/source/conf.py,old,build,dist -max-complexity = 10 +max-complexity = 12 max-line-length = 90 [testenv]