Write a custom artifact handler¶
Important
See this guide to understand experiment artifacts in
lazyscribe.
In this guide, we’ll walk through how you can write a custom artifact handler.
Building the handler¶
Each handler is a subclass of lazyscribe.artifacts.base.Artifact, with
the following mandatory class variables:
alias: this is the value used to select the handler when logging artifacts (handler="json", for example).suffix: the suffix of the output artifact on disk (e.g.jsonfor the JSON handler).binary: Whether or not the output artifact is a binary file type.output_only: Whether or not the output file can be reconstructed as a Python object on read.
Additional class variables are used to capture metadata about the environment or artifact. Let’s create a handler for text files to demonstrate.
from typing import ClassVar
from attrs import define
from slugify import slugify
from lazyscribe.artifacts.base import Artifact
@define(auto_attribs=True)
class TextArtifact(Artifact):
"""Handler for Text artifacts."""
alias: ClassVar[str] = "text"
suffix: ClassVar[str] = "txt"
binary: ClassVar[bool] = False
output_only: ClassVar[bool] = False
Next, we have to write a construct method to build our artifact handler. If we had
additional metadata to capture, this is where we would capture it
(see lazyscribe.artifacts.json.JSONArtifact) for an example. The signature of the
construct method is fixed.
from datetime import datetime, timezone
from io import IOBase
from typing import Any, ClassVar
from attrs import define
from lazyscribe.artifacts.base import Artifact
@define(auto_attribs=True)
class TextArtifact(Artifact):
"""Handler for Text artifacts."""
alias: ClassVar[str] = "text"
suffix: ClassVar[str] = "txt"
binary: ClassVar[bool] = False
output_only: ClassVar[bool] = False
@classmethod
def construct(
cls,
name: str,
value: Any = None,
fname: str | None = None,
created_at: datetime | None = None,
writer_kwargs: dict | None = None,
version: int = 0,
dirty: bool = True,
**kwargs: Any
) -> TextArtifact:
"""Construct the handler class."""
created_at = created_at or datetime.now(timezone.utc)
return cls(
name=name,
value=value,
fname=fname or f"{slugify(name)}-{slugify(created_at.strftime('%Y%m%d%H%M%S'))}.{cls.suffix}",
created_at=created_at,
writer_kwargs=writer_kwargs or {},
version=version,
dirty=dirty,
)
Finally, we have to write the I/O methods, read and write. Both of these
methods should expect a file buffer from the fsspec filesystem.
class TextArtifact(Artifact):
...
@classmethod
def read(cls, buf: IOBase, **kwargs: Any) -> Any:
"""Read in the artifact."""
return buf.read()
@classmethod
def write(cls, obj: Any, buf: IOBase, **kwargs: Any) -> None:
"""Write the content to a text file."""
buf.write(obj)
You have a new custom handler!
Using the handler¶
There are two ways to make your custom handler visible to lazyscribe.
Entry points (for packages)¶
You can register your artifact handler using entry points in the
lazyscribe.artifact_type group. For example, suppose we distributed our
TextArtifact class as myproject.artifacts.TextArtifact. In the pyproject.toml
for myproject, we can include the following:
[project.entry-points."lazyscribe.artifact_type"]
text = "myproject.artifacts:TextArtifact"
Then, you can use lazyscribe.experiment.Experiment.log_artifact() with handler="text".
Subclass scanning¶
If you’re experimenting or you’re not writing your handler as part of a package, you can still use the custom handler. All you need to do is make sure the class has been imported in the module where you are logging experiments:
from mymodule import TextArtifact
from lazyscribe import Project
project = Project(...)
with project.log(...) as exp:
exp.log_artifact(..., handler="text")
This method works by looking for all available subclasses of lazyscribe.artifacts.base.Artifact
at runtime.