expr_tracker 0.1.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.
- expr_tracker-0.1.0/.gitignore +12 -0
- expr_tracker-0.1.0/PKG-INFO +10 -0
- expr_tracker-0.1.0/README.md +0 -0
- expr_tracker-0.1.0/pyproject.toml +22 -0
- expr_tracker-0.1.0/src/expr_tracker/__init__.py +9 -0
- expr_tracker-0.1.0/src/expr_tracker/alert.py +55 -0
- expr_tracker-0.1.0/src/expr_tracker/jsonl.py +64 -0
- expr_tracker-0.1.0/src/expr_tracker/tracker.py +155 -0
- expr_tracker-0.1.0/tests/test_tracker.py +14 -0
- expr_tracker-0.1.0/uv.lock +1426 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: expr_tracker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: HSPK <whxway@whu.edu.cn>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: jsonlines>=4.0.0
|
|
8
|
+
Requires-Dist: loguru>=0.7.3
|
|
9
|
+
Requires-Dist: slark>=0.1.27
|
|
10
|
+
Requires-Dist: wandb>=0.21.0
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "expr_tracker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "HSPK", email = "whxway@whu.edu.cn" }]
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"jsonlines>=4.0.0",
|
|
10
|
+
"loguru>=0.7.3",
|
|
11
|
+
"slark>=0.1.27",
|
|
12
|
+
"wandb>=0.21.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"ipykernel>=6.30.1",
|
|
22
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LarkBackend:
|
|
5
|
+
def __init__(self, **kwargs):
|
|
6
|
+
from slark import Lark
|
|
7
|
+
|
|
8
|
+
self.lark = Lark(**kwargs)
|
|
9
|
+
|
|
10
|
+
def publish_alert(
|
|
11
|
+
self,
|
|
12
|
+
title: str,
|
|
13
|
+
text: str,
|
|
14
|
+
subtitle: str | None = None,
|
|
15
|
+
level: Literal["info", "warning", "error"] = "info",
|
|
16
|
+
traceback: str | None = None,
|
|
17
|
+
):
|
|
18
|
+
if level == "info" or level == "warning":
|
|
19
|
+
self.lark.webhook.post_success_card(
|
|
20
|
+
msg=text, title=title, subtitle=subtitle
|
|
21
|
+
)
|
|
22
|
+
elif level == "error":
|
|
23
|
+
self.lark.webhook.post_error_card(
|
|
24
|
+
msg=text, traceback=traceback or "", title=title, subtitle=subtitle
|
|
25
|
+
)
|
|
26
|
+
else:
|
|
27
|
+
raise ValueError(
|
|
28
|
+
f"Unsupported level: {level}. Supported levels are 'info', 'warning', and 'error'."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def alert(
|
|
33
|
+
title: str,
|
|
34
|
+
text: str,
|
|
35
|
+
subtitle: str | None = None,
|
|
36
|
+
traceback: str | None = None,
|
|
37
|
+
level: Literal["info", "warning", "error"] = "info",
|
|
38
|
+
backends: list[Literal["lark", "slack", "email"]] = ["lark"],
|
|
39
|
+
):
|
|
40
|
+
"""
|
|
41
|
+
Send an alert message to specified backends.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
title (str): The title of the alert.
|
|
45
|
+
text (str): The main content of the alert.
|
|
46
|
+
subtitle (str | None): Optional subtitle for the alert.
|
|
47
|
+
level (Literal["info", "warning", "error"]): The severity level of the alert.
|
|
48
|
+
backends (list[Literal["lark", "slack", "email"]]): List of backends to send the alert to.
|
|
49
|
+
"""
|
|
50
|
+
for backend in backends:
|
|
51
|
+
if backend == "lark":
|
|
52
|
+
lark_backend = LarkBackend()
|
|
53
|
+
lark_backend.publish_alert(title, text, subtitle, level, traceback)
|
|
54
|
+
else:
|
|
55
|
+
raise NotImplementedError(f"Backend '{backend}' is not implemented.")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from loguru import logger
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def custom_json_encoder(obj):
|
|
6
|
+
if isinstance(obj, Path):
|
|
7
|
+
return str(obj)
|
|
8
|
+
raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JsonlTracker:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def init(
|
|
16
|
+
self,
|
|
17
|
+
project: str,
|
|
18
|
+
name: str | None = None,
|
|
19
|
+
config: dict | None = None,
|
|
20
|
+
dir: str | None = None,
|
|
21
|
+
print_to_screen: bool = False,
|
|
22
|
+
print_handle=print,
|
|
23
|
+
**kwargs,
|
|
24
|
+
):
|
|
25
|
+
self.project = project
|
|
26
|
+
self.name = name
|
|
27
|
+
if dir is None:
|
|
28
|
+
dir = "./tracker/jsonl"
|
|
29
|
+
self.log_dir = Path(dir) / self.project / self.name
|
|
30
|
+
self.config_fp = self.log_dir / "config.json"
|
|
31
|
+
self.log_fp = self.log_dir / "metrics.jsonl"
|
|
32
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
if self.config_fp.exists():
|
|
34
|
+
logger.warning(
|
|
35
|
+
f"Config file {self.config_fp} already exists. It will be overwritten."
|
|
36
|
+
)
|
|
37
|
+
if config is not None:
|
|
38
|
+
with open(self.config_fp, "w") as f:
|
|
39
|
+
import json
|
|
40
|
+
|
|
41
|
+
json.dump(config, f, indent=4, default=custom_json_encoder)
|
|
42
|
+
self.print_to_screen = print_to_screen
|
|
43
|
+
self.print_handle = print_handle
|
|
44
|
+
self.current_step = (
|
|
45
|
+
0 if not self.log_fp.exists() else len(self.log_fp.read_text().splitlines())
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def log(self, metrics: dict, step: int | None = None):
|
|
49
|
+
import jsonlines
|
|
50
|
+
|
|
51
|
+
if not self.log_fp.parent.exists():
|
|
52
|
+
self.log_fp.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
if step is not None:
|
|
55
|
+
self.current_step = step
|
|
56
|
+
metrics = {"_step": self.current_step, **metrics}
|
|
57
|
+
with jsonlines.open(self.log_fp, mode="a") as writer:
|
|
58
|
+
writer.write(metrics)
|
|
59
|
+
self.current_step += 1
|
|
60
|
+
if self.print_to_screen:
|
|
61
|
+
self.print_handle(f"{metrics}")
|
|
62
|
+
|
|
63
|
+
def finish(self):
|
|
64
|
+
pass
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from contextvars import ContextVar
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
_tracker: ContextVar["Tracker | None"] = ContextVar("tracker", default=None)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_backend(backend: str | object):
|
|
9
|
+
if isinstance(backend, str):
|
|
10
|
+
backend = backend.lower()
|
|
11
|
+
if backend == "wandb":
|
|
12
|
+
try:
|
|
13
|
+
import wandb
|
|
14
|
+
|
|
15
|
+
return wandb
|
|
16
|
+
except ImportError:
|
|
17
|
+
raise ImportError("WandB backend is not installed.")
|
|
18
|
+
elif backend == "jsonl":
|
|
19
|
+
from .jsonl import JsonlTracker
|
|
20
|
+
|
|
21
|
+
return JsonlTracker()
|
|
22
|
+
elif backend == "trackio":
|
|
23
|
+
try:
|
|
24
|
+
import trackio
|
|
25
|
+
|
|
26
|
+
return trackio
|
|
27
|
+
except ImportError:
|
|
28
|
+
raise ImportError("TrackIO backend is not installed.")
|
|
29
|
+
else:
|
|
30
|
+
return backend
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Tracker:
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
project: str,
|
|
37
|
+
name: str | None = None,
|
|
38
|
+
entity: str | None = None,
|
|
39
|
+
dir: str | None = None,
|
|
40
|
+
notes: str | None = None,
|
|
41
|
+
tags: list[str] | None = None,
|
|
42
|
+
resume: bool | Literal["allow", "never", "must", "auto"] | None = "allow",
|
|
43
|
+
config: dict | None = None,
|
|
44
|
+
backends: list[Literal["wandb", "jsonl", "trackio"]] = [
|
|
45
|
+
"wandb",
|
|
46
|
+
"trackio",
|
|
47
|
+
"jsonl",
|
|
48
|
+
],
|
|
49
|
+
backend_kwargs: dict[str, dict] | None = None,
|
|
50
|
+
**kwargs,
|
|
51
|
+
):
|
|
52
|
+
if kwargs:
|
|
53
|
+
logger.info(
|
|
54
|
+
f"Unrecognized keyword arguments passed to Tracker: {kwargs}. "
|
|
55
|
+
"These will be ignored."
|
|
56
|
+
)
|
|
57
|
+
self.backend = {b: get_backend(b) for b in backends}
|
|
58
|
+
if config is None:
|
|
59
|
+
config = {}
|
|
60
|
+
if backend_kwargs is None:
|
|
61
|
+
backend_kwargs = {}
|
|
62
|
+
for b, t in self.backend.items():
|
|
63
|
+
if b == "trackio":
|
|
64
|
+
t.init(
|
|
65
|
+
project=project,
|
|
66
|
+
name=name,
|
|
67
|
+
config=config.update(
|
|
68
|
+
{
|
|
69
|
+
"trackio.notes": notes,
|
|
70
|
+
"trackio.tags": tags,
|
|
71
|
+
"trackio.entity": entity,
|
|
72
|
+
"trackio.resume": resume,
|
|
73
|
+
}
|
|
74
|
+
),
|
|
75
|
+
**backend_kwargs.get(b, {}),
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
t.init(
|
|
79
|
+
project=project,
|
|
80
|
+
name=name,
|
|
81
|
+
entity=entity,
|
|
82
|
+
dir=dir,
|
|
83
|
+
notes=notes,
|
|
84
|
+
tags=tags,
|
|
85
|
+
resume=resume,
|
|
86
|
+
id=name,
|
|
87
|
+
config=config,
|
|
88
|
+
**backend_kwargs.get(b, {}),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def init(
|
|
93
|
+
project: str,
|
|
94
|
+
name: str | None = None,
|
|
95
|
+
entity: str | None = None,
|
|
96
|
+
dir: str | None = None,
|
|
97
|
+
notes: str | None = None,
|
|
98
|
+
tags: list[str] | None = None,
|
|
99
|
+
resume: bool | Literal["allow", "never", "must", "auto"] | None = "allow",
|
|
100
|
+
config: dict | None = None,
|
|
101
|
+
backends: list[Literal["wandb", "jsonl", "trackio"]] = ["wandb", "jsonl"],
|
|
102
|
+
backend_kwargs: dict[str, dict] | None = None,
|
|
103
|
+
**kwargs,
|
|
104
|
+
) -> Tracker:
|
|
105
|
+
"""
|
|
106
|
+
Initialize the tracker with the given parameters.
|
|
107
|
+
"""
|
|
108
|
+
if _tracker.get() is not None:
|
|
109
|
+
raise RuntimeError("Tracker is already initialized. Call finish() first.")
|
|
110
|
+
|
|
111
|
+
tracker = Tracker(
|
|
112
|
+
project=project,
|
|
113
|
+
name=name,
|
|
114
|
+
entity=entity,
|
|
115
|
+
dir=dir,
|
|
116
|
+
notes=notes,
|
|
117
|
+
tags=tags,
|
|
118
|
+
resume=resume,
|
|
119
|
+
config=config,
|
|
120
|
+
backends=backends,
|
|
121
|
+
backend_kwargs=backend_kwargs,
|
|
122
|
+
**kwargs,
|
|
123
|
+
)
|
|
124
|
+
_tracker.set(tracker)
|
|
125
|
+
return tracker
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def log(metrics: dict, step: int | None = None):
|
|
129
|
+
tracker = _tracker.get()
|
|
130
|
+
if tracker is None:
|
|
131
|
+
raise RuntimeError("Tracker is not initialized. Call init() first.")
|
|
132
|
+
for b in tracker.backend.values():
|
|
133
|
+
b.log(metrics, step=step)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def finish():
|
|
137
|
+
tracker = _tracker.get()
|
|
138
|
+
if tracker is None:
|
|
139
|
+
raise RuntimeError("Tracker is not initialized. Call init() first.")
|
|
140
|
+
for b in tracker.backend.values():
|
|
141
|
+
b.finish()
|
|
142
|
+
_tracker.set(None)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def info():
|
|
146
|
+
tracker = _tracker.get()
|
|
147
|
+
if tracker is None:
|
|
148
|
+
raise RuntimeError("Tracker is not initialized. Call init() first.")
|
|
149
|
+
info = {}
|
|
150
|
+
for b, t in tracker.backend.items():
|
|
151
|
+
if b == "wandb":
|
|
152
|
+
info["wandb"] = {"url": t.run.url}
|
|
153
|
+
if b == "jsonl":
|
|
154
|
+
info["jsonl"] = {"log_dir": t.log_dir.as_posix()}
|
|
155
|
+
return info
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
def test_expr_tracker():
|
|
2
|
+
import expr_tracker as et
|
|
3
|
+
|
|
4
|
+
et.init(
|
|
5
|
+
project="test_project",
|
|
6
|
+
name="test_run",
|
|
7
|
+
dir="./tests/logs",
|
|
8
|
+
config={"param": 42},
|
|
9
|
+
print_to_screen=True,
|
|
10
|
+
backends=["jsonl", "wandb"],
|
|
11
|
+
)
|
|
12
|
+
for i in range(5):
|
|
13
|
+
et.log({"metric": i * 10}, step=i)
|
|
14
|
+
et.finish()
|