diff --git a/hypha/http.py b/hypha/http.py index a03f5ec5..b69d7e63 100644 --- a/hypha/http.py +++ b/hypha/http.py @@ -16,78 +16,6 @@ from hypha.utils import GzipRoute from hypha import __version__ as VERSION -SERVICES_OPENAPI_SCHEMA = { - "openapi": "3.1.0", - "info": { - "title": "Hypha Services", - "description": "Providing access to services in Hypha", - "version": f"v{VERSION}", - }, - "paths": { - "/call": { - "post": { - "description": "Call a service function", - "operationId": "CallServiceFunction", - "requestBody": { - "required": True, - "description": "The request body type depends on each service function schema", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceFunctionSchema" - } - } - }, - }, - "deprecated": False, - } - }, - "/list": { - "get": { - "description": "List services under a workspace", - "operationId": "ListServices", - "parameters": [ - { - "name": "workspace", - "in": "query", - "description": "Workspace name", - "required": True, - "schema": {"type": "string"}, - } - ], - "deprecated": False, - } - }, - }, - "components": { - "schemas": { - "ServiceFunctionSchema": { - "type": "object", - "properties": { - "workspace": { - "description": "Workspace name, optional if the service_id contains workspace name", - "type": "string", - }, - "service_id": { - "description": "Service ID, format: workspace/client_id:service_id", - "type": "string", - }, - "function_key": { - "description": "Function key, can contain dot to refer to deeper object", - "type": "string", - }, - "function_kwargs": { - "description": "Function keyword arguments, must be set according to the function schema", - "type": "object", - }, - }, - "required": ["service_id", "function_key"], - } - } - }, -} - - class MsgpackResponse(Response): """Response class for msgpack encoding.""" @@ -283,7 +211,7 @@ async def list_all_workspaces( content={"success": False, "detail": str(exp)}, ) - @router.get("/{workspace}/info") + @router.get("/workspaces/{workspace}") async def get_workspace_info( workspace: str, user_info: login_optional = Depends(login_optional), @@ -325,35 +253,7 @@ async def list_services( """List services under a workspace.""" return await get_workspace_services(workspace, user_info) - @router.get("/services/{service_id:path}") - async def get_service_info( - service_id: str, - user_info: login_optional = Depends(login_optional), - ): - """Get service info.""" - assert "/" in service_id, "service_id should contain workspace name" - assert ":" in service_id, "service_id should contain client_id" - workspace, service_id = service_id.split("/") - return await get_service_info(workspace, service_id, user_info) - - @router.post("/services/{workspace}/{service_id}/{function_key}") - async def call_service_function_post( - workspace: str, - service_id: str, - function_key: str, - request: Request, - user_info: login_optional = Depends(login_optional), - ): - """Call a service function by keys.""" - function_kwargs = await extracted_kwargs(request, use_function_kwargs=False) - response_type = detected_response_type(request) - return await service_function( - (workspace, service_id, function_key), - function_kwargs, - response_type, - user_info, - ) - + @router.get("/services//{workspace}") @router.get("/{workspace}/services") async def get_workspace_services( workspace: str, @@ -379,6 +279,7 @@ async def get_workspace_services( content={"success": False, "detail": str(exp)}, ) + @router.get("/services/{workspace}/{service_id}") @router.get("/{workspace}/services/{service_id}") async def get_service_info( workspace: str, @@ -412,32 +313,11 @@ async def _call_service_function(func, kwargs): results = _rpc.encode(results) return results + @router.get("/services/{workspace}/{service_id}/{function_key}") + @router.post("/services/{workspace}/{service_id}/{function_key}") @router.get("/{workspace}/services/{service_id}/{function_key}") - async def service_function_get( - workspace: str, - service_id: str, - function_key: str, - request: Request, - user_info: login_optional = Depends(login_optional), - ): - """Run service function by keys.""" - function_kwargs = await extracted_kwargs(request, use_function_kwargs=False) - response_type = detected_response_type(request) - try: - return await service_function( - (workspace, service_id, function_key), - function_kwargs, - response_type, - user_info, - ) - except Exception: - return JSONResponse( - status_code=400, - content={"success": False, "detail": traceback.format_exc()}, - ) - @router.post("/{workspace}/services/{service_id}/{function_key}") - async def service_function_post( + async def call_service_function( workspace: str, service_id: str, function_key: str, diff --git a/hypha/runner/__init__.py b/hypha/runner/__init__.py index 1dddf2a0..d08232b8 100644 --- a/hypha/runner/__init__.py +++ b/hypha/runner/__init__.py @@ -15,21 +15,21 @@ logging.basicConfig(stream=sys.stdout) -logger = logging.getLogger("browser-runner") +logger = logging.getLogger("app-runner") logger.setLevel(logging.INFO) -async def export_service(plugin_api, config, hypha_rpc): +async def export_service(app_api, config, hypha_rpc): try: wm = await connect_to_server(config) hypha_rpc.api.update(wm) # make the api available to the app rpc = wm.rpc - if not isinstance(plugin_api, dict) and inspect.isclass(type(plugin_api)): - plugin_api = {a: getattr(plugin_api, a) for a in dir(plugin_api)} + if not isinstance(app_api, dict) and inspect.isclass(type(app_api)): + app_api = {a: getattr(app_api, a) for a in dir(app_api)} # Copy the app name as the default name - plugin_api["id"] = "default" - plugin_api["name"] = config.get("name", "default") - svc = await rpc.register_service(plugin_api, overwrite=True, notify=True) + app_api["id"] = "default" + app_api["name"] = config.get("name", "default") + svc = await rpc.register_service(app_api, overwrite=True, notify=True) svc = await rpc.get_remote_service(svc["id"]) if svc.setup: await svc.setup() @@ -53,26 +53,26 @@ def export(api, config=None): return hypha_rpc -async def run_plugin(plugin_file, default_config, quit_on_ready=False): +async def run_app(app_file, default_config, quit_on_ready=False): """Load app file.""" loop = asyncio.get_event_loop() - if os.path.isfile(plugin_file): - async with aiofiles.open(plugin_file, "r", encoding="utf-8") as fil: + if os.path.isfile(app_file): + async with aiofiles.open(app_file, "r", encoding="utf-8") as fil: content = await fil.read() - elif plugin_file.startswith("http"): - with urllib.request.urlopen(plugin_file) as response: + elif app_file.startswith("http"): + with urllib.request.urlopen(app_file) as response: content = response.read().decode("utf-8") # remove query string - plugin_file = plugin_file.split("?")[0] + app_file = app_file.split("?")[0] else: - raise Exception(f"Invalid input app file path: {plugin_file}") + raise Exception(f"Invalid input app file path: {app_file}") - if plugin_file.endswith(".py"): - filename, _ = os.path.splitext(os.path.basename(plugin_file)) + if app_file.endswith(".py"): + filename, _ = os.path.splitext(os.path.basename(app_file)) default_config["name"] = filename[:32] hypha_rpc = await patch_hypha_rpc(default_config) exec(content, globals()) # pylint: disable=exec-used - logger.info("Plugin executed") + logger.info("app executed") if quit_on_ready: @@ -84,28 +84,28 @@ def done_callback(fut): hypha_rpc.ready.add_done_callback(done_callback) - elif plugin_file.endswith(".imjoy.html"): + elif app_file.endswith(".imjoy.html"): # load config found = re.findall("\n(.*)", content, re.DOTALL)[0] if "json" in found[0]: - plugin_config = json.loads(found[1]) + app_config = json.loads(found[1]) elif "yaml" in found[0]: - plugin_config = yaml.safe_load(found[1]) - default_config.update(plugin_config) + app_config = yaml.safe_load(found[1]) + default_config.update(app_config) hypha_rpc = await patch_hypha_rpc(default_config) # load script found = re.findall("", content, re.DOTALL)[0] if "python" in found[0]: exec(found[1], globals()) # pylint: disable=exec-used - logger.info("Plugin executed") + logger.info("app executed") if quit_on_ready: hypha_rpc.ready.add_done_callback(lambda fut: loop.stop()) else: raise RuntimeError( - f"Invalid script type ({found[0]}) in file {plugin_file}" + f"Invalid script type ({found[0]}) in file {app_file}" ) else: - raise RuntimeError(f"Invalid script file type ({plugin_file})") + raise RuntimeError(f"Invalid script file type ({app_file})") async def start(args): @@ -116,7 +116,7 @@ async def start(args): "workspace": args.workspace, "token": args.token, } - await run_plugin(args.file, default_config, quit_on_ready=args.quit_on_ready) + await run_app(args.file, default_config, quit_on_ready=args.quit_on_ready) except Exception: # pylint: disable=broad-except logger.exception("Failed to run app, exiting.") loop = asyncio.get_event_loop() diff --git a/tests/example_service_script.py b/tests/example_service_script.py index 8b30e46b..1bc6b0e1 100644 --- a/tests/example_service_script.py +++ b/tests/example_service_script.py @@ -35,7 +35,7 @@ async def start_service(server_url, service_id, workspace=None, token=None): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Test services") parser.add_argument( - "--server-url", type=str, default="https://ai.imjoy.io/", help="The server url" + "--server-url", type=str, default="https://ai.imjoy.io", help="The server url" ) parser.add_argument( "--service-id", type=str, default="test-service", help="The service id" diff --git a/tests/test_http.py b/tests/test_http.py index 69b34465..39cfae0c 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -130,7 +130,7 @@ async def test_http_proxy(minio_server, fastapi_server, test_user_token): assert workspace in response response = requests.get( - f"{SERVER_URL}/{workspace}/info", headers={"Authorization": f"Bearer {token}"} + f"{SERVER_URL}/workspaces/{workspace}", headers={"Authorization": f"Bearer {token}"} ) assert response.ok, response.json()["detail"] response = response.json()