Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web socket integration attempt 2 #109

Merged
merged 18 commits into from
Dec 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
475 changes: 270 additions & 205 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ futures-util = "0.3.15"
matchit = "0.4.3"
actix-http = "3.0.0-beta.8"
socket2 = { version = "0.4.1", features = ["all"] }
actix = "0.12.0"
actix-web-actors = "4.0.0-beta.1"

[package.metadata.maturin]
name = "robyn"
62 changes: 60 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,17 @@ Robyn supports every HTTP request method. The examples of some of them are below
```


### Having Dynamic Routes
You can now add params in the routes and access them from the request object.

## Returning a JSON Response
```python3
@app.post("/jsonify/:id")
async def json(request):
print(request["params"]["id"])
return jsonify({"hello": "world"})
```

### Returning a JSON Response
You can also serve JSON responses when serving HTTP request using the following way.

```python3
Expand Down Expand Up @@ -139,4 +148,53 @@ app.add_header("server", "robyn")

```

To see a complete service in action, you can go to the folder [../test_python/test.py](../test_python/test.py)
## WebSockets

You can now serve websockets using Robyn.

Firstly, you need to create a WebSocket Class and wrap it around your Robyn app.

```python3
from robyn import Robyn, static_file, jsonify, WS


app = Robyn(__file__)
websocket = WS(app, "/web_socket")
```

Now, you can define 3 methods for every web_socket for their lifecycle, they are as follows:

```python3
@websocket.on("message")
def connect():
global i
i+=1
if i==0:
return "Whaaat??"
elif i==1:
return "Whooo??"
elif i==2:
return "*chika* *chika* Slim Shady."
elif i==3:
i= -1
return ""

@websocket.on("close")
def close():
return "Goodbye world, from ws"

@websocket.on("connect")
def message():
return "Hello world, from ws"

