liblaf-cherries 0.5.2__tar.gz → 0.5.4__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.
Files changed (57) hide show
  1. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/PKG-INFO +1 -1
  2. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/_entrypoint.py +2 -0
  3. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/_version.py +2 -2
  4. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/core/_run.py +10 -13
  5. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/core/_utils.py +3 -21
  6. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/plugins/comet.py +90 -17
  7. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/plugins/git_.py +13 -8
  8. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/plugins/local.py +12 -2
  9. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/plugins/logging.py +2 -2
  10. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/profiles/_default.py +0 -1
  11. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/profiles/_playground.py +1 -0
  12. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/.gitignore +0 -0
  13. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/LICENSE +0 -0
  14. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/README.md +0 -0
  15. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/pyproject.toml +0 -0
  16. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/__init__.py +0 -0
  17. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/__init__.pyi +0 -0
  18. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/_version.pyi +0 -0
  19. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/__init__.py +0 -0
  20. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/__init__.pyi +0 -0
  21. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/_config.py +0 -0
  22. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/asset/__init__.py +0 -0
  23. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/asset/__init__.pyi +0 -0
  24. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/asset/_meta.py +0 -0
  25. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/asset/_registry.py +0 -0
  26. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/asset/resolvers/__init__.py +0 -0
  27. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/asset/resolvers/__init__.pyi +0 -0
  28. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/asset/resolvers/_abc.py +0 -0
  29. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/asset/resolvers/_series.py +0 -0
  30. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/config/asset/resolvers/_vtk.py +0 -0
  31. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/core/__init__.py +0 -0
  32. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/core/__init__.pyi +0 -0
  33. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/core/_impl.py +0 -0
  34. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/core/_plugin.py +0 -0
  35. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/core/_spec.py +0 -0
  36. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/core/typing.py +0 -0
  37. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/meta/__init__.py +0 -0
  38. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/meta/__init__.pyi +0 -0
  39. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/meta/_git.py +0 -0
  40. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/meta/_name.py +0 -0
  41. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/path_utils/__init__.py +0 -0
  42. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/path_utils/__init__.pyi +0 -0
  43. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/path_utils/_convert.py +0 -0
  44. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/path_utils/_path.py +0 -0
  45. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/path_utils/_special.py +0 -0
  46. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/plugins/__init__.py +0 -0
  47. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/plugins/__init__.pyi +0 -0
  48. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/plugins/dvc.py +0 -0
  49. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/profiles/__init__.py +0 -0
  50. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/profiles/__init__.pyi +0 -0
  51. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/profiles/_abc.py +0 -0
  52. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/profiles/_factory.py +0 -0
  53. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/py.typed +0 -0
  54. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/typing.py +0 -0
  55. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/utils/__init__.py +0 -0
  56. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/utils/__init__.pyi +0 -0
  57. {liblaf_cherries-0.5.2 → liblaf_cherries-0.5.4}/src/liblaf/cherries/utils/_functools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liblaf-cherries
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: 🍒 Sweet experiment tracking with Comet, DVC, and Git integration.
5
5
  Project-URL: Changelog, https://github.com/liblaf/cherries/blob/main/CHANGELOG.md
6
6
  Project-URL: Documentation, https://cherries.readthedocs.io/
@@ -1,3 +1,4 @@
1
+ import datetime
1
2
  import inspect
2
3
  import itertools
3
4
  from collections.abc import Callable, Mapping, Sequence
@@ -10,6 +11,7 @@ from liblaf.cherries import core, profiles
10
11
 
11
12
 
12
13
  def end(*args, **kwargs) -> None:
14
+ core.active_run.log_other("cherries.end_time", datetime.datetime.now().astimezone())
13
15
  core.active_run.end(*args, **kwargs)
14
16
 
15
17
 
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.5.2'
32
- __version_tuple__ = version_tuple = (0, 5, 2)
31
+ __version__ = version = '0.5.4'
32
+ __version_tuple__ = version_tuple = (0, 5, 4)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,4 +1,3 @@
1
- import contextlib
2
1
  import datetime
3
2
  from collections.abc import Mapping
4
3
  from pathlib import Path
@@ -37,8 +36,12 @@ class Run(Plugin):
37
36
  return path_utils.exp_dir()
38
37
 
39
38
  @plugin_cached_property
40
- def name(self) -> str:
41
- return self.start_time.strftime("%Y-%m-%dT%H%M%S")
39
+ def exp_name(self) -> str:
40
+ return (
41
+ self.entrypoint.relative_to(self.project_dir)
42
+ .as_posix()
43
+ .removeprefix("exp/")
44
+ )
42
45
 
43
46
  @plugin_property
44
47
  def params(self) -> Mapping[str, Any]:
@@ -72,7 +75,7 @@ class Run(Plugin):
72
75
  @spec(first_result=True)
