somestuff-oepnvstuff 0.2.3__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,118 @@
1
+ """oepnvstuff — GTFS / GTFS-Realtime public-transport (ÖPNV) tooling.
2
+
3
+ Validates whether an open GTFS-Realtime feed actually carries real-time data
4
+ (delays / actual times) for configurable transit lines at a configurable
5
+ station, either as a one-shot check or as a polling watch loop with pluggable
6
+ ``on_*`` handlers (console reporting, MQTT publishing, …).
7
+
8
+ Central logging setup lives here (loguru), mirroring ucmstuff's setup. The
9
+ modules in this package (and third-party libraries such as ``requests``/
10
+ ``urllib3`` and ``paho-mqtt``) log via the stdlib ``logging`` module, so
11
+ :func:`configure_logging` installs an intercept handler that funnels those
12
+ records into loguru instead of rewriting every call site.
13
+ """
14
+
15
+ import inspect
16
+ import logging
17
+ import os
18
+ import sys
19
+ from collections.abc import Callable
20
+ from types import FrameType
21
+ from typing import TYPE_CHECKING
22
+
23
+ from loguru import logger as glogger
24
+ from tabulate import tabulate
25
+
26
+ if TYPE_CHECKING:
27
+ from loguru import Record
28
+
29
+ __version__ = "0.2.3"
30
+
31
+ __all__ = ["__version__", "configure_logging", "print_banner"]
32
+
33
+
34
+ def _loguru_skiplog_filter(record: "Record") -> bool:
35
+ """Decide whether a loguru record should be emitted.
36
+
37
+ Args:
38
+ record: The loguru record about to be handled by a sink.
39
+
40
+ Returns:
41
+ ``False`` for records whose ``extra['skiplog']`` flag is truthy (they are
42
+ hidden), ``True`` otherwise.
43
+ """
44
+ return not record.get("extra", {}).get("skiplog", False)
45
+
46
+
47
+ class _InterceptHandler(logging.Handler):
48
+ """Route stdlib ``logging`` records into loguru with correct call-site info.
49
+
50
+ Every module in this package uses ``logging.getLogger(...)``; installing this
51
+ on the root logger lets :func:`configure_logging` govern all of them (format,
52
+ level, colour) without touching the individual call sites.
53
+ """
54
+
55
+ def emit(self, record: logging.LogRecord) -> None:
56
+ """Forward a single stdlib record to loguru.
57
+
58
+ Args:
59
+ record: The stdlib log record to re-emit through loguru, preserving
60
+ its level, message, exception info and originating call site.
61
+ """
62
+ # Map the stdlib level name to a loguru level, falling back to the number.
63
+ level: str | int
64
+ try:
65
+ level = glogger.level(record.levelname).name
66
+ except ValueError:
67
+ level = record.levelno
68
+ # Walk out of the logging module so {module}/{function}/{line} point at the
69
+ # real caller rather than at this handler (canonical loguru intercept recipe).
70
+ frame: FrameType | None = inspect.currentframe()
71
+ depth = 0
72
+ while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):
73
+ frame = frame.f_back
74
+ depth += 1
75
+ glogger.bind(classname=record.name).opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
76
+
77
+
78
+ def configure_logging(
79
+ verbose: bool = False, loguru_filter: Callable[["Record"], bool] = _loguru_skiplog_filter
80
+ ) -> None:
81
+ """Configure a default ``loguru`` sink and funnel stdlib logging into it.
82
+
83
+ Args:
84
+ verbose: ``True`` logs at DEBUG, otherwise honours ``LOGURU_LEVEL`` (INFO
85
+ by default).
86
+ loguru_filter: Predicate deciding, per loguru record, whether it is emitted
87
+ by the sink. Defaults to :func:`_loguru_skiplog_filter`.
88
+ """
89
+ level_name: str = "DEBUG" if verbose else os.getenv("LOGURU_LEVEL", "INFO")
90
+ os.environ["LOGURU_LEVEL"] = level_name
91
+ glogger.remove()
92
+ logger_fmt: str = (
93
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{module}</cyan>::<cyan>{extra[classname]}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>"
94
+ )
95
+ glogger.add(sys.stderr, level=level_name, format=logger_fmt, filter=loguru_filter) # type: ignore[arg-type]
96
+ glogger.configure(extra={"classname": "None", "skiplog": False})
97
+ # Replace any stdlib handlers with the intercept so all ``logging`` output
98
+ # (this package's and third-party libraries') flows through loguru.
99
+ logging.basicConfig(handlers=[_InterceptHandler()], level=logging.getLevelNamesMapping()[level_name], force=True)
100
+
101
+
102
+ def print_banner() -> None:
103
+ """Log the startup banner with version, build time and project links."""
104
+ startup_rows = [
105
+ ["version", __version__],
106
+ ["buildtime", os.getenv("BUILDTIME", "unknown")],
107
+ ["github", "https://github.com/vroomfondel/somestuff/tree/main/somestuff/oepnvstuff"],
108
+ ["Docker Hub", "https://hub.docker.com/r/xomoxcc/somestuff"],
109
+ ]
110
+ table_str = tabulate(startup_rows, tablefmt="mixed_grid")
111
+ lines = table_str.split("\n")
112
+ table_width = len(lines[0])
113
+ title = "oepnv GTFS-realtime checker starting up"
114
+ title_border = "┍" + "━" * (table_width - 2) + "┑"
115
+ title_row = "│ " + title.center(table_width - 4) + " │"
116
+ separator = lines[0].replace("┍", "┝").replace("┑", "┥").replace("┯", "┿")
117
+
118
+ glogger.opt(raw=True).info(f"\n{title_border}\n{title_row}\n{separator}\n{'\n'.join(lines[1:])}\n")