mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-11 12:30:25 +08:00
Implement JSON API for note
This commit is contained in:
@@ -38,6 +38,7 @@ from ._intermediate_values import has_intermediate_values
|
||||
from ._search_space import get_search_space
|
||||
from ._serializer import serialize_study_detail
|
||||
from ._serializer import serialize_study_summary
|
||||
from . import _note as note
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -281,6 +282,25 @@ def create_app(storage: BaseStorage) -> Bottle:
|
||||
],
|
||||
}
|
||||
|
||||
@app.post("/api/studies/<study_id:int>/note")
|
||||
@handle_json_api_exception
|
||||
def save_note(study_id: int) -> BottleViewReturn:
|
||||
response.content_type = "application/json"
|
||||
|
||||
system_attrs = storage.get_study_system_attrs(study_id)
|
||||
req_note_ver = request.json.get("version", None)
|
||||
req_note_body = request.json.get("body", None)
|
||||
if req_note_ver is None or req_note_body is None:
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": "Invalid request."}
|
||||
if not note.version_is_incremented(system_attrs, req_note_ver):
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": "The text you are editing has changed. Please copy your edits and refresh the page."}
|
||||
|
||||
note.save_note(storage, study_id, req_note_ver, req_note_body)
|
||||
response.status = 204 # No content
|
||||
return {}
|
||||
|
||||
@app.get("/static/<filename:path>")
|
||||
def send_static(filename: str) -> BottleViewReturn:
|
||||
return static_file(filename, root=STATIC_DIR)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
from typing import Dict, Any
|
||||
from typing import TypedDict
|
||||
import math
|
||||
|
||||
from optuna.storages import BaseStorage
|
||||
|
||||
SYSTEM_ATTR_MAX_LENGTH = 2045
|
||||
NOTE_VER_KEY = "dashboard:note_ver"
|
||||
NOTE_STR_KEY_PREFIX = "dashboard:note_str:"
|
||||
|
||||
NoteType = TypedDict(
|
||||
"NoteType",
|
||||
{
|
||||
"version": int,
|
||||
"body": str,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_note_from_system_attrs(system_attrs: Dict[str, Any]) -> NoteType:
|
||||
if NOTE_VER_KEY not in system_attrs:
|
||||
return {
|
||||
"version": 0,
|
||||
"body": "",
|
||||
}
|
||||
note_ver = int(system_attrs[NOTE_VER_KEY])
|
||||
note_attrs: Dict[str, str] = {
|
||||
key: value
|
||||
for key, value in system_attrs.items()
|
||||
if key.startswith(NOTE_STR_KEY_PREFIX)
|
||||
}
|
||||
return {
|
||||
"version": note_ver,
|
||||
"body": concat_body(note_attrs)
|
||||
}
|
||||
|
||||
|
||||
def version_is_incremented(system_attrs: Dict[str, Any], req_note_ver: int) -> bool:
|
||||
db_note_ver = system_attrs.get(NOTE_VER_KEY, 0)
|
||||
return req_note_ver == db_note_ver + 1
|
||||
|
||||
|
||||
def save_note(storage: BaseStorage, study_id: int, ver: int, body: str) -> None:
|
||||
storage.set_study_system_attr(study_id, NOTE_VER_KEY, ver)
|
||||
|
||||
attrs = split_body(body)
|
||||
for k, v in attrs.items():
|
||||
storage.set_study_system_attr(study_id, k, v)
|
||||
|
||||
# Clear previous messages
|
||||
all_note_attrs: Dict[str, str] = {
|
||||
key: value
|
||||
for key, value in storage.get_study_system_attrs(study_id).items()
|
||||
if key.startswith(NOTE_STR_KEY_PREFIX)
|
||||
}
|
||||
if len(all_note_attrs) > len(attrs):
|
||||
for i in range(len(attrs), len(all_note_attrs)):
|
||||
storage.set_study_system_attr(study_id, f"{NOTE_STR_KEY_PREFIX}{i}", "")
|
||||
|
||||
|
||||
def split_body(note_str: str) -> Dict[str, str]:
|
||||
note_len = len(note_str)
|
||||
attrs = {}
|
||||
for i in range(math.ceil(note_len / SYSTEM_ATTR_MAX_LENGTH)):
|
||||
start = i * SYSTEM_ATTR_MAX_LENGTH
|
||||
end = min((i + 1) * SYSTEM_ATTR_MAX_LENGTH, note_len)
|
||||
attrs[f"{NOTE_STR_KEY_PREFIX}{i}"] = note_str[start:end]
|
||||
return attrs
|
||||
|
||||
|
||||
def concat_body(note_attrs: Dict[str, str]) -> str:
|
||||
return "".join(note_attrs[f"{NOTE_STR_KEY_PREFIX}{i}"] for i in range(len(note_attrs)))
|
||||
@@ -10,6 +10,8 @@ from optuna.distributions import BaseDistribution
|
||||
from optuna.study import StudySummary
|
||||
from optuna.trial import FrozenTrial
|
||||
|
||||
from . import _note as note
|
||||
|
||||
|
||||
try:
|
||||
from typing import TypedDict
|
||||
@@ -111,6 +113,7 @@ def serialize_study_detail(
|
||||
serialized["intersection_search_space"] = serialize_search_space(intersection)
|
||||
serialized["union_search_space"] = serialize_search_space(union)
|
||||
serialized["has_intermediate_values"] = has_intermediate_values
|
||||
serialized["note"] = note.get_note_from_system_attrs(summary.system_attrs)
|
||||
return serialized
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import {Card, CardContent, useTheme, TextField} from "@mui/material";
|
||||
import React, {FC} from "react";
|
||||
|
||||
|
||||
export const Note: FC<{studyId: number}> = ({studyId}) => {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
margin: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<TextField />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from optuna_dashboard import _note as note
|
||||
|
||||
|
||||
class NoteTestCase(TestCase):
|
||||
@patch("optuna_dashboard._note.SYSTEM_ATTR_MAX_LENGTH", 5)
|
||||
def test_split_and_concat_note_body(self) -> None:
|
||||
for dummy_body_str, attr_len in [
|
||||
("012", 1),
|
||||
("01234", 1),
|
||||
("012345", 2),
|
||||
]:
|
||||
with self.subTest(f"with_{dummy_body_str}_{attr_len}"):
|
||||
attrs = note.split_body(dummy_body_str)
|
||||
assert len(attrs) == attr_len
|
||||
actual = note.concat_body(attrs)
|
||||
assert dummy_body_str == actual
|
||||
Reference in New Issue
Block a user