Skip to content

Commit

Permalink
Enhance Hypha RPC and support reconnection (#641)
Browse files Browse the repository at this point in the history
* Improve security

* Fix format

* Support custom messages and queue

* Upgrade imjoy-rpc to 0.5.57

* Integrate with hypha-rpc

* Format code

* Refactor check_client

* Add code to cleanup queue

* clean up s3 minio

* connect workspace loader

* Imporve load_workspace

* Fix generate_token

* Format code

* Improved life time managing

* Remove sse

* Add more info for connection

* Fix ASGI

* Adjust apps path

* simplify asgi

* Improve error message for disconnection

* Fix card controller

* Fix startup functions

* Fix tests for workspace loader

* Simplify workspace

* Restore long string tests

* Support server reconnection

* Fix tests

* Fix test

* remove success

* allow setting reconnection token life time

* Skip caching

* Small fix

* Add notice for the breaking changes

* release it as beta version

* Support custom messages and queue

* Upgrade imjoy-rpc to 0.5.57

* Integrate with hypha-rpc

* Format code

* Refactor check_client

* Add code to cleanup queue

* clean up s3 minio

* connect workspace loader

* Imporve load_workspace

* Fix generate_token

* Format code

* Improved life time managing

* Remove sse

* Add more info for connection

* Fix ASGI

* Adjust apps path

* simplify asgi

* Improve error message for disconnection

* Fix card controller

* Fix startup functions

* Fix tests for workspace loader

* Simplify workspace

* Restore long string tests

* Support server reconnection

* Fix tests

* Fix test

* remove success

* allow setting reconnection token life time

* Skip caching

* Small fix

* Add notice for the breaking changes

* release it as beta version

* Fix typing

* Improve login app

* Rename event handles

* Improve permission handling

* Fix visibility enum value

* Fix format

* Fix wait_for in python 3.11
  • Loading branch information
oeway authored Jul 31, 2024
1 parent cf80286 commit c95e046
Show file tree
Hide file tree
Showing 72 changed files with 4,275 additions and 3,505 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

Hypha is an application framework for large-scale data management and AI model serving, it allows creating computational platforms consists of computational and user interface components.

Hypha server act as a hub for connecting different components through [imjoy-rpc](https://github.com/imjoy-team/imjoy-rpc).
Hypha server act as a hub for connecting different components through [hypya-rpc](https://github.com/oeway/hypha-rpc).

## Breaking changes

The new version of Hypha (0.20.0+) improves the RPC connection to make it more stable and secure, most importantly it supports automatic reconnection when the connection is lost. This also means breaking changes to the previous version. In the new version you will need a new library called `hypha-rpc` (instead of the hypha submodule in the `imjoy-rpc` module) to connect to the server.

See https://ha.amun.ai for more detailed usage.
2 changes: 1 addition & 1 deletion docs/_sidebar.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
* [Getting Started](/getting-started)
* [ImJoy RPC V2](/imjoy-rpc)
* [Hypha RPC](/hypha-rpc)
* [Serverless Functions](/serverless-functions)
* [Serve ASGI Web Apps](/asgi-apps)
* [Development](/development)
2 changes: 1 addition & 1 deletion docs/asgi-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The following example shows how to use FastAPI to create a web application in th

```python
import asyncio
from imjoy_rpc.hypha import connect_to_server
from hypha_rpc import connect_to_server

import micropip

Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- We use [`black`](https://github.com/ambv/black) for code formatting.

```
git clone [email protected]:imjoy-team/hypha.git
git clone [email protected]:amun-ai/hypha.git
# Enter directory.
cd hypha
# Install all development requirements and package in development mode.
Expand Down
86 changes: 57 additions & 29 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,28 @@ After running the command, you can access files from these directories via the H

Hypha provides native support for Python and JavaScript clients. For other languages, you can use the built-in HTTP proxy of Hypha (see details in a later section about "Login and Using Services from the HTTP proxy").

Ensure that the server is running, and you can connect to it using the `hypha` module under `imjoy-rpc` in a client script. You can either register a service or use an existing service.
Ensure that the server is running, and you can connect to it using the `hypha` module under `hypha-rpc` in a client script. You can either register a service or use an existing service.

**WARNING: If you use hypha older than 0.15.x or lower, you will need to use `imjoy-rpc` instead of `hypha-rpc`.**

### Registering a Service

To register a service in Python, install the `imjoy-rpc` library:
To register a service in Python, install the `hypha-rpc` library:

```bash
pip install imjoy-rpc
pip install hypha-rpc
```

The following code registers a service called "Hello World" with the ID "hello-world" and a function called `hello()`. The function takes a single argument, `name`, and prints "Hello" followed by the name. The function also returns a string containing "Hello" followed by the name.

We provide two versions of the code: an asynchronous version for native CPython or Pyodide-based Python in the browser (without thread support), and a synchronous version for native Python with thread support (more details about the [synchronous wrapper](/imjoy-rpc?id=synchronous-wrapper)):
We provide two versions of the code: an asynchronous version for native CPython or Pyodide-based Python in the browser (without thread support), and a synchronous version for native Python with thread support (more details about the [synchronous wrapper](/hypha-rpc?id=synchronous-wrapper)):

<!-- tabs:start -->
#### ** Asynchronous Worker **

```python
import asyncio
from imjoy_rpc.hypha import connect_to_server
from hypha_rpc import connect_to_server

async def start_server(server_url):
server = await connect_to_server({"server_url": server_url})
Expand Down Expand Up @@ -105,7 +107,7 @@ if __name__ == "__main__":
#### ** Synchronous Worker **

```python
from imjoy_rpc.hypha.sync import connect_to_server
from hypha_rpc.sync import connect_to_server

def start_server(server_url):
server = connect_to_server({"server_url": server_url})
Expand Down Expand Up @@ -142,22 +144,22 @@ If you keep the Python service running, you can connect to it from either a Pyth

#### Python Client

Install the `imjoy-rpc` library:
Install the `hypha-rpc` library:

```bash
pip install imjoy-rpc
pip install hypha-rpc
```

Use the following code to connect to the server and access the service. The code first connects to the server and then gets the service by its ID. The service can then be used like a normal Python object.

Similarily, you can also use the `connect_to_server_sync` function to connect to the server synchronously (available since `imjoy-rpc>=0.5.25.post0`).
Similarily, you can also use the `connect_to_server_sync` function to connect to the server synchronously.

<!-- tabs:start -->
#### ** Asynchronous Client **

```python
import asyncio
from imjoy_rpc.hypha import connect_to_server
from hypha_rpc import connect_to_server

async def main():
server = await connect_to_server({"server_url": "http://localhost:9000"})
Expand All @@ -175,7 +177,7 @@ if __name__ == "__main__":

```python
import asyncio
from imjoy_rpc.hypha.sync import connect_to_server
from hypha_rpc.sync import connect_to_server

def main():
server = connect_to_server({"server_url": "http://localhost:9000"})
Expand All @@ -191,14 +193,14 @@ if __name__ == "__main__":
```
<!-- tabs:end -->

**NOTE: In Python, the recommended way to interact with the server to use asynchronous functions with `asyncio`. However, if you need to use synchronous functions, you can use `from imjoy_rpc.hypha.sync import login, connect_to_server` (available since `imjoy-rpc>=0.5.25.post0`) instead. The have the exact same arguments as the asynchronous versions. For more information, see [Synchronous Wrapper](/imjoy-rpc?id=synchronous-wrapper)**
**NOTE: In Python, the recommended way to interact with the server to use asynchronous functions with `asyncio`. However, if you need to use synchronous functions, you can use `from hypha_rpc.sync import login, connect_to_server` instead. The have the exact same arguments as the asynchronous versions. For more information, see [Synchronous Wrapper](/hypha-rpc?id=synchronous-wrapper)**

#### JavaScript Client

Include the following script in your HTML file to load the `imjoy-rpc` client:
Include the following script in your HTML file to load the `hypha-rpc` client:

```html
<script src="https://cdn.jsdelivr.net/npm/imjoy-rpc@0.5.6/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hypha-rpc@0.1.10/dist/hypha-rpc-websocket.min.js"></script>
```

Use the following code in JavaScript to connect to the server and access an existing service:
Expand All @@ -214,7 +216,7 @@ async function main(){

### Peer-to-Peer Connection via WebRTC

By default all the clients connected to Hypha server communicate via the websocket connection or the HTTP proxy. This is suitable for most use cases which involves lightweight data exchange. However, if you need to transfer large data or perform real-time communication, you can use the WebRTC connection between clients. With imjoy-rpc, you can easily create a WebRTC connection between two clients easily. See the [WebRTC support in ImJoy RPC V2](/imjoy-rpc?id=peer-to-peer-connection-via-webrtc) for more details.
By default all the clients connected to Hypha server communicate via the websocket connection or the HTTP proxy. This is suitable for most use cases which involves lightweight data exchange. However, if you need to transfer large data or perform real-time communication, you can use the WebRTC connection between clients. With hypha-rpc, you can easily create a WebRTC connection between two clients easily. See the [WebRTC support](/hypha-rpc?id=peer-to-peer-connection-via-webrtc) for more details.

### User Login and Token-Based Authentication

Expand All @@ -226,7 +228,7 @@ Here is an example of how the login process works using the `login()` function:
#### ** Asynchronous Client **

```python
from imjoy_rpc.hypha import login, connect_to_server
from hypha_rpc import login, connect_to_server

token = await login({"server_url": "https://ai.imjoy.io"})
# A login URL will be printed to the console
Expand All @@ -240,7 +242,7 @@ server = await connect_to_server({"server_url": "https://ai.imjoy.io", "token":
#### ** Synchronous Client **

```python
from imjoy_rpc.hypha.sync import login, connect_to_server
from hypha_rpc.sync import login, connect_to_server

token = login({"server_url": "https://ai.imjoy.io"})
server = connect_to_server({"server_url": "https://ai.imjoy.io", "token": token})
Expand Down Expand Up @@ -276,13 +278,10 @@ token = await login(

If no `login_callback` is passed, the login URL will be printed to the console. You can also pass a callback function as `login_callback` to perform custom actions during the login process.

For example, here is a callback function for displaying the login URL, QR code to print the QR code to the console), or launching a browser for the user to log in:
For example, here is a callback function for displaying the login URL, or launching a browser for the user to login:

```python

# Require `pip install qrcode[pil]`
from imjoy_rpc.qr import display_qrcode

async def callback(context):
"""
Callback function for login.
Expand All @@ -295,24 +294,50 @@ async def callback(context):
- report_url: the report URL
- key: the key for the login
"""
print(f"By passing login: {context['login_url']}")
# Show the QR code for login
display_qrcode(context["login_url"])
print(f"Go to login url: {context['login_url']}")
```

### Service Authorization
### User Authentication and Service Authorization

Hypha provide built-in support for user authentication, and based on the user context, you can also customize the service authorization within each service function.

In the previous example, we registered a public service (`config.visibility = "public"`) that can be accessed by any client. If you want to limit service access to a subset of clients, there are two ways to provide authorization.

1. Connecting to the Same Workspace: Set `config.visibility` to `"private"`. Authorization is achieved by generating a token from the client that registered the service (using `server.config.workspace` and `server.generate_token()`). Another client can connect to the same workspace using the token (`connect_to_server({"workspace": xxxx, "token": xxxx, "server_url": xxxx})`).

2. Using User Context: When registering a service, set `config.require_context` to `True` and `config.visibility` to `"public"` (or `"private"` to limit access for clients from the same workspace). Each service function needs to accept a keyword argument called `context`. The server will provide the context information containing `user` for each service function call. The service function can then check whether `context.user["id"]` is allowed to access the service. On the client side, you need to log in and generate a token by calling the `login({"server_url": xxxx})` function. The token is then used in `connect_to_server({"token": xxxx, "server_url": xxxx})`.

Here is an example for how to enable the user context in the service function:
```python

async def start_service(server):
"""Hypha startup function."""

def test(x, context=None): # Note that the context argument is added
current_user = context["user"]
if current_user["email"] not in authorized_users:
raise Exception(f"User {current_user['email']} is not authorized to access this service.")
print(f"Test: {x}")

# Register a test service
await server.register_service(
{
"id": "test-service",
"config": {
"visibility": "public",
"require_context": True, # enable user context
},
"test": test,
}
)
```

By default, hypha server uses a user authentication system based on [Auth0](https://auth0.com) controlled by us. You can also setup your own auth0 account to use it with your own hypha server. See [Setup Authentication](./setup-authentication) for more details.

### Login and Using Services from the HTTP proxy

#### Obtain login token via http requests
For clients other than Python or javascript (without imjoy-rpc) support, you can use Hypha server's built-in HTTP proxy for services to obtain the token. Here is an example of how to obtain the token via HTTP requests:
For clients other than Python or javascript (without hypha-rpc) support, you can use Hypha server's built-in HTTP proxy for services to obtain the token. Here is an example of how to obtain the token via HTTP requests:

First, let's initiate the login process by calling the `start` function of the `hypha-login` service:
```bash
Expand All @@ -335,7 +360,7 @@ For the above request:

This should wait and return the token if the user has successfully logged in, otherwise, it will return a timeout error.

For details, see the python implementation of the login function [here](https://github.com/imjoy-team/imjoy-rpc/blob/master/python/imjoy_rpc/hypha/websocket_client.py#L175).
For details, see the python implementation of the login function [here](https://github.com/imjoy-team/imjoy-rpc/blob/master/python/hypha_rpc/hypha/websocket_client.py#L175).

#### Using the Token in Service Requests

Expand Down Expand Up @@ -367,15 +392,18 @@ Here's an example of `example-startup-function.py`:
async def hypha_startup(server):
"""Hypha startup function."""

def test(x):
print(f"Test: {x}")

# Register a test service
await server.register_service(
{
"id": "test-service",
"config": {
"visibility": "public",
"require_context": True,
"require_context": False,
},
"test": lambda x: print(f"Test: {x}"),
"test": test,
}
)
```
Expand Down
4 changes: 2 additions & 2 deletions docs/hypha-in-pyodide.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ Pyodide is a Python runtime for the web, which allows you to run Python code in
```python
import micropip

await micropip.install(['hypha', 'imjoy-rpc'])
await micropip.install(['hypha', 'hypha-rpc'])

import asyncio
from imjoy_rpc.hypha import login, connect_to_server
from hypha_rpc import login, connect_to_server

from hypha.server import get_argparser, create_application

Expand Down
42 changes: 21 additions & 21 deletions docs/hypha-quick-tour.ipynb

Large diffs are not rendered by default.

30 changes: 13 additions & 17 deletions docs/hypha_data_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,6 @@ def put(self, obj_type: str, value: any, name: str, comment: str = ""):
if obj_type == "file":
data = value
assert isinstance(data, (str, bytes)), "Value must be a string or bytes"
if isinstance(data, str) and data.startswith("file://"):
# File URL examples:
# Absolute URL: `file:///home/data/myfile.png`
# Relative URL: `file://./myimage.png`, or `file://myimage.png`
with open(data.replace("file://", ""), "rb") as fil:
data = fil.read()
mime_type, _ = mimetypes.guess_type(name)
self.storage[obj_id] = {
"type": obj_type,
Expand Down Expand Up @@ -73,21 +67,23 @@ def http_get(self, scope, context=None):
if obj["type"] == "file":
data = obj["value"]
if isinstance(data, str):
if not os.path.isfile(data):
return {
"status": 404,
"headers": {"Content-Type": "text/plain"},
"body": "File not found: " + data,
}
with open(data, "rb") as fil:
data = fil.read()
if data.startswith("file://"):
file_path = data.replace("file://", "")
if not os.path.isfile(file_path):
return {
"status": 404,
"headers": {"Content-Type": "text/plain"},
"body": "File not found: " + file_path,
}
with open(file_path, "rb") as fil:
data = fil.read()
headers = {
"Content-Type": obj["mime_type"],
"Content-Length": str(len(obj["value"])),
"Content-Length": str(len(data)),
"Content-Disposition": f'inline; filename="{obj["name"].split("/")[-1]}"',
}

return {"status": 200, "headers": headers, "body": obj["value"]}
return {"status": 200, "headers": headers, "body": data}
else:
return {
"status": 200,
Expand Down Expand Up @@ -119,7 +115,7 @@ def remove(self, obj_id: str):


async def test_data_store(server_url="https://ai.imjoy.io"):
from imjoy_rpc.hypha import connect_to_server, login
from hypha_rpc import connect_to_server, login

token = await login({"server_url": server_url})
server = await connect_to_server({"server_url": server_url, "token": token})
Expand Down
1 change: 1 addition & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
homepage: homepage,
alias: {
"/overview": "https://raw.githubusercontent.com/amun-ai/hypha/main/README.md",
"/hypha-rpc": "https://raw.githubusercontent.com/oeway/hypha-rpc/main/README.md",
"/imjoy-rpc": "https://raw.githubusercontent.com/imjoy-team/imjoy-rpc/master/imjoy-rpc-v2.md"
},
search: {
Expand Down
Loading

0 comments on commit c95e046

Please sign in to comment.