robo-goggles 0.1.0__py3-none-any.whl
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.
- goggles/__init__.py +786 -0
- goggles/_core/integrations/__init__.py +26 -0
- goggles/_core/integrations/console.py +111 -0
- goggles/_core/integrations/storage.py +382 -0
- goggles/_core/integrations/wandb.py +253 -0
- goggles/_core/logger.py +602 -0
- goggles/_core/routing.py +127 -0
- goggles/config.py +68 -0
- goggles/decorators.py +81 -0
- goggles/history/__init__.py +39 -0
- goggles/history/buffer.py +185 -0
- goggles/history/spec.py +143 -0
- goggles/history/types.py +9 -0
- goggles/history/utils.py +191 -0
- goggles/media.py +284 -0
- goggles/shutdown.py +70 -0
- goggles/types.py +79 -0
- robo_goggles-0.1.0.dist-info/METADATA +600 -0
- robo_goggles-0.1.0.dist-info/RECORD +22 -0
- robo_goggles-0.1.0.dist-info/WHEEL +5 -0
- robo_goggles-0.1.0.dist-info/licenses/LICENSE +21 -0
- robo_goggles-0.1.0.dist-info/top_level.txt +1 -0
goggles/__init__.py
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
"""Goggles: Structured logging and experiment tracking.
|
|
2
|
+
|
|
3
|
+
This package provides a stable public API for logging experiments, metrics,
|
|
4
|
+
and media in a consistent and composable way.
|
|
5
|
+
|
|
6
|
+
>>> import goggles as gg
|
|
7
|
+
>>>
|
|
8
|
+
>>> with gg.run("experiment_42"):
|
|
9
|
+
>>> logger = gg.get_logger("train", seed=0)
|
|
10
|
+
>>> logger.info("Training started.")
|
|
11
|
+
>>> logger.scalar("train/loss", 0.123, step=0)
|
|
12
|
+
|
|
13
|
+
See Also:
|
|
14
|
+
- README.md for detailed usage examples.
|
|
15
|
+
- API docs for full reference of public interfaces.
|
|
16
|
+
- Internal implementations live under `goggles/_core/`
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import portal
|
|
23
|
+
from collections import defaultdict
|
|
24
|
+
from typing import (
|
|
25
|
+
Any,
|
|
26
|
+
Callable,
|
|
27
|
+
Callable,
|
|
28
|
+
ClassVar,
|
|
29
|
+
FrozenSet,
|
|
30
|
+
List,
|
|
31
|
+
Literal,
|
|
32
|
+
Optional,
|
|
33
|
+
Protocol,
|
|
34
|
+
Dict,
|
|
35
|
+
Set,
|
|
36
|
+
overload,
|
|
37
|
+
runtime_checkable,
|
|
38
|
+
)
|
|
39
|
+
from typing_extensions import Self
|
|
40
|
+
import logging
|
|
41
|
+
import os
|
|
42
|
+
|
|
43
|
+
from .types import Kind, Event, VectorField, Video, Image, Vector, Metrics
|
|
44
|
+
from ._core.integrations import *
|
|
45
|
+
from .decorators import timeit, trace_on_error
|
|
46
|
+
from .shutdown import GracefulShutdown
|
|
47
|
+
from .config import load_configuration, save_configuration
|
|
48
|
+
|
|
49
|
+
# Goggles port for bus communication
|
|
50
|
+
GOGGLES_PORT = os.getenv("GOGGLES_PORT", "2304")
|
|
51
|
+
|
|
52
|
+
# Handler registry for custom handlers
|
|
53
|
+
_HANDLER_REGISTRY: Dict[str, type] = {}
|
|
54
|
+
GOGGLES_HOST = os.getenv("GOGGLES_HOST", "localhost")
|
|
55
|
+
GOGGLES_ASYNC = os.getenv("GOGGLES_ASYNC", "0").lower() in ("1", "true", "yes")
|
|
56
|
+
|
|
57
|
+
# Cache the implementation after first use to avoid repeated imports
|
|
58
|
+
__impl_get_bus: Optional[Callable[[], EventBus]] = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _make_text_logger(
|
|
62
|
+
name: Optional[str],
|
|
63
|
+
scope: str,
|
|
64
|
+
to_bind: dict[str, Any],
|
|
65
|
+
) -> TextLogger:
|
|
66
|
+
from ._core.logger import CoreTextLogger
|
|
67
|
+
|
|
68
|
+
return CoreTextLogger(name=name, scope=scope, to_bind=to_bind)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _make_goggles_logger(
|
|
72
|
+
name: Optional[str],
|
|
73
|
+
scope: str,
|
|
74
|
+
to_bind: dict[str, Any],
|
|
75
|
+
) -> GogglesLogger:
|
|
76
|
+
from ._core.logger import CoreGogglesLogger
|
|
77
|
+
|
|
78
|
+
return CoreGogglesLogger(name=name, scope=scope, to_bind=to_bind)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Public API
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@overload
|
|
87
|
+
def get_logger(
|
|
88
|
+
name: Optional[str] = None,
|
|
89
|
+
/,
|
|
90
|
+
*,
|
|
91
|
+
scope: str = "global",
|
|
92
|
+
**to_bind: Any,
|
|
93
|
+
) -> TextLogger: ...
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@overload
|
|
97
|
+
def get_logger(
|
|
98
|
+
name: Optional[str] = None,
|
|
99
|
+
/,
|
|
100
|
+
*,
|
|
101
|
+
with_metrics: Literal[True],
|
|
102
|
+
scope: str = "global",
|
|
103
|
+
**to_bind: Any,
|
|
104
|
+
) -> GogglesLogger: ...
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@overload
|
|
108
|
+
def get_logger(name: Optional[str] = None, /, **to_bind: Any) -> TextLogger: ...
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_logger(
|
|
112
|
+
name: Optional[str] = None,
|
|
113
|
+
/,
|
|
114
|
+
*,
|
|
115
|
+
with_metrics: bool = False,
|
|
116
|
+
scope: str = "global",
|
|
117
|
+
**to_bind: Any,
|
|
118
|
+
) -> TextLogger | GogglesLogger:
|
|
119
|
+
"""Return a structured logger (text-only by default, metrics-enabled on opt-in).
|
|
120
|
+
|
|
121
|
+
This is the primary entry point for obtaining Goggles' structured loggers.
|
|
122
|
+
Depending on the active run and configuration, the returned adapter will
|
|
123
|
+
inject structured context (e.g., `RunContext` info) and persistent fields
|
|
124
|
+
into each emitted log record.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
name (Optional[str]): Logger name. If None, the root logger is used.
|
|
128
|
+
with_metrics (bool): If True, return a logger exposing `.metrics`.
|
|
129
|
+
scope (str): The logging scope, e.g., "global" or "run".
|
|
130
|
+
**to_bind (Any): Fields persisted and injected into every record.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Union[TextLogger, GogglesLogger]: A text-only `TextLogger` by default,
|
|
134
|
+
or a `GogglesLogger` when `with_metrics=True`.
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
>>> # Text-only
|
|
138
|
+
>>> log = get_logger("eval", dataset="CIFAR10")
|
|
139
|
+
>>> log.info("starting")
|
|
140
|
+
>>>
|
|
141
|
+
>>> # Explicit metrics surface
|
|
142
|
+
>>> tlog = get_logger("train", with_metrics=True, seed=0)
|
|
143
|
+
>>> tlog.scalar("loss", 0.42, step=1)
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
if with_metrics:
|
|
147
|
+
return _make_goggles_logger(name, scope, to_bind)
|
|
148
|
+
else:
|
|
149
|
+
return _make_text_logger(name, scope, to_bind)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@runtime_checkable
|
|
153
|
+
class TextLogger(Protocol):
|
|
154
|
+
"""Protocol for Goggles' structured logger adapters.
|
|
155
|
+
|
|
156
|
+
This protocol defines the expected interface for logger adapters returned
|
|
157
|
+
by `goggles.get_logger()`. It extends standard Python logging methods with
|
|
158
|
+
support for persistent bound fields.
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
>>> log = get_logger("goggles")
|
|
162
|
+
>>> log.info("Hello, Goggles!", user="alice")
|
|
163
|
+
>>> run_log = log.bind(run_id="exp42")
|
|
164
|
+
>>> run_log.debug("Debugging info", step=1)
|
|
165
|
+
... # Both log records include any persistent bound fields.
|
|
166
|
+
... # The second record also includes run_id="exp42".
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def bind(self, /, *, scope: str = "global", **fields: Any) -> Self:
|
|
171
|
+
"""Return a new adapter with `fields` merged into persistent state.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
scope (str): The binding scope, e.g., "global" or "run".
|
|
175
|
+
**fields (Any): Key-value pairs to bind persistently.
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Self: A new `TextLogger` instance
|
|
180
|
+
with updated bound fields and scope.
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
...
|
|
184
|
+
|
|
185
|
+
def log(
|
|
186
|
+
self,
|
|
187
|
+
severity: int,
|
|
188
|
+
msg: str,
|
|
189
|
+
/,
|
|
190
|
+
*,
|
|
191
|
+
step: Optional[int] = None,
|
|
192
|
+
time: Optional[float] = None,
|
|
193
|
+
**extra: Any,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Log message at the given severity with optional structured extras.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
severity (int): Numeric log level (e.g., logging.INFO).
|
|
199
|
+
msg (str): The log message.
|
|
200
|
+
step (Optional[int]): The step number.
|
|
201
|
+
time (Optional[float]): The timestamp.
|
|
202
|
+
**extra (Any):
|
|
203
|
+
Additional structured key-value pairs for this record.
|
|
204
|
+
|
|
205
|
+
"""
|
|
206
|
+
if severity >= logging.CRITICAL:
|
|
207
|
+
self.critical(msg, step=step, time=time, **extra)
|
|
208
|
+
elif severity >= logging.ERROR:
|
|
209
|
+
self.error(msg, step=step, time=time, **extra)
|
|
210
|
+
elif severity >= logging.WARNING:
|
|
211
|
+
self.warning(msg, step=step, time=time, **extra)
|
|
212
|
+
elif severity >= logging.INFO:
|
|
213
|
+
self.info(msg, step=step, time=time, **extra)
|
|
214
|
+
elif severity >= logging.DEBUG:
|
|
215
|
+
self.debug(msg, step=step, time=time, **extra)
|
|
216
|
+
else:
|
|
217
|
+
# Below DEBUG level; no-op by default.
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
def debug(
|
|
221
|
+
self,
|
|
222
|
+
msg: str,
|
|
223
|
+
/,
|
|
224
|
+
*,
|
|
225
|
+
step: Optional[int] = None,
|
|
226
|
+
time: Optional[float] = None,
|
|
227
|
+
**extra: Any,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Log a DEBUG message with optional structured extras.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
msg (str): The log message.
|
|
233
|
+
step (Optional[int]): The step number.
|
|
234
|
+
time (Optional[float]): The timestamp.
|
|
235
|
+
**extra (Any):
|
|
236
|
+
Additional structured key-value pairs for this record.
|
|
237
|
+
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
def info(
|
|
241
|
+
self,
|
|
242
|
+
msg: str,
|
|
243
|
+
/,
|
|
244
|
+
*,
|
|
245
|
+
step: Optional[int] = None,
|
|
246
|
+
time: Optional[float] = None,
|
|
247
|
+
**extra: Any,
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Log an INFO message with optional structured extras.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
msg (str): The log message.
|
|
253
|
+
step (Optional[int]): The step number.
|
|
254
|
+
time (Optional[float]): The timestamp.
|
|
255
|
+
**extra (Any):
|
|
256
|
+
Additional structured key-value pairs for this record.
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
def warning(
|
|
261
|
+
self,
|
|
262
|
+
msg: str,
|
|
263
|
+
/,
|
|
264
|
+
*,
|
|
265
|
+
step: Optional[int] = None,
|
|
266
|
+
time: Optional[float] = None,
|
|
267
|
+
**extra: Any,
|
|
268
|
+
) -> None:
|
|
269
|
+
"""Log a WARNING message with optional structured extras.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
msg (str): The log message.
|
|
273
|
+
step (Optional[int]): The step number.
|
|
274
|
+
time (Optional[float]): The timestamp.
|
|
275
|
+
**extra (Any):
|
|
276
|
+
Additional structured key-value pairs for this record.
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
def error(
|
|
281
|
+
self,
|
|
282
|
+
msg: str,
|
|
283
|
+
/,
|
|
284
|
+
*,
|
|
285
|
+
step: Optional[int] = None,
|
|
286
|
+
time: Optional[float] = None,
|
|
287
|
+
**extra: Any,
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Log an ERROR message with optional structured extras.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
msg (str): The log message.
|
|
293
|
+
step (Optional[int]): The step number.
|
|
294
|
+
time (Optional[float]): The timestamp.
|
|
295
|
+
**extra (Any):
|
|
296
|
+
Additional structured key-value pairs for this record.
|
|
297
|
+
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def critical(
|
|
301
|
+
self,
|
|
302
|
+
msg: str,
|
|
303
|
+
/,
|
|
304
|
+
*,
|
|
305
|
+
step: Optional[int] = None,
|
|
306
|
+
time: Optional[float] = None,
|
|
307
|
+
**extra: Any,
|
|
308
|
+
) -> None:
|
|
309
|
+
"""Log a CRITICAL message with current exception info attached.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
msg (str): The log message.
|
|
313
|
+
step (Optional[int]): The step number.
|
|
314
|
+
time (Optional[float]): The timestamp.
|
|
315
|
+
**extra (Any):
|
|
316
|
+
Additional structured key-value pairs for this record.
|
|
317
|
+
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@runtime_checkable
|
|
322
|
+
class DataLogger(Protocol):
|
|
323
|
+
"""Protocol for logging metrics, media, artifacts, and analytics data."""
|
|
324
|
+
|
|
325
|
+
def push(
|
|
326
|
+
self,
|
|
327
|
+
metrics: Metrics,
|
|
328
|
+
*,
|
|
329
|
+
step: Optional[int] = None,
|
|
330
|
+
time: Optional[float] = None,
|
|
331
|
+
**extra: Dict[str, Any],
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Emit a batch of scalar metrics.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
metrics (Metrics): (Name,value) pairs.
|
|
337
|
+
step (Optional[int]): Optional global step index.
|
|
338
|
+
time (Optional[float]): Optional global timestamp.
|
|
339
|
+
**extra (Dict[str, Any]):
|
|
340
|
+
Additional routing metadata (e.g., split="train").
|
|
341
|
+
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
def scalar(
|
|
345
|
+
self,
|
|
346
|
+
name: str,
|
|
347
|
+
value: float | int,
|
|
348
|
+
*,
|
|
349
|
+
step: Optional[int] = None,
|
|
350
|
+
time: Optional[float] = None,
|
|
351
|
+
**extra: Dict[str, Any],
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Emit a single scalar metric.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
name (str): Metric name.
|
|
357
|
+
value (float|int): Metric value.
|
|
358
|
+
step (Optional[int]): Optional global step index.
|
|
359
|
+
time (Optional[float]): Optional global timestamp.
|
|
360
|
+
**extra (Dict[str, Any]):
|
|
361
|
+
Additional routing metadata (e.g., split="train").
|
|
362
|
+
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
def image(
|
|
366
|
+
self,
|
|
367
|
+
image: Image,
|
|
368
|
+
*,
|
|
369
|
+
name: Optional[str] = None,
|
|
370
|
+
format: str = "png",
|
|
371
|
+
step: Optional[int] = None,
|
|
372
|
+
time: Optional[float] = None,
|
|
373
|
+
**extra: Dict[str, Any],
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Emit an image artifact (encoded bytes).
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
name (str): Artifact name.
|
|
379
|
+
image (Image): Image.
|
|
380
|
+
format (str): Image format, e.g., "png", "jpeg".
|
|
381
|
+
step (Optional[int]): Optional global step index.
|
|
382
|
+
time (Optional[float]): Optional global timestamp.
|
|
383
|
+
**extra: Dict[str, Any]: Additional routing metadata.
|
|
384
|
+
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
def video(
|
|
388
|
+
self,
|
|
389
|
+
video: Video,
|
|
390
|
+
*,
|
|
391
|
+
name: Optional[str] = None,
|
|
392
|
+
fps: int = 30,
|
|
393
|
+
format: str = "gif",
|
|
394
|
+
step: Optional[int] = None,
|
|
395
|
+
time: Optional[float] = None,
|
|
396
|
+
**extra: Dict[str, Any],
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Emit a video artifact (encoded bytes).
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
video (Video): Video.
|
|
402
|
+
name (Optional[str]): Artifact name.
|
|
403
|
+
fps (int): Frames per second.
|
|
404
|
+
format (str): Video format, e.g., "gif", "mp4".
|
|
405
|
+
step (Optional[int]): Optional global step index.
|
|
406
|
+
time (Optional[float]): Optional global timestamp.
|
|
407
|
+
**extra (Dict[str, Any]): Additional routing metadata.
|
|
408
|
+
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
def artifact(
|
|
412
|
+
self,
|
|
413
|
+
data: Any,
|
|
414
|
+
*,
|
|
415
|
+
name: Optional[str] = None,
|
|
416
|
+
format: str = "bin",
|
|
417
|
+
step: Optional[int] = None,
|
|
418
|
+
time: Optional[float] = None,
|
|
419
|
+
**extra: Dict[str, Any],
|
|
420
|
+
) -> None:
|
|
421
|
+
"""Emit a generic artifact (encoded bytes).
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
data (bytes): Artifact data.
|
|
425
|
+
name (Optional[str]): Artifact name.
|
|
426
|
+
format (str): Artifact format, e.g., "txt", "bin".
|
|
427
|
+
step (Optional[int]): Optional global step index.
|
|
428
|
+
time (Optional[float]): Optional global timestamp.
|
|
429
|
+
**extra (Dict[str, Any]): Additional routing metadata.
|
|
430
|
+
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
def vector_field(
|
|
434
|
+
self,
|
|
435
|
+
vector_field: VectorField,
|
|
436
|
+
*,
|
|
437
|
+
name: Optional[str] = None,
|
|
438
|
+
step: Optional[int] = None,
|
|
439
|
+
time: Optional[float] = None,
|
|
440
|
+
**extra: Dict[str, Any],
|
|
441
|
+
) -> None:
|
|
442
|
+
"""Emit a vector field artifact.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
vector_field (VectorField): Vector field data.
|
|
446
|
+
name (Optional[str]): Artifact name.
|
|
447
|
+
step (Optional[int]): Optional global step index.
|
|
448
|
+
time (Optional[float]): Optional global timestamp.
|
|
449
|
+
**extra (Dict[str, Any]): Additional routing metadata.
|
|
450
|
+
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
def histogram(
|
|
454
|
+
self,
|
|
455
|
+
histogram: Vector,
|
|
456
|
+
*,
|
|
457
|
+
name: Optional[str] = None,
|
|
458
|
+
step: Optional[int] = None,
|
|
459
|
+
time: Optional[float] = None,
|
|
460
|
+
**extra: Dict[str, Any],
|
|
461
|
+
) -> None:
|
|
462
|
+
"""Emit a histogram artifact.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
histogram (Vector): Histogram data.
|
|
466
|
+
name (Optional[str]): Artifact name.
|
|
467
|
+
step (Optional[int]): Optional global step index.
|
|
468
|
+
time (Optional[float]): Optional global timestamp.
|
|
469
|
+
**extra (Dict[str, Any]): Additional routing metadata.
|
|
470
|
+
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@runtime_checkable
|
|
475
|
+
class GogglesLogger(TextLogger, DataLogger, Protocol):
|
|
476
|
+
"""Protocol for Goggles loggers with metrics support.
|
|
477
|
+
|
|
478
|
+
Composite logger combining text logging with a metrics facet.
|
|
479
|
+
|
|
480
|
+
Examples:
|
|
481
|
+
>>> # Text-only
|
|
482
|
+
>>> log = get_logger("eval", dataset="CIFAR10")
|
|
483
|
+
>>> log.info("starting")
|
|
484
|
+
>>>
|
|
485
|
+
>>> # Explicit metrics surface
|
|
486
|
+
>>> tlog = get_logger("train", with_metrics=True, seed=0)
|
|
487
|
+
>>> tlog.scalar("loss", 0.42, step=1)
|
|
488
|
+
>>> tlog.info("Training step completed")
|
|
489
|
+
... # Both log records include any persistent bound fields.
|
|
490
|
+
... # The second record also includes run_id="exp42".
|
|
491
|
+
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@runtime_checkable
|
|
496
|
+
class Handler(Protocol):
|
|
497
|
+
"""Protocol for EventBus handlers.
|
|
498
|
+
|
|
499
|
+
Attributes:
|
|
500
|
+
name (str): Stable handler identifier for diagnostics.
|
|
501
|
+
capabilities (FrozenSet[Kind]):
|
|
502
|
+
Supported kinds, e.g. {'logs','metrics','artifacts', ...}.
|
|
503
|
+
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
name: str
|
|
507
|
+
capabilities: ClassVar[FrozenSet[Kind]]
|
|
508
|
+
|
|
509
|
+
def can_handle(self, kind: Kind) -> bool:
|
|
510
|
+
"""Return whether this handler can process events of the given kind.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
kind (Kind):
|
|
514
|
+
The kind of event ("log", "metric", "image", "artifact").
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
bool: True if the handler can process the event kind,
|
|
518
|
+
False otherwise.
|
|
519
|
+
|
|
520
|
+
"""
|
|
521
|
+
...
|
|
522
|
+
|
|
523
|
+
def handle(self, event: Event) -> None:
|
|
524
|
+
"""Handle an emitted event.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
event (Event): The event to handle.
|
|
528
|
+
|
|
529
|
+
"""
|
|
530
|
+
|
|
531
|
+
def open(self) -> None:
|
|
532
|
+
"""Initialize the handler (called when entering a scope)."""
|
|
533
|
+
|
|
534
|
+
def close(self) -> None:
|
|
535
|
+
"""Flush and release resources (called when leaving a scope).
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
run (Optional[RunContext]): The active run context if any.
|
|
539
|
+
|
|
540
|
+
"""
|
|
541
|
+
|
|
542
|
+
def to_dict(self) -> Dict:
|
|
543
|
+
"""Serialize the handler.
|
|
544
|
+
|
|
545
|
+
This method is needed during attachment. Will be called before binding.
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
(dict) A dictionary that allows to instantiate the Handler.
|
|
549
|
+
Must contain:
|
|
550
|
+
- "cls": The handler class name.
|
|
551
|
+
- "data": The handler data to be used in from_dict.
|
|
552
|
+
|
|
553
|
+
"""
|
|
554
|
+
...
|
|
555
|
+
|
|
556
|
+
@classmethod
|
|
557
|
+
def from_dict(cls, serialized: Dict) -> Self:
|
|
558
|
+
"""De-serialize the handler.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
serialized (Dict): Serialized handler with handler.to_dict
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Self: The Handler instance.
|
|
565
|
+
|
|
566
|
+
"""
|
|
567
|
+
...
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# ---------------------------------------------------------------------------
|
|
571
|
+
# EventBus and run management
|
|
572
|
+
# ---------------------------------------------------------------------------
|
|
573
|
+
class EventBus:
|
|
574
|
+
"""Protocol for the process-wide event router."""
|
|
575
|
+
|
|
576
|
+
handlers: Dict[str, Handler]
|
|
577
|
+
scopes: Dict[str, Set[str]]
|
|
578
|
+
|
|
579
|
+
def __init__(self):
|
|
580
|
+
super().__init__()
|
|
581
|
+
self.handlers: Dict[str, Handler] = {}
|
|
582
|
+
self.scopes: Dict[str, Set[str]] = defaultdict(set)
|
|
583
|
+
|
|
584
|
+
def shutdown(self) -> None:
|
|
585
|
+
"""Shutdown the EventBus and close all handlers."""
|
|
586
|
+
copy_map = {
|
|
587
|
+
scope: handlers_names.copy()
|
|
588
|
+
for scope, handlers_names in self.scopes.items()
|
|
589
|
+
}
|
|
590
|
+
for scope, handlers_names in copy_map.items():
|
|
591
|
+
for handler_name in handlers_names:
|
|
592
|
+
self.detach(handler_name, scope)
|
|
593
|
+
|
|
594
|
+
def attach(self, handlers: List[dict], scopes: List[str]) -> None:
|
|
595
|
+
"""Attach a handler under the given scope.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
handlers (List[dict]):
|
|
599
|
+
The serialized handlers to attach to the scopes.
|
|
600
|
+
scopes (List[str]): The scopes under which to attach.
|
|
601
|
+
|
|
602
|
+
"""
|
|
603
|
+
for handler_dict in handlers:
|
|
604
|
+
handler_class = _get_handler_class(handler_dict["cls"])
|
|
605
|
+
handler = handler_class.from_dict(handler_dict["data"])
|
|
606
|
+
if handler.name not in self.handlers:
|
|
607
|
+
# Initialize handler and store it
|
|
608
|
+
handler.open()
|
|
609
|
+
self.handlers[handler.name] = handler
|
|
610
|
+
|
|
611
|
+
# Add to requested scopes
|
|
612
|
+
for scope in scopes:
|
|
613
|
+
if scope not in self.scopes:
|
|
614
|
+
self.scopes[scope] = set()
|
|
615
|
+
self.scopes[scope].add(handler.name)
|
|
616
|
+
|
|
617
|
+
def detach(self, handler_name: str, scope: str) -> None:
|
|
618
|
+
"""Detach a handler from the given scope.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
handler_name (str): The name of the handler to detach.
|
|
622
|
+
scope (str): The scope from which to detach.
|
|
623
|
+
|
|
624
|
+
Raises:
|
|
625
|
+
ValueError: If the handler was not attached under the requested scope.
|
|
626
|
+
|
|
627
|
+
"""
|
|
628
|
+
if scope not in self.scopes or handler_name not in self.scopes[scope]:
|
|
629
|
+
raise ValueError(
|
|
630
|
+
f"Handler '{handler_name}' not attached under scope '{scope}'"
|
|
631
|
+
)
|
|
632
|
+
self.scopes[scope].remove(handler_name)
|
|
633
|
+
if not self.scopes[scope]:
|
|
634
|
+
del self.scopes[scope]
|
|
635
|
+
if not any(handler_name in self.scopes[s] for s in self.scopes):
|
|
636
|
+
self.handlers[handler_name].close()
|
|
637
|
+
del self.handlers[handler_name]
|
|
638
|
+
|
|
639
|
+
def emit(self, event: Dict | Event) -> None:
|
|
640
|
+
"""Emit an event to eligible handlers (errors isolated per handler).
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
event (dict | Event): The event (serialized) to emit, or an Event instance.
|
|
644
|
+
|
|
645
|
+
"""
|
|
646
|
+
if isinstance(event, dict):
|
|
647
|
+
event = Event.from_dict(event)
|
|
648
|
+
elif not isinstance(event, Event):
|
|
649
|
+
raise TypeError(f"emit expects a dict or Event, got {type(event)!r}")
|
|
650
|
+
|
|
651
|
+
if event.scope not in self.scopes:
|
|
652
|
+
return
|
|
653
|
+
|
|
654
|
+
for handler_name in self.scopes[event.scope]:
|
|
655
|
+
handler = self.handlers.get(handler_name)
|
|
656
|
+
if handler and handler.can_handle(event.kind):
|
|
657
|
+
handler.handle(event)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def get_bus() -> portal.Client:
|
|
661
|
+
"""Return the process-wide EventBus singleton client.
|
|
662
|
+
|
|
663
|
+
The EventBus owns handlers and routes events based on scope and kind.
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
portal.Client: The singleton EventBus client.
|
|
667
|
+
|
|
668
|
+
"""
|
|
669
|
+
global __impl_get_bus
|
|
670
|
+
if __impl_get_bus is None:
|
|
671
|
+
from ._core.routing import get_bus as _impl_get_bus
|
|
672
|
+
|
|
673
|
+
__impl_get_bus = _impl_get_bus # type: ignore
|
|
674
|
+
return __impl_get_bus() # type: ignore
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def attach(handler: Handler, scopes: List[str] = ["global"]) -> None:
|
|
678
|
+
"""Attach a handler to the global EventBus under the specified scopes.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
handler (Handler): The handler to attach.
|
|
682
|
+
scopes (List[str]): The scopes under which to attach.
|
|
683
|
+
|
|
684
|
+
Raises:
|
|
685
|
+
ValueError: If the handler disallows the requested scope.
|
|
686
|
+
|
|
687
|
+
"""
|
|
688
|
+
bus = get_bus()
|
|
689
|
+
bus.attach([handler.to_dict()], scopes)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def detach(handler_name: str, scope: str) -> None:
|
|
693
|
+
"""Detach a handler from the global EventBus under the specified scope.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
handler_name (str): The name of the handler to detach.
|
|
697
|
+
scope (str): The scope from which to detach.
|
|
698
|
+
|
|
699
|
+
Raises:
|
|
700
|
+
ValueError: If the handler was not attached under the requested scope.
|
|
701
|
+
|
|
702
|
+
"""
|
|
703
|
+
bus = get_bus()
|
|
704
|
+
bus.detach(handler_name, scope)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def finish() -> None:
|
|
708
|
+
"""Shutdown the global EventBus and close all handlers."""
|
|
709
|
+
bus = get_bus()
|
|
710
|
+
bus.shutdown().result()
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def register_handler(handler_class: type) -> None:
|
|
714
|
+
"""Register a custom handler class for serialization/deserialization.
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
handler_class: The handler class to register. Must have a __name__ attribute.
|
|
718
|
+
|
|
719
|
+
Example:
|
|
720
|
+
class CustomHandler(gg.ConsoleHandler):
|
|
721
|
+
pass
|
|
722
|
+
|
|
723
|
+
gg.register_handler(CustomHandler)
|
|
724
|
+
|
|
725
|
+
"""
|
|
726
|
+
_HANDLER_REGISTRY[handler_class.__name__] = handler_class
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _get_handler_class(class_name: str) -> type:
|
|
730
|
+
"""Get a handler class by name from registry or globals.
|
|
731
|
+
|
|
732
|
+
Args:
|
|
733
|
+
class_name: Name of the handler class.
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
The handler class.
|
|
737
|
+
|
|
738
|
+
Raises:
|
|
739
|
+
KeyError: If the handler class is not found.
|
|
740
|
+
|
|
741
|
+
"""
|
|
742
|
+
# First check the registry for custom handlers
|
|
743
|
+
if class_name in _HANDLER_REGISTRY:
|
|
744
|
+
return _HANDLER_REGISTRY[class_name]
|
|
745
|
+
|
|
746
|
+
# Fall back to globals for built-in handlers
|
|
747
|
+
if class_name in globals():
|
|
748
|
+
return globals()[class_name]
|
|
749
|
+
|
|
750
|
+
raise KeyError(
|
|
751
|
+
f"Handler class '{class_name}' not found. "
|
|
752
|
+
f"Available handlers: {list(_HANDLER_REGISTRY.keys()) + [k for k in globals().keys() if k.endswith('Handler')]}"
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
try:
|
|
757
|
+
from ._core.integrations.wandb import WandBHandler
|
|
758
|
+
except ImportError:
|
|
759
|
+
WandBHandler = None
|
|
760
|
+
|
|
761
|
+
__all__ = [
|
|
762
|
+
"TextLogger",
|
|
763
|
+
"GogglesLogger",
|
|
764
|
+
"get_logger",
|
|
765
|
+
"attach",
|
|
766
|
+
"detach",
|
|
767
|
+
"register_handler",
|
|
768
|
+
"load_configuration",
|
|
769
|
+
"save_configuration",
|
|
770
|
+
"timeit",
|
|
771
|
+
"trace_on_error",
|
|
772
|
+
"GracefulShutdown",
|
|
773
|
+
"ConsoleHandler",
|
|
774
|
+
"LocalStorageHandler",
|
|
775
|
+
"WandBHandler",
|
|
776
|
+
]
|
|
777
|
+
|
|
778
|
+
# ---------------------------------------------------------------------------
|
|
779
|
+
# Import-time safety
|
|
780
|
+
# ---------------------------------------------------------------------------
|
|
781
|
+
|
|
782
|
+
# Attach a NullHandler so importing goggles never emits logs by default.
|
|
783
|
+
|
|
784
|
+
_logger = logging.getLogger(__name__)
|
|
785
|
+
if not any(isinstance(h, logging.NullHandler) for h in _logger.handlers):
|
|
786
|
+
_logger.addHandler(logging.NullHandler())
|