73
76
  def get_url(self) -> str: ...
74
77
 
75
- @spec(delegate=False)
78
+ @spec
76
79
  def log_asset(
77
80
  self,
78
81
  path: PathLike,
@@ -80,12 +83,7 @@ class Run(Plugin):
80
83
  *,
81
84
  metadata: Mapping[str, Any] | None = None,
82
85
  **kwargs,
83
- ) -> None:
84
- if name is None:
85
- path = Path(path)
86
- with contextlib.suppress(ValueError):
87
- name = path.relative_to(self.data_dir)
88
- self.delegate("log_asset", (path, name), {"metadata": metadata, **kwargs})
86
+ ) -> None: ...
89
87
 
90
88
  @spec
91
89
  def log_input(
@@ -150,9 +148,8 @@ class Run(Plugin):
150
148
  **kwargs,
151
149
  ) -> None: ...
152
150
 
153
- @spec(delegate=False)
154
- def start(self, *args, **kwargs) -> None:
155
- self.delegate("start", args, kwargs)
151
+ @spec
152
+ def start(self, *args, **kwargs) -> None: ...
156
153
 
157
154
 
158
155
  active_run: Run = Run()
@@ -1,24 +1,6 @@
1
1
  import functools
2
2
  from collections.abc import Callable
3
- from typing import TYPE_CHECKING, Any, Self, overload, override
4
-
5
- import wrapt
6
-
7
- if TYPE_CHECKING:
8
- from ._plugin import Plugin
9
-
10
-
11
- def delegate_property_to_root[C: Callable](func: C) -> C:
12
- @wrapt.decorator
13
- def wrapper(
14
- wrapped: Callable, instance: "Plugin", args: tuple, kwargs: dict[str, Any]
15
- ) -> None:
16
- # TODO: make it work with `@functools.cached_property`
17
- if instance.plugin_root is not instance:
18
- return wrapped(*args, **kwargs)
19
- return getattr(instance.plugin_root, wrapped.__name__)
20
-
21
- return func
3
+ from typing import Any, Self, overload, override
22
4
 
23
5
 
24
6
  class PluginCachedProperty[T](functools.cached_property[T]):
@@ -30,7 +12,7 @@ class PluginCachedProperty[T](functools.cached_property[T]):
30
12
  def __get__(self, instance: object | None, owner: type | None = None) -> Self | T:
31
13
  if instance is None:
32
14
  return super().__get__(instance, owner)
33
- if (parent := getattr(instance, "_plugin_parent", None)) is not None:
15
+ while (parent := getattr(instance, "_plugin_parent", None)) is not None:
34
16
  instance = parent
35
17
  return super().__get__(instance, owner)
36
18
 
@@ -50,7 +32,7 @@ class PluginProperty[T](property):
50
32
  ) -> Self | T:
51
33
  if instance is None:
52
34
  return super().__get__(instance, owner)
53
- if (parent := getattr(instance, "_plugin_parent", None)) is not None:
35
+ while (parent := getattr(instance, "_plugin_parent", None)) is not None:
54
36
  instance = parent
55
37
  return super().__get__(instance, owner)
56
38
 
@@ -7,21 +7,36 @@ import comet_ml
7
7
  import cytoolz as toolz
8
8
  import dvc.api
9
9
  import dvc.exceptions
10
+ import git
11
+ from loguru import logger
10
12
 
11
13
  from liblaf import grapes
12
14
  from liblaf.cherries import core, path_utils
13
15
  from liblaf.cherries.typing import PathLike
14
16
 
15
17
 
18
+ @attrs.frozen
19
+ class Asset:
20
+ path: PathLike
21
+ name: PathLike | None
22
+ metadata: Mapping[str, Any] | None = None
23
+ kwargs: Mapping[str, Any] = attrs.field(factory=dict)
24
+
25
+
16
26
  @attrs.define
17
27
  class Comet(core.Run):
18
28
  disabled: bool = attrs.field(default=False)
19
29
  exp: comet_ml.CometExperiment = attrs.field(default=None)
30
+ _assets_git: list[Asset] = attrs.field(factory=list)
20
31
 
21
32
  @override
22
- @core.impl(after=("Logging",))
33
+ @core.impl(after=("Git", "Logging"))
23
34
  def end(self, *args, **kwargs) -> None:
24
- return self.exp.end()
35
+ try:
36
+ self._log_asset_git_end()
37
+ except git.GitError as err:
38
+ logger.exception(err)
39
+ self.exp.end()
25
40
 
26
41
  @override
27
42
  @core.impl
@@ -44,23 +59,14 @@ class Comet(core.Run):
44
59
  self,
45
60
  path: PathLike,
46
61
  name: PathLike | None = None,
