lazyscribe 2.0.0a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.3
2
+ Name: lazyscribe
3
+ Version: 2.0.0a0
4
+ Summary: Lightweight and lazy experiment logging
5
+ Author: Akshay Gupta
6
+ Author-email: Akshay Gupta <akgcodes@gmail.com>
7
+ License: MIT license
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Natural Language :: English
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: attrs>=21.2.0,<=25.3.0
17
+ Requires-Dist: fsspec>=0.4.0,<=2025.5.1
18
+ Requires-Dist: python-slugify>=5.0.0,<=8.0.4
19
+ Requires-Dist: commitizen ; extra == 'build'
20
+ Requires-Dist: uv ; extra == 'build'
21
+ Requires-Dist: lazyscribe[build] ; extra == 'dev'
22
+ Requires-Dist: lazyscribe[docs] ; extra == 'dev'
23
+ Requires-Dist: lazyscribe[qa] ; extra == 'dev'
24
+ Requires-Dist: lazyscribe[tests] ; extra == 'dev'
25
+ Requires-Dist: furo ; extra == 'docs'
26
+ Requires-Dist: matplotlib ; extra == 'docs'
27
+ Requires-Dist: pandas ; extra == 'docs'
28
+ Requires-Dist: pillow ; extra == 'docs'
29
+ Requires-Dist: prefect>=1.0,<2 ; extra == 'docs'
30
+ Requires-Dist: scikit-learn ; extra == 'docs'
31
+ Requires-Dist: sphinx ; extra == 'docs'
32
+ Requires-Dist: sphinx-gallery ; extra == 'docs'
33
+ Requires-Dist: sphinx-inline-tabs ; extra == 'docs'
34
+ Requires-Dist: edgetest ; extra == 'qa'
35
+ Requires-Dist: edgetest-pip-tools ; extra == 'qa'
36
+ Requires-Dist: mypy ; extra == 'qa'
37
+ Requires-Dist: pip-tools ; extra == 'qa'
38
+ Requires-Dist: pre-commit ; extra == 'qa'
39
+ Requires-Dist: pyproject-fmt ; extra == 'qa'
40
+ Requires-Dist: ruff==0.7.3 ; extra == 'qa'
41
+ Requires-Dist: types-python-slugify ; extra == 'qa'
42
+ Requires-Dist: prefect>=1.0,<2 ; extra == 'tests'
43
+ Requires-Dist: pytest ; extra == 'tests'
44
+ Requires-Dist: pytest-cov ; extra == 'tests'
45
+ Requires-Dist: scikit-learn ; extra == 'tests'
46
+ Requires-Dist: time-machine ; extra == 'tests'
47
+ Requires-Python: >=3.10.0
48
+ Project-URL: documentation, https://lazyscribe.github.io/lazyscribe/
49
+ Project-URL: repository, https://github.com/lazyscribe/lazyscribe
50
+ Provides-Extra: build
51
+ Provides-Extra: dev
52
+ Provides-Extra: docs
53
+ Provides-Extra: qa
54
+ Provides-Extra: tests
55
+ Description-Content-Type: text/markdown
56
+
57
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![PyPI](https://img.shields.io/pypi/v/lazyscribe)](https://pypi.org/project/lazyscribe/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lazyscribe)](https://pypi.org/project/lazyscribe/) [![Documentation Status](https://github.com/lazyscribe/lazyscribe/actions/workflows/docs.yml/badge.svg)](https://lazyscribe.github.io/lazyscribe/) [![codecov](https://codecov.io/github/lazyscribe/lazyscribe/branch/main/graph/badge.svg?token=M5BHYS2SSU)](https://codecov.io/github/lazyscribe/lazyscribe)
58
+
59
+ # Lightweight, lazy experiment logging
60
+
61
+ ``lazyscribe`` is a lightweight package for model experiment logging. It creates a single JSON
62
+ file per project, and an experiment is only added to the file when code finishes (errors won't
63
+ result in partially finished experiments in your project log).
64
+
65
+ ``lazyscribe`` also has functionality to allow for multiple people to work on a single project.
66
+ You can merge projects together and update the list of experiments to create a single, authoritative
67
+ view of all executed experiments.
68
+
69
+ # Installation
70
+
71
+ Python 3.10 and above is required. Use `pip` to install:
72
+ ```console
73
+ $ python -m pip install lazyscribe
74
+ ```
75
+
76
+ # Basic Usage
77
+
78
+ The basic usage involves instantiating a ``Project`` and using the context manager to log
79
+ an experiment:
80
+
81
+ ```python
82
+ import json
83
+
84
+ from lazyscribe import Project
85
+
86
+ project = Project(fpath="project.json")
87
+ with project.log(name="My experiment") as exp:
88
+ exp.log_metric("auroc", 0.5)
89
+ exp.log_parameter("algorithm", "lightgbm")
90
+ ```
91
+
92
+ You've created an experiment! You can view the experimental data by using ``list``:
93
+
94
+ ```python
95
+ print(json.dumps(list(project), indent=4))
96
+ ```
97
+
98
+ ```json
99
+ [
100
+ {
101
+ "name": "My experiment",
102
+ "author": "<AUTHOR>",
103
+ "last_updated_by": "<AUTHOR>",
104
+ "metrics": {
105
+ "auroc": 0.5
106
+ },
107
+ "parameters": {
108
+ "algorithm": "lightgbm"
109
+ },
110
+ "created_at": "<CREATED_AT>",
111
+ "last_updated": "<LAST_UPDATED>",
112
+ "dependencies": [],
113
+ "short_slug": "my-experiment",
114
+ "slug": "my-experiment-<CREATED_AT>",
115
+ "tests": [],
116
+ "artifacts": []
117
+ }
118
+ ]
119
+ ```
120
+
121
+ Once you've finished, save the project to ``project.json``:
122
+
123
+ ```python
124
+ project.save()
125
+ ```
126
+
127
+ Later on, you can read the project back in read-only mode (`"r"`), append mode (`"a"`),
128
+ or editable mode (`"w+"`):
129
+
130
+ ```python
131
+ project = Project("project.json", mode="r")
132
+ with project.log(name="New experiment") as exp: # Raises a ReadOnlyError
133
+ ...
134
+ ```
@@ -0,0 +1,78 @@
1
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![PyPI](https://img.shields.io/pypi/v/lazyscribe)](https://pypi.org/project/lazyscribe/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lazyscribe)](https://pypi.org/project/lazyscribe/) [![Documentation Status](https://github.com/lazyscribe/lazyscribe/actions/workflows/docs.yml/badge.svg)](https://lazyscribe.github.io/lazyscribe/) [![codecov](https://codecov.io/github/lazyscribe/lazyscribe/branch/main/graph/badge.svg?token=M5BHYS2SSU)](https://codecov.io/github/lazyscribe/lazyscribe)
2
+
3
+ # Lightweight, lazy experiment logging
4
+
5
+ ``lazyscribe`` is a lightweight package for model experiment logging. It creates a single JSON
6
+ file per project, and an experiment is only added to the file when code finishes (errors won't
7
+ result in partially finished experiments in your project log).
8
+
9
+ ``lazyscribe`` also has functionality to allow for multiple people to work on a single project.
10
+ You can merge projects together and update the list of experiments to create a single, authoritative
11
+ view of all executed experiments.
12
+
13
+ # Installation
14
+
15
+ Python 3.10 and above is required. Use `pip` to install:
16
+ ```console
17
+ $ python -m pip install lazyscribe
18
+ ```
19
+
20
+ # Basic Usage
21
+
22
+ The basic usage involves instantiating a ``Project`` and using the context manager to log
23
+ an experiment:
24
+
25
+ ```python
26
+ import json
27
+
28
+ from lazyscribe import Project
29
+
30
+ project = Project(fpath="project.json")
31
+ with project.log(name="My experiment") as exp:
32
+ exp.log_metric("auroc", 0.5)
33
+ exp.log_parameter("algorithm", "lightgbm")
34
+ ```
35
+
36
+ You've created an experiment! You can view the experimental data by using ``list``:
37
+
38
+ ```python
39
+ print(json.dumps(list(project), indent=4))
40
+ ```
41
+
42
+ ```json
43
+ [
44
+ {
45
+ "name": "My experiment",
46
+ "author": "<AUTHOR>",
47
+ "last_updated_by": "<AUTHOR>",
48
+ "metrics": {
49
+ "auroc": 0.5
50
+ },
51
+ "parameters": {
52
+ "algorithm": "lightgbm"
53
+ },
54
+ "created_at": "<CREATED_AT>",
55
+ "last_updated": "<LAST_UPDATED>",
56
+ "dependencies": [],
57
+ "short_slug": "my-experiment",
58
+ "slug": "my-experiment-<CREATED_AT>",
59
+ "tests": [],
60
+ "artifacts": []
61
+ }
62
+ ]
63
+ ```
64
+
65
+ Once you've finished, save the project to ``project.json``:
66
+
67
+ ```python
68
+ project.save()
69
+ ```
70
+
71
+ Later on, you can read the project back in read-only mode (`"r"`), append mode (`"a"`),
72
+ or editable mode (`"w+"`):
73
+
74
+ ```python
75
+ project = Project("project.json", mode="r")
76
+ with project.log(name="New experiment") as exp: # Raises a ReadOnlyError
77
+ ...
78
+ ```
@@ -0,0 +1,9 @@
1
+ """Main module."""
2
+
3
+ from lazyscribe._meta import __version__ # noqa: F401
4
+ from lazyscribe.experiment import Experiment
5
+ from lazyscribe.project import Project
6
+ from lazyscribe.repository import Repository
7
+ from lazyscribe.test import Test
8
+
9
+ __all__: list[str] = ["Experiment", "Project", "Repository", "Test"]
@@ -0,0 +1,3 @@
1
+ """Version."""
2
+
3
+ __version__ = "2.0.0a0"
@@ -0,0 +1,126 @@
1
+ """Util methods."""
2
+
3
+ import inspect
4
+ import json
5
+ from collections.abc import Iterator
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+ from attrs import Attribute, asdict, fields, filters
10
+
11
+ from lazyscribe.artifacts.base import Artifact
12
+ from lazyscribe.exception import ArtifactLoadError
13
+
14
+
15
+ def serializer(inst: type, field: "Attribute[Any]", value: Any) -> Any:
16
+ """Datetime and dependencies converter for :meth:`attrs.asdict`.
17
+
18
+ Parameters
19
+ ----------
20
+ inst : type
21
+ Included for compatibility.
22
+ field : attrs.Attribute[Any]
23
+ The field name.
24
+ value : Any
25
+ The field value.
26
+
27
+ Returns
28
+ -------
29
+ Any
30
+ Converted value for easy serialization.
31
+ """
32
+ if isinstance(value, datetime):
33
+ return value.isoformat(timespec="seconds")
34
+ if field is not None and field.name == "dependencies":
35
+ deps: list[str] = [f"{exp.project}|{exp.slug}" for exp in value.values()]
36
+ return deps
37
+ if field is not None and field.name == "tests":
38
+ tests: list[dict[str, Any]] = [asdict(test) for test in value]
39
+ return tests
40
+ if field is not None and field.name == "artifacts":
41
+ art: list[dict[str, Any]] = list(serialize_artifacts(value))
42
+ return art
43
+
44
+ return value
45
+
46
+
47
+ def serialize_artifacts(alist: list[Artifact]) -> Iterator[dict[str, Any]]:
48
+ """Serialize list of artifacts."""
49
+ yield from (
50
+ {
51
+ **asdict(
52
+ artifact,
53
+ filter=filters.exclude(
54
+ fields(type(artifact)).value,
55
+ fields(type(artifact)).writer_kwargs,
56
+ fields(type(artifact)).dirty,
57
+ ),
58
+ value_serializer=lambda _, __, value: value.isoformat(
59
+ timespec="seconds"
60
+ )
61
+ if isinstance(value, datetime)
62
+ else value,
63
+ ),
64
+ "handler": artifact.alias,
65
+ }
66
+ for artifact in alist
67
+ )
68
+
69
+
70
+ def utcnow() -> datetime:
71
+ """Return the naive datetime now in UTC.
72
+
73
+ Returns
74
+ -------
75
+ datetime.datetime
76
+ Now in UTC, without timezone info.
77
+ """
78
+ return datetime.now(timezone.utc).replace(tzinfo=None)
79
+
80
+
81
+ def validate_artifact_environment(artifact: Artifact) -> None:
82
+ """Validate the artifact handler environment.
83
+
84
+ Parameters
85
+ ----------
86
+ artifact : Artifact
87
+ An artifact handler instantiated from project and/or repository metadata.
88
+
89
+ Raises
90
+ ------
91
+ lazyscribe.exception.ArtifactLoadError
92
+ Raised if the runtime environment does not match artifact metadata.
93
+ """
94
+ # Construct the handler with relevant parameters.
95
+ artifact_attrs: dict[str, Any] = {
96
+ x: y
97
+ for x, y in inspect.getmembers(artifact)
98
+ if not x.startswith("_") and not inspect.ismethod(y)
99
+ }
100
+ # Exclude parameters that don't define equality
101
+ exclude_names: list[str] = [
102
+ attr.name for attr in fields(type(artifact)) if not attr.eq
103
+ ]
104
+ construct_params: list[str] = [
105
+ param_name
106
+ for param_name, param in inspect.signature(
107
+ artifact.construct
108
+ ).parameters.items()
109
+ if param_name not in exclude_names or param.default == param.empty
110
+ ]
111
+ artifact_attrs = {
112
+ key: value for key, value in artifact_attrs.items() if key in construct_params
113
+ }
114
+
115
+ curr_handler = type(artifact).construct(**artifact_attrs, dirty=False)
116
+ # Validate the handler
117
+ if curr_handler != artifact:
118
+ field_filters = filters.exclude(
119
+ *[attr for attr in fields(type(artifact)) if not attr.eq]
120
+ )
121
+ raise ArtifactLoadError(
122
+ "Runtime environments do not match. Artifact parameters:\n\n"
123
+ f"{json.dumps(asdict(artifact, filter=field_filters))}"
124
+ "\n\nCurrent parameters:\n\n"
125
+ f"{json.dumps(asdict(curr_handler, filter=field_filters))}"
126
+ )
@@ -0,0 +1,51 @@
1
+ """Import the handlers."""
2
+
3
+ from importlib.metadata import entry_points
4
+
5
+ from lazyscribe.artifacts.base import Artifact
6
+
7
+ __all__: list[str] = ["_get_handler"]
8
+
9
+
10
+ def _get_handler(alias: str) -> type[Artifact]:
11
+ """Retrieve a specific handler based on the alias.
12
+
13
+ Parameters
14
+ ----------
15
+ alias : str
16
+ The alias for the handler.
17
+
18
+ Returns
19
+ -------
20
+ type[lazyscribe.artifacts.base.Artifact]
21
+ The artifact handler class.
22
+ """
23
+ entry = entry_points(group="lazyscribe.artifact_type")
24
+
25
+ for full_artifact_class in entry: # search through entrypoints first
26
+ if full_artifact_class.name == alias:
27
+ try:
28
+ mod = full_artifact_class.load()
29
+ except ImportError as imp:
30
+ raise ImportError(
31
+ f"Unable to import handler for {alias} through entry points"
32
+ ) from imp
33
+
34
+ if not isinstance(mod, type):
35
+ raise TypeError(f"{full_artifact_class} is not a class")
36
+
37
+ break
38
+
39
+ else:
40
+ for obj in Artifact.__subclasses__(): # search through experiment subclasses
41
+ if obj.alias == alias:
42
+ mod = obj
43
+ break
44
+
45
+ # no handler found in both entrypoints or subclass
46
+ else:
47
+ raise ValueError(
48
+ f"No handler available with the name {alias} in `artifact_type` group."
49
+ )
50
+
51
+ return mod # type: ignore
@@ -0,0 +1,144 @@
1
+ """Base class for new artifact handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABCMeta, abstractmethod
6
+ from datetime import datetime
7
+ from io import IOBase
8
+ from typing import Any, ClassVar
9
+
10
+ from attrs import define, field
11
+
12
+
13
+ @define
14
+ class Artifact(metaclass=ABCMeta):
15
+ """Generic artifact handler that defines the expected interface.
16
+
17
+ Artifact handlers are not meant to be initialized directly.
18
+
19
+ Attributes
20
+ ----------
21
+ alias : str
22
+ The alias for the artifact handler. This value will be supplied to
23
+ :py:meth:`lazyscribe.experiment.Experiment.log_artifact`.
24
+ (A class attribute.)
25
+ suffix : str
26
+ The standard suffix for the files written and read by this handler.
27
+ (A class attribute.)
28
+ binary : bool
29
+ Whether or not the file format for the handler is binary in nature. This
30
+ affects whether or not the file handler uses ``w`` or ``wb``.
31
+ (A class attribute.)
32
+ output_only : bool
33
+ Whether or not the file output by the handler is meant to be read as the orginal project.
34
+ (A class attribute.)
35
+ name : str
36
+ The name of the artifact.
37
+ fname : str
38
+ The filename of the artifact.
39
+ value : Any
40
+ The value for the artifact.
41
+ writer_kwargs : dict
42
+ User provided keyword arguments for writing an artifact. Provided when
43
+ the artifact is logged to an experiment.
44
+ version : int
45
+ Version of the artifact.
46
+ dirty : bool
47
+ Whether or not this artifact should be saved when :py:meth:`lazyscribe.project.Project.save`
48
+ or :py:meth:`lazyscribe.repository.Repository.save` is called. This decision is based
49
+ on whether the artifact is new or has been updated.
50
+ """
51
+
52
+ alias: ClassVar[str]
53
+ suffix: ClassVar[str]
54
+ binary: ClassVar[bool]
55
+ output_only: ClassVar[
56
+ bool
57
+ ] # Describes if the artifact will reconstruct to a Python object on read
58
+ name: str = field(eq=False)
59
+ fname: str = field(eq=False)
60
+ value: Any = field(eq=False)
61
+ writer_kwargs: dict[str, Any] = field(eq=False)
62
+ created_at: datetime = field(eq=False)
63
+ version: int = field(eq=False)
64
+ dirty: bool = field(eq=False)
65
+
66
+ @classmethod
67
+ @abstractmethod
68
+ def construct(
69
+ cls,
70
+ name: str,
71
+ value: Any = None,
72
+ fname: str | None = None,
73
+ created_at: datetime | None = None,
74
+ writer_kwargs: dict[str, Any] | None = None,
75
+ version: int = 0,
76
+ dirty: bool = True,
77
+ **kwargs: Any,
78
+ ) -> Artifact:
79
+ """Construct the artifact handler.
80
+
81
+ This method should use environment variables to capture information that
82
+ is relevant to compatibility between runtime environments.
83
+
84
+ Parameters
85
+ ----------
86
+ name : str
87
+ The name of the artifact.
88
+ value : Any, optional (default None)
89
+ The value for the artifact.
90
+ fname : str, optional (default None)
91
+ The filename for the artifact. If set to ``None`` or not provided, it will be derived from
92
+ the name of the artifact and the suffix for the class.
93
+ created_at : datetime.datetime, optional (default ``lazyscribe._utils.utcnow()``)
94
+ When the artifact was created.
95
+ writer_kwargs : dict, optional (default {})
96
+ Keyword arguments for writing an artifact to the filesystem. Provided when an artifact
97
+ is logged to an experiment.
98
+ version : int, optional (default 0)
99
+ Integer version to be used for versioning artifacts.
100
+ dirty : bool, optional (default True)
101
+ Whether or not this artifact should be saved when :py:meth:`lazyscribe.project.Project.save`
102
+ or :py:meth:`lazyscribe.repository.Repository.save` is called. This decision is based
103
+ on whether the artifact is new or has been updated.
104
+ **kwargs
105
+ Other keyword arguments.
106
+
107
+ Returns
108
+ -------
109
+ Artifact
110
+ The artifact.
111
+ """
112
+
113
+ @classmethod
114
+ @abstractmethod
115
+ def read(cls, buf: IOBase, **kwargs: Any) -> Any:
116
+ """Read in the artifact.
117
+
118
+ Parameters
119
+ ----------
120
+ buf : file-like object
121
+ The buffer from a ``fsspec`` filesystem.
122
+ **kwargs
123
+ Keyword arguments for the read method.
124
+
125
+ Returns
126
+ -------
127
+ Any
128
+ The artifact object.
129
+ """
130
+
131
+ @classmethod
132
+ @abstractmethod
133
+ def write(cls, obj: Any, buf: IOBase, **kwargs: Any) -> None:
134
+ """Write the artifact to the filesystem.
135
+
136
+ Parameters
137
+ ----------
138
+ obj : Any
139
+ The object to write to the buffer.
140
+ buf : file-like object
141
+ The buffer from a ``fsspec`` filesystem.
142
+ **kwargs
143
+ Keyword arguments for the write method.
144
+ """
@@ -0,0 +1,138 @@
1
+ """Artifact handler for JSON-serializable objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from datetime import datetime
7
+ from io import IOBase
8
+ from json import dump, load
9
+ from typing import Any, ClassVar
10
+
11
+ from attrs import define, field
12
+ from slugify import slugify
13
+
14
+ from lazyscribe._utils import utcnow
15
+ from lazyscribe.artifacts.base import Artifact
16
+
17
+
18
+ @define(auto_attribs=True)
19
+ class JSONArtifact(Artifact):
20
+ """Handler for JSON-serializable objects.
21
+
22
+ .. important::
23
+
24
+ This class is not meant to be initialized directly. Please use the ``construct``
25
+ method.
26
+
27
+ .. note::
28
+
29
+ For the attributes documentation, see also "Attributes" of :py:class:`lazyscribe.artifacts.base.Artifact`.
30
+
31
+ Attributes
32
+ ----------
33
+ alias : str = "json"
34
+ suffix : str = "json"
35
+ binary : bool = False
36
+ output_only : bool = False
37
+
38
+ python_version : str
39
+ Minor Python version (e.g. ``"3.10"``).
40
+ """
41
+
42
+ alias: ClassVar[str] = "json"
43
+ suffix: ClassVar[str] = "json"
44
+ binary: ClassVar[bool] = False
45
+ output_only: ClassVar[bool] = False
46
+ python_version: str = field()
47
+
48
+ @classmethod
49
+ def construct(
50
+ cls,
51
+ name: str,
52
+ value: Any = None,
53
+ fname: str | None = None,
54
+ created_at: datetime | None = None,
55
+ writer_kwargs: dict[str, Any] | None = None,
56
+ version: int = 0,
57
+ dirty: bool = True,
58
+ **kwargs: Any,
59
+ ) -> JSONArtifact:
60
+ """Construct the handler class.
61
+
62
+ Parameters
63
+ ----------
64
+ name : str
65
+ The name of the artifact.
66
+ value : Any, optional (default None)
67
+ The value for the artifact. The default value of ``None`` is used when
68
+ an experiment is loaded from the project JSON.
69
+ fname : str, optional (default None)
70
+ The filename for the artifact. If set to ``None`` or not provided, it will be derived from
71
+ the name of the artifact and the suffix for the class.
72
+ created_at : datetime.datetime, optional (default ``lazyscribe._utils.utcnow()``)
73
+ When the artifact was created.
74
+ writer_kwargs : dict[str, Any], optional (default {})
75
+ Keyword arguments for writing an artifact to the filesystem. Provided when an artifact
76
+ is logged to an experiment.
77
+ version : int, optional (default 0)
78
+ Integer version to be used for versioning artifacts.
79
+ dirty : bool, optional (default True)
80
+ Whether or not this artifact should be saved when :py:meth:`lazyscribe.project.Project.save`
81
+ or :py:meth:`lazyscribe.repository.Repository.save` is called. This decision is based
82
+ on whether the artifact is new or has been updated.
83
+ python_version : str, optional
84
+ Minor Python version (e.g. ``"3.10"``).
85
+
86
+ Returns
87
+ -------
88
+ JSONArtifact
89
+ The artifact.
90
+ """
91
+ python_version = kwargs.get("python_version") or ".".join(
92
+ str(i) for i in sys.version_info[:2]
93
+ )
94
+ created_at = created_at or utcnow()
95
+ return cls(
96
+ name=name,
97
+ value=value,
98
+ fname=fname
99
+ or f"{slugify(name)}-{slugify(created_at.strftime('%Y%m%d%H%M%S'))}.{cls.suffix}",
100
+ created_at=created_at,
101
+ writer_kwargs=writer_kwargs or {},
102
+ version=version,
103
+ dirty=dirty,
104
+ python_version=python_version,
105
+ )
106
+
107
+ @classmethod
108
+ def read(cls, buf: IOBase, **kwargs: Any) -> Any:
109
+ """Read in the JSON file.
110
+
111
+ Parameters
112
+ ----------
113
+ buf : file-like object
114
+ The buffer from a ``fsspec`` filesystem.
115
+ **kwargs
116
+ Keyword arguments for :py:meth:`json.load`
117
+
118
+ Returns
119
+ -------
120
+ Any
121
+ The deserialized JSON file.
122
+ """
123
+ return load(buf, **kwargs)
124
+
125
+ @classmethod
126
+ def write(cls, obj: Any, buf: IOBase, **kwargs: Any) -> None:
127
+ """Write the content to a JSON file.
128
+
129
+ Parameters
130
+ ----------
131
+ obj : object
132
+ The JSON-serializable object.
133
+ buf : file-like object
134
+ The buffer from a ``fsspec`` filesystem.
135
+ **kwargs
136
+ Keyword arguments for :py:meth:`json.dump`.
137
+ """
138
+ dump(obj, buf, **kwargs)