liblaf-cherries 0.1.6__tar.gz → 0.2.0__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 (60) hide show
  1. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/PKG-INFO +5 -3
  2. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/pyproject.toml +5 -9
  3. liblaf_cherries-0.2.0/src/liblaf/cherries/__init__.pyi +50 -0
  4. liblaf_cherries-0.2.0/src/liblaf/cherries/_entrypoint.py +56 -0
  5. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/_version.py +2 -2
  6. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/config/_asset.py +3 -3
  7. liblaf_cherries-0.2.0/src/liblaf/cherries/core/__init__.pyi +20 -0
  8. liblaf_cherries-0.2.0/src/liblaf/cherries/core/_impl.py +58 -0
  9. liblaf_cherries-0.2.0/src/liblaf/cherries/core/_plugin.py +96 -0
  10. liblaf_cherries-0.2.0/src/liblaf/cherries/core/_run.py +42 -0
  11. liblaf_cherries-0.2.0/src/liblaf/cherries/core/_spec.py +71 -0
  12. liblaf_cherries-0.2.0/src/liblaf/cherries/core/typed.py +2 -0
  13. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/meta/_git.py +3 -3
  14. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/meta/_name.py +3 -3
  15. {liblaf_cherries-0.1.6/src/liblaf/cherries/pathutils → liblaf_cherries-0.2.0/src/liblaf/cherries/paths}/_path.py +9 -7
  16. liblaf_cherries-0.2.0/src/liblaf/cherries/plugins/__init__.pyi +3 -0
  17. liblaf_cherries-0.2.0/src/liblaf/cherries/plugins/logging.py +25 -0
  18. liblaf_cherries-0.2.0/src/liblaf/cherries/profiles/__init__.pyi +13 -0
  19. liblaf_cherries-0.2.0/src/liblaf/cherries/profiles/_abc.py +10 -0
  20. liblaf_cherries-0.2.0/src/liblaf/cherries/profiles/_default.py +12 -0
  21. liblaf_cherries-0.2.0/src/liblaf/cherries/profiles/_factory.py +21 -0
  22. liblaf_cherries-0.2.0/src/liblaf/cherries/profiles/_playground.py +13 -0
  23. liblaf_cherries-0.1.6/src/liblaf/cherries/__init__.pyi +0 -129
  24. liblaf_cherries-0.1.6/src/liblaf/cherries/_run.py +0 -51
  25. liblaf_cherries-0.1.6/src/liblaf/cherries/core/__init__.pyi +0 -12
  26. liblaf_cherries-0.1.6/src/liblaf/cherries/core/_exp.py +0 -119
  27. liblaf_cherries-0.1.6/src/liblaf/cherries/core/_spec.py +0 -177
  28. liblaf_cherries-0.1.6/src/liblaf/cherries/integration/__init__.pyi +0 -72
  29. liblaf_cherries-0.1.6/src/liblaf/cherries/integration/_abc.py +0 -144
  30. liblaf_cherries-0.1.6/src/liblaf/cherries/integration/_exp.py +0 -109
  31. liblaf_cherries-0.1.6/src/liblaf/cherries/integration/comet.py +0 -142
  32. liblaf_cherries-0.1.6/src/liblaf/cherries/integration/dvc.py +0 -51
  33. liblaf_cherries-0.1.6/src/liblaf/cherries/integration/git.py +0 -44
  34. liblaf_cherries-0.1.6/src/liblaf/cherries/integration/logging.py +0 -45
  35. liblaf_cherries-0.1.6/src/liblaf/cherries/presets/__init__.pyi +0 -5
  36. liblaf_cherries-0.1.6/src/liblaf/cherries/presets/_default.py +0 -46
  37. liblaf_cherries-0.1.6/src/liblaf/cherries/presets/_playground.py +0 -11
  38. liblaf_cherries-0.1.6/src/liblaf/cherries/presets/typed.py +0 -5
  39. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/.gitignore +0 -0
  40. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/LICENSE +0 -0
  41. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/README.md +0 -0
  42. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/__init__.py +0 -0
  43. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/_version.pyi +0 -0
  44. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/config/__init__.py +0 -0
  45. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/config/__init__.pyi +0 -0
  46. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/config/_config.py +0 -0
  47. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/core/__init__.py +0 -0
  48. {liblaf_cherries-0.1.6/src/liblaf/cherries/integration → liblaf_cherries-0.2.0/src/liblaf/cherries/meta}/__init__.py +0 -0
  49. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/meta/__init__.pyi +0 -0
  50. {liblaf_cherries-0.1.6/src/liblaf/cherries/meta → liblaf_cherries-0.2.0/src/liblaf/cherries/paths}/__init__.py +0 -0
  51. {liblaf_cherries-0.1.6/src/liblaf/cherries/pathutils → liblaf_cherries-0.2.0/src/liblaf/cherries/paths}/__init__.pyi +0 -0
  52. {liblaf_cherries-0.1.6/src/liblaf/cherries/pathutils → liblaf_cherries-0.2.0/src/liblaf/cherries/paths}/_convert.py +0 -0
  53. {liblaf_cherries-0.1.6/src/liblaf/cherries/pathutils → liblaf_cherries-0.2.0/src/liblaf/cherries/paths}/_special.py +0 -0
  54. {liblaf_cherries-0.1.6/src/liblaf/cherries/pathutils → liblaf_cherries-0.2.0/src/liblaf/cherries/plugins}/__init__.py +0 -0
  55. {liblaf_cherries-0.1.6/src/liblaf/cherries/presets → liblaf_cherries-0.2.0/src/liblaf/cherries/profiles}/__init__.py +0 -0
  56. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/py.typed +0 -0
  57. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/typed.py +0 -0
  58. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/utils/__init__.py +0 -0
  59. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/src/liblaf/cherries/utils/__init__.pyi +0 -0
  60. {liblaf_cherries-0.1.6 → liblaf_cherries-0.2.0}/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.1.6
