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.
@@ -0,0 +1,602 @@
1
+ """Internal logger implementation.
2
+
3
+ WARNING: This module is an internal implementation detail of Goggles'
4
+ logging system. It is not part of the public API.
5
+
6
+ External code should not import from this module. Instead, depend on:
7
+ - `goggles.TextLogger`, `goggles.GogglesLogger` (protocol / interface), and
8
+ - `goggles.get_logger()` (factory returning a TextLogger/GogglesLogger).
9
+ """
10
+
11
+ import logging
12
+ import inspect
13
+ from typing import Any, Dict, Mapping, Optional, Any
14
+ from typing_extensions import Self
15
+
16
+ from goggles import TextLogger, GogglesLogger, Event, GOGGLES_ASYNC
17
+ from goggles.types import Metrics, Image, Video, VectorField, Vector
18
+
19
+
20
+ class CoreTextLogger(TextLogger):
21
+ """Internal concrete implementation of the TextLogger protocol.
22
+
23
+ This adapter wraps a `logging.Logger` and maintains a dictionary of
24
+ persistent, structured fields ("bound" context). Each log call merges
25
+ the bound context with per-call extras before delegating to the underlying
26
+ logger.
27
+
28
+ Notes:
29
+ * This class is **internal** to Goggles. Do not rely on its presence,
30
+ constructor, or attributes from external code.
31
+ * External users should obtain a `TextLogger` via
32
+ `goggles.get_logger()` and program against the protocol.
33
+
34
+ Attributes:
35
+ _logger: Underlying `logging.Logger` instance. Internal use only.
36
+ _bound: Persistent structured fields merged into each record.
37
+ Internal use only.
38
+ _client: EventBus client for emitting structured events.
39
+
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ scope: str,
45
+ name: Optional[str] = None,
46
+ to_bind: Optional[Mapping[str, Any]] = None,
47
+ ):
48
+ """Initialize the CoreTextLogger.
49
+
50
+ Args:
51
+ scope (str): Scope to bind the logger to (e.g., "global", "run", ecc.).
52
+ name (str): Name of the logger.
53
+ to_bind (Optional[Mapping[str, Any]]):
54
+ Optional initial persistent context to bind.
55
+
56
+ """
57
+ from goggles._core.routing import get_bus
58
+
59
+ self.name = name
60
+ self._scope = scope
61
+ self._bound: Dict[str, Any] = dict(to_bind or {})
62
+ self._client = get_bus()
63
+
64
+ def bind(self, /, *, scope: str = "global", **fields: Any) -> Self:
65
+ """Return a new logger with `fields` merged into persistent context.
66
+
67
+ This method does not mutate the current instance. It returns a new
68
+ adapter whose bound context is the shallow merge of the existing bound
69
+ dictionary and `fields`. Keys in `fields` overwrite existing keys.
70
+
71
+ Args:
72
+ scope: Scope to bind the new logger under (e.g., "global" or "run").
73
+ **fields: Key-value pairs to bind into the new logger's context.
74
+
75
+ Returns:
76
+ Self: A new adapter with the merged persistent context.
77
+
78
+ Raises:
79
+ TypeError: If provided keys are not strings (may occur in stricter
80
+ configurations; current implementation assumes string keys).
81
+
82
+ Examples:
83
+ >>> log = get_logger("goggles") # via public API
84
+ >>> run_log = log.bind(scope="exp42", module="train")
85
+ >>> run_log.info("Initialized")
86
+
87
+ """
88
+ self._bound = {**self._bound, **fields}
89
+ self._scope = scope
90
+
91
+ return self
92
+
93
+ def get_bound(self) -> Dict[str, Any]:
94
+ """Get a copy of the current persistent bound context.
95
+
96
+ Returns:
97
+ Dict[str, Any]: A shallow copy of the bound context dictionary.
98
+
99
+ """
100
+ return dict(self._bound)
101
+
102
+ def debug(
103
+ self,
104
+ msg: str,
105
+ /,
106
+ *,
107
+ step: Optional[int] = None,
108
+ time: Optional[float] = None,
109
+ **extra: Any,
110
+ ) -> None:
111
+ """Log a DEBUG message with optional per-call structured fields.
112
+
113
+ Args:
114
+ msg: Human-readable message.
115
+ step: Step number associated with the event.
116
+ time: Timestamp of the event in seconds since epoch.
117
+ **extra: Per-call structured fields merged with the bound context.
118
+
119
+ """
120
+ filepath, lineno = _caller_id()
121
+ future = self._client.emit(
122
+ Event(
123
+ kind="log",
124
+ scope=self._scope,
125
+ payload=msg,
126
+ filepath=filepath,
127
+ lineno=lineno,
128
+ level=logging.DEBUG,
129
+ step=step,
130
+ time=time,
131
+ extra={**self._bound, **extra},
132
+ ).to_dict()
133
+ )
134
+ if not GOGGLES_ASYNC:
135
+ future.result()
136
+
137
+ def info(
138
+ self,
139
+ msg: str,
140
+ /,
141
+ *,
142
+ step: Optional[int] = None,
143
+ time: Optional[float] = None,
144
+ **extra: Any,
145
+ ) -> None:
146
+ """Log an INFO message with optional structured extras.
147
+
148
+ Args:
149
+ msg (str): The log message.
150
+ step (Optional[int]): The step number.
151
+ time (Optional[float]): The timestamp.
152
+ **extra (Any):
153
+ Additional structured key-value pairs for this record.
154
+
155
+ """
156
+ filepath, lineno = _caller_id()
157
+ future = self._client.emit(
158
+ Event(
159
+ kind="log",
160
+ scope=self._scope,
161
+ payload=msg,
162
+ filepath=filepath,
163
+ lineno=lineno,
164
+ level=logging.INFO,
165
+ step=step,
166
+ time=time,
167
+ extra={**self._bound, **extra},
168
+ ).to_dict()
169
+ )
170
+
171
+ if not GOGGLES_ASYNC:
172
+ future.result()
173
+
174
+ def warning(
175
+ self,
176
+ msg: str,
177
+ /,
178
+ *,
179
+ step: Optional[int] = None,
180
+ time: Optional[float] = None,
181
+ **extra: Any,
182
+ ) -> None:
183
+ """Log a WARNING message with optional structured extras.
184
+
185
+ Args:
186
+ msg: Human-readable message.
187
+ step (Optional[int]): The step number.
188
+ time (Optional[float]): The timestamp.
189
+ **extra: Per-call structured fields merged with the bound context.
190
+
191
+ """
192
+ filepath, lineno = _caller_id()
193
+ future = self._client.emit(
194
+ Event(
195
+ kind="log",
196
+ scope=self._scope,
197
+ payload=msg,
198
+ filepath=filepath,
199
+ lineno=lineno,
200
+ level=logging.WARNING,
201
+ step=step,
202
+ time=time,
203
+ extra={**self._bound, **extra},
204
+ ).to_dict()
205
+ )
206
+
207
+ if not GOGGLES_ASYNC:
208
+ future.result()
209
+
210
+ def error(
211
+ self,
212
+ msg: str,
213
+ /,
214
+ *,
215
+ step: Optional[int] = None,
216
+ time: Optional[float] = None,
217
+ **extra: Any,
218
+ ) -> None:
219
+ """Log an ERROR message with optional per-call structured fields.
220
+
221
+ Args:
222
+ msg: Human-readable message.
223
+ step (Optional[int]): The step number.
224
+ time (Optional[float]): The timestamp.
225
+ **extra: Per-call structured fields merged with the bound context.
226
+
227
+ """
228
+ filepath, lineno = _caller_id()
229
+ future = self._client.emit(
230
+ Event(
231
+ kind="log",
232
+ scope=self._scope,
233
+ payload=msg,
234
+ level=logging.ERROR,
235
+ filepath=filepath,
236
+ lineno=lineno,
237
+ step=step,
238
+ time=time,
239
+ extra={**self._bound, **extra},
240
+ ).to_dict()
241
+ )
242
+
243
+ if not GOGGLES_ASYNC:
244
+ future.result()
245
+
246
+ def critical(
247
+ self,
248
+ msg: str,
249
+ /,
250
+ *,
251
+ step: Optional[int] = None,
252
+ time: Optional[float] = None,
253
+ **extra: Dict[str, Any],
254
+ ) -> None:
255
+ """Log a CRITICAL message with optional per-call structured fields.
256
+
257
+ Args:
258
+ msg: Human-readable message.
259
+ step (Optional[int]): The step number.
260
+ time (Optional[float]): The timestamp.
261
+ **extra: Per-call structured fields merged with the bound context.
262
+
263
+ """
264
+ filepath, lineno = _caller_id()
265
+ future = self._client.emit(
266
+ Event(
267
+ kind="log",
268
+ scope=self._scope,
269
+ payload=msg,
270
+ level=logging.CRITICAL,
271
+ filepath=filepath,
272
+ lineno=lineno,
273
+ step=step,
274
+ time=time,
275
+ extra={**self._bound, **extra},
276
+ ).to_dict()
277
+ )
278
+
279
+ if not GOGGLES_ASYNC:
280
+ future.result()
281
+
282
+ def __repr__(self) -> str:
283
+ """Return a developer-friendly string representation.
284
+
285
+ Returns:
286
+ str: String representation showing the underlying
287
+ logger and bound context.
288
+
289
+ """
290
+ return (
291
+ f"{self.__class__.__name__}(name={self.name!r}, " f"bound={self._bound!r})"
292
+ )
293
+
294
+
295
+ class CoreGogglesLogger(GogglesLogger, CoreTextLogger):
296
+ """A GogglesLogger that is also a CoreTextLogger."""
297
+
298
+ def push(
299
+ self,
300
+ metrics: Metrics,
301
+ *,
302
+ step: Optional[int] = None,
303
+ time: Optional[float] = None,
304
+ **extra: Dict[str, Any],
305
+ ) -> None:
306
+ """Emit a batch of scalar metrics.
307
+
308
+ Args:
309
+ metrics (Metrics): (Name,value) pairs.
310
+ step (Optional[int]): Optional global step index.
311
+ time (Optional[float]): Optional global timestamp.
312
+ **extra (Dict[str, Any]):
313
+ Additional routing metadata (e.g., split="train").
314
+
315
+ """
316
+ filepath, lineno = _caller_id()
317
+ future = self._client.emit(
318
+ Event(
319
+ kind="metric",
320
+ scope=self._scope,
321
+ payload=metrics,
322
+ level=None,
323
+ filepath=filepath,
324
+ lineno=lineno,
325
+ step=step,
326
+ time=time,
327
+ extra={**self._bound, **extra},
328
+ ).to_dict()
329
+ )
330
+
331
+ if not GOGGLES_ASYNC:
332
+ future.result()
333
+
334
+ def scalar(
335
+ self,
336
+ name: str,
337
+ value: float | int,
338
+ *,
339
+ step: Optional[int] = None,
340
+ time: Optional[float] = None,
341
+ **extra: Dict[str, Any],
342
+ ) -> None:
343
+ """Emit a single scalar metric.
344
+
345
+ Args:
346
+ name (str): Metric name.
347
+ value (float|int): Metric value.
348
+ step (Optional[int]): Optional global step index.
349
+ time (Optional[float]): Optional global timestamp.
350
+ **extra (Dict[str, Any]):
351
+ Additional routing metadata (e.g., split="train").
352
+
353
+ """
354
+ filepath, lineno = _caller_id()
355
+ future = self._client.emit(
356
+ Event(
357
+ kind="metric",
358
+ scope=self._scope,
359
+ payload={name: value},
360
+ level=None,
361
+ filepath=filepath,
362
+ lineno=lineno,
363
+ step=step,
364
+ time=time,
365
+ extra={**self._bound, **extra},
366
+ ).to_dict()
367
+ )
368
+
369
+ if not GOGGLES_ASYNC:
370
+ future.result()
371
+
372
+ def image(
373
+ self,
374
+ image: Image,
375
+ *,
376
+ name: Optional[str] = None,
377
+ format: str = "png",
378
+ step: Optional[int] = None,
379
+ time: Optional[float] = None,
380
+ **extra: Dict[str, Any],
381
+ ) -> None:
382
+ """Emit an image artifact (encoded bytes).
383
+
384
+ Args:
385
+ name (str): Artifact name.
386
+ image (Image): Image.
387
+ format (str): Image format, e.g., "png", "jpeg".
388
+ step (Optional[int]): Optional global step index.
389
+ time (Optional[float]): Optional global timestamp.
390
+ **extra: Dict[str, Any]: Additional routing metadata.
391
+
392
+ """
393
+ filepath, lineno = _caller_id()
394
+ extra = {**self._bound, **extra}
395
+ if name is not None:
396
+ extra["name"] = name
397
+ extra["format"] = format
398
+ future = self._client.emit(
399
+ Event(
400
+ kind="image",
401
+ scope=self._scope,
402
+ payload=image,
403
+ level=None,
404
+ filepath=filepath,
405
+ lineno=lineno,
406
+ step=step,
407
+ time=time,
408
+ extra=extra,
409
+ ).to_dict()
410
+ )
411
+
412
+ if not GOGGLES_ASYNC:
413
+ future.result()
414
+
415
+ def video(
416
+ self,
417
+ video: Video,
418
+ *,
419
+ name: Optional[str] = None,
420
+ fps: int = 30,
421
+ format: str = "gif",
422
+ step: Optional[int] = None,
423
+ time: Optional[float] = None,
424
+ **extra: Dict[str, Any],
425
+ ) -> None:
426
+ """Emit a video artifact (encoded bytes).
427
+
428
+ Args:
429
+ video (Video): Video.
430
+ name (Optional[str]): Artifact name.
431
+ fps (int): Frames per second.
432
+ format (str): Video format, e.g., "gif", "mp4".
433
+ step (Optional[int]): Optional global step index.
434
+ time (Optional[float]): Optional global timestamp.
435
+ **extra (Dict[str, Any]): Additional routing metadata.
436
+
437
+ """
438
+ filepath, lineno = _caller_id()
439
+ extra = {**self._bound, **extra}
440
+ if name is not None:
441
+ extra["name"] = name
442
+ extra["fps"] = fps
443
+ extra["format"] = format
444
+
445
+ future = self._client.emit(
446
+ Event(
447
+ kind="video",
448
+ scope=self._scope,
449
+ payload=video,
450
+ level=None,
451
+ filepath=filepath,
452
+ lineno=lineno,
453
+ step=step,
454
+ time=time,
455
+ extra=extra,
456
+ ).to_dict()
457
+ )
458
+
459
+ if not GOGGLES_ASYNC:
460
+ future.result()
461
+
462
+ def artifact(
463
+ self,
464
+ data: Any,
465
+ *,
466
+ name: Optional[str] = None,
467
+ format: str = "bin",
468
+ step: Optional[int] = None,
469
+ time: Optional[float] = None,
470
+ **extra: Dict[str, Any],
471
+ ) -> None:
472
+ """Emit a generic artifact (encoded bytes).
473
+
474
+ Args:
475
+ name (str): Artifact name.
476
+ data (bytes): Artifact data.
477
+ format (str): Artifact format, e.g., "txt", "bin".
478
+ step (Optional[int]): Optional global step index.
479
+ time (Optional[float]): Optional global timestamp.
480
+ **extra (Dict[str, Any]): Additional routing metadata.
481
+
482
+ """
483
+ filepath, lineno = _caller_id()
484
+ extra = {**self._bound, **extra}
485
+ if name is not None:
486
+ extra["name"] = name
487
+ extra["format"] = format
488
+
489
+ future = self._client.emit(
490
+ Event(
491
+ kind="artifact",
492
+ scope=self._scope,
493
+ payload=data,
494
+ level=None,
495
+ filepath=filepath,
496
+ lineno=lineno,
497
+ step=step,
498
+ time=time,
499
+ extra=extra,
500
+ ).to_dict()
501
+ )
502
+
503
+ if not GOGGLES_ASYNC:
504
+ future.result()
505
+
506
+ def vector_field(
507
+ self,
508
+ vector_field: VectorField,
509
+ *,
510
+ name: Optional[str] = None,
511
+ step: Optional[int] = None,
512
+ time: Optional[float] = None,
513
+ **extra: Dict[str, Any],
514
+ ) -> None:
515
+ """Emit a vector field artifact.
516
+
517
+ Args:
518
+ vector_field (VectorField): Vector field data.
519
+ name (Optional[str]): Artifact name.
520
+ step (Optional[int]): Optional global step index.
521
+ time (Optional[float]): Optional global timestamp.
522
+ **extra (Dict[str, Any]): Additional routing metadata.
523
+
524
+ """
525
+ filepath, lineno = _caller_id()
526
+ extra = {**self._bound, **extra}
527
+ if name is not None:
528
+ extra["name"] = name
529
+
530
+ future = self._client.emit(
531
+ Event(
532
+ kind="vector_field",
533
+ scope=self._scope,
534
+ payload=vector_field,
535
+ level=None,
536
+ filepath=filepath,
537
+ lineno=lineno,
538
+ step=step,
539
+ time=time,
540
+ extra=extra,
541
+ ).to_dict()
542
+ )
543
+
544
+ if not GOGGLES_ASYNC:
545
+ future.result()
546
+
547
+ def histogram(
548
+ self,
549
+ histogram: Vector,
550
+ *,
551
+ name: Optional[str] = None,
552
+ step: Optional[int] = None,
553
+ time: Optional[float] = None,
554
+ **extra: Dict[str, Any],
555
+ ) -> None:
556
+ """Emit a histogram artifact.
557
+
558
+ Args:
559
+ name (str): Artifact name.
560
+ histogram (Vector): Histogram data.
561
+ step (Optional[int]): Optional global step index.
562
+ time (Optional[float]): Optional global timestamp.
563
+ **extra (Dict[str, Any]): Additional routing metadata.
564
+
565
+ """
566
+ filepath, lineno = _caller_id()
567
+ extra = {**self._bound, **extra}
568
+ if name is not None:
569
+ extra["name"] = name
570
+
571
+ future = self._client.emit(
572
+ Event(
573
+ kind="histogram",
574
+ scope=self._scope,
575
+ payload=histogram,
576
+ level=None,
577
+ filepath=filepath,
578
+ lineno=lineno,
579
+ step=step,
580
+ time=time,
581
+ extra=extra,
582
+ ).to_dict()
583
+ )
584
+
585
+ if not GOGGLES_ASYNC:
586
+ future.result()
587
+
588
+
589
+ def _caller_id() -> tuple[str, int]:
590
+ """Get the caller's filepath and line number for logging purposes.
591
+
592
+ Returns:
593
+ tuple[str, int]: A tuple of (file path, line number).
594
+
595
+ """
596
+ frame = inspect.currentframe()
597
+ if frame is None or frame.f_back is None or frame.f_back.f_back is None:
598
+ return ("<unknown>", 0)
599
+ caller_frame = frame.f_back.f_back
600
+ filename = caller_frame.f_code.co_filename
601
+ line_number = caller_frame.f_lineno
602
+ return (filename, line_number)