diff --git a/jhub_apps/service/models.py b/jhub_apps/service/models.py index 7edd5f51..c2555160 100644 --- a/jhub_apps/service/models.py +++ b/jhub_apps/service/models.py @@ -2,6 +2,7 @@ from datetime import datetime from typing import Any, Dict, List, Optional + from pydantic import BaseModel @@ -51,7 +52,7 @@ class UserOptions(BaseModel): jhub_app: bool display_name: str description: str - thumbnail: typing.Optional[str] = str() + thumbnail: str = None filepath: typing.Optional[str] = str() framework: str = "panel" custom_command: typing.Optional[str] = str() diff --git a/jhub_apps/service/service.py b/jhub_apps/service/service.py index b7953408..56405f8d 100644 --- a/jhub_apps/service/service.py +++ b/jhub_apps/service/service.py @@ -1,8 +1,11 @@ +import typing import dataclasses import os from datetime import timedelta -from fastapi import APIRouter, Depends, status, Request +from fastapi import APIRouter, Depends, status, Request, File, UploadFile, Form, HTTPException +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel, ValidationError from starlette.responses import RedirectResponse from jhub_apps.service.auth import create_access_token @@ -14,7 +17,8 @@ from fastapi.templating import Jinja2Templates from jhub_apps.hub_client.hub_client import HubClient -from jhub_apps.service.utils import get_conda_envs, get_jupyterhub_config, get_spawner_profiles +from jhub_apps.service.utils import get_conda_envs, get_jupyterhub_config, get_spawner_profiles, \ + encode_file_to_data_url from jhub_apps.spawner.types import FRAMEWORKS app = FastAPI() @@ -62,6 +66,7 @@ async def login(request: Request): authorization_url = os.environ["PUBLIC_HOST"] + "/hub/api/oauth2/authorize?response_type=code&client_id=service-japps" return RedirectResponse(authorization_url, status_code=302) + @router.get("/server/", description="Get all servers") @router.get("/server/{server_name}", description="Get a server by server name") async def get_server(user: User = Depends(get_current_user), server_name=None): @@ -81,12 +86,31 @@ async def get_server(user: User = Depends(get_current_user), server_name=None): return user_servers +class Checker: + def __init__(self, model: BaseModel): + self.model = model + + def __call__(self, data: str = Form(...)): + try: + return self.model.model_validate_json(data) + except ValidationError as e: + raise HTTPException( + detail=jsonable_encoder(e.errors()), + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + ) + + @router.post("/server/") async def create_server( - # request: Request, - server: ServerCreation, + server: ServerCreation = Depends(Checker(ServerCreation)), + thumbnail: typing.Optional[UploadFile] = File(...), user: User = Depends(get_current_user), ): + if thumbnail: + thumbnail_contents = await thumbnail.read() + server.user_options.thumbnail = encode_file_to_data_url( + thumbnail.filename, thumbnail_contents + ) hub_client = HubClient() return hub_client.create_server( username=user.name, diff --git a/jhub_apps/service/utils.py b/jhub_apps/service/utils.py index 5c78f605..0e99f160 100644 --- a/jhub_apps/service/utils.py +++ b/jhub_apps/service/utils.py @@ -1,3 +1,4 @@ +import base64 import os from jupyterhub.app import JupyterHub @@ -44,3 +45,14 @@ def get_spawner_profiles(config): raise ValueError( f"Invalid value for config.KubeSpawner.profile_list: {profile_list}" ) + + +def encode_file_to_data_url(filename, file_contents): + """Converts image file to data url to display in browser.""" + base64_encoded = base64.b64encode(file_contents) + filename_ = filename.lower() + mime_type = "image/png" + if filename_.endswith(".jpg") or filename_.endswith(".jpeg"): + mime_type = "image/jpeg" + data_url = f"data:{mime_type};base64,{base64_encoded.decode('utf-8')}" + return data_url diff --git a/jhub_apps/tests/test_api.py b/jhub_apps/tests/test_api.py index 8becd8cd..f357b7f3 100644 --- a/jhub_apps/tests/test_api.py +++ b/jhub_apps/tests/test_api.py @@ -1,4 +1,6 @@ import dataclasses +import io +import json from unittest.mock import patch from jhub_apps.hub_client.hub_client import HubClient @@ -34,16 +36,22 @@ def test_api_get_server(get_user, client): @patch.object(HubClient, "create_server") def test_api_create_server(create_server, client): from jhub_apps.service.models import UserOptions - create_server_response = {"user": "aktech"} create_server.return_value = create_server_response user_options = mock_user_options() - body = {"servername": "panel-app", "user_options": user_options} - response = client.post("/server/", json=body) + thumbnail = b"contents of thumbnail" + in_memory_file = io.BytesIO(thumbnail) + response = client.post( + "/server/", + data={'data': json.dumps({"servername": "panel-app", "user_options": user_options})}, + files={'thumbnail': ('image.jpeg', in_memory_file)} + ) + final_user_options = UserOptions(**user_options) + final_user_options.thumbnail = "data:image/jpeg;base64,Y29udGVudHMgb2YgdGh1bWJuYWls" create_server.assert_called_once_with( username=MOCK_USER.name, servername="panel-app", - user_options=UserOptions(**user_options), + user_options=final_user_options, ) assert response.json() == create_server_response