From 9536aba369d6a76df3f6f3afaf3754a50e0ea655 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Mon, 21 Feb 2022 21:26:53 +0000 Subject: [PATCH 1/6] First step to changing response objectss --- src/processor.rs | 48 ++++++++++++++++-------------------- src/web_socket_connection.rs | 2 ++ 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/processor.rs b/src/processor.rs index 7d84e52e9..7e60ffbe5 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -216,6 +216,8 @@ async fn execute_http_function( route_params: HashMap, queries: HashMap, number_of_params: u8, + // need to change this to return a response struct + // create a custom struct for this ) -> Result { let mut data: Option> = None; @@ -272,35 +274,25 @@ async fn execute_http_function( let output = output.await?; let res = Python::with_gil(|py| -> PyResult { - let string_contents = output.clone(); - let contents = output.into_ref(py).downcast::(); - match contents { - Ok(res) => { - // static file or json here - let contains_response_type = res.contains("response_type")?; - match contains_response_type { - true => { - let response_type: &str = - res.get_item("response_type").unwrap().extract()?; - if response_type == "static_file" { - // static file here and serve string - let file_path = res.get_item("file_path").unwrap().extract()?; - Ok(read_file(file_path)) - } else { - Err(PyErr::from_instance("Server Error".into_py(py).as_ref(py))) - } - } - false => { - Err(PyErr::from_instance("Server Error".into_py(py).as_ref(py))) - } + let res = output.into_ref(py).downcast::().unwrap(); + // static file or json here + let contains_response_type = res.contains("response_type")?; + match contains_response_type { + true => { + let response_type: &str = + res.get_item("response_type").unwrap().extract()?; + if response_type == "static_file" { + // static file here and serve string + let file_path = res.get_item("file_path").unwrap().extract()?; + Ok(read_file(file_path)) + } else if response_type == "text" { + let output_response = res.get_item("text").unwrap().extract()?; + Ok(output_response) + } else { + Err(PyErr::from_instance("Server Error".into_py(py).as_ref(py))) } } - Err(_) => { - // this means that this is basic string output - // and a json serialized string will be parsed here - let contents: &str = string_contents.extract(py)?; - Ok(contents.to_string()) - } + false => Err(PyErr::from_instance("Server Error".into_py(py).as_ref(py))), } })?; @@ -328,6 +320,8 @@ async fn execute_http_function( 2_u8..=u8::MAX => handler.call1((request,)), }; let output: &str = output?.extract()?; + // also convert to object here + // also check why don't sync functions have file handling enabled Ok(output.to_string()) }) }) diff --git a/src/web_socket_connection.rs b/src/web_socket_connection.rs index c2182c22d..a450e1c5d 100644 --- a/src/web_socket_connection.rs +++ b/src/web_socket_connection.rs @@ -14,6 +14,8 @@ use std::sync::Arc; #[derive(Clone)] struct MyWs { router: HashMap, + // can probably try removing arc from here + // and use clone_ref() event_loop: Arc, } From 4f14703d905e02f689c7c71b9d165e8e7ed78018 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Thu, 24 Feb 2022 17:13:09 +0000 Subject: [PATCH 2/6] Try refactor of response --- robyn/__init__.py | 55 ++++++++++++++++++++++++++++++++++++++++------- src/processor.rs | 37 +++++++++++-------------------- 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/robyn/__init__.py b/robyn/__init__.py index 936c0f826..e23dca64d 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -51,16 +51,55 @@ def add_route(self, route_type, endpoint, handler): """ We will add the status code here only """ + async def async_inner_handler(*args): + res = await handler(args) + if type(res) == "dict": + if "status_code" not in res: + res["status_code"] = 200 + else: + response = { + "status_code": 200, + "body": res, + "type": "text" + } + res = response + return res + + def inner_handler(*args): + res = handler(args) + if type(res) == "dict": + if "status_code" not in res: + res["status_code"] = 200 + else: + response = { + "status_code": 200, + "body": res, + "type": "text" + } + res = response + return res + number_of_params = len(signature(handler).parameters) - self.routes.append( - ( - route_type, - endpoint, - handler, - asyncio.iscoroutinefunction(handler), - number_of_params, + if asyncio.iscoroutinefunction(handler): + self.routes.append( + ( + route_type, + endpoint, + async_inner_handler, + True, + number_of_params, + ) + ) + else: + self.routes.append( + ( + route_type, + endpoint, + inner_handler, + False, + number_of_params, + ) ) - ) def add_middleware_route(self, route_type, endpoint, handler): """ diff --git a/src/processor.rs b/src/processor.rs index 7e60ffbe5..9b74619f2 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::str::FromStr; use std::sync::Arc; use actix_web::{http::Method, web, HttpRequest, HttpResponse, HttpResponseBuilder}; @@ -63,8 +64,11 @@ pub async fn handle_request( }; let mut response = HttpResponse::Ok(); + let status_code = + actix_http::StatusCode::from_str(contents.get("status_code").unwrap()).unwrap(); apply_headers(&mut response, headers); - response.body(contents) + response.status(status_code); + response.body(contents.get("body").unwrap().to_owned()) } pub async fn handle_middleware_request( @@ -218,7 +222,7 @@ async fn execute_http_function( number_of_params: u8, // need to change this to return a response struct // create a custom struct for this -) -> Result { +) -> Result> { let mut data: Option> = None; if req.method() == Method::POST @@ -273,27 +277,10 @@ async fn execute_http_function( })?; let output = output.await?; - let res = Python::with_gil(|py| -> PyResult { - let res = output.into_ref(py).downcast::().unwrap(); - // static file or json here - let contains_response_type = res.contains("response_type")?; - match contains_response_type { - true => { - let response_type: &str = - res.get_item("response_type").unwrap().extract()?; - if response_type == "static_file" { - // static file here and serve string - let file_path = res.get_item("file_path").unwrap().extract()?; - Ok(read_file(file_path)) - } else if response_type == "text" { - let output_response = res.get_item("text").unwrap().extract()?; - Ok(output_response) - } else { - Err(PyErr::from_instance("Server Error".into_py(py).as_ref(py))) - } - } - false => Err(PyErr::from_instance("Server Error".into_py(py).as_ref(py))), - } + let res = Python::with_gil(|py| -> PyResult> { + let res: HashMap = + output.into_ref(py).downcast::()?.extract()?; + Ok(res) })?; Ok(res) @@ -319,10 +306,10 @@ async fn execute_http_function( // this is done to accomodate any future params 2_u8..=u8::MAX => handler.call1((request,)), }; - let output: &str = output?.extract()?; + let output: HashMap = output?.extract()?; // also convert to object here // also check why don't sync functions have file handling enabled - Ok(output.to_string()) + Ok(output) }) }) .await? From 8eea3b3a1366881f9c36708d54d0ec65979b5398 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Thu, 24 Feb 2022 17:27:28 +0000 Subject: [PATCH 3/6] Update int fix --- robyn/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robyn/__init__.py b/robyn/__init__.py index e23dca64d..4244cf5a5 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -55,10 +55,10 @@ async def async_inner_handler(*args): res = await handler(args) if type(res) == "dict": if "status_code" not in res: - res["status_code"] = 200 + res["status_code"] = "200" else: response = { - "status_code": 200, + "status_code": "200", "body": res, "type": "text" } From ef5e0687c884bbf99a1278b4796887005c4862ef Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Mon, 28 Feb 2022 22:04:07 +0000 Subject: [PATCH 4/6] Refactor code --- integration_tests/base_routes.py | 5 +- robyn/__init__.py | 153 +++++-------------------------- robyn/router.py | 142 ++++++++++++++++++++++++++++ src/processor.rs | 2 + 4 files changed, 168 insertions(+), 134 deletions(-) create mode 100644 robyn/router.py diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index e148c8e7e..90d677773 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -43,8 +43,9 @@ async def hello(request): global callCount callCount += 1 message = "Called " + str(callCount) + " times" - print(message) - return jsonify(request) + print(message, request) + return {"status_code": "200", "body": "hello", "type": "text"} + # return @app.before_request("/") diff --git a/robyn/__init__.py b/robyn/__init__.py index 4244cf5a5..bec6cfb52 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -13,6 +13,7 @@ from .processpool import spawn_process from .log_colors import Colors from .ws import WS +from .router import Router, MiddlewareRouter, WebSocketRouter # 3rd party imports and exports from multiprocess import Process @@ -32,15 +33,14 @@ def __init__(self, file_object): self.dev = self.parser.is_dev() self.processes = self.parser.num_processes() self.workers = self.parser.workers() - self.routes = [] + self.router = Router() + self.middleware_router = MiddlewareRouter() + self.web_socket_router = WebSocketRouter() self.headers = [] - self.routes = [] - self.middlewares = [] - self.web_sockets = {} self.directories = [] self.event_handlers = {} - def add_route(self, route_type, endpoint, handler): + def _add_route(self, route_type, endpoint, handler): """ [This is base handler for all the decorators] @@ -51,77 +51,7 @@ def add_route(self, route_type, endpoint, handler): """ We will add the status code here only """ - async def async_inner_handler(*args): - res = await handler(args) - if type(res) == "dict": - if "status_code" not in res: - res["status_code"] = "200" - else: - response = { - "status_code": "200", - "body": res, - "type": "text" - } - res = response - return res - - def inner_handler(*args): - res = handler(args) - if type(res) == "dict": - if "status_code" not in res: - res["status_code"] = 200 - else: - response = { - "status_code": 200, - "body": res, - "type": "text" - } - res = response - return res - - number_of_params = len(signature(handler).parameters) - if asyncio.iscoroutinefunction(handler): - self.routes.append( - ( - route_type, - endpoint, - async_inner_handler, - True, - number_of_params, - ) - ) - else: - self.routes.append( - ( - route_type, - endpoint, - inner_handler, - False, - number_of_params, - ) - ) - - def add_middleware_route(self, route_type, endpoint, handler): - """ - [This is base handler for the middleware decorator] - - :param route_type [str]: [??] - :param endpoint [str]: [endpoint for the route added] - :param handler [function]: [represents the sync or async function passed as a handler for the route] - """ - - """ We will add the status code here only - """ - number_of_params = len(signature(handler).parameters) - self.middlewares.append( - ( - route_type, - endpoint, - handler, - asyncio.iscoroutinefunction(handler), - number_of_params, - ) - ) + self.router.add_route(route_type, endpoint, handler) def before_request(self, endpoint): """ @@ -129,27 +59,7 @@ def before_request(self, endpoint): :param endpoint [str]: [endpoint to server the route] """ - - # This inner function is basically a wrapper arround the closure(decorator) - # being returned. - # It takes in a handler and converts it in into a closure - # and returns the arguments. - # Arguments are returned as they could be modified by the middlewares. - def inner(handler): - async def async_inner_handler(*args): - await handler(args) - return args - - def inner_handler(*args): - handler(*args) - return args - - if asyncio.iscoroutinefunction(handler): - self.add_middleware_route("BEFORE_REQUEST", endpoint, async_inner_handler) - else: - self.add_middleware_route("BEFORE_REQUEST", endpoint, inner_handler) - - return inner + return self.middleware_router.add_before_request(endpoint) def after_request(self, endpoint): """ @@ -157,27 +67,7 @@ def after_request(self, endpoint): :param endpoint [str]: [endpoint to server the route] """ - - # This inner function is basically a wrapper arround the closure(decorator) - # being returned. - # It takes in a handler and converts it in into a closure - # and returns the arguments. - # Arguments are returned as they could be modified by the middlewares. - def inner(handler): - async def async_inner_handler(*args): - await handler(args) - return args - - def inner_handler(*args): - handler(*args) - return args - - if asyncio.iscoroutinefunction(handler): - self.add_middleware_route("AFTER_REQUEST", endpoint, async_inner_handler) - else: - self.add_middleware_route("AFTER_REQUEST", endpoint, inner_handler) - - return inner + return self.middleware_router.add_after_request(endpoint) def add_directory( self, route, directory_path, index_file=None, show_files_listing=False @@ -188,7 +78,7 @@ def add_header(self, key, value): self.headers.append((key, value)) def add_web_socket(self, endpoint, ws): - self.web_sockets[endpoint] = ws + self.web_socket_router.add_route(endpoint, ws) def _add_event_handler(self, event_type: str, handler): print(f"Add event {event_type} handler") @@ -213,7 +103,6 @@ def start(self, url="127.0.0.1", port=5000): if not self.dev: workers = self.workers socket = SocketHeld(url, port) - print(self.middlewares) for _ in range(self.processes): copied_socket = socket.try_clone() p = Process( @@ -221,9 +110,9 @@ def start(self, url="127.0.0.1", port=5000): args=( self.directories, self.headers, - self.routes, - self.middlewares, - self.web_sockets, + self.router.get_routes(), + self.middleware_router.get_routes(), + self.web_socket_router.get_routes(), self.event_handlers, copied_socket, workers, @@ -256,7 +145,7 @@ def get(self, endpoint): """ def inner(handler): - self.add_route("GET", endpoint, handler) + self._add_route("GET", endpoint, handler) return inner @@ -268,7 +157,7 @@ def post(self, endpoint): """ def inner(handler): - self.add_route("POST", endpoint, handler) + self._add_route("POST", endpoint, handler) return inner @@ -280,7 +169,7 @@ def put(self, endpoint): """ def inner(handler): - self.add_route("PUT", endpoint, handler) + self._add_route("PUT", endpoint, handler) return inner @@ -292,7 +181,7 @@ def delete(self, endpoint): """ def inner(handler): - self.add_route("DELETE", endpoint, handler) + self._add_route("DELETE", endpoint, handler) return inner @@ -304,7 +193,7 @@ def patch(self, endpoint): """ def inner(handler): - self.add_route("PATCH", endpoint, handler) + self._add_route("PATCH", endpoint, handler) return inner @@ -316,7 +205,7 @@ def head(self, endpoint): """ def inner(handler): - self.add_route("HEAD", endpoint, handler) + self._add_route("HEAD", endpoint, handler) return inner @@ -328,7 +217,7 @@ def options(self, endpoint): """ def inner(handler): - self.add_route("OPTIONS", endpoint, handler) + self._add_route("OPTIONS", endpoint, handler) return inner @@ -340,7 +229,7 @@ def connect(self, endpoint): """ def inner(handler): - self.add_route("CONNECT", endpoint, handler) + self._add_route("CONNECT", endpoint, handler) return inner @@ -352,6 +241,6 @@ def trace(self, endpoint): """ def inner(handler): - self.add_route("TRACE", endpoint, handler) + self._add_route("TRACE", endpoint, handler) return inner diff --git a/robyn/router.py b/robyn/router.py new file mode 100644 index 000000000..447e7b573 --- /dev/null +++ b/robyn/router.py @@ -0,0 +1,142 @@ +from abc import ABC, abstractmethod +from inspect import signature +from asyncio import iscoroutinefunction + + +class BaseRouter(ABC): + @abstractmethod + def add_route(*args): + ... + + +class Router(BaseRouter): + def __init__(self) -> None: + super().__init__() + self.routes = [] + + def _format_response(self, res): + # handle file handlers + response = {} + if type(res) == dict: + if "status_code" not in res: + print("Getting here") + res["status_code"] = "200" + response = res + else: + response = { + "status_code": "200", + "body": res["body"], + "type": "text", + } + print("Setting the response", response) + else: + response = {"status_code": "200", "body": res, "type": "text"} + + return response + + def add_route(self, route_type, endpoint, handler): + async def async_inner_handler(*args): + response = self._format_response(await handler(*args)) + print(f"This is the response in python: {response}") + return response + + def inner_handler(*args): + response = self._format_response(handler(*args)) + return response + + number_of_params = len(signature(handler).parameters) + if iscoroutinefunction(handler): + self.routes.append( + ( + route_type, + endpoint, + async_inner_handler, + True, + number_of_params, + ) + ) + else: + self.routes.append( + ( + route_type, + endpoint, + inner_handler, + False, + number_of_params, + ) + ) + + def get_routes(self): + return self.routes + + +class MiddlewareRouter(BaseRouter): + def __init__(self) -> None: + super().__init__() + self.routes = [] + + def add_route(self, route_type, endpoint, handler): + number_of_params = len(signature(handler).parameters) + self.routes.append( + ( + route_type, + endpoint, + handler, + iscoroutinefunction(handler), + number_of_params, + ) + ) + + # These inner function is basically a wrapper arround the closure(decorator) + # being returned. + # It takes in a handler and converts it in into a closure + # and returns the arguments. + # Arguments are returned as they could be modified by the middlewares. + def add_after_request(self, endpoint): + def inner(handler): + async def async_inner_handler(*args): + await handler(args) + return args + + def inner_handler(*args): + handler(*args) + return args + + if iscoroutinefunction(handler): + self.add_route("AFTER_REQUEST", endpoint, async_inner_handler) + else: + self.add_route("AFTER_REQUEST", endpoint, inner_handler) + + return inner + + def add_before_request(self, endpoint): + def inner(handler): + async def async_inner_handler(*args): + await handler(args) + return args + + def inner_handler(*args): + handler(*args) + return args + + if iscoroutinefunction(handler): + self.add_route("BEFORE_REQUEST", endpoint, async_inner_handler) + else: + self.add_route("BEFORE_REQUEST", endpoint, inner_handler) + + return inner + + def get_routes(self): + return self.routes + + +class WebSocketRouter(BaseRouter): + def __init__(self) -> None: + super().__init__() + self.routes = {} + + def add_route(self, endpoint, web_socket): + self.routes[endpoint] = web_socket + + def get_routes(self): + return self.routes diff --git a/src/processor.rs b/src/processor.rs index 9b74619f2..7608bedf0 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -278,6 +278,8 @@ async fn execute_http_function( let output = output.await?; let res = Python::with_gil(|py| -> PyResult> { + println!("This is the result of the code {:?}", output); + let res: HashMap = output.into_ref(py).downcast::()?.extract()?; Ok(res) From 6d827cd09dc944790d4c392cdbf052594853a348 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Mon, 28 Feb 2022 23:11:54 +0000 Subject: [PATCH 5/6] Handle html file types --- robyn/responses.py | 7 +++++-- robyn/router.py | 2 +- src/processor.rs | 10 +++++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/robyn/responses.py b/robyn/responses.py index a754b7562..2202a76fa 100644 --- a/robyn/responses.py +++ b/robyn/responses.py @@ -9,8 +9,11 @@ def static_file(file_path): """ return { - "response_type": "static_file", - "file_path": file_path + "type": "static_file", + "file_path": file_path, + # this is a hack for now + "body": "", + "status_code": "200", } diff --git a/robyn/router.py b/robyn/router.py index 447e7b573..5420a95bc 100644 --- a/robyn/router.py +++ b/robyn/router.py @@ -26,7 +26,7 @@ def _format_response(self, res): response = { "status_code": "200", "body": res["body"], - "type": "text", + **res } print("Setting the response", response) else: diff --git a/src/processor.rs b/src/processor.rs index 7608bedf0..425449ffe 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -280,8 +280,16 @@ async fn execute_http_function( let res = Python::with_gil(|py| -> PyResult> { println!("This is the result of the code {:?}", output); - let res: HashMap = + let mut res: HashMap = output.into_ref(py).downcast::()?.extract()?; + + let response_type = res.get("type").unwrap(); + + if response_type == "static_file" { + let file_path = res.get("file_path").unwrap(); + let contents = read_file(file_path); + res.insert("body".to_owned(), contents.to_owned()); + } Ok(res) })?; From 5353b75d3d494f26b40659b115127b7927a27f0e Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Tue, 1 Mar 2022 21:58:22 +0000 Subject: [PATCH 6/6] Add integration tests --- integration_tests/base_routes.py | 11 ++++++++++- integration_tests/test_status_code.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 integration_tests/test_status_code.py diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 90d677773..c20762038 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -45,7 +45,16 @@ async def hello(request): message = "Called " + str(callCount) + " times" print(message, request) return {"status_code": "200", "body": "hello", "type": "text"} - # return + + +@app.get('/404') +def return_404(): + return {"status_code": "404", "body": "hello", "type": "text"} + + +@app.post('/404') +def return_404_post(): + return {"status_code": "404", "body": "hello", "type": "text"} @app.before_request("/") diff --git a/integration_tests/test_status_code.py b/integration_tests/test_status_code.py new file mode 100644 index 000000000..5f94eeb03 --- /dev/null +++ b/integration_tests/test_status_code.py @@ -0,0 +1,13 @@ +import requests + +BASE_URL = "http://127.0.0.1:5000" + + +def test_404_status_code(session): + res = requests.get(f"{BASE_URL}/404") + assert res.status_code == 404 + + +def test_404_post_request_status_code(session): + r = requests.post(f"{BASE_URL}/404") + assert r.status_code == 404