sibi-dst 2025.8.8__py3-none-any.whl → 2025.8.9__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.
@@ -4,12 +4,12 @@ import logging
4
4
  import os
5
5
  import sys
6
6
  import time
7
- from contextlib import contextmanager, nullcontext
7
+ from contextlib import contextmanager, nullcontext, suppress
8
8
  from logging import LoggerAdapter
9
9
  from logging.handlers import RotatingFileHandler
10
- from typing import Optional, Dict, Any
10
+ from typing import Optional, Dict, Any, Union
11
11
 
12
- # OpenTelemetry (optional)
12
+ # --- OpenTelemetry (optional) ---
13
13
  try:
14
14
  from opentelemetry import trace
15
15
  from opentelemetry._logs import set_logger_provider, get_logger_provider
@@ -20,25 +20,15 @@ try:
20
20
  from opentelemetry.sdk.resources import Resource
21
21
  from opentelemetry.sdk.trace import TracerProvider
22
22
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
23
- from opentelemetry.trace import Tracer as OTelTracer
24
23
  _OTEL_AVAILABLE = True
25
24
  except Exception:
26
- # OTel is optional; keep class working without it
27
- LoggerProvider = TracerProvider = OTelTracer = object # type: ignore
28
- LoggingHandler = object # type: ignore
29
25
  _OTEL_AVAILABLE = False
30
26
 
31
27
 
32
28
  class Logger:
33
29
  """
34
30
  Process-safe logger with optional OpenTelemetry integration.
35
-
36
- Backward-compatible surface:
37
- - Logger.default_logger(...)
38
- - .debug/.info/.warning/.error/.critical
39
- - .set_level(level), .shutdown()
40
- - .bind(**extra) -> LoggerAdapter, .bound(**extra) ctx manager
41
- - .start_span(name, attributes=None), .trace_function(span_name=None)
31
+ Idempotent handler setup. No propagation to root.
42
32
  """
43
33
 
44
34
  DEBUG = logging.DEBUG
@@ -47,8 +37,8 @@ class Logger:
47
37
  ERROR = logging.ERROR
48
38
  CRITICAL = logging.CRITICAL
49
39
 
50
- # prevent attaching duplicate handlers per (logger_name, file_path) in process
51
- _handler_keys_attached: set[tuple[str, str]] = set()
40
+ # idempotency guards per process
41
+ _attached_keys: set[tuple[str, str]] = set() # (logger_name, sink_id)
52
42
  _otel_initialized_names: set[str] = set()
53
43
 
