Implement user API for Artifacts

This commit is contained in:
c-bata
2023-01-14 00:22:38 +09:00
parent 29233909a2
commit ded0200904
6 changed files with 196 additions and 3 deletions
+8 -1
View File
@@ -36,6 +36,7 @@ from optuna.version import __version__ as optuna_ver
from packaging import version
from . import _note as note
from . import artifact
from ._cached_extra_study_property import get_cached_extra_study_property
from ._importance import get_param_importance_from_trials_cache
from ._pareto_front import get_pareto_front_trials
@@ -201,7 +202,11 @@ def get_trials(storage: BaseStorage, study_id: int, ttl_seconds: int = 10) -> li
return trials
def create_app(storage: BaseStorage, debug: bool = False) -> Bottle:
def create_app(
storage: BaseStorage,
artifact_backend: Optional[artifact.ArtifactBackend] = None,
debug: bool = False,
) -> Bottle:
app = Bottle()
update_schema_compatibility_flags(storage)
@@ -448,6 +453,8 @@ def create_app(storage: BaseStorage, debug: bool = False) -> Bottle:
filename = gz_filename
return static_file(filename, root=STATIC_DIR)
if artifact_backend is not None:
artifact.register_artifact_route(app, storage, artifact_backend)
return app
+6 -2
View File
@@ -12,6 +12,7 @@ from optuna.study import StudySummary
from optuna.trial import FrozenTrial
from . import _note as note
from . import artifact
from ._named_objectives import get_objective_names
@@ -162,7 +163,7 @@ def serialize_frozen_trial(
"distribution": serialize_distribution(distribution),
}
)
trial_system_attrs = getattr(trial, "_system_attrs", {})
trial_system_attrs: dict[str, Any] = getattr(trial, "_system_attrs", {})
fixed_params = trial_system_attrs.get("fixed_params", {})
serialized = {
"trial_id": trial._trial_id,
@@ -175,8 +176,11 @@ def serialize_frozen_trial(
for param_name in fixed_params
],
"user_attrs": serialize_attrs(trial.user_attrs),
"system_attrs": serialize_attrs(trial_system_attrs),
"system_attrs": serialize_attrs(
{k: trial_system_attrs[k] for k in trial_system_attrs if not k.startswith("dashboard")}
),
"note": note.get_note_from_system_attrs(study_system_attrs, trial._trial_id),
"artifacts": artifact._list_artifacts(study_system_attrs, trial._trial_id),
}
serialized_intermediate_values: list[IntermediateValue] = []
+132
View File
@@ -0,0 +1,132 @@
from __future__ import annotations
import json
import mimetypes
import os.path
from typing import Any
from typing import BinaryIO
from typing import Optional
import uuid
from bottle import Bottle
from bottle import response
import optuna
from optuna.storages import BaseStorage
try:
from typing import Protocol
from typing import TypedDict
except ImportError:
from typing_extensions import Protocol # type: ignore
from typing_extensions import TypedDict # type: ignore
ARTIFACTS_ATTR_PREFIX = "dashboard:artifacts:"
ArtifactMeta = TypedDict(
"ArtifactMeta",
{
"artifact_id": str,
"mimetype": str,
"encoding": str,
"filename": str,
},
)
class ArtifactBackend(Protocol):
def open(self, artifact_id: str) -> BinaryIO:
...
def write(self, artifact_id: str, content_body: BinaryIO) -> None:
...
def register_artifact_route(
app: Bottle, storage: BaseStorage, artifact_backend: ArtifactBackend
) -> None:
@app.get("/artifacts/<trial_id:int>/<artifact_id:re:[0-9a-fA-F-]+>")
def proxy_artifact(trial_id: int, artifact_id: str) -> bytes:
if artifact_backend is None:
response.status = 400 # Bad Request
return b"Cannot access to the artifacts."
artifact_dict = _get_artifact_meta(storage, trial_id, artifact_id)
response.set_header("Content-Type", artifact_dict["mimetype"])
response.set_header("Content-Encodings", artifact_dict["encoding"])
with artifact_backend.open(artifact_id) as f:
body = f.read()
return body
def upload_artifact(
backend: ArtifactBackend,
trial: optuna.Trial,
file_path: str,
*,
mimetype: Optional[str] = None,
encoding: Optional[str] = None,
) -> str:
"""Upload an artifact (files), which is associated with the trial.
Example:
.. code-block:: python
import optuna
from optuna_dashboard.artifact import upload_artifact
from optuna_dashboard.artifact.file_system import FileSystemBackend
artifact_backend = FileSystemBackend("./tmp/")
def objective(trial: optuna.Trial) -> float:
... = trial.suggest_float("x", -10, 10)
file_path = generate_example_png(...)
upload_artifact(artifact_backend, trial, file_path)
return ...
"""
filename = os.path.basename(file_path)
guess_mimetype, guess_encoding = mimetypes.guess_type(filename)
mimetype = mimetype or guess_mimetype
encoding = encoding or guess_encoding
if mimetype is None or encoding is None:
raise ValueError("Failed to guess mimetype and encoding. Please explicitly specify them.")
storage = trial.storage
trial_id = trial._trial_id
artifact_id = str(uuid.uuid4())
artifact: ArtifactMeta = {
"artifact_id": artifact_id,
"mimetype": mimetype,
"encoding": encoding,
"filename": filename,
}
attr_key = _artifact_prefix(trial_id=trial_id) + artifact_id
storage.set_study_system_attr(trial_id, attr_key, json.dumps(artifact))
with open(file_path, "rb") as f:
backend.write(artifact_id, f)
return artifact_id
def _artifact_prefix(trial_id: int) -> str:
return ARTIFACTS_ATTR_PREFIX + f"{trial_id}:"
def _get_artifact_meta(storage: BaseStorage, trial_id: int, artifact_id: str) -> ArtifactMeta:
artifact_key = ARTIFACTS_ATTR_PREFIX + artifact_id
storage.get_trial_system_attrs(trial_id)
for key, value in storage.get_trial_system_attrs(trial_id).items():
if key == artifact_key:
return json.loads(value)
raise ValueError("Artifact not found")
def _list_artifacts(study_system_attrs: dict[str, Any], trial_id: int) -> list[ArtifactMeta]:
return [
json.loads(value)
for key, value in study_system_attrs.items()
if key.startswith(_artifact_prefix(trial_id))
]
+27
View File
@@ -0,0 +1,27 @@
from __future__ import annotations
import os
from typing import BinaryIO
from typing import TYPE_CHECKING
from . import ArtifactBackend
class FileSystemBackend:
def __init__(self, base_path: str) -> None:
self._base_path = base_path
def open(self, artifact_id: str) -> BinaryIO:
filepath = os.path.join(self._base_path, artifact_id)
return open(filepath, "rb")
def write(self, artifact_id: str, content_body: BinaryIO) -> None:
filepath = os.path.join(self._base_path, artifact_id)
with open(filepath, "wb") as f:
f.write(content_body.read())
if TYPE_CHECKING:
# A mypy-runtime assertion to ensure that LocalArtifactBackend
# implements all abstract methods in ArtifactBackendProtocol.
_: ArtifactBackend = FileSystemBackend("")
+1
View File
@@ -30,6 +30,7 @@ dependencies = [
"optuna>=2.4.0",
"packaging",
"scikit-learn",
'typing-extensions; python_version<"3.8"',
]
dynamic = ["version"]
+22
View File
@@ -0,0 +1,22 @@
import io
import tempfile
from unittest import TestCase
from optuna_dashboard.artifact.file_system import FileSystemBackend
class FileSystemBackendTestCase(TestCase):
def setUp(self) -> None:
self.dir = tempfile.TemporaryDirectory()
def tearDown(self) -> None:
self.dir.cleanup()
def test_upload_download(self) -> None:
artifact_id = "dummy-uuid"
dummy_content = b"Hello World"
backend = FileSystemBackend(self.dir.name)
backend.write(artifact_id, io.BytesIO(dummy_content))
with backend.open(artifact_id) as f:
actual = f.read()
self.assertEqual(actual, dummy_content)