3
+ Version: 0.2.0
4
4
  Summary: Add your description here
5
5
  Project-URL: Changelog, https://github.com/liblaf/cherries/blob/main/CHANGELOG.md
6
6
  Project-URL: Documentation, https://liblaf.github.io/cherries/
@@ -31,17 +31,19 @@ Classifier: Topic :: System :: Logging
31
31
  Classifier: Topic :: Utilities
32
32
  Classifier: Typing :: Typed
33
33
  Requires-Python: >=3.12
34
+ Requires-Dist: attrs<26,>=25.0.0
34
35
  Requires-Dist: comet-ml<4,>=3.0.0
35
36
  Requires-Dist: dvc[webdav]<4,>=3.0.0
36
37
  Requires-Dist: environs<15,>=14.0.0
37
38
  Requires-Dist: gitpython<4,>=3.0.0
38
- Requires-Dist: lazy-loader<0.5,>=0.4.0
39
- Requires-Dist: liblaf-grapes<0.4,>=0.3.0
39
+ Requires-Dist: lazy-loader<0.5,>=0.4
40
+ Requires-Dist: liblaf-grapes<0.7,>=0.6.0
40
41
  Requires-Dist: loguru<0.8,>=0.7.0
41
42
  Requires-Dist: networkx<4,>=3.0.0
42
43
  Requires-Dist: pydantic-settings<3,>=2.0.0
43
44
  Requires-Dist: pydantic<3,>=2.0.0
44
45
  Requires-Dist: rich<15,>=14.0.0
46
+ Requires-Dist: wrapt<2,>=1.0.0
45
47
  Description-Content-Type: text/markdown
46
48
 
47
49
  <div align="center" markdown><a name="readme-top"></a>
@@ -30,13 +30,7 @@ docs = [
30
30
  "pymdown-extensions",
31
31
  "ruff",
32
32
  ]
