auto-trainer-core 2.0.1.dev349__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.
Files changed (73) hide show
  1. auto_trainer_core-2.0.1.dev349.dist-info/METADATA +19 -0
  2. auto_trainer_core-2.0.1.dev349.dist-info/RECORD +73 -0
  3. auto_trainer_core-2.0.1.dev349.dist-info/WHEEL +5 -0
  4. auto_trainer_core-2.0.1.dev349.dist-info/top_level.txt +1 -0
  5. autotrainer/core/__init__.py +319 -0
  6. autotrainer/core/analysis/__init__.py +6 -0
  7. autotrainer/core/analysis/alarm_monitor.py +484 -0
  8. autotrainer/core/analysis/audio_spectrum_data.py +27 -0
  9. autotrainer/core/analysis/audio_spectrum_monitor.py +134 -0
  10. autotrainer/core/analysis/auto_tunnel_fan_monitor.py +92 -0
  11. autotrainer/core/analysis/detector.py +179 -0
  12. autotrainer/core/analysis/external_doors_monitor.py +124 -0
  13. autotrainer/core/analysis/global_animal_presence_monitor.py +69 -0
  14. autotrainer/core/analysis/head_fix_measurement.py +15 -0
  15. autotrainer/core/analysis/headbar_pressure_monitor.py +158 -0
  16. autotrainer/core/analysis/load_cell_monitor.py +377 -0
  17. autotrainer/core/analysis/load_cell_tare_monitor.py +217 -0
  18. autotrainer/core/analysis/measurement_data.py +80 -0
  19. autotrainer/core/analysis/pellet_position_monitor.py +108 -0
  20. autotrainer/core/analysis/sensor_analysis.py +390 -0
  21. autotrainer/core/analysis/system_fault_monitor.py +81 -0
  22. autotrainer/core/analysis/system_maintenance_monitor.py +88 -0
  23. autotrainer/core/animal/__init__.py +1 -0
  24. autotrainer/core/animal/animal_subject.py +208 -0
  25. autotrainer/core/capture.py +25 -0
  26. autotrainer/core/configuration/__init__.py +88 -0
  27. autotrainer/core/configuration/alarm_configuration.py +55 -0
  28. autotrainer/core/configuration/animal_presence_configuration.py +17 -0
  29. autotrainer/core/configuration/behavior_configuration.py +323 -0
  30. autotrainer/core/configuration/camera_configuration.py +108 -0
  31. autotrainer/core/configuration/external_doors_monitor_configuration.py +9 -0
  32. autotrainer/core/configuration/hardware_configuration.py +30 -0
  33. autotrainer/core/configuration/inference_configuration.py +26 -0
  34. autotrainer/core/configuration/load_cell_config.py +61 -0
  35. autotrainer/core/configuration/persistence_configuration.py +29 -0
  36. autotrainer/core/configuration/presence_detection_configuration.py +9 -0
  37. autotrainer/core/configuration/system_configuration.py +211 -0
  38. autotrainer/core/configuration/system_fault_config.py +9 -0
  39. autotrainer/core/configuration/system_maintenance_config.py +17 -0
  40. autotrainer/core/diamond_triangle_config.py +150 -0
  41. autotrainer/core/event/__init__.py +54 -0
  42. autotrainer/core/event/api_event_kind.py +5 -0
  43. autotrainer/core/event/api_event_plugin.py +57 -0
  44. autotrainer/core/event/event_info.py +53 -0
  45. autotrainer/core/event/event_manager.py +296 -0
  46. autotrainer/core/event/event_manager_plugin.py +45 -0
  47. autotrainer/core/event/file_event_plugin.py +91 -0
  48. autotrainer/core/event/logger_event_plugin.py +43 -0
  49. autotrainer/core/fixed_array_multiqueue.py +263 -0
  50. autotrainer/core/fixed_array_queue.py +124 -0
  51. autotrainer/core/frame_index.py +34 -0
  52. autotrainer/core/interfaces.py +347 -0
  53. autotrainer/core/logging.py +714 -0
  54. autotrainer/core/message/__init__.py +26 -0
  55. autotrainer/core/message/audio_spectrum_message.py +21 -0
  56. autotrainer/core/message/measurement_message.py +61 -0
  57. autotrainer/core/message/message_handler.py +158 -0
  58. autotrainer/core/message/motor_configuration_message.py +128 -0
  59. autotrainer/core/message/system_command_kind.py +74 -0
  60. autotrainer/core/message/system_message_handler.py +122 -0
  61. autotrainer/core/message/system_status_message.py +112 -0
  62. autotrainer/core/multiproc.py +50 -0
  63. autotrainer/core/notification/__init__.py +2 -0
  64. autotrainer/core/notification/notification_center.py +80 -0
  65. autotrainer/core/notification/trigger_notification.py +27 -0
  66. autotrainer/core/observable_object.py +105 -0
  67. autotrainer/core/perf_monitor.py +77 -0
  68. autotrainer/core/pose_elements.py +196 -0
  69. autotrainer/core/project/__init__.py +19 -0
  70. autotrainer/core/project/project_info.py +486 -0
  71. autotrainer/core/queue_util.py +60 -0
  72. autotrainer/core/reach_event.py +38 -0
  73. autotrainer/core/video_detection.py +326 -0