54
44
  def __init__(
@@ -56,7 +46,7 @@ class Logger:
56
46
  log_dir: str,
57
47
  logger_name: str,
58
48
  log_file: str,
59
- log_level: int = logging.DEBUG,
49
+ log_level: int = logging.INFO,
60
50
  enable_otel: bool = False,
61
51
  otel_service_name: Optional[str] = None,
62
52
  otel_stream_name: Optional[str] = None,
@@ -69,38 +59,35 @@ class Logger:
69
59
  self.log_level = log_level
70
60
 
71
61
  self.enable_otel = bool(enable_otel and _OTEL_AVAILABLE)
72
- self.otel_service_name = (otel_service_name or logger_name).strip() or "app"
62
+ self.otel_service_name = (otel_service_name or logger_name or "app").strip()
73
63
  self.otel_stream_name = (otel_stream_name or "").strip() or None
74
64
  self.otel_endpoint = otel_endpoint
75
65
  self.otel_insecure = otel_insecure
76
66
 
77
- self.logger_provider: Optional[LoggerProvider] = None
78
- self.tracer_provider: Optional[TracerProvider] = None
79
- self.tracer: Optional[OTelTracer] = None
67
+ self.logger_provider = None
68
+ self.tracer_provider = None
69
+ self.tracer = None
80
70
 
81
- self._core_logger: logging.Logger = logging.getLogger(self.logger_name)
82
- self._core_logger.setLevel(self.log_level)
83
- self._core_logger.propagate = False
71
+ self._core: logging.Logger = logging.getLogger(self.logger_name)
72
+ self._core.setLevel(self.log_level)
73
+ self._core.propagate = False
84
74
 
85
- # public handle (may be adapter)
86
- self.logger: logging.Logger | LoggerAdapter = self._core_logger
75
+ # public handle (may be LoggerAdapter)
76
+ self.logger: Union[logging.Logger, LoggerAdapter] = self._core
87
77
 
88
- self._setup_standard_handlers()
78
+ self._setup_handlers()
89
79
  if self.enable_otel:
90
- self._setup_otel_if_needed()
80
+ self._setup_otel()
91
81
 
92
- # expose adapter with default extras if OTel stream requested
93
82
  if self.enable_otel and self.otel_stream_name:
94
- attributes = {
83
+ attrs = {
95
84
  "log_stream": self.otel_stream_name,
96
85
  "log_service_name": self.otel_service_name,
97
86
  "logger_name": self.logger_name,
98
87
  }
99
- self.logger = LoggerAdapter(self._core_logger, extra=attributes)
88
+ self.logger = LoggerAdapter(self._core, extra=attrs)
100
89
 
101
- # -------------------------
102
- # Public API
103
- # -------------------------
90
+ # ---------------- Public API ----------------
104
91
 
105
92
  @classmethod
106
93
  def default_logger(
@@ -116,14 +103,11 @@ class Logger:
116
103
  otel_insecure: bool = False,
117
104
  ) -> "Logger":
118
105
  try:
119
- frame = sys._getframe(1)
120
- caller_name = frame.f_globals.get("__name__", "default_logger")
106
+ caller_name = sys._getframe(1).f_globals.get("__name__", "default_logger")
121
107
  except Exception:
122
108
  caller_name = "default_logger"
123
-
124
109
  logger_name = logger_name or caller_name
125
110
  log_file = log_file or logger_name
126
-
127
111
  return cls(
128
112
  log_dir=log_dir,
129
113
  logger_name=logger_name,
@@ -136,55 +120,11 @@ class Logger:
136
120
  otel_insecure=otel_insecure,
137
121
  )
138
122
 
139
- def shutdown(self):
140
- """Flush/close OTel providers and Python logging handlers."""
141
- try:
142
- if self.enable_otel:
143
- if isinstance(self.logger_provider, LoggerProvider):
144
- try:
145
- self._core_logger.info("Flushing OpenTelemetry logs...")
146
- self.logger_provider.force_flush()
147
- except Exception:
148
- pass
149
- try:
150
- self._core_logger.info("Shutting down OpenTelemetry logs...")
151
- self.logger_provider.shutdown()
152
- except Exception:
153
- pass
123
+ def set_level(self, level: int) -> None:
124
+ self._core.setLevel(level)
154
125
 
155
- if isinstance(self.tracer_provider, TracerProvider):
156
- try:
157
- self._core_logger.info("Flushing OpenTelemetry traces...")
158
- self.tracer_provider.force_flush()
159
- except Exception:
160
- pass
161
- try:
162
- self._core_logger.info("Shutting down OpenTelemetry traces...")
163
- self.tracer_provider.shutdown()
164
- except Exception:
165
- pass
166
- finally:
167
- # Close our handlers explicitly to release file descriptors
168
- for h in list(self._core_logger.handlers):
169
- try:
170
- h.flush()
171
- except Exception:
172
- pass
173
- try:
174
- h.close()
175
- except Exception:
176
- pass
177
- try:
178
- self._core_logger.removeHandler(h)
179
- except Exception:
180
- pass
181
- logging.shutdown()
182
-
183
- def set_level(self, level: int):
184
- self._core_logger.setLevel(level)
185
-
186
- # passthrough convenience
187
- def _log(self, level: int, msg: str, *args, **kwargs):
126
+ # passthrough
127
+ def _log(self, level: int, msg: str, *args, **kwargs) -> None:
188
128
  extra = kwargs.pop("extra", None)
189
129
  if extra is not None:
190
130
  if isinstance(self.logger, LoggerAdapter):
@@ -195,171 +135,156 @@ class Logger:
195
135
  else:
196
136
  self.logger.log(level, msg, *args, **kwargs)
197
137
 
198
- def debug(self, msg: str, *args, **kwargs): self._log(logging.DEBUG, msg, *args, **kwargs)
199
- def info(self, msg: str, *args, **kwargs): self._log(logging.INFO, msg, *args, **kwargs)
200
- def warning(self, msg: str, *args, **kwargs): self._log(logging.WARNING, msg, *args, **kwargs)
201
- def error(self, msg: str, *args, **kwargs): self._log(logging.ERROR, msg, *args, **kwargs)
202
- def critical(self, msg: str, *args, **kwargs): self._log(logging.CRITICAL, msg, *args, **kwargs)
138
+ def debug(self, msg: str, *a, **k): self._log(logging.DEBUG, msg, *a, **k)
139
+ def info(self, msg: str, *a, **k): self._log(logging.INFO, msg, *a, **k)
140
+ def warning(self, msg: str, *a, **k): self._log(logging.WARNING, msg, *a, **k)
141
+ def error(self, msg: str, *a, **k): self._log(logging.ERROR, msg, *a, **k)
142
+ def critical(self, msg: str, *a, **k): self._log(logging.CRITICAL, msg, *a, **k)
203
143
 
204
144
  def bind(self, **extra: Any) -> LoggerAdapter:
205
145
  if isinstance(self.logger, LoggerAdapter):
206
- merged = {**self.logger.extra, **extra}
207
- return LoggerAdapter(self.logger.logger, merged)
146
+ return LoggerAdapter(self.logger.logger, {**self.logger.extra, **extra})
208
147
  return LoggerAdapter(self.logger, extra)
209
148
 
210
149
  @contextmanager
211
150
  def bound(self, **extra: Any):
212
- adapter = self.bind(**extra)
213
- yield adapter
151
+ yield self.bind(**extra)
214
152
 
215
153
  def start_span(self, name: str, attributes: Optional[Dict[str, Any]] = None):
216
154
  if not (self.enable_otel and _OTEL_AVAILABLE and self.tracer):
217
- # keep API but no-op cleanly
218
- self.warning("Tracing is disabled or not initialized. Cannot start span.")
219
155
  return nullcontext()
220
-
221
156
  cm = self.tracer.start_as_current_span(name)
222
-
223
157
  class _SpanCtx:
224
158
  def __enter__(_self):
225
159
  span = cm.__enter__()
226
160
  if attributes:
227
161
  for k, v in attributes.items():
228
- try:
162
+ with suppress(Exception):
229
163
  span.set_attribute(k, v)
230
- except Exception:
231
- pass
232
164
  return span
233
-
234
- def __exit__(_self, exc_type, exc, tb):
235
- return cm.__exit__(exc_type, exc, tb)
236
-
165
+ def __exit__(_self, et, ev, tb):
166
+ return cm.__exit__(et, ev, tb)
237
167
  return _SpanCtx()
238
168
 
239
169
  def trace_function(self, span_name: Optional[str] = None):
240
- def decorator(func):
241
- def wrapper(*args, **kwargs):
170
+ def deco(func):
171
+ def wrapper(*a, **k):
242
172
  name = span_name or func.__name__
243
173
  with self.start_span(name):
244
- return func(*args, **kwargs)
174
+ return func(*a, **k)
245
175
  return wrapper
246
- return decorator
176
+ return deco
177
+
178
+ def shutdown(self) -> None:
179
+ try:
180
+ if self.enable_otel and _OTEL_AVAILABLE:
181
+ if self.logger_provider:
182
+ with suppress(Exception):
183
+ self._core.info("Flushing OpenTelemetry logs...")
184
+ self.logger_provider.force_flush()
185
+ with suppress(Exception):
186
+ self._core.info("Shutting down OpenTelemetry logs...")
187
+ self.logger_provider.shutdown()
188
+ if self.tracer_provider:
189
+ with suppress(Exception):
190
+ self._core.info("Flushing OpenTelemetry traces...")
191
+ self.tracer_provider.force_flush()
192
+ with suppress(Exception):
193
+ self._core.info("Shutting down OpenTelemetry traces...")
194
+ self.tracer_provider.shutdown()
195
+ finally:
196
+ for h in list(self._core.handlers):
197
+ with suppress(Exception): h.flush()
198
+ with suppress(Exception): h.close()
199
+ with suppress(Exception): self._core.removeHandler(h)
200
+ logging.shutdown()
247
201
 
248
- # -------------------------
249
- # Internal setup
250
- # -------------------------
202
+ # ---------------- Internal ----------------
251
203
 
252
- def _setup_standard_handlers(self):
204
+ def _setup_handlers(self) -> None:
253
205
  os.makedirs(self.log_dir, exist_ok=True)
254
206
  calling_script = os.path.splitext(os.path.basename(sys.argv[0]))[0]
255
207
  log_file_path = os.path.join(self.log_dir, f"{self.log_file}_{calling_script}.log")
256
- key = (self.logger_name, os.path.abspath(log_file_path))
208
+ file_key = (self.logger_name, os.path.abspath(log_file_path))
209
+ console_key = (self.logger_name, "__console__")
257
210
 
258
- formatter = logging.Formatter(
211
+ fmt = logging.Formatter(
259
212
  "[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s",
260
213
  datefmt="%Y-%m-%d %H:%M:%S",
261
214
  )
262
- formatter.converter = time.gmtime # UTC timestamps
263
-
264
- # attach once per process for this logger/file combo
265
- if key not in self._handler_keys_attached:
266
- file_handler = RotatingFileHandler(
267
- log_file_path, maxBytes=5 * 1024 * 1024, backupCount=5, delay=True
268
- )
269
- file_handler.setFormatter(formatter)
270
- self._core_logger.addHandler(file_handler)
215
+ fmt.converter = time.gmtime # UTC
271
216
 
272
- console_handler = logging.StreamHandler(sys.stdout)
273
- console_handler.setFormatter(formatter)
274
- self._core_logger.addHandler(console_handler)
217
+ if file_key not in self._attached_keys:
218
+ fh = RotatingFileHandler(log_file_path, maxBytes=5 * 1024 * 1024, backupCount=5, delay=True)
219
+ fh.setFormatter(fmt)
220
+ self._core.addHandler(fh)
221
+ self._attached_keys.add(file_key)
275
222
 
276
- self._handler_keys_attached.add(key)
223
+ if console_key not in self._attached_keys:
224
+ ch = logging.StreamHandler(sys.stdout)
225
+ ch.setFormatter(fmt)
226
+ self._core.addHandler(ch)
227
+ self._attached_keys.add(console_key)
277
228
 
278
229
  def _normalize_otlp_endpoint(self, ep: str) -> str:
279
230
  if "://" not in ep:
280
231
  ep = ("http://" if self.otel_insecure else "https://") + ep
281
232
  return ep
282
233
 
283
- def _setup_otel_if_needed(self):
284
- """
285
- Initialize OTel once per logger_name within the process to avoid
286
- clobbering providers (important under reloaders).
287
- """
234
+ def _setup_otel(self) -> None:
288
235
  if not _OTEL_AVAILABLE:
289
- self._core_logger.warning("OpenTelemetry not available — skipping OTel setup.")
236
+ self._core.warning("OpenTelemetry not available — skipping OTel setup.")
290
237
  return
291
238
  if self.logger_name in self._otel_initialized_names:
292
- # already initialized for this logger name in this process
293
- self.tracer = trace.get_tracer(self.logger_name)
239
+ with suppress(Exception):
240
+ self.tracer = trace.get_tracer(self.logger_name)
294
241
  return
295
242
 
296
- # Create resources
297
- resource_attrs = {
298
- "service.name": self.otel_service_name,
299
- "logger.name": self.logger_name,
300
- }
243
+ # resources
244
+ attrs = {"service.name": self.otel_service_name, "logger.name": self.logger_name}
301
245
  if self.otel_stream_name:
302
- resource_attrs["log.stream"] = self.otel_stream_name
303
- resource = Resource.create(resource_attrs)
246
+ attrs["log.stream"] = self.otel_stream_name
247
+ resource = Resource.create(attrs)
304
248
 
305
- # Respect any existing providers to avoid breaking apps that configured OTel elsewhere
249
+ # providers (reuse if already set globally)
306
250
  existing_lp = None
307
- try:
251
+ with suppress(Exception):
308
252
  existing_lp = get_logger_provider()
309
- except Exception:
310
- pass
311
-
312
- if not isinstance(existing_lp, LoggerProvider):
253
+ if getattr(existing_lp, "add_log_record_processor", None):
254
+ self.logger_provider = existing_lp
255
+ else:
313
256
  self.logger_provider = LoggerProvider(resource=resource)
314
257
  set_logger_provider(self.logger_provider)
315
- else:
316
- # reuse existing; don’t overwrite global provider
317
- self.logger_provider = existing_lp # type: ignore
318
258
 
319
259
  existing_tp = None
320
- try:
260
+ with suppress(Exception):
321
261
  existing_tp = trace.get_tracer_provider()
322
- except Exception:
323
- pass
324
-
325
- if not isinstance(existing_tp, TracerProvider):
262
+ if getattr(existing_tp, "add_span_processor", None):
263
+ self.tracer_provider = existing_tp
264
+ else:
326
265
  self.tracer_provider = TracerProvider(resource=resource)
327
266
  trace.set_tracer_provider(self.tracer_provider)
328
- else:
329
- self.tracer_provider = existing_tp # type: ignore
330
267
 
331
268
  endpoint = self._normalize_otlp_endpoint(self.otel_endpoint)
332
269
 
333
- # Logs exporter + processor (only if we created our own provider)
270
+ # exporters/processors (only if we own the providers we created above)
334
271
  if isinstance(self.logger_provider, LoggerProvider):
335
- try:
272
+ with suppress(Exception):
336
273
  log_exporter = OTLPLogExporter(endpoint=endpoint, insecure=self.otel_insecure)
337
274
  self.logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
338
- except Exception as e:
339
- self._core_logger.warning(f"Failed to attach OTel log exporter: {e}")
340
275
 
341
- # Traces exporter + processor (only if we created our own provider)
342
276
  if isinstance(self.tracer_provider, TracerProvider):
343
- try:
277
+ with suppress(Exception):
344
278
  span_exporter = OTLPSpanExporter(endpoint=endpoint, insecure=self.otel_insecure)
345
279
  self.tracer_provider.add_span_processor(BatchSpanProcessor(span_exporter))
346
- except Exception as e:
347
- self._core_logger.warning(f"Failed to attach OTel span exporter: {e}")
348
-
349
- # Attach OTel LoggingHandler once
350
- if not any(type(h).__name__ == "LoggingHandler" for h in self._core_logger.handlers):
351
- try:
352
- otel_handler = LoggingHandler(level=logging.NOTSET, logger_provider=self.logger_provider) # type: ignore
353
- self._core_logger.addHandler(otel_handler)
354
- except Exception as e:
355
- self._core_logger.warning(f"Failed to attach OTel logging handler: {e}")
356
-
357
- # Tracer handle
358
- try:
280
+
281
+ # attach OTel log handler once
282
+ if not any(type(h).__name__ == "LoggingHandler" for h in self._core.handlers):
283
+ with suppress(Exception):
284
+ self._core.addHandler(LoggingHandler(level=logging.NOTSET, logger_provider=self.logger_provider)) # type: ignore
285
+
286
+ with suppress(Exception):
359
287
  self.tracer = trace.get_tracer(self.logger_name)
360
- except Exception:
361
- self.tracer = None
362
288
 
363
289
  self._otel_initialized_names.add(self.logger_name)
364
- self._core_logger.info("OpenTelemetry logging/tracing initialized.")
365
-
290
+ self._core.info("OpenTelemetry logging/tracing initialized.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sibi-dst
3
- Version: 2025.8.8
3
+ Version: 2025.8.9
4
4
  Summary: Data Science Toolkit
5
5
  Author: Luis Valverde
6
6
  Author-email: lvalverdeb@gmail.com
@@ -54,7 +54,7 @@ sibi_dst/utils/file_age_checker.py,sha256=44B3lwH_PLwzMfiKkgvJKjKx-qSgITIXxKfNbd
54
54
  sibi_dst/utils/file_utils.py,sha256=cm__02IKCfEOzAKAZwdNIjnRL8H4XtPa6hKcj510pto,1310
55
55
  sibi_dst/utils/filepath_generator.py,sha256=Ke_OwBjLJkNMeOP0QjbLIZpSMkzhAIxKyf4hZ5P5re0,12916
56
56
  sibi_dst/utils/iceberg_saver.py,sha256=l1UWJWrLqe2OxCdP1mRyXlG9It1-F3MN_ZvHPmxqRJ4,5253
57
- sibi_dst/utils/log_utils.py,sha256=vhShRtCklQgVx9j7ustG6-r1bOyfgVTyHktOC_hmw5Y,14283
57
+ sibi_dst/utils/log_utils.py,sha256=1xXTDfwMwWIdj37hjyXSpHx3ft2GMiXsAfxq9AArMTY,11588
58
58
  sibi_dst/utils/manifest_manager.py,sha256=9y4cV-Ig8O-ekhApp_UObTY-cTsl-bGnvKIThItEzg4,7394
59
59
  sibi_dst/utils/parquet_saver.py,sha256=XUDLpMRqkKvBTdUhckhRzQyyLSaI9q5iCqcmeyHc-0Q,9609
60
60
  sibi_dst/utils/periods.py,sha256=8eTGi-bToa6_a8Vwyg4fkBPryyzft9Nzy-3ToxjqC8c,1434
@@ -87,6 +87,6 @@ sibi_dst/v2/df_helper/core/_params_config.py,sha256=DYx2drDz3uF-lSPzizPkchhy-kxR
87
87
  sibi_dst/v2/df_helper/core/_query_config.py,sha256=Y8LVSyaKuVkrPluRDkQoOwuXHQxner1pFWG3HPfnDHM,441
88
88
  sibi_dst/v2/utils/__init__.py,sha256=6H4cvhqTiFufnFPETBF0f8beVVMpfJfvUs6Ne0TQZNY,58
89
89
  sibi_dst/v2/utils/log_utils.py,sha256=rfk5VsLAt-FKpv6aPTC1FToIPiyrnHAFFBAkHme24po,4123
90
- sibi_dst-2025.8.8.dist-info/METADATA,sha256=I6-rFEYpwCpVnxyplZCFR39982IX0FjQ_8GUusW1OqA,2671
91
- sibi_dst-2025.8.8.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
92
- sibi_dst-2025.8.8.dist-info/RECORD,,
90
+ sibi_dst-2025.8.9.dist-info/METADATA,sha256=euFCqxH_OCKd1_56ino8Dg9k9FfZszwrNf-8wnJSX4s,2671
91
+ sibi_dst-2025.8.9.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
92
+ sibi_dst-2025.8.9.dist-info/RECORD,,