Merge pull request #180 from optuna/note

Note component to set study's description
This commit is contained in:
Masashi Shibata
2022-03-19 17:06:15 +09:00
committed by GitHub
12 changed files with 626 additions and 59 deletions
+11 -6
View File
@@ -43,7 +43,8 @@ const trials = [
const study_direction: StudyDirection = "minimize" as StudyDirection
const study_detail = {
const studyDetail = {
id: 1,
name: "study_0",
directions: [study_direction],
datetime_start: new Date("2021-06-15T00:00:00"),
@@ -74,11 +75,15 @@ const study_detail = {
},
],
has_intermediate_values: false,
note: {
version: 0,
body: "",
},
}
it("Sort TrialTable by trial number", () => {
const { getAllByRole, getByText } = render(
<TrialTable studyDetail={study_detail} />
<TrialTable studyDetail={studyDetail} />
)
const rows = getAllByRole("row")
expect(within(rows[1]).getByText("0")).toBeTruthy()
@@ -93,7 +98,7 @@ it("Sort TrialTable by trial number", () => {
it("Sort TrialTable by value", () => {
const { getAllByRole, getByText } = render(
<TrialTable studyDetail={study_detail} />
<TrialTable studyDetail={studyDetail} />
)
fireEvent.click(getByText("Value"))
const rows = getAllByRole("row")
@@ -108,7 +113,7 @@ it("Sort TrialTable by value", () => {
it("Sort TrialTable by duration", () => {
const { getAllByRole, getByText } = render(
<TrialTable studyDetail={study_detail} />
<TrialTable studyDetail={studyDetail} />
)
fireEvent.click(getByText("Duration(ms)"))
const rows = getAllByRole("row")
@@ -123,7 +128,7 @@ it("Sort TrialTable by duration", () => {
it("Sort TrialTable by state", () => {
const { getAllByRole, getByText } = render(
<TrialTable studyDetail={study_detail} />
<TrialTable studyDetail={studyDetail} />
)
fireEvent.click(getByText("State"))
const rows = getAllByRole("row")
@@ -137,7 +142,7 @@ it("Sort TrialTable by state", () => {
})
it("Filter trials by state", () => {
const { queryAllByText } = render(<TrialTable studyDetail={study_detail} />)
const { queryAllByText } = render(<TrialTable studyDetail={studyDetail} />)
expect(queryAllByText("Fail").length).toBe(1)
// Click 'Complete' state
+30 -13
View File
@@ -34,6 +34,7 @@ from optuna.study import StudySummary
from optuna.trial import FrozenTrial
from optuna.trial import TrialState
from . import _note as note
from ._intermediate_values import has_intermediate_values
from ._search_space import get_search_space
from ._serializer import serialize_study_detail
@@ -71,10 +72,11 @@ trials_cache: Dict[int, List[FrozenTrial]] = {}
trials_last_fetched_at: Dict[int, datetime] = {}
def handle_json_api_exception(view: BottleView) -> BottleView:
def json_api_view(view: BottleView) -> BottleView:
@functools.wraps(view)
def decorated(*args: List[Any], **kwargs: Dict[str, Any]) -> BottleViewReturn:
try:
response.content_type = "application/json"
response_body = view(*args, **kwargs)
return response_body
except Exception as e:
@@ -140,9 +142,8 @@ def create_app(storage: BaseStorage) -> Bottle:
return INDEX_HTML
@app.get("/api/studies")
@handle_json_api_exception
@json_api_view
def list_study_summaries() -> BottleViewReturn:
response.content_type = "application/json"
summaries = [
serialize_study_summary(summary)
for summary in storage.get_all_study_summaries()
@@ -152,10 +153,8 @@ def create_app(storage: BaseStorage) -> Bottle:
}
@app.post("/api/studies")
@handle_json_api_exception
@json_api_view
def create_study() -> BottleViewReturn:
response.content_type = "application/json"
study_name = request.json.get("study_name", None)
directions = request.json.get("directions", [])
if (
@@ -190,10 +189,8 @@ def create_app(storage: BaseStorage) -> Bottle:
return {"study_summary": serialize_study_summary(summary)}
@app.delete("/api/studies/<study_id:int>")
@handle_json_api_exception
@json_api_view
def delete_study(study_id: int) -> BottleViewReturn:
response.content_type = "application/json"
try:
storage.delete_study(study_id)
except KeyError:
@@ -203,9 +200,8 @@ def create_app(storage: BaseStorage) -> Bottle:
return ""
@app.get("/api/studies/<study_id:int>")
@handle_json_api_exception
@json_api_view
def get_study_detail(study_id: int) -> BottleViewReturn:
response.content_type = "application/json"
try:
after = int(request.params["after"])
assert after >= 0
@@ -229,10 +225,9 @@ def create_app(storage: BaseStorage) -> Bottle:
)
@app.get("/api/studies/<study_id:int>/param_importances")
@handle_json_api_exception
@json_api_view
def get_param_importances(study_id: int) -> BottleViewReturn:
# TODO(chenghuzi): add support for selecting params via query parameters.
response.content_type = "application/json"
objective_id = int(request.params.get("objective_id", 0))
try:
study_name = storage.get_study_name_from_id(study_id)
@@ -281,6 +276,28 @@ def create_app(storage: BaseStorage) -> Bottle:
],
}
@app.put("/api/studies/<study_id:int>/note")
@json_api_view
def save_note(study_id: int) -> BottleViewReturn:
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."}
system_attrs = storage.get_study_system_attrs(study_id)
if not note.version_is_incremented(system_attrs, req_note_ver):
response.status = 409 # Conflict
return {
"reason": "The text you are editing has changed. "
"Please copy your edits and refresh the page.",
"note": note.get_note_from_system_attrs(system_attrs),
}
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)
+77
View File
@@ -0,0 +1,77 @@
import math
from typing import Any
from typing import Dict
from optuna.storages import BaseStorage
try:
from typing import TypedDict
except ImportError:
from typing_extensions import TypedDict
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))
)
+3
View File
@@ -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
+40 -6
View File
@@ -5,6 +5,7 @@ import {
getStudySummariesAPI,
createNewStudyAPI,
deleteStudyAPI,
saveNoteAPI,
} from "./apiClient"
import { studyDetailsState, studySummariesState } from "./state"
@@ -15,6 +16,12 @@ export const actionCreator = () => {
const [studyDetails, setStudyDetails] =
useRecoilState<StudyDetails>(studyDetailsState)
const setStudyDetailState = (studyId: number, study: StudyDetail) => {
const newVal = Object.assign({}, studyDetails)
newVal[studyId] = study
setStudyDetails(newVal)
}
const updateStudySummaries = (successMsg?: string) => {
getStudySummariesAPI()
.then((studySummaries: StudySummary[]) => {
@@ -49,15 +56,15 @@ export const actionCreator = () => {
? studyDetails[studyId].trials.slice(0, nLocalFixedTrials)
: []
study.trials = study.trials.concat(currentFixedTrials)
const newVal = Object.assign({}, studyDetails)
newVal[studyId] = study
setStudyDetails(newVal)
setStudyDetailState(studyId, study)
})
.catch((err) => {
const reason = err.response?.data.reason
enqueueSnackbar(`Failed to fetch study (reason=${reason})`, {
variant: "error",
})
if (reason !== undefined) {
enqueueSnackbar(`Failed to fetch study (reason=${reason})`, {
variant: "error",
})
}
console.log(err)
})
}
@@ -95,11 +102,38 @@ export const actionCreator = () => {
})
}
const saveNote = (studyId: number, note: Note): Promise<void> => {
return saveNoteAPI(studyId, note)
.then(() => {
const newStudy = Object.assign({}, studyDetails[studyId])
newStudy.note = note
setStudyDetailState(studyId, newStudy)
enqueueSnackbar(`Success to save the note`, {
variant: "success",
})
})
.catch((err) => {
if (err.response.status === 409) {
const newStudy = Object.assign({}, studyDetails[studyId])
newStudy.note = err.response.data.note
setStudyDetailState(studyId, newStudy)
}
const reason = err.response?.data.reason
if (reason !== undefined) {
enqueueSnackbar(`Failed: ${reason}`, {
variant: "error",
})
}
throw err
})
}
return {
updateStudyDetail,
updateStudySummaries,
createNewStudy,
deleteStudy,
saveNote,
}
}
+17
View File
@@ -45,6 +45,10 @@ interface StudyDetailResponse {
intersection_search_space: SearchSpace[]
union_search_space: SearchSpace[]
has_intermediate_values: boolean
note: {
version: number
body: string
}
}
export const getStudyDetailAPI = (
@@ -62,6 +66,7 @@ export const getStudyDetailAPI = (
return convertTrialResponse(trial)
})
return {
id: studyId,
name: res.data.name,
datetime_start: new Date(res.data.datetime_start),
directions: res.data.directions,
@@ -72,6 +77,7 @@ export const getStudyDetailAPI = (
union_search_space: res.data.union_search_space,
intersection_search_space: res.data.intersection_search_space,
has_intermediate_values: res.data.has_intermediate_values,
note: res.data.note,
}
})
}
@@ -178,6 +184,17 @@ export const deleteStudyAPI = (studyId: number) => {
})
}
export const saveNoteAPI = (
studyId: number,
note: { version: number; body: string }
): Promise<void> => {
return axiosInstance
.put<void>(`/api/studies/${studyId}/note`, note)
.then((res) => {
return
})
}
interface ParamImportancesResponse {
target_name: string
param_importances: ParamImportance[]
+128
View File
@@ -0,0 +1,128 @@
import {
Box,
Button,
Card,
CardContent,
TextField,
Typography,
useTheme,
} from "@mui/material"
import React, { FC, createRef, useState, useEffect } from "react"
import LoadingButton from "@mui/lab/LoadingButton"
import SaveIcon from "@mui/icons-material/Save"
import { actionCreator } from "../action"
export const Note: FC<{
studyId: number
latestNote: Note
}> = ({ studyId, latestNote }) => {
const theme = useTheme()
const [saving, setSaving] = useState(false)
const [edited, setEdited] = useState(false)
const [curNote, setCurNote] = useState({ version: 0, body: "" })
const textAreaRef = createRef<HTMLTextAreaElement>()
const action = actionCreator()
const notLatest = latestNote.version > curNote.version
useEffect(() => {
setCurNote(latestNote)
}, [])
useEffect(() => {
if (edited) {
window.onbeforeunload = (e) => {
e.returnValue = "Are you okay to discard your changes?"
}
} else {
window.onbeforeunload = null
}
}, [edited])
const handleSave = () => {
const nextVersion = curNote.version + 1
const newNote = {
version: nextVersion,
body: textAreaRef.current ? textAreaRef.current.value : "",
}
setSaving(true)
action
.saveNote(studyId, newNote)
.then(() => {
setCurNote(newNote)
window.onbeforeunload = null
})
.finally(() => {
setSaving(false)
})
}
const handleRefresh = () => {
if (!textAreaRef.current) {
console.log("Unexpectedly, textarea is not found.")
return
}
textAreaRef.current.value = latestNote.body
setCurNote(latestNote)
window.onbeforeunload = null
}
return (
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<Typography variant="h6" sx={{ fontSize: "1.25rem", fontWeight: 600 }}>
Note
</Typography>
<TextField
disabled={saving}
minRows={5}
multiline={true}
placeholder="Take a note (The note is saved to study's system_attrs)"
sx={{ width: "100%", margin: `${theme.spacing(1)} 0` }}
inputProps={{ style: { resize: "vertical" } }}
inputRef={textAreaRef}
defaultValue={curNote.body}
onChange={() => {
const cur = textAreaRef.current ? textAreaRef.current.value : ""
setEdited(cur !== curNote.body)
}}
/>
<Box
sx={{ display: "flex", flexDirection: "row", alignItems: "center" }}
>
{notLatest && !saving && (
<>
<Typography
sx={{
color: theme.palette.error.main,
fontSize: "0.8rem",
display: "inline",
}}
>
The text you are editing has updated. Do you want to discard
your changes and refresh the textarea?
</Typography>
<Button
variant="text"
onClick={handleRefresh}
color="error"
size="small"
sx={{ textDecoration: "underline" }}
>
Yes
</Button>
</>
)}
<Box sx={{ flexGrow: 1 }} />
<LoadingButton
onClick={handleSave}
loading={saving}
loadingPosition="start"
startIcon={<SaveIcon />}
variant="contained"
disabled={!edited}
>
Save
</LoadingButton>
</Box>
</CardContent>
</Card>
)
}
@@ -37,6 +37,7 @@ import { GraphIntermediateValues } from "./GraphIntermediateValues"
import { GraphSlice } from "./GraphSlice"
import { GraphHistory } from "./GraphHistory"
import { GraphParetoFront } from "./GraphParetoFront"
import { Note } from "./Note"
import { actionCreator } from "../action"
import { studyDetailsState } from "../state"
@@ -58,9 +59,10 @@ interface Preference {
graphParetoFrontChecked: boolean
graphParallelCoordinateChecked: boolean
graphIntermediateValuesChecked: boolean
edfChecked: boolean
graphEdfChecked: boolean
graphHyperparameterImportancesChecked: boolean
graphSliceChecked: boolean
noteEditorChecked: boolean
reloadInterval: number
}
@@ -78,9 +80,10 @@ export const StudyDetail: FC<{
graphParetoFrontChecked: true,
graphParallelCoordinateChecked: true,
graphIntermediateValuesChecked: true,
edfChecked: true,
graphEdfChecked: true,
graphHyperparameterImportancesChecked: true,
graphSliceChecked: true,
noteEditorChecked: true,
reloadInterval: 10,
})
useEffect(() => {
@@ -101,7 +104,7 @@ export const StudyDetail: FC<{
const handleClose = () => {
setPrefOpen(false)
}
const handleChartShownChange = (
const handlePreferenceOnChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setPreferences({
@@ -159,7 +162,7 @@ export const StudyDetail: FC<{
control={
<Checkbox
checked={preferences.graphHistoryChecked}
onChange={handleChartShownChange}
onChange={handlePreferenceOnChange}
name="graphHistoryChecked"
/>
}
@@ -172,7 +175,7 @@ export const StudyDetail: FC<{
control={
<Checkbox
checked={preferences.graphParetoFrontChecked}
onChange={handleChartShownChange}
onChange={handlePreferenceOnChange}
name="graphParetoFrontChecked"
/>
}
@@ -182,7 +185,7 @@ export const StudyDetail: FC<{
control={
<Checkbox
checked={preferences.graphParallelCoordinateChecked}
onChange={handleChartShownChange}
onChange={handlePreferenceOnChange}
name="graphParallelCoordinateChecked"
/>
}
@@ -197,7 +200,7 @@ export const StudyDetail: FC<{
control={
<Checkbox
checked={preferences.graphIntermediateValuesChecked}
onChange={handleChartShownChange}
onChange={handlePreferenceOnChange}
name="graphIntermediateValuesChecked"
/>
}
@@ -206,8 +209,8 @@ export const StudyDetail: FC<{
<FormControlLabel
control={
<Checkbox
checked={preferences.edfChecked}
onChange={handleChartShownChange}
checked={preferences.graphEdfChecked}
onChange={handlePreferenceOnChange}
name="edfChecked"
/>
}
@@ -217,7 +220,7 @@ export const StudyDetail: FC<{
control={
<Checkbox
checked={preferences.graphHyperparameterImportancesChecked}
onChange={handleChartShownChange}
onChange={handlePreferenceOnChange}
name="graphHyperparameterImportancesChecked"
/>
}
@@ -227,12 +230,23 @@ export const StudyDetail: FC<{
control={
<Checkbox
checked={preferences.graphSliceChecked}
onChange={handleChartShownChange}
onChange={handlePreferenceOnChange}
name="graphSliceChecked"
/>
}
label="Slice"
/>
<FormLabel component="legend">Editor</FormLabel>
<FormControlLabel
control={
<Checkbox
checked={preferences.noteEditorChecked}
onChange={handlePreferenceOnChange}
name="noteEditorChecked"
/>
}
label="NoteEditor"
/>
</FormGroup>
</MuiDialogContent>
</Dialog>
@@ -354,7 +368,7 @@ export const StudyDetail: FC<{
</CardContent>
</Card>
) : null}
{preferences.edfChecked ? (
{preferences.graphEdfChecked ? (
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<Edf study={studyDetail} />
@@ -382,6 +396,9 @@ export const StudyDetail: FC<{
<Card sx={{ margin: theme.spacing(2) }}>
<TrialTable studyDetail={studyDetail} />
</Card>
{studyDetail !== null && preferences.noteEditorChecked ? (
<Note studyId={studyIdNumber} latestNote={studyDetail.note} />
) : null}
</div>
</Container>
</div>
+7
View File
@@ -45,6 +45,11 @@ declare interface Attribute {
value: string
}
declare interface Note {
version: number
body: string
}
declare interface Trial {
trial_id: number
study_id: number
@@ -70,6 +75,7 @@ declare interface StudySummary {
}
declare interface StudyDetail {
id: number
name: string
directions: StudyDirection[]
datetime_start: Date
@@ -78,6 +84,7 @@ declare interface StudyDetail {
intersection_search_space: SearchSpace[]
union_search_space: SearchSpace[]
has_intermediate_values: boolean
note: Note
}
declare interface StudyDetails {
+264 -22
View File
@@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"@mui/icons-material": "^5.4.4",
"@mui/lab": "^5.0.0-alpha.73",
"@mui/material": "^5.4.4",
"axios": "^0.21.2",
"notistack": "^2.0.3",
@@ -2740,6 +2741,75 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"node_modules/@date-io/core": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.13.1.tgz",
"integrity": "sha512-pVI9nfkf2qClb2Cxdq0Q4zJhdawMG4ybWZUVGifT78FDwzRMX2SwXBb55s5NRJk0HcIicDuxktmCtemZqMH1Zg=="
},
"node_modules/@date-io/date-fns": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.13.1.tgz",
"integrity": "sha512-8fmfwjiLMpFLD+t4NBwDx0eblWnNcgt4NgfT/uiiQTGI81fnPu9tpBMYdAcuWxaV7LLpXgzLBx1SYWAMDVUDQQ==",
"dependencies": {
"@date-io/core": "^2.13.1"
},
"peerDependencies": {
"date-fns": "^2.0.0"
},
"peerDependenciesMeta": {
"date-fns": {
"optional": true
}
}
},
"node_modules/@date-io/dayjs": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.13.1.tgz",
"integrity": "sha512-5bL4WWWmlI4uGZVScANhHJV7Mjp93ec2gNeUHDqqLaMZhp51S0NgD25oqj/k0LqBn1cdU2MvzNpk/ObMmVv5cQ==",
"dependencies": {
"@date-io/core": "^2.13.1"
},
"peerDependencies": {
"dayjs": "^1.8.17"
},
"peerDependenciesMeta": {
"dayjs": {
"optional": true
}
}
},
"node_modules/@date-io/luxon": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.13.1.tgz",
"integrity": "sha512-yG+uM7lXfwLyKKEwjvP8oZ7qblpmfl9gxQYae55ifbwiTs0CoCTkYkxEaQHGkYtTqGTzLqcb0O9Pzx6vgWg+yg==",
"dependencies": {
"@date-io/core": "^2.13.1"
},
"peerDependencies": {
"luxon": "^1.21.3 || ^2.x"
},
"peerDependenciesMeta": {
"luxon": {
"optional": true
}
}
},
"node_modules/@date-io/moment": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-2.13.1.tgz",
"integrity": "sha512-XX1X/Tlvl3TdqQy2j0ZUtEJV6Rl8tOyc5WOS3ki52He28Uzme4Ro/JuPWTMBDH63weSWIZDlbR7zBgp3ZA2y1A==",
"dependencies": {
"@date-io/core": "^2.13.1"
},
"peerDependencies": {
"moment": "^2.24.0"
},
"peerDependenciesMeta": {
"moment": {
"optional": true
}
}
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz",
@@ -3681,6 +3751,91 @@
}
}
},
"node_modules/@mui/lab": {
"version": "5.0.0-alpha.73",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.73.tgz",
"integrity": "sha512-10Uj0Atc7gBTXKX4VV38P6RdqTQrJZxcl3HeEcytIO1S3NAGfc7gZ3Hdpnhtj5U8kcRJZZPH9LtrBbMZzxU/1A==",
"dependencies": {
"@babel/runtime": "^7.17.2",
"@date-io/date-fns": "^2.13.1",
"@date-io/dayjs": "^2.13.1",
"@date-io/luxon": "^2.13.1",
"@date-io/moment": "^2.13.1",
"@mui/base": "5.0.0-alpha.72",
"@mui/system": "^5.5.1",
"@mui/utils": "^5.4.4",
"clsx": "^1.1.1",
"prop-types": "^15.7.2",
"react-is": "^17.0.2",
"react-transition-group": "^4.4.2",
"rifm": "^0.12.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
},
"peerDependencies": {
"@mui/material": "^5.0.0",
"@types/react": "^16.8.6 || ^17.0.0",
"date-fns": "^2.25.0",
"dayjs": "^1.10.7",
"luxon": "^1.28.0 || ^2.0.0",
"moment": "^2.29.1",
"react": "^17.0.0",
"react-dom": "^17.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"date-fns": {
"optional": true
},
"dayjs": {
"optional": true
},
"luxon": {
"optional": true
},
"moment": {
"optional": true
}
}
},
"node_modules/@mui/lab/node_modules/@mui/base": {
"version": "5.0.0-alpha.72",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.72.tgz",
"integrity": "sha512-WCAooa9eqbsC68LhyKtDBRumH4hV1eRZ0A3SDKFHSwYG9fCOdsFv/H1dIYRJM0rwD45bMnuDiG3Qmx7YsTiptw==",
"dependencies": {
"@babel/runtime": "^7.17.2",
"@emotion/is-prop-valid": "^1.1.2",
"@mui/utils": "^5.4.4",
"@popperjs/core": "^2.11.3",
"clsx": "^1.1.1",
"prop-types": "^15.7.2",
"react-is": "^17.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
},
"peerDependencies": {
"@types/react": "^16.8.6 || ^17.0.0",
"react": "^17.0.0",
"react-dom": "^17.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.4.4.tgz",
@@ -3782,17 +3937,17 @@
}
},
"node_modules/@mui/system": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.4.4.tgz",
"integrity": "sha512-Zjbztq2o/VRuRRCWjG44juRrPKYLQMqtQpMHmMttGu5BnvK6PAPW3WOY0r1JCAwLhbd8Kug9nyhGQYKETjo+tQ==",
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.5.1.tgz",
"integrity": "sha512-2hynI4hN8304hOCT8sc4knJviwUUYJ7XK3mXwQ0nagVGOPnWSOad/nYADm7K0vdlCeUXLIbDbe7oNN3Kaiu2kA==",
"dependencies": {
"@babel/runtime": "^7.17.2",
"@mui/private-theming": "^5.4.4",
"@mui/styled-engine": "^5.4.4",
"@mui/types": "^7.1.2",
"@mui/types": "^7.1.3",
"@mui/utils": "^5.4.4",
"clsx": "^1.1.1",
"csstype": "^3.0.10",
"csstype": "^3.0.11",
"prop-types": "^15.7.2"
},
"engines": {
@@ -3821,9 +3976,9 @@
}
},
"node_modules/@mui/types": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.2.tgz",
"integrity": "sha512-SD7O1nVzqG+ckQpFjDhXPZjRceB8HQFHEvdLLrPhlJy4lLbwEBbxK74Tj4t6Jgk0fTvLJisuwOutrtYe9P/xBQ==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.3.tgz",
"integrity": "sha512-DDF0UhMBo4Uezlk+6QxrlDbchF79XG6Zs0zIewlR4c0Dt6GKVFfUtzPtHCH1tTbcSlq/L2bGEdiaoHBJ9Y1gSA==",
"peerDependencies": {
"@types/react": "*"
},
@@ -3891,9 +4046,9 @@
}
},
"node_modules/@popperjs/core": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
"integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==",
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz",
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -10287,6 +10442,14 @@
"node": ">=0.10.0"
}
},
"node_modules/rifm": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz",
"integrity": "sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==",
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -13674,6 +13837,43 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"@date-io/core": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.13.1.tgz",
"integrity": "sha512-pVI9nfkf2qClb2Cxdq0Q4zJhdawMG4ybWZUVGifT78FDwzRMX2SwXBb55s5NRJk0HcIicDuxktmCtemZqMH1Zg=="
},
"@date-io/date-fns": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.13.1.tgz",
"integrity": "sha512-8fmfwjiLMpFLD+t4NBwDx0eblWnNcgt4NgfT/uiiQTGI81fnPu9tpBMYdAcuWxaV7LLpXgzLBx1SYWAMDVUDQQ==",
"requires": {
"@date-io/core": "^2.13.1"
}
},
"@date-io/dayjs": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.13.1.tgz",
"integrity": "sha512-5bL4WWWmlI4uGZVScANhHJV7Mjp93ec2gNeUHDqqLaMZhp51S0NgD25oqj/k0LqBn1cdU2MvzNpk/ObMmVv5cQ==",
"requires": {
"@date-io/core": "^2.13.1"
}
},
"@date-io/luxon": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.13.1.tgz",
"integrity": "sha512-yG+uM7lXfwLyKKEwjvP8oZ7qblpmfl9gxQYae55ifbwiTs0CoCTkYkxEaQHGkYtTqGTzLqcb0O9Pzx6vgWg+yg==",
"requires": {
"@date-io/core": "^2.13.1"
}
},
"@date-io/moment": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-2.13.1.tgz",
"integrity": "sha512-XX1X/Tlvl3TdqQy2j0ZUtEJV6Rl8tOyc5WOS3ki52He28Uzme4Ro/JuPWTMBDH63weSWIZDlbR7zBgp3ZA2y1A==",
"requires": {
"@date-io/core": "^2.13.1"
}
},
"@discoveryjs/json-ext": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz",
@@ -14375,6 +14575,42 @@
"@babel/runtime": "^7.17.2"
}
},
"@mui/lab": {
"version": "5.0.0-alpha.73",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.73.tgz",
"integrity": "sha512-10Uj0Atc7gBTXKX4VV38P6RdqTQrJZxcl3HeEcytIO1S3NAGfc7gZ3Hdpnhtj5U8kcRJZZPH9LtrBbMZzxU/1A==",
"requires": {
"@babel/runtime": "^7.17.2",
"@date-io/date-fns": "^2.13.1",
"@date-io/dayjs": "^2.13.1",
"@date-io/luxon": "^2.13.1",
"@date-io/moment": "^2.13.1",
"@mui/base": "5.0.0-alpha.72",
"@mui/system": "^5.5.1",
"@mui/utils": "^5.4.4",
"clsx": "^1.1.1",
"prop-types": "^15.7.2",
"react-is": "^17.0.2",
"react-transition-group": "^4.4.2",
"rifm": "^0.12.1"
},
"dependencies": {
"@mui/base": {
"version": "5.0.0-alpha.72",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.72.tgz",
"integrity": "sha512-WCAooa9eqbsC68LhyKtDBRumH4hV1eRZ0A3SDKFHSwYG9fCOdsFv/H1dIYRJM0rwD45bMnuDiG3Qmx7YsTiptw==",
"requires": {
"@babel/runtime": "^7.17.2",
"@emotion/is-prop-valid": "^1.1.2",
"@mui/utils": "^5.4.4",
"@popperjs/core": "^2.11.3",
"clsx": "^1.1.1",
"prop-types": "^15.7.2",
"react-is": "^17.0.2"
}
}
}
},
"@mui/material": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.4.4.tgz",
@@ -14415,24 +14651,24 @@
}
},
"@mui/system": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.4.4.tgz",
"integrity": "sha512-Zjbztq2o/VRuRRCWjG44juRrPKYLQMqtQpMHmMttGu5BnvK6PAPW3WOY0r1JCAwLhbd8Kug9nyhGQYKETjo+tQ==",
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.5.1.tgz",
"integrity": "sha512-2hynI4hN8304hOCT8sc4knJviwUUYJ7XK3mXwQ0nagVGOPnWSOad/nYADm7K0vdlCeUXLIbDbe7oNN3Kaiu2kA==",
"requires": {
"@babel/runtime": "^7.17.2",
"@mui/private-theming": "^5.4.4",
"@mui/styled-engine": "^5.4.4",
"@mui/types": "^7.1.2",
"@mui/types": "^7.1.3",
"@mui/utils": "^5.4.4",
"clsx": "^1.1.1",
"csstype": "^3.0.10",
"csstype": "^3.0.11",
"prop-types": "^15.7.2"
}
},
"@mui/types": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.2.tgz",
"integrity": "sha512-SD7O1nVzqG+ckQpFjDhXPZjRceB8HQFHEvdLLrPhlJy4lLbwEBbxK74Tj4t6Jgk0fTvLJisuwOutrtYe9P/xBQ==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.3.tgz",
"integrity": "sha512-DDF0UhMBo4Uezlk+6QxrlDbchF79XG6Zs0zIewlR4c0Dt6GKVFfUtzPtHCH1tTbcSlq/L2bGEdiaoHBJ9Y1gSA==",
"requires": {}
},
"@mui/utils": {
@@ -14474,9 +14710,9 @@
}
},
"@popperjs/core": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
"integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA=="
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz",
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg=="
},
"@sinonjs/commons": {
"version": "1.8.3",
@@ -19265,6 +19501,12 @@
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true
},
"rifm": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz",
"integrity": "sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==",
"requires": {}
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+1
View File
@@ -17,6 +17,7 @@
"license": "MIT",
"dependencies": {
"@mui/icons-material": "^5.4.4",
"@mui/lab": "^5.0.0-alpha.73",
"@mui/material": "^5.4.4",
"axios": "^0.21.2",
"notistack": "^2.0.3",
+19
View File
@@ -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