@@ -0,0 +1,714 @@
1
+ import copy
2
+ import dataclasses
3
+ import functools
4
+ import itertools
5
+ import logging
6
+ import logging.handlers
7
+ import multiprocessing
8
+ import operator
9
+ import signal
10
+ import threading
11
+ import time
12
+ from pathlib import Path
13
+ from logging import LogRecord
14
+ from queue import Empty
15
+ from multiprocessing import Process
16
+ from typing import Optional, Dict, Union, List
17
+
18
+ import sys
19
+ import verboselogs
20
+ import coloredlogs
21
+ from datetime import datetime
22
+
23
+ from autotrainer.core.multiproc import get_mp_ctx
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ _LogLevelT = Union[str, int]
28
+
29
+
30
+ _already_setup = False
31
+ _base_logger: logging.Logger = logging.root
32
+ _multiprocess_log_queue: Optional[multiprocessing.Queue] = None
33
+ _queue_listener: Optional[logging.handlers.QueueListener] = None
34
+ _queue_handler: Optional[logging.Handler] = None
35
+ _console_handler: Optional[logging.StreamHandler] = None
36
+ _root_handler = None
37
+
38
+
39
+ DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s %(message)s"
40
+ MULTIPROC_LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s[%(processName)s.%(process)d-%(threadName)s.%(thread_id)s] %(message)s"
41
+
42
+
43
+ # these loggers can be too verbose:
44
+ _limit_loggers_level = {
45
+ 'botocore': {
46
+ 'level': 'INFO'
47
+ },
48
+ 'boto3': {
49
+ 'level': 'INFO'
50
+ },
51
+ 'urllib3': {
52
+ 'level': 'INFO'
53
+ },
54
+ 'py4j': {
55
+ 'level': 'INFO'
56
+ },
57
+ 'h5py': {
58
+ 'level': 'INFO'
59
+ },
60
+ 'watchdog': {
61
+ 'level': 'INFO'
62
+ }
63
+ }
64
+
65
+
66
+ class DateTimeFormats:
67
+ # could be an enum eventually
68
+ hour_time_precise = "%H:%M:%S.%f"
69
+ month_day_time_precise = f"%m/%d {hour_time_precise}"
70
+ year_precise = f"%Y/%m/%d {hour_time_precise}"
71
+
72
+
73
+ DEFAULT_FIELD_STYLES = dict(
74
+ asctime=dict(color='white', bold=False),
75
+ hostname=dict(color='magenta'),
76
+ levelname=dict(color='blue', bold=True),
77
+ name=dict(color='cyan', bold=False),
78
+ programname=dict(color='cyan'),
79
+ username=dict(color='yellow'),
80
+ )
81
+
82
+
83
+ DEFAULT_LEVEL_STYLES = dict(
84
+ spam=dict(color='white', faint=True),
85
+ debug=dict(color='white', bold=False, faint=False),
86
+ verbose=dict(color='white', bold=True),
87
+ info=dict(color='blue', bold=False, faint=True),
88
+ notice=dict(color='magenta', faint=True),
89
+ warning=dict(color='yellow'),
90
+ success=dict(color='green', bold=False),
91
+ error=dict(color='red', bold=False, faint=True),
92
+ critical=dict(color='red', bold=True),
93
+ )
94
+
95
+
96
+ @dataclasses.dataclass
97
+ class LogConfig:
98
+ base_logger_name: Optional[str] = None # i.e: "root" logger if None
99
+ logger_level: _LogLevelT = logging.NOTSET
100
+ root_level: _LogLevelT = logging.NOTSET
101
+ log_format: str = MULTIPROC_LOG_FORMAT
102
+ date_format: str = DateTimeFormats.hour_time_precise
103
+ time_precision: int = 3, # for sub seconds precision, nbr of digits after the dot.
104
+ level_styles: Dict[str, Dict[str, str]] = dataclasses.field(default_factory=lambda: copy.deepcopy(DEFAULT_LEVEL_STYLES))
105
+ field_styles: Dict[str, Dict[str, str]] = dataclasses.field(default_factory=lambda: copy.deepcopy(DEFAULT_FIELD_STYLES))
106
+ stream: str = "sys.stdout"
107
+
108
+
109
+ def make_console_handler(cfg: LogConfig):
110
+ stream = sys.stdout
111
+ console_handler = logging.StreamHandler(stream=stream)
112
+ console_handler.name = "console_handler"
113
+ console_handler.addFilter(thread_id_filter)
114
+ fmt = ColoredPreciseTimeFormatter(
115
+ cfg.log_format,
116
+ level_styles=cfg.level_styles,
117
+ field_styles=cfg.field_styles,
118
+ datefmt=cfg.date_format,
119
+ time_precision=cfg.time_precision,
120
+ )
121
+ console_handler.setFormatter(fmt)
122
+ return console_handler
123
+
124
+
125
+ def listener_command(func):
126
+ """Relay the given func to the log queue listener proc side"""
127
+ @functools.wraps(func)
128
+ def wrapped(self: "LogQueueListenerProc", *args, **kwargs):
129
+ self._send_command(func.__name__, (args, kwargs))
130
+ return wrapped
131
+
132
+
133
+ class LogQueueListenerProc(Process):
134
+
135
+ def __init__(
136
+ self,
137
+ log_queue,
138
+ log_config: LogConfig,
139
+ ):
140
+ super().__init__(daemon=True)
141
+ self._queue = log_queue
142
+ mp_ctx = get_mp_ctx()
143
+ self._command_queue = mp_ctx.Queue()
144
+ self._command_executed = mp_ctx.Event()
145
+ self._log_config = log_config
146
+ self._listener: Optional[WithThreadIdQueueListener] = None
147
+ self._console_handler = None
148
+ self._file_handler: Optional[logging.FileHandler] = None
149
+
150
+ def _send_command(self, cmd, data):
151
+ self._command_executed.clear()
152
+ self._command_queue.put((cmd, data))
153
+ if not self._command_executed.wait(5):
154
+ logger.warning("command %s timeout after 5s, skipping waiting")
155
+
156
+ @listener_command
157
+ def set_handler_level(self, name, level):
158
+ """Set handler level"""
159
+
160
+ def _set_handler_level(self, name, level):
161
+ if name == "console_handler":
162
+ self._console_handler.setLevel(level)
163
+ else:
164
+ logger.verbose("unhandled handler name: %r", name)
165
+
166
+ @listener_command
167
+ def set_logger_level(self, name, level):
168
+ """Set logger level"""
169
+
170
+ @staticmethod
171
+ def _set_logger_level(name, level):
172
+ logging.getLogger(name).setLevel(level)
173
+ logger.debug("set_logger_level(%s, %s)", name, level)
174
+
175
+ @listener_command
176
+ def add_file_handler(self, path):
177
+ """Add file handler to path"""
178
+
179
+ def _add_file_handler(self, path: Path):
180
+ logger.info("Adding file handler to %s", path)
181
+ file_handler = logging.FileHandler(path)
182
+ file_handler.addFilter(thread_id_filter)
183
+ file_handler.setFormatter(
184
+ PreciseTimeFormatter(
185
+ MULTIPROC_LOG_FORMAT,
186
+ datefmt=DateTimeFormats.year_precise,
187
+ time_precision=6,
188
+ )
189
+ )
190
+ file_handler.setLevel(verboselogs.SPAM + 1) # writes everything up to DEBUG which reaches it
191
+ self._listener.handlers += (file_handler,)
192
+ self._file_handler = file_handler
193
+ logger.info("logging.root.handlers=%s ; listener_handlers=%s",
194
+ logging.root.handlers, self._listener.handlers)
195
+ return file_handler
196
+
197
+ @listener_command
198
+ def switch_file_handler(self, path: Path):
199
+ """Switch current, or set new, file handler to path"""
200
+
201
+ def _switch_file_handler(self, path: Path):
202
+ logger.info("Switching file handler to %s", path)
203
+ prev = self._file_handler
204
+ self._add_file_handler(path)
205
+ if prev is not None:
206
+ logger.verbose("removing previous handler %s", prev)
207
+ self._listener.handlers = tuple(
208
+ handler for handler in self._listener.handlers
209
+ if handler != prev
210
+ )
211
+ logger.debug("new handlers: %s", self._listener.handlers)
212
+ prev.close()
213
+
214
+ #
215
+
216
+ def stop(self):
217
+ self._command_queue.put(None)
218
+ # os.kill(self.pid, signal.SIGINT)
219
+ self.join()
220
+
221
+ def run(self):
222
+ # print(f"{logging.root.handlers}")
223
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
224
+
225
+ cfg = self._log_config
226
+ #
227
+ console_handler = self._console_handler = make_console_handler(cfg)
228
+
229
+ # NB: start the listener as soon as possible
230
+ listener = self._listener = WithThreadIdQueueListener(
231
+ self._queue,
232
+ console_handler,
233
+ respect_handler_level=True, # False, # True,
234
+ )
235
+ listener.start()
236
+
237
+ base_logger = get_verbose_logger(cfg.base_logger_name)
238
+ base_logger.addHandler(console_handler)
239
+
240
+ base_logger.setLevel(logging.INFO)
241
+ base_logger.info("Starting log queue listener. config=%s", self._log_config)
242
+ base_logger.setLevel(cfg.root_level)
243
+
244
+ # NB: must be installed AFTER the listener is started
245
+ install_log_exception_hook()
246
+
247
+ command_q = self._command_queue
248
+ command_executed = self._command_executed.set
249
+ while True:
250
+ try:
251
+ data = command_q.get()
252
+ except Empty:
253
+ continue
254
+ if data is None:
255
+ break
256
+ cmd = data[0]
257
+ args, kwargs = data[1]
258
+ meth = getattr(self, f"_{cmd}", None)
259
+ if meth is None:
260
+ logger.warning("unknown command: %sr", cmd)
261
+ command_executed()
262
+ continue
263
+ try:
264
+ meth(*args, **kwargs)
265
+ except Exception as err:
266
+ logger.error("Failed executing cmd %r: %s", cmd, err)
267
+ finally:
268
+ command_executed()
269
+ # end while True
270
+ listener.stop()
271
+
272
+
273
+ def main_exception_hook(exc_type, exc_value, exc_traceback):
274
+ logger.exception("Fatal unhandled main-thread exception: %s",
275
+ exc_value, exc_info=(exc_type, exc_value, exc_traceback))
276
+
277
+
278
+ def thread_exception_hook(arg):
279
+ logger.exception("Fatal unhandled thread exception: %s", arg.exc_value)
280
+
281
+
282
+ def install_log_exception_hook():
283
+ sys.excepthook = main_exception_hook
284
+ threading.excepthook = thread_exception_hook
285
+
286
+
287
+ def get_root_handler():
288
+ return _root_handler
289
+
290
+
291
+ def get_console_handler() -> logging.Handler:
292
+ return _console_handler
293
+
294
+
295
+ def get_multiprocess_log_queue() -> Optional[multiprocessing.Queue]:
296
+ return _multiprocess_log_queue
297
+
298
+
299
+ def get_log_queue_listener() -> Optional[LogQueueListenerProc]:
300
+ return _queue_listener
301
+
302
+
303
+ def get_log_queue_handler():
304
+ return _queue_handler
305
+
306
+
307
+ class ThreadIdFilter(logging.Filter):
308
+
309
+ def filter(self, record):
310
+ """Inject thread_id to log records"""
311
+ if not hasattr(record, "thread_id"):
312
+ record.thread_id = threading.get_native_id()
313
+ return True
314
+
315
+
316
+ thread_id_filter = ThreadIdFilter()
317
+
318
+
319
+ class PreciseTimeFormatter(logging.Formatter):
320
+ """A logger formatter with time precision handling"""
321
+
322
+ converter = datetime.fromtimestamp
323
+
324
+ def __init__(self, *args, time_precision: int = 3, **kwargs):
325
+ self._time_precision = time_precision
326
+ super().__init__(*args, **kwargs)
327
+
328
+ def formatTime(self, record, datefmt=None):
329
+ ct = self.converter(record.created)
330
+ if not datefmt:
331
+ datefmt = DateTimeFormats.year_precise # "%Y-%m-%d %H:%M:%S.%f"
332
+ if self._time_precision > 0 and "%f" in datefmt:
333
+ msec_len = len(str(int(record.msecs)))
334
+ v = str(record.msecs).replace(".", "")
335
+ v0 = "0" * (3 - msec_len)
336
+ v = (v0 + v)[:self._time_precision]
337
+ else:
338
+ v = ""
339
+ with_dot = ".%f" in datefmt
340
+ rep = f".%f" if with_dot and self._time_precision == 0 else "%f"
341
+ datefmt = datefmt.replace(rep, v)
342
+ s = ct.strftime(datefmt)
343
+ return s
344
+
345
+
346
+ class ColoredPreciseTimeFormatter(PreciseTimeFormatter, coloredlogs.ColoredFormatter):
347
+ """A colored logger formatter with time precision handling"""
348
+
349
+
350
+ def stop_multiproc_logging():
351
+ global _multiprocess_log_queue, _queue_listener, _queue_handler, _console_handler, _already_setup
352
+
353
+ mp_log_queue = _multiprocess_log_queue
354
+ q_listener = _queue_listener
355
+ q_handler = _queue_handler
356
+
357
+ if _already_setup and q_listener is not None:
358
+ _already_setup = False
359
+ # Must remove the handler before closing it:
360
+ for handler in _base_logger.handlers:
361
+ if isinstance(handler, WithThreadIdQueueHandler):
362
+ _base_logger.removeHandler(handler)
363
+
364
+ if q_listener is not None:
365
+ # must be before following log queue close()
366
+ q_listener.stop()
367
+ _queue_listener = None
368
+
369
+ if mp_log_queue is not None:
370
+ mp_log_queue.close()
371
+ mp_log_queue.join_thread()
372
+ _multiprocess_log_queue = None
373
+
374
+ if q_handler is not None:
375
+ q_handler.close()
376
+ _queue_handler = None
377
+
378
+
379
+ def repr_all_loggers():
380
+ vals = []
381
+ for k, v in itertools.chain(dict(root=logging.root).items(), logging.Logger.manager.loggerDict.items()):
382
+ if not isinstance(v, logging.PlaceHolder):
383
+ vals.append(
384
+ f"+ [{k.ljust(20)}] {v.__class__.__name__} lvl={v.level} prop={v.propagate} disabled={v.disabled}")
385
+ for h in v.handlers:
386
+ if not isinstance(v, logging.PlaceHolder):
387
+ vals.append(f" +++ {h.name}[{h.__class__.__name__}] lvl={h.level}")
388
+ return "\n".join(vals)
389
+
390
+
391
+ def repr_logger(obj: logging.Logger):
392
+ return (f"{obj.__class__.__name__}({obj.name} lvl={obj.level} prop={obj.propagate} disabled={obj.disabled} "
393
+ f"handlers={obj.handlers})")
394
+
395
+
396
+ def repr_handler(obj: logging.Handler):
397
+ return f"{obj.__class__.__name__}({obj.name} lvl={obj.level})"
398
+
399
+
400
+ class WithThreadIdQueueListener(logging.handlers.QueueListener):
401
+
402
+ def __init__(self, *args, **kwargs):
403
+ super().__init__(*args, **kwargs)
404
+ self._2_sorter = []
405
+ self._q_2_sorter_lock = threading.Lock()
406
+ self._delay_buffer = 0.25 # aggregate records up to that age, and only output when older than that, sorted
407
+ self._recheck_delay = 0.1 # delay between re-aggregate/sorts
408
+ self._sorter_thread = threading.Thread(target=self._sorter, daemon=True)
409
+ self._sorter_thread.start()
410
+
411
+ def _get_2_sorter(self):
412
+ with self._q_2_sorter_lock:
413
+ buff = self._2_sorter
414
+ self._2_sorter = []
415
+ return buff
416
+
417
+ def _sorter(self):
418
+ buffer: List[LogRecord] = []
419
+ recheck_delay = self._recheck_delay
420
+ p_next_sort = time.perf_counter() + recheck_delay
421
+ get_created = operator.attrgetter("created")
422
+ while True:
423
+ new_recs = self._get_2_sorter()
424
+ want_quit = False
425
+ while any(r is None for r in new_recs):
426
+ want_quit = True
427
+ new_recs.remove(None)
428
+ buffer.extend(new_recs)
429
+ p_now = time.perf_counter()
430
+ if p_now < p_next_sort and not want_quit:
431
+ if len(new_recs) == 0:
432
+ time.sleep(0.005)
433
+ else:
434
+ p_next_sort = p_now + recheck_delay
435
+ buffer = sorted(buffer, key=get_created)
436
+ t_now = time.time()
437
+ idx = 0
438
+ for record in buffer:
439
+ if t_now - record.created < self._delay_buffer and not want_quit:
440
+ break
441
+ self._handle(record)
442
+ idx += 1
443
+ del buffer[:idx]
444
+ #
445
+ if want_quit:
446
+ break
447
+
448
+ def _handle(self, record):
449
+ for handler in self.handlers:
450
+ if not self.respect_handler_level:
451
+ process = True
452
+ else:
453
+ process = record.levelno >= handler.level
454
+ if process:
455
+ handler.handle(record)
456
+
457
+ def handle(self, record):
458
+ record = self.prepare(record)
459
+ with self._q_2_sorter_lock:
460
+ self._2_sorter.append(record)
461
+
462
+ def prepare(self, record):
463
+ thread_id_filter.filter(record)
464
+ return record
465
+
466
+ def stop(self):
467
+ super().stop()
468
+ with self._q_2_sorter_lock:
469
+ self._2_sorter.append(None)
470
+ self._sorter_thread.join(5)
471
+
472
+
473
+ class WithThreadIdQueueHandler(logging.handlers.QueueHandler):
474
+
475
+ def prepare(self, record):
476
+ record = super().prepare(record)
477
+ thread_id_filter.filter(record)
478
+ return record
479
+
480
+
481
+ class VerboseLoggerWithThreadId(verboselogs.VerboseLogger):
482
+ def filter(self, record):
483
+ return thread_id_filter.filter(record)
484
+
485
+
486
+ class RelayHandler(logging.Handler):
487
+
488
+ def __init__(self, listener: LogQueueListenerProc):
489
+ super().__init__()
490
+ self.listener = listener
491
+
492
+ def setLevel(self, level):
493
+ self.listener.set_handler_level(self.name, level)
494
+
495
+
496
+ class LoggerWriter:
497
+ def __init__(self, lvl):
498
+ self._lvl = lvl
499
+
500
+ def write(self, msg):
501
+ logging.root.log(self._lvl, msg)
502
+
503
+ def flush(self):
504
+ pass
505
+
506
+
507
+ def setup_logging(
508
+ name: str = "autotrainer",
509
+ *,
510
+ base_logger_name: Optional[str] = None, # i.e: "root" logger if None
511
+ logger_level: _LogLevelT = logging.NOTSET,
512
+ root_level: _LogLevelT = logging.NOTSET,
513
+ log_format: str = MULTIPROC_LOG_FORMAT,
514
+ date_format: str = DateTimeFormats.hour_time_precise,
515
+ time_precision: int = 3, # for sub seconds precision, nbr of digits after the dot.
516
+ level_styles: Optional[Dict[str, Dict[str, str]]] = None,
517
+ field_styles: Optional[Dict[str, Dict[str, str]]] = None,
518
+ multiprocess_enabled: bool = False,
519
+ fork_method: str = "spawn",
520
+ ) -> verboselogs.VerboseLogger:
521
+ global _already_setup
522
+ global _multiprocess_log_queue, _queue_listener, _queue_handler, _console_handler, _root_handler
523
+
524
+ if _already_setup:
525
+ return get_verbose_logger(name)
526
+
527
+ # actually we do not require this:
528
+ # verboselogs.install() # thx to get_verbose_logger function above.
529
+
530
+ if level_styles is None:
531
+ level_styles = DEFAULT_LEVEL_STYLES
532
+ if field_styles is None:
533
+ field_styles = DEFAULT_FIELD_STYLES
534
+ #
535
+ cfg = LogConfig(
536
+ base_logger_name=base_logger_name,
537
+ logger_level=logger_level,
538
+ root_level=root_level,
539
+ log_format=log_format,
540
+ date_format=date_format,
541
+ time_precision=time_precision,
542
+ level_styles=level_styles,
543
+ field_styles=field_styles,
544
+ # stream: TextIO = sys.stdout,
545
+ )
546
+ #
547
+ stop_multiproc_logging()
548
+ #
549
+ # pre-set these too verbose loggers level:
550
+ for _limit_name, v in _limit_loggers_level.items():
551
+ logging.getLogger(_limit_name).setLevel(v["level"])
552
+ #
553
+ base_logger = get_verbose_logger(base_logger_name)
554
+ # set the base logger level before creating possible dedicated subproc log handling:
555
+ base_logger.setLevel(root_level)
556
+ #
557
+ if multiprocess_enabled:
558
+ # using queue created using the desired fork method context:
559
+ multiproc_ctx = multiprocessing.get_context(fork_method)
560
+ _multiprocess_log_queue = log_queue = multiproc_ctx.Queue()
561
+
562
+ listener = LogQueueListenerProc(log_queue, cfg)
563
+ listener.start()
564
+ _queue_listener = listener # keep global ref to ensure it stays alive
565
+ queue_handler = WithThreadIdQueueHandler(log_queue)
566
+ queue_handler.name = "queue_handler"
567
+ _queue_handler = queue_handler # keep global ref to ensure it stays alive
568
+ root_handler = _root_handler = queue_handler
569
+ _console_handler = RelayHandler(listener)
570
+ _console_handler.name = "console_handler" # "fake" it so that it will relay to the correct handler
571
+ logging.Logger.setLevel = lambda self, lvl: listener.set_logger_level(self.name, lvl)
572
+ else:
573
+ _console_handler = console_handler = make_console_handler(cfg)
574
+ root_handler = _root_handler = console_handler
575
+
576
+ base_logger.addHandler(root_handler)
577
+
578
+ #
579
+ # recursion issue atm, could be left todo:
580
+ # sys.stdout = LoggerWriter(logging.root.info)
581
+ # sys.stderr = LoggerWriter(logging.root.warning)
582
+
583
+ # logger.debug("Setup logging ; base_logger=%s", repr_logger(base_logger))
584
+
585
+ get_verbose_logger("transitions").setLevel(logger_level)
586
+ get_verbose_logger("tools").setLevel(logger_level)
587
+ get_verbose_logger("autotrainer").setLevel(logger_level)
588
+ get_verbose_logger("inference_algorithms").setLevel(logger_level)
589
+
590
+ for _limit_name, v in _limit_loggers_level.items():
591
+ logging.getLogger(_limit_name).setLevel(v["level"])
592
+
593
+ desired_logger = get_verbose_logger(name)
594
+ desired_logger.setLevel(logger_level)
595
+
596
+ # logger.info("all loggers:\n%s", repr_all_loggers())
597
+ # logger.info("top logger: %s", repr_logger(top_logger))
598
+ # logger.info("console: level=%s", c_h.level)
599
+ # logger.info("queue: level=%s", q_h.level)
600
+ # logger.info("autotrainer: level=%s prop=%s handlers=%s",
601
+ # top_logger.level, top_logger.propagate, top_logger.handlers)
602
+
603
+ _already_setup = True
604
+
605
+ install_log_exception_hook()
606
+
607
+ return desired_logger
608
+
609
+
610
+ def set_logger_level(context: Dict[str, Union[str, int]]):
611
+ for name, value in context.items():
612
+ logging.getLogger(name).setLevel(value)
613
+
614
+
615
+ def make_log_dict_config(
616
+ *,
617
+ root_log_level: int = logging.NOTSET,
618
+ log_queue: Optional[multiprocessing.Queue] = None,
619
+ ) -> Optional[Dict]:
620
+ # usable by logging.config.dictConfig
621
+ if log_queue is None:
622
+ log_queue = get_multiprocess_log_queue()
623
+ if log_queue is None:
624
+ return None
625
+ dct_cfg = {
626
+ 'version': 1,
627
+ 'disable_existing_loggers': False,
628
+ 'handlers': {
629
+ 'queue': {
630
+ 'class': 'autotrainer.core.logging.WithThreadIdQueueHandler',
631
+ 'queue': log_queue,
632
+ 'level': logging.NOTSET, # pass everything to the listener
633
+ }
634
+ },
635
+ # root logger is here:
636
+ 'root': {
637
+ 'handlers': ['queue'],
638
+ # with its own level here:
639
+ 'level': logging.NOTSET, # root_log_level,
640
+ # FORCE NOTSET to relay everything so that file handler can properly get DEBUG as well
641
+ },
642
+ # but eventual level of other loggers have to be defined here:
643
+ 'loggers': copy.deepcopy(_limit_loggers_level),
644
+ }
645
+ return dct_cfg
646
+
647
+
648
+ def get_verbose_logger(name: Optional[str] = None) -> VerboseLoggerWithThreadId:
649
+ obj = logging.getLogger(name)
650
+ if not isinstance(obj, VerboseLoggerWithThreadId):
651
+ obj.__class__ = VerboseLoggerWithThreadId
652
+ assert isinstance(obj, VerboseLoggerWithThreadId)
653
+ return obj
654
+
655
+
656
+ def get_log_file_location(*, log_base_dir: str = "", full_format: str):
657
+ if not log_base_dir:
658
+ log_base_dir = Path.home().joinpath("Documents/RawDataLocal")
659
+ else:
660
+ log_base_dir = Path(log_base_dir)
661
+
662
+ date_stamp = datetime.now().strftime("%Y%m%d")
663
+ # pre-format with index=0, and create parent dir
664
+ log_location = Path(full_format.format(
665
+ log_location=log_base_dir,
666
+ date_stamp=date_stamp,
667
+ idx=0,
668
+ ))
669
+ log_dir = log_location.parent
670
+ try:
671
+ log_dir.mkdir(parents=True, exist_ok=True)
672
+ except Exception as err:
673
+ logger.error("Failed to create directory for log location %s: %s", log_location, err)
674
+ raise
675
+ # then check for index:
676
+ tot_prev_log_files = len(tuple(log_dir.glob("*.log")))
677
+ log_location = Path(full_format.format(
678
+ log_location=log_base_dir,
679
+ date_stamp=date_stamp,
680
+ idx=tot_prev_log_files + 1,
681
+ ))
682
+
683
+ return log_location
684
+
685
+
686
+ _prev_file_handler: Optional[logging.FileHandler] = None
687
+
688
+ def set_log_location(log_file: Path):
689
+ global _prev_file_handler
690
+ logger.verbose("Setting log file to %s", log_file)
691
+ #
692
+ q_listener = get_log_queue_listener()
693
+ if q_listener is not None:
694
+ q_listener.switch_file_handler(log_file)
695
+ else:
696
+ file_handler = logging.FileHandler(log_file)
697
+ file_handler.addFilter(thread_id_filter)
698
+ file_handler.setFormatter(
699
+ PreciseTimeFormatter(
700
+ MULTIPROC_LOG_FORMAT,
701
+ datefmt=DateTimeFormats.year_precise,
702
+ time_precision=6,
703
+ )
704
+ )
705
+ file_handler.setLevel(verboselogs.SPAM + 1) # writes everything up to DEBUG which reaches it
706
+ logging.root.addHandler(file_handler)
707
+ if _prev_file_handler is not None:
708
+ logging.root.removeHandler(_prev_file_handler)
709
+ _prev_file_handler = file_handler
710
+
711
+
712
+ # finally:
713
+
714
+ logger = get_verbose_logger(__name__) # noqa