47
- *,
48
- metadata: Mapping[str, Any] | None = None,
49
62
  **kwargs,
50
63
  ) -> None:
51
- path = Path(path)
64
+ if self._log_asset_git(path, name, **kwargs):
65
+ return
66
+ if self._log_asset_dvc(path, name, **kwargs):
67
+ return
52
68
  name = path_utils.as_posix(name)
53
- try:
54
- # ? I don't know why, but `dvc.api.get_url` only works with this. Maybe a DVC bug?
55
- dvc_path: Path = path.absolute().relative_to(Path.cwd())
56
- uri: str = dvc.api.get_url(str(dvc_path))
57
- except dvc.exceptions.OutputNotFoundError:
58
- self.exp.log_asset(path, name, metadata=metadata, **kwargs) # pyright: ignore[reportArgumentType]
59
- else:
60
- dvc_file: Path = path.with_name(path.name + ".dvc")
61
- dvc_meta: Mapping[str, Any] = grapes.yaml.load(dvc_file)
62
- metadata = toolz.merge(metadata or {}, dvc_meta["outs"][0])
63
- self.exp.log_remote_asset(uri, name, metadata=metadata, **kwargs) # pyright: ignore[reportArgumentType]
69
+ self.exp.log_asset(path, name, **kwargs)
64
70
 
65
71
  @override
66
72
  @core.impl(after=("Dvc",))
@@ -130,6 +136,73 @@ class Comet(core.Run):
130
136
  self.exp = comet_ml.start(
131
137
  project_name=self.project_name,
132
138
  experiment_config=comet_ml.ExperimentConfig(
133
- disabled=self.disabled, name=self.name
139
+ disabled=self.disabled, name=self.exp_name
134
140
  ),
135
141
  )
142
+
143
+ def _log_asset_dvc(
144
+ self,
145
+ path: PathLike,
146
+ name: PathLike | None = None,
147
+ *,
148
+ metadata: Mapping[str, Any] | None = None,
149
+ **kwargs,
150
+ ) -> bool:
151
+ path = Path(path)
152
+ name = path_utils.as_posix(name)
153
+ try:
154
+ # ? I don't know why, but `dvc.api.get_url` only works with this. Maybe a DVC bug?
155
+ dvc_path: Path = path.absolute().relative_to(Path.cwd())
156
+ uri: str = dvc.api.get_url(str(dvc_path))
157
+ except dvc.exceptions.OutputNotFoundError:
158
+ return False
159
+ dvc_file: Path = path.with_name(path.name + ".dvc")
160
+ dvc_meta: Mapping[str, Any] = grapes.yaml.load(dvc_file)
161
+ metadata: dict[str, Mapping] = toolz.merge(metadata or {}, dvc_meta["outs"][0])
162
+ self.exp.log_remote_asset(uri, name, metadata=metadata, **kwargs)
163
+ return True
164
+
165
+ def _log_asset_git(
166
+ self,
167
+ path: PathLike,
168
+ name: PathLike | None = None,
169
+ *,
170
+ metadata: Mapping[str, Any] | None = None,
171
+ **kwargs,
172
+ ) -> bool:
173
+ try:
174
+ repo = git.Repo(search_parent_directories=True)
175
+ except git.InvalidGitRepositoryError:
176
+ return False
177
+ try:
178
+ if repo.ignored(path):
179
+ return False
180
+ except git.GitCommandError:
181
+ # `path` may be outside repository
182
+ return False
183
+ self._assets_git.append(
184
+ Asset(path=path, name=name, metadata=metadata, kwargs=kwargs)
185
+ )
186
+ return True
187
+
188
+ def _log_asset_git_end(self) -> None:
189
+ if len(self._assets_git) == 0:
190
+ return
191
+ repo = git.Repo(search_parent_directories=True)
192
+ info: grapes.git.GitInfo = grapes.git.info()
193
+ for asset in self._assets_git:
194
+ uri: str
195
+ match str(info.platform):
196
+ case "github":
197
+ path: Path = Path(asset.path).absolute()
198
+ path_rel: str = path.relative_to(repo.working_tree_dir).as_posix() # pyright: ignore[reportArgumentType]
199
+ sha: str = repo.head.commit.hexsha
200
+ uri = f"https://{info.host}/{info.owner}/{info.repo}/raw/{sha}/{path_rel}"
201
+ case _:
202
+ uri = path_utils.as_posix(asset.path)
203
+ self.exp.log_remote_asset(
204
+ uri,
205
+ path_utils.as_posix(asset.name),
206
+ metadata=dict(asset.metadata) if asset.metadata is not None else None,
207
+ **asset.kwargs,
208
+ )
@@ -6,6 +6,7 @@ from typing import Any, override
6
6
 
7
7
  import attrs
8
8
  import git
9
+ from loguru import logger
9
10
 