33
- test = [
34
- "pytest",
35
- "pytest-benchmark",
36
- "pytest-codspeed",
37
- "pytest-cov",
38
- "pytest-xdist",
39
- ]
33
+ test = ["pytest", "pytest-codspeed", "pytest-cov", "pytest-xdist"]
40
34
 
41
35
  [project]
42
36
  authors = [
@@ -64,17 +58,19 @@ classifiers = [
64
58
  "Typing :: Typed",
65
59
  ]
66
60
  dependencies = [
61
+ "attrs>=25.0.0,<26",
67
62
  "comet-ml>=3.0.0,<4",
68
63
  "dvc[webdav]>=3.0.0,<4",
69
64
  "environs>=14.0.0,<15",
70
65
  "gitpython>=3.0.0,<4",
71
- "lazy-loader>=0.4.0,<0.5",
72
- "liblaf-grapes>=0.3.0,<0.4",
66
+ "lazy-loader>=0.4,<0.5",
67
+ "liblaf-grapes>=0.6.0,<0.7",
73
68
  "loguru>=0.7.0,<0.8",
74
69
  "networkx>=3.0.0,<4",
75
70
  "pydantic-settings>=2.0.0,<3",
76
71
  "pydantic>=2.0.0,<3",
77
72
  "rich>=14.0.0,<15",
73
+ "wrapt>=1.0.0,<2",
78
74
  ]
79
75
  description = "Add your description here"
80
76
  dynamic = ["version"]
@@ -0,0 +1,50 @@
1
+ from . import config, core, meta, paths
2
+ from ._entrypoint import end, run, start
3
+ from .config import BaseConfig, input, output # noqa: A004
4
+ from .core import Plugin, Run, active_run, log_asset, log_metrics
5
+ from .paths import (
6
+ as_os_path,
7
+ as_path,
8
+ as_posix,
9
+ data,
10
+ entrypoint,
11
+ exp_dir,
12
+ git_root,
13
+ git_root_safe,
14
+ inputs,
15
+ outputs,
16
+ params,
17
+ path,
18
+ src,
19
+ )
20
+
21
+ __all__ = [
22
+ "BaseConfig",
23
+ "Plugin",
24
+ "Run",
25
+ "active_run",
26
+ "as_os_path",
27
+ "as_path",
28
+ "as_posix",
29
+ "config",
30
+ "core",
31
+ "data",
32
+ "end",
33
+ "entrypoint",
34
+ "exp_dir",
35
+ "git_root",
36
+ "git_root_safe",
37
+ "input",
38
+ "inputs",
39
+ "log_asset",
40
+ "log_metrics",
41
+ "meta",
42
+ "output",
43
+ "outputs",
44
+ "params",
45
+ "path",
46
+ "paths",
47
+ "run",
48
+ "src",
49
+ "start",
50
+ ]
@@ -0,0 +1,56 @@
1
+ import inspect
2
+ from collections.abc import Callable, Mapping, Sequence
3
+ from typing import Any
4
+
5
+ from liblaf.cherries import core, profiles
6
+
7
+
8
+ def end() -> None:
9
+ core.active_run.end()
10
+
11
+
12
+ def run[T](main: Callable[..., T], *, profile: profiles.ProfileLike | None = None) -> T:
13
+ run: core.Run = start(profile=profile)
14
+ args: Sequence[Any]
15
+ kwargs: Mapping[str, Any]
16
+ args, kwargs = _make_args(main)
17
+ # TODO: log config & inputs
18
+ result: T = main(*args, **kwargs)
19
+ # TODO: log outputs
20
+ run.end()
21
+ return result
22
+
23
+
24
+ def start(
25
+ profile: profiles.ProfileLike | None = None,
26
+ ) -> core.Run:
27
+ run: core.Run = profiles.factory(profile).init()
28
+ run.start()
29
+ # TODO: log metadata
30
+ return run
31
+
32
+
33
+ def _make_args(func: Callable) -> tuple[Sequence[Any], Mapping[str, Any]]:
34
+ signature: inspect.Signature = inspect.signature(func)
35
+ args: list[Any] = []
36
+ kwargs: dict[str, Any] = {}
37
+ for name, param in signature.parameters.items():
38
+ match param.kind:
39
+ case (
40
+ inspect.Parameter.POSITIONAL_ONLY
41
+ | inspect.Parameter.POSITIONAL_OR_KEYWORD
42
+ ):
43
+ args.append(_make_arg(param))
44
+ case inspect.Parameter.KEYWORD_ONLY:
45
+ kwargs[name] = _make_arg(param)
46
+ case _:
47
+ pass
48
+ return args, kwargs
49
+
50
+
51
+ def _make_arg(param: inspect.Parameter) -> Any:
52
+ if param.default is not inspect.Parameter.empty:
53
+ return param.default
54
+ if param.annotation is not inspect.Parameter.empty:
55
+ return param.annotation()
56
+ return None
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.1.6'
21
- __version_tuple__ = version_tuple = (0, 1, 6)
20
+ __version__ = version = '0.2.0'
21
+ __version_tuple__ = version_tuple = (0, 2, 0)
@@ -6,7 +6,7 @@ from typing import Any
6
6
  import pydantic
7
7
 
8
8
  from liblaf import grapes
9
- from liblaf.cherries import pathutils as _path
9
+ from liblaf.cherries import paths
10
10
  from liblaf.cherries.typed import PathLike
11
11
 
12
12
 
@@ -61,12 +61,12 @@ def get_outputs(cfg: pydantic.BaseModel) -> list[Path]:
61
61
 
62
62
 
63
63
  def input(path: PathLike, extra: PathProvider | None = None, **kwargs) -> Path: # noqa: A001
64
- field_info: pydantic.fields.FieldInfo = pydantic.Field(_path.data(path), **kwargs) # pyright: ignore[reportAssignmentType]
64
+ field_info: pydantic.fields.FieldInfo = pydantic.Field(paths.data(path), **kwargs) # pyright: ignore[reportAssignmentType]
65
65
  field_info.metadata.append(MetaAsset(kind=AssetKind.INPUT, extra=extra))
66
66
  return field_info # pyright: ignore[reportReturnType]
67
67
 
68
68
 
69
69
  def output(path: PathLike, extra: PathProvider | None = None, **kwargs) -> Path:
70
- field_info: pydantic.fields.FieldInfo = pydantic.Field(_path.data(path), **kwargs) # pyright: ignore[reportAssignmentType]
70
+ field_info: pydantic.fields.FieldInfo = pydantic.Field(paths.data(path), **kwargs) # pyright: ignore[reportAssignmentType]
71
71
  field_info.metadata.append(MetaAsset(kind=AssetKind.OUTPUT, extra=extra))
72
72
  return field_info # pyright: ignore[reportReturnType]
@@ -0,0 +1,20 @@
1
+ from ._impl import ImplInfo, get_impl_info, impl
2
+ from ._plugin import Plugin
3
+ from ._run import Run, active_run, log_asset, log_metrics
4
+ from ._spec import SpecInfo, spec
5
+ from .typed import MethodName, PluginId
6
+
7
+ __all__ = [
8
+ "ImplInfo",
9
+ "MethodName",
10
+ "Plugin",
11
+ "PluginId",
12
+ "Run",
13
+ "SpecInfo",
14
+ "active_run",
15
+ "get_impl_info",
16
+ "impl",
17
+ "log_asset",
18
+ "log_metrics",
19
+ "spec",
20
+ ]
@@ -0,0 +1,58 @@
1
+ import functools
2
+ from collections.abc import Callable, Iterable
3
+ from typing import Any, overload
4
+
5
+ import attrs
6
+
7
+ from liblaf import grapes
8
+
9
+ from .typed import PluginId
10
+
11
+
12
+ @attrs.define
13
+ class ImplInfo:
14
+ after: Iterable[PluginId] = attrs.field(default=())
15
+ before: Iterable[PluginId] = attrs.field(default=())
16
+ priority: int = 0
17
+
18
+
19
+ @overload
20
+ def impl[C: Callable](
21
+ func: C,
22
+ /,
23
+ *,
24
+ priority: int = 0,
25
+ after: Iterable[PluginId] = (),
26
+ before: Iterable[PluginId] = (),
27
+ ) -> C: ...
28
+ @overload
29
+ def impl[C: Callable](
30
+ *,
31
+ priority: int = 0,
32
+ after: Iterable[PluginId] = (),
33
+ before: Iterable[PluginId] = (),
34
+ ) -> Callable[[C], C]: ...
35
+ def impl(
36
+ func: Callable | None = None,
37
+ /,
38
+ priority: int = 0,
39
+ after: Iterable[PluginId] = (),
40
+ before: Iterable[PluginId] = (),
41
+ ) -> Any:
42
+ if func is None:
43
+ return functools.partial(impl, priority=priority, after=after, before=before)
44
+ info = ImplInfo(after=after, before=before, priority=priority)
45
+
46
+ @grapes.decorator(attrs={"_self_impl": info})
47
+ def wrapper(
48
+ wrapped: Callable, _instance: Any, args: tuple, kwargs: dict[str, Any]
49
+ ) -> Any:
50
+ return wrapped(*args, **kwargs)
51
+
52
+ return wrapper(func)
53
+
54
+
55
+ def get_impl_info(func: Callable | None) -> ImplInfo | None:
56
+ if func is None:
57
+ return None
58
+ return getattr(func, "_self_impl", None)
@@ -0,0 +1,96 @@
1
+ import inspect
2
+ from collections.abc import Mapping, MutableMapping, Sequence
3
+ from typing import Any, Self
4
+
5
+ import attrs
6
+ import networkx as nx
7
+
8
+ from ._impl import ImplInfo, get_impl_info
9
+ from ._spec import SpecInfo
10
+ from .typed import MethodName
11
+
12
+
13
+ @attrs.define
14
+ class Plugin:
15
+ plugins: dict[str, "Plugin"] = attrs.field(factory=dict)
16
+
17
+ _plugin_parent: Self | None = attrs.field(default=None)
18
+ _sort_plugins_cache: MutableMapping[MethodName, Sequence["Plugin"]] = attrs.field(
19
+ factory=dict, init=False
20
+ )
21
+
22
+ @classmethod
23
+ def plugin_id_cls(cls) -> str:
24
+ return cls.__name__
25
+
26
+ @property
27
+ def plugin_id(self) -> str:
28
+ return self.plugin_id_cls()
29
+
30
+ @property
31
+ def plugin_root(self) -> Self:
32
+ if self._plugin_parent is None:
33
+ return self
34
+ return self._plugin_parent.plugin_root
35
+
36
+ @property
37
+ def specs(self) -> dict[str, SpecInfo]:
38
+ return {
39
+ name: method._self_spec # noqa: SLF001
40
+ for name, method in inspect.getmembers(
41
+ type(self), lambda m: getattr(m, "_self_spec", None) is not None
42
+ )
43
+ }
44
+
45
+ def delegate(
46
+ self,
47
+ method: MethodName,
48
+ args: Sequence[Any] = (),
49
+ kwargs: Mapping[str, Any] = {},
50
+ *,
51
+ first_result: bool = False,
52
+ ) -> Any:
53
+ results: list[Any] = []
54
+ for plugin in self._sort_plugins_cache.get(method, []):
55
+ result: Any = getattr(plugin, method)(*args, **kwargs)
56
+ if result is None:
57
+ continue
58
+ if first_result:
59
+ return result
60
+ results.append(result)
61
+ return results
62
+
63
+ def register(self, plugin: "Plugin") -> None:
64
+ plugin._plugin_parent = self # noqa: SLF001
65
+ self.plugins[plugin.plugin_id] = plugin
66
+
67
+ def _prepare(self) -> None:
68
+ for method in self.specs:
69
+ self._sort_plugins_cache[method] = self._sort_plugins(
70
+ method, refresh_cache=True
71
+ )
72
+
73
+ def _sort_plugins(
74
+ self, method: str, *, refresh_cache: bool = False
75
+ ) -> Sequence["Plugin"]:
76
+ if refresh_cache or method not in self._sort_plugins_cache:
77
+ plugin_infos: dict[str, ImplInfo] = {
78
+ plugin_id: info
79
+ for plugin_id, plugin in self.plugins.items()
80
+ if (info := get_impl_info(getattr(plugin, method, None))) is not None
81
+ }
82
+ graph = nx.DiGraph()
83
+ for plugin_id, impl_info in plugin_infos.items():
84
+ graph.add_node(plugin_id)
85
+ for after in impl_info.after:
86
+ graph.add_edge(after, plugin_id)
87
+ for before in impl_info.before:
88
+ graph.add_edge(plugin_id, before)
89
+ self._sort_plugins_cache[method] = tuple(
90
+ plugin
91
+ for plugin_id in nx.lexicographical_topological_sort(
92
+ graph, key=lambda node: plugin_infos[node].priority
93
+ )
94
+ if (plugin := self.plugins.get(plugin_id)) is not None
95
+ )
96
+ return self._sort_plugins_cache[method]
@@ -0,0 +1,42 @@
1
+ from pathlib import Path
2
+
3
+ import attrs
4
+
5
+ from liblaf.cherries import paths
6
+
7
+ from ._plugin import Plugin
8
+ from ._spec import spec
9
+
10
+
11
+ @attrs.define
12
+ class Run(Plugin):
13
+ """.
14
+
15
+ References:
16
+ 1. [Experiment - Comet Docs](https://www.comet.com/docs/v2/api-and-sdk/python-sdk/reference/Experiment/)
17
+ 2. [Logger | ClearML](https://clear.ml/docs/latest/docs/references/sdk/logger)
18
+ 3. [MLflow Tracking APIs | MLflow](https://www.mlflow.org/docs/latest/ml/tracking/tracking-api/)
19
+ """
20
+
21
+ @property
22
+ def exp_dir(self) -> Path:
23
+ return paths.exp_dir(absolute=True)
24
+
25
+ @spec
26
+ def end(self, *args, **kwargs) -> None: ...
27
+
28
+ @spec
29
+ def log_asset(self, *args, **kwargs) -> None: ...
30
+
31
+ @spec
32
+ def log_metrics(self, *args, **kwargs) -> None: ...
33
+
34
+ @spec(delegate=False)
35
+ def start(self, *args, **kwargs) -> None:
36
+ self._prepare()
37
+ self.delegate("start", args, kwargs)
38
+
39
+
40
+ active_run: Run = Run()
41
+ log_asset = active_run.log_asset
42
+ log_metrics = active_run.log_metrics
@@ -0,0 +1,71 @@
1
+ import functools
2
+ import inspect
3
+ from collections.abc import Callable, Mapping, Sequence
4
+ from typing import Any, Protocol, overload
5
+
6
+ import attrs
7
+
8
+ from liblaf import grapes
9
+
10
+ from .typed import MethodName
11
+
12
+
13
+ class Plugin(Protocol):
14
+ def delegate(
15
+ self,
16
+ method: MethodName,
17
+ args: Sequence[Any],
18
+ kwargs: Mapping[str, Any],
19
+ *,
20
+ first_result: bool = False,
21
+ ) -> Any: ...
22
+
23
+
24
+ @attrs.define
25
+ class SpecInfo:
26
+ delegate: bool = attrs.field(default=True)
27
+ first_result: bool = attrs.field(default=False)
28
+
29
+
30
+ @overload
31
+ def spec[C: Callable](
32
+ func: C, /, *, delegate: bool = True, first_result: bool = False
33
+ ) -> C: ...
34
+ @overload
35
+ def spec[C: Callable](
36
+ *, delegate: bool = True, first_result: bool = False
37
+ ) -> Callable[[C], C]: ...
38
+ def spec(
39
+ func: Callable | None = None,
40
+ /,
41
+ *,
42
+ delegate: bool = True,
43
+ first_result: bool = False,
44
+ ) -> Any:
45
+ if func is None:
46
+ return functools.partial(spec, delegate=delegate, first_result=first_result)
47
+
48
+ info = SpecInfo(delegate=delegate, first_result=first_result)
49
+
50
+ @grapes.decorator(attrs={"_self_spec": info})
51
+ def wrapper(
52
+ wrapped: Callable, instance: Plugin, args: tuple, kwargs: dict[str, Any]
53
+ ) -> Any:
54
+ if info.delegate:
55
+ return instance.delegate(
56
+ wrapped.__name__, args, kwargs, first_result=info.first_result
57
+ )
58
+ return wrapped(*args, **kwargs)
59
+
60
+ return wrapper(func)
61
+
62
+
63
+ def collect_specs(cls: type[Plugin] | Plugin) -> dict[str, SpecInfo]:
64
+ if isinstance(cls, type):
65
+ cls = type(cls)
66
+ return {
67
+ name: method._self_spec # noqa: SLF001
68
+ for name, method in inspect.getmembers(
69
+ cls, lambda m: getattr(m, "_self_spec", None) is not None
70
+ )
71
+ }
@@ -0,0 +1,2 @@
1
+ type MethodName = str
2
+ type PluginId = str
@@ -3,7 +3,7 @@ import subprocess as sp
3
3
  import git
4
4
 
5
5
  from liblaf import grapes
6
- from liblaf.cherries import pathutils as _path
6
+ from liblaf.cherries import paths
7
7
 
8
8
 
9
9
  def git_auto_commit(
@@ -40,10 +40,10 @@ def git_commit_url(sha: str | None = None) -> str:
40
40
 
41
41
  def git_info() -> grapes.git.GitInfo:
42
42
  info: grapes.git.GitInfo = grapes.git.info(
43
- _path.entrypoint(absolute=True), search_parent_directories=True
43
+ paths.entrypoint(absolute=True), search_parent_directories=True
44
44
  )
45
45
  return info
46
46
 
47
47
 
48
48
  def _repo() -> git.Repo:
49
- return git.Repo(_path.entrypoint(absolute=True), search_parent_directories=True)
49
+ return git.Repo(paths.entrypoint(absolute=True), search_parent_directories=True)
@@ -3,7 +3,7 @@ from pathlib import Path
3
3
  import git.exc
4
4
 
5
5
  from liblaf import grapes
6
- from liblaf.cherries import pathutils as _path
6
+ from liblaf.cherries import paths
7
7
 
8
8
  from ._git import git_info
9
9
 
@@ -18,8 +18,8 @@ def project_name() -> str:
18
18
 
19
19
 
20
20
  def exp_name() -> str:
21
- exp_dir: Path = _path.entrypoint(absolute=False)
22
- exp_name: str = _path.as_posix(exp_dir)
21
+ exp_dir: Path = paths.entrypoint(absolute=False)
22
+ exp_name: str = paths.as_posix(exp_dir)
23
23
  exp_name = exp_name.removeprefix("exp")
24
24
  exp_name = exp_name.removeprefix("/")
25
25
  return exp_name
@@ -1,4 +1,5 @@
1
1
  import sys
2
+ from collections.abc import Container
2
3
  from pathlib import Path
3
4
 
4
5
  import git
@@ -27,7 +28,7 @@ def git_root_safe() -> Path:
27
28
  try:
28
29
  return git_root()
29
30
  except git.exc.InvalidGitRepositoryError:
30
- logger.warning("Not in a git repository, using current directory")
31
+ logger.warning("Not in a git repository, using current directory", once=True)
31
32
  return _entrypoint_absolute().parent
32
33
 
33
34
 
@@ -50,15 +51,16 @@ def _entrypoint_relative() -> Path:
50
51
  return path.relative_to(git_root_safe())
51
52
 
52
53
 
54
+ EXP_DIR_NAMES: Container[str] = {"exp", "experiment", "experiments", "exps", "src"}
55
+
56
+
53
57
  @utils.cache
54
58
  def _exp_dir_absolute() -> Path:
55
59
  entrypoint: Path = _entrypoint_absolute()
56
- for path in entrypoint.parents:
57
- if (path / "exp.cherries.toml").is_file():
58
- return path
59
- if (path / "src").is_dir():
60
- return path
61
- return git_root_safe()
60
+ parent: Path = entrypoint.parent
61
+ if parent.name in EXP_DIR_NAMES:
62
+ return parent.parent
63
+ return parent
62
64
 
63
65
 
64
66
  @utils.cache
@@ -0,0 +1,3 @@
1
+ from .logging import Logging
2
+
3
+ __all__ = ["Logging"]
@@ -0,0 +1,25 @@
1
+ from typing import override
2
+
3
+ from liblaf import grapes
4
+ from liblaf.cherries import core
5
+
6
+
7
+ class Logging(core.Run):
8
+ @override
9
+ @core.impl
10
+ def start(self, *args, **kwargs) -> None:
11
+ profile = grapes.logging.profiles.ProfileCherries(
12
+ handlers=[
13
+ grapes.logging.rich_handler(),
14
+ grapes.logging.file_handler(sink=self.plugin_root.exp_dir / "run.log"),
15
+ ]
16
+ )
17
+ grapes.logging.init(profile=profile)
18
+
19
+ @override
20
+ @core.impl
21
+ def end(self, *args, **kwargs) -> None:
22
+ if (self.plugin_root.exp_dir / "run.log").exists():
23
+ self.plugin_root.log_asset(self.plugin_root.exp_dir / "run.log")
24
+ if (self.plugin_root.exp_dir / "run.log.jsonl").exists():
25
+ self.plugin_root.log_asset(self.plugin_root.exp_dir / "run.log.jsonl")
@@ -0,0 +1,13 @@
1
+ from ._abc import Profile
2
+ from ._default import ProfileDefault
3
+ from ._factory import ProfileLike, ProfileName, factory
4
+ from ._playground import ProfilePlayground
5
+
6
+ __all__ = [
7
+ "Profile",
8
+ "ProfileDefault",
9
+ "ProfileLike",
10
+ "ProfileName",
11
+ "ProfilePlayground",
12
+ "factory",
13
+ ]
@@ -0,0 +1,10 @@
1
+ import abc
2
+
3
+ import autoregistry
4
+
5
+ from liblaf.cherries import core
6
+
7
+
8
+ class Profile(abc.ABC, autoregistry.Registry, prefix="Profile"):
9
+ @abc.abstractmethod
10
+ def init(self) -> core.Run: ...
@@ -0,0 +1,12 @@
1
+ from typing import override
2
+
3
+ from liblaf.cherries import core
4
+
5
+ from ._playground import ProfilePlayground
6
+
7
+
8
+ class ProfileDefault(ProfilePlayground):
9
+ @override # impl Profile
10
+ def init(self) -> core.Run:
11
+ run: core.Run = super().init()
12
+ return run
@@ -0,0 +1,21 @@
1
+ from typing import Literal
2
+
3
+ from ._abc import Profile
4
+
5
+ # ensure profiles are registered
6
+ from ._default import ProfileDefault
7
+ from ._playground import ProfilePlayground # noqa: F401
8
+
9
+ # for code-completion
10
+ type ProfileName = Literal["default", "playground"] | str # noqa: PYI051
11
+ type ProfileLike = ProfileName | Profile | type[Profile]
12
+
13
+
14
+ def factory(profile: ProfileLike | None = None) -> Profile:
15
+ if profile is None:
16
+ return ProfileDefault()
17
+ if isinstance(profile, str):
18
+ return Profile[profile]()
19
+ if isinstance(profile, Profile):
20
+ return profile
21
+ return profile()