```


## MutliCore Scaling

The three methods:
- "message" is called when the socket receives a message
- "close" is called when the socket is disconnected
- "connect" is called when the socket connects

To see a complete service in action, you can go to the folder [../integration_tests/base_routes.py](../integration_tests/base_routes.py)
2 changes: 0 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
## Future Roadmap

- Integrate actix router
- Integrate WebSockets
- Reveal Logo
- Add session/cookie plugins
- Add the plugin documentation

31 changes: 25 additions & 6 deletions integration_tests/base_routes.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@


# robyn_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../robyn")
# sys.path.insert(0, robyn_path)

from robyn import Robyn, static_file, jsonify, SocketHeld
from robyn import Robyn, static_file, jsonify, WS
import asyncio
import os
import pathlib

app = Robyn(__file__)
websocket = WS(app, "/web_socket")
i = -1

@websocket.on("message")
def connect():
global i
i+=1
if i==0:
return "Whaaat??"
elif i==1:
return "Whooo??"
elif i==2:
i = -1
return "*chika* *chika* Slim Shady."

@websocket.on("close")
def close():
print("Hello world")
return "Hello world, from ws"

@websocket.on("connect")
def message():
print("Hello world")
return "Hello world, from ws"


callCount = 0
Expand Down
1 change: 1 addition & 0 deletions integration_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

@pytest.fixture
def session():
subprocess.call(["freeport", "5000"])
os.environ["ROBYN_URL"] = "127.0.0.1"
current_file_path = pathlib.Path(__file__).parent.resolve()
base_routes = os.path.join(current_file_path, "./base_routes.py")
Expand Down
18 changes: 18 additions & 0 deletions integration_tests/test_web_sockets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from websockets import connect
import asyncio

BASE_URL = "ws://127.0.0.1:5000"

def test_web_socket(session):
async def start_ws(uri):
async with connect(uri) as websocket:
await websocket.send("My name is?")
assert( await websocket.recv() == "Whaaat??")
await websocket.send("My name is?")
assert( await websocket.recv() == "Whooo??")
await websocket.send("My name is?")
assert( await websocket.recv() == "*chika* *chika* Slim Shady.")

asyncio.run(start_ws(f"{BASE_URL}/web_socket"))


10 changes: 9 additions & 1 deletion robyn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .dev_event_handler import EventHandler
from .processpool import spawn_process
from .log_colors import Colors
from .ws import WS


# 3rd party imports and exports
Expand All @@ -36,6 +37,7 @@ def __init__(self, file_object):
self.headers = []
self.routes = []
self.directories = []
self.web_sockets = {}


def add_route(self, route_type, endpoint, handler):
Expand All @@ -62,6 +64,10 @@ def add_header(self, key, value):

def remove_header(self, key):
self.server.remove_header(key)

def add_web_socket(self, endpoint, ws):
self.web_sockets[endpoint] = ws


def start(self, url="127.0.0.1", port=5000):
"""
Expand All @@ -76,7 +82,7 @@ def start(self, url="127.0.0.1", port=5000):
copied = socket.try_clone()
p = Process(
target=spawn_process,
args=(url, port, self.directories, self.headers, self.routes, copied, f"Process {process_number}", workers),
args=(url, port, self.directories, self.headers, self.routes, self.web_sockets, copied, f"Process {process_number}", workers),
)
p.start()

Expand All @@ -95,6 +101,7 @@ def start(self, url="127.0.0.1", port=5000):
observer.stop()
observer.join()


def get(self, endpoint):
"""
[The @app.get decorator to add a get route]
Expand Down Expand Up @@ -195,3 +202,4 @@ def inner(handler):

return inner


8 changes: 7 additions & 1 deletion robyn/processpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
mp.allow_connection_pickling()


def spawn_process(url, port, directories, headers, routes, socket, process_name, workers):
def spawn_process(url, port, directories, headers, routes, web_sockets, socket, process_name, workers):
"""
This function is called by the main process handler to create a server runtime.
This functions allows one runtime per process.
Expand Down Expand Up @@ -51,5 +51,11 @@ def spawn_process(url, port, directories, headers, routes, socket, process_name,
route_type, endpoint, handler, is_async, number_of_params = route
server.add_route(route_type, endpoint, handler, is_async, number_of_params)

for endpoint in web_sockets:
web_socket = web_sockets[endpoint]
print(web_socket.methods)
server.add_web_socket_route(endpoint, web_socket.methods["connect"], web_socket.methods["close"], web_socket.methods["message"])


server.start(url, port, socket, process_name, workers)
asyncio.get_event_loop().run_forever()
2 changes: 2 additions & 0 deletions robyn/test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ watchdog
requests==2.26.0
uvloop==0.16.0
multiprocess==0.70.12.2
websockets==10.1
freeport==0.1.15
27 changes: 27 additions & 0 deletions robyn/ws.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import asyncio
from inspect import signature

class WS:
"""This is the python wrapper for the web socket that will be used here.
"""
def __init__(self, robyn_object, endpoint) -> None:
self.robyn_object = robyn_object
self.endpoint = endpoint
self.methods = {}

def on(self, type):
def inner(handler):
if type not in ["connect", "close", "message"]:
raise Exception(f"Socket method {type} does not exist")
else:
self.methods[type] = ( handler, self._is_async(handler), self._num_params(handler) )
self.robyn_object.add_web_socket(self.endpoint, self)

return inner

def _num_params(self, handler):
return len(signature(handler).parameters)

def _is_async(self, handler):
return asyncio.iscoroutinefunction(handler)

1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod router;
mod server;
mod shared_socket;
mod types;
mod web_socket_connection;

use server::Server;
use shared_socket::SocketHeld;
Expand Down
5 changes: 3 additions & 2 deletions src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub async fn handle_request(
req: &HttpRequest,
route_params: HashMap<String, String>,
) -> HttpResponse {
let contents = match execute_function(
let contents = match execute_http_function(
function,
payload,
headers,
Expand Down Expand Up @@ -79,8 +79,9 @@ fn read_file(file_path: &str) -> String {
String::from_utf8_lossy(&buf).to_string()
}

// Change this!
#[inline]
async fn execute_function(
async fn execute_http_function(
function: PyFunction,
payload: &mut web::Payload,
headers: &Headers,
Expand Down
Loading