10
11
  from liblaf import grapes
11
12
  from liblaf.cherries import core
@@ -14,6 +15,7 @@ from liblaf.cherries.typing import PathLike
14
15
 
15
16
  @attrs.define
16
17
  class Git(core.Run):
18
+ commit: bool = True
17
19
  inputs: list[Path] = attrs.field(factory=list)
18
20
  outputs: list[Path] = attrs.field(factory=list)
19
21
  repo: git.Repo = attrs.field(default=None)
@@ -22,12 +24,15 @@ class Git(core.Run):
22
24
  @override
23
25
  @core.impl(after=("Dvc",))
24
26
  def end(self, *args, **kwargs) -> None:
25
- if not self.repo.is_dirty(untracked_files=True):
26
- return
27
- self.repo.git.add(all=True)
28
- subprocess.run(["git", "status"], check=False)
29
- message: str = self._make_commit_message()
30
- self.repo.git.commit(message=message, no_verify=not self.verify)
27
+ if self.commit and self.repo.is_dirty(untracked_files=True):
28
+ try:
29
+ self.repo.git.add(all=True)
30
+ subprocess.run(["git", "status"], check=False)
31
+ message: str = self._make_commit_message()
32
+ self.repo.git.commit(message=message, no_verify=not self.verify)
33
+ except git.GitCommandError as err:
34
+ logger.exception(err)
35
+ self.plugin_root.log_other("cherries.git.sha", self.repo.head.commit.hexsha)
31
36
 
32
37
  @override
33
38
  @core.impl
@@ -49,10 +54,10 @@ class Git(core.Run):
49
54
  @override
50
55
  @core.impl
51
56
  def start(self, *args, **kwargs) -> None:
52
- self.repo = git.Repo(self.project_dir, search_parent_directories=True)
57
+ self.repo = git.Repo(search_parent_directories=True)
53
58
 
54
59
  def _make_commit_message(self) -> str:
55
- name: str = self.name
60
+ name: str = self.exp_name
56
61
  message: str = f"chore(cherries): {name}\n\n"
57
62
  meta: dict[str, Any] = {}
58
63
  if url := self.url:
@@ -20,8 +20,13 @@ class Local(core.Run):
20
20
  name: PathLike | None = None,
21
21
  **kwargs,
22
22
  ) -> None:
23
+ path = Path(path)
23
24
  if name is None:
24
- name = Path(path).name
25
+ path_absolute: Path = path.resolve()
26
+ if path_absolute.is_relative_to(self.exp_dir):
27
+ name = path_absolute.relative_to(self.exp_dir)
28
+ else:
29
+ name = path
25
30
  target: Path = self.folder / name
26
31
  self._copy(path, target)
27
32
 
@@ -54,7 +59,12 @@ class Local(core.Run):
54
59
  @override
55
60
  @core.impl
56
61
  def start(self, *args, **kwargs) -> None:
57
- self.folder = self.exp_dir / ".cherries" / self.name
62
+ self.folder = (
63
+ self.exp_dir
64
+ / ".cherries"
65
+ / self.entrypoint.stem
66
+ / self.start_time.strftime("%Y-%m-%dT%H%M%S")
67
+ )
58
68
  entrypoint: Path = self.entrypoint
59
69
  self.log_asset(entrypoint, f"src/{entrypoint.name}")
60
70
 
@@ -11,7 +11,7 @@ from liblaf.cherries import core
11
11
  class Logging(core.Run):
12
12
  @property
13
13
  def log_file(self) -> Path:
14
- return self.exp_dir / "run.log"
14
+ return self.exp_dir / "logs" / self.entrypoint.with_suffix(".log").name
15
15
 
16
16
  @override
17
17
  @core.impl
@@ -21,4 +21,4 @@ class Logging(core.Run):
21
21
  @override
22
22
  @core.impl
23
23
  def end(self, *args, **kwargs) -> None:
24
- self.log_asset(self.log_file)
24
+ self.plugin_root.log_asset(self.log_file, "run.log")
@@ -10,7 +10,6 @@ class ProfileDefault(Profile):
10
10
  def init(self) -> core.Run:
11
11
  run: core.Run = core.active_run
12
12
  run.register(plugins.Comet())
13
- run.register(plugins.Dvc())
14
13
  run.register(plugins.Git())
15
14
  run.register(plugins.Local())
16
15
  run.register(plugins.Logging())
@@ -10,6 +10,7 @@ class ProfilePlayground(Profile):
10
10
  def init(self) -> core.Run:
11
11
  run: core.Run = core.active_run
12
12
  run.register(plugins.Comet(disabled=True))
13
+ run.register(plugins.Git(commit=False))
13
14
  run.register(plugins.Local())
14
15
  run.register(plugins.Logging())
15
16
  return run
File without changes