Spaces:
Runtime error
Runtime error
File size: 7,800 Bytes
f4f6aba 31a1df6 dd68837 31a1df6 dd68837 31a1df6 f4f6aba 94e602d 31a1df6 f4f6aba 94e602d 31a1df6 f4f6aba 94e602d 31a1df6 dd68837 94e602d 31a1df6 dd68837 31a1df6 dd68837 31a1df6 dd68837 31a1df6 dd68837 31a1df6 f4f6aba 94e602d 31a1df6 dd68837 31a1df6 dd68837 31a1df6 dd68837 31a1df6 dd68837 31a1df6 f4f6aba 94e602d 31a1df6 dd68837 94e602d 31a1df6 dd68837 31a1df6 dd68837 31a1df6 dd68837 31a1df6 dd68837 31a1df6 dd68837 31a1df6 2174ccd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
"""Functionality around tasks
Tasks are used to implement "undo" and "redo" functionality.
"""
from __future__ import annotations
import shutil
from pathlib import Path
from tempfile import mkdtemp
from uuid import uuid4
from skops import card
from skops.card._model_card import PlotSection, split_subsection_names
from streamlit.runtime.uploaded_file_manager import UploadedFile
class Task:
"""(Abstract) base class for tasks"""
def do(self) -> None:
raise NotImplementedError
def undo(self) -> None:
raise NotImplementedError
class TaskState:
"""Tracking the state of tasks"""
def __init__(self) -> None:
self.done_list: list[Task] = []
self.undone_list: list[Task] = []
def undo(self) -> None:
if not self.done_list:
return
task = self.done_list.pop(-1)
task.undo()
self.undone_list.append(task)
def redo(self) -> None:
if not self.undone_list:
return
task = self.undone_list.pop(-1)
task.do()
self.done_list.append(task)
def add(self, task: Task) -> None:
task.do()
self.done_list.append(task)
self.undone_list.clear()
def reset(self) -> None:
self.done_list.clear()
self.undone_list.clear()
class AddSectionTask(Task):
"""Add a new text section"""
def __init__(
self,
model_card: card.Card,
title: str,
content: str,
) -> None:
self.model_card = model_card
self.title = title
self.key = title + " " + str(uuid4())[:6]
self.content = content
def do(self) -> None:
self.model_card.add(**{self.key: self.content})
section = self.model_card.select(self.key)
section.title = split_subsection_names(self.title)[-1]
def undo(self) -> None:
self.model_card.delete(self.key)
class AddFigureTask(Task):
"""Add a new figure section
Figure always starts out with dummy image cat.png.
"""
def __init__(
self,
model_card: card.Card,
path: Path,
title: str,
content: str,
) -> None:
self.model_card = model_card
self.title = title
# Create a unique file name, since the same image can exist more than
# once per model card.
fname = Path(content)
stem = fname.stem
suffix = fname.suffix
uniq = str(uuid4())[:6]
new_fname = str(path / stem) + "_" + uniq + suffix
self.key = title + " " + uniq
self.content = Path(new_fname)
def do(self) -> None:
shutil.copy("cat.png", self.content)
self.model_card.add_plot(**{self.key: self.content})
section = self.model_card.select(self.key)
section.title = split_subsection_names(self.title)[-1]
section.is_fig = True # type: ignore
def undo(self) -> None:
self.content.unlink(missing_ok=True)
self.model_card.delete(self.key)
class DeleteSectionTask(Task):
"""Delete a section
The section is not completely removed from the underlying data structure,
but only turned invisible.
"""
def __init__(
self,
model_card: card.Card,
key: str,
path: Path | None,
) -> None:
self.model_card = model_card
self.key = key
# when 'deleting' a file, move it to a temp file
self.path = path
self.tmp_path = Path(mkdtemp(prefix="skops-")) / str(uuid4())
def do(self) -> None:
self.model_card.select(self.key).visible = False
if self.path:
shutil.move(self.path, self.tmp_path)
def undo(self) -> None:
self.model_card.select(self.key).visible = True
if self.path:
shutil.move(self.tmp_path, self.path)
class UpdateSectionTask(Task):
"""Change the title or content of a text section"""
def __init__(
self,
model_card: card.Card,
key: str,
old_name: str,
new_name: str,
old_content: str,
new_content: str,
) -> None:
self.model_card = model_card
self.key = key
self.old_name = old_name
self.new_name = new_name
self.old_content = old_content
self.new_content = new_content
def do(self) -> None:
section = self.model_card.select(self.key)
new_title = split_subsection_names(self.new_name)[-1]
section.title = new_title
section.content = self.new_content
def undo(self) -> None:
section = self.model_card.select(self.key)
old_title = split_subsection_names(self.old_name)[-1]
section.title = old_title
section.content = self.old_content
class UpdateFigureTask(Task):
"""Change the title or image of a figure section
Changing the title is easy, just replace it and be done.
Changing the figure is a bit more tricky. The old figure is in the hf_path
under its old name. The new figure is an UploadFile object. For the DO
operation, move the old figure to a temporary file and store the UploadFile
content to a new file (which may have a different name).
For the UNDO operation, delete the new figure (its content is still stored
in the UploadFile) and move back the old figure from its temporary file to
the original location (with its original name).
"""
def __init__(
self,
model_card: card.Card,
key: str,
old_name: str,
new_name: str,
data: UploadedFile | None,
new_path: Path | None,
old_path: Path | None,
) -> None:
self.model_card = model_card
self.key = key
self.old_name = old_name
self.new_name = new_name
self.old_data = self.model_card.select(self.key).content
self.new_path = new_path
self.old_path = old_path
# when 'deleting' the old image, move to temp path
self.tmp_path = Path(mkdtemp(prefix="skops-")) / str(uuid4())
if not data:
self.new_data = self.old_data
else:
self.new_data = data
def do(self) -> None:
section = self.model_card.select(self.key)
new_title = split_subsection_names(self.new_name)[-1]
section.title = self.title = new_title
if self.new_data == self.old_data: # image is same
return
# write figure
# note: this can still be the same image if the image is a file, there
# is no test to check, e.g., the hash of the image
shutil.move(self.old_path, self.tmp_path)
with open(self.new_path, "wb") as f:
f.write(self.new_data.getvalue())
section.content = PlotSection(
alt_text=self.new_data.name,
path=self.new_path,
)
def undo(self) -> None:
section = self.model_card.select(self.key)
old_title = split_subsection_names(self.old_name)[-1]
section.title = old_title
if self.new_data == self.old_data: # image is same
return
self.new_path.unlink(missing_ok=True)
shutil.move(self.tmp_path, self.old_path)
section.content = self.old_data
class AddMetricsTask(Task):
"""Add new metrics"""
def __init__(
self,
model_card: card.Card,
metrics: dict[str, str | int | float],
) -> None:
self.model_card = model_card
self.old_metrics = model_card._metrics.copy()
self.new_metrics = metrics
def do(self) -> None:
self.model_card._metrics.clear()
self.model_card.add_metrics(**self.new_metrics)
def undo(self) -> None:
self.model_card._metrics.clear()
self.model_card.add_metrics(**self.old_metrics)
|