sibi-dst 2025.8.1__py3-none-any.whl → 2025.8.2__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.
sibi_dst/utils/base.py CHANGED
@@ -10,8 +10,9 @@ from sibi_dst.utils import Logger
10
10
 
11
11
  class ManagedResource(abc.ABC):
12
12
  """
13
- Boilerplate ABC for components that manage a logger and an fsspec filesystem
14
- with sync/async lifecycle helpers.
13
+ Boilerplate ABC for components that manage a logger and an optional fsspec filesystem,
14
+ with sync/async lifecycle helpers, lazy FS creation via an optional factory, and
15
+ configurable cleanup-error logging.
15
16
  """
16
17
 
17
18
  def __init__(
@@ -19,46 +20,60 @@ class ManagedResource(abc.ABC):
19
20
  *,
20
21
  verbose: bool = False,
21
22
  debug: bool = False,
23
+ log_cleanup_errors: bool = True,
22
24
  logger: Optional[Logger] = None,
23
25
  fs: Optional[fsspec.AbstractFileSystem] = None,
24
26
  fs_factory: Optional[Callable[[], fsspec.AbstractFileSystem]] = None,
25
27
  **_: object,
26
28
  ) -> None:
29
+ # ---- Declared upfront for type checkers
30
+ self.logger: Logger
31
+ self.fs: Optional[fsspec.AbstractFileSystem] = None
32
+ self._fs_factory: Optional[Callable[[], fsspec.AbstractFileSystem]] = None
33
+ self._owns_logger: bool = False
34
+ self._owns_fs: bool = False
35
+ self._is_closed: bool = False
36
+ self._closing: bool = False
37
+ self._close_lock = threading.RLock()
38
+
27
39
  self.verbose = verbose
28
40
  self.debug = debug
41
+ self._log_cleanup_errors = log_cleanup_errors
29
42
 
30
- # --- Logger ownership ---
43
+ # ---- Logger ownership
31
44
  if logger is None:
32
45
  self.logger = Logger.default_logger(logger_name=self.__class__.__name__)
33
46
  self._owns_logger = True
34
- self.logger.set_level(Logger.DEBUG if self.debug else Logger.INFO)
47
+ level = Logger.DEBUG if self.debug else (Logger.INFO if self.verbose else Logger.WARNING)
48
+ self.logger.set_level(level)
35
49
  else:
36
50
  self.logger = logger
37
- self._owns_logger = False
38
- # Do NOT mutate external logger level
51
+ self._owns_logger = False # do not mutate external logger
39
52
 
40
- # --- FS ownership ---
41
- self._owns_fs = fs is None
53
+ # ---- FS ownership & lazy creation
42
54
  if fs is not None:
43
- self.fs: Optional[fsspec.AbstractFileSystem] = fs
55
+ self.fs = fs
56
+ self._owns_fs = False
57
+ self._fs_factory = None
44
58
  elif fs_factory is not None:
45
- created = fs_factory()
46
- if not isinstance(created, fsspec.AbstractFileSystem):
47
- raise TypeError(
48
- f"fs_factory() must return fsspec.AbstractFileSystem, got {type(created)!r}"
49
- )
50
- self.fs = created
59
+ # Lazy: don't create until first use
60
+ self._fs_factory = fs_factory
61
+ self._owns_fs = True # we will own it *if* created
62
+ self.fs = None
51
63
  else:
52
- self.fs = None # optional; subclasses may not need fs
64
+ self.fs = None
65
+ self._owns_fs = False
66
+ self._fs_factory = None
53
67
 
54
- self._is_closed = False
55
- self._close_lock = threading.RLock()
68
+ # Register a GC-time finalizer that does not capture self
69
+ self_ref = weakref.ref(self)
70
+ self._finalizer = weakref.finalize(self, self._finalize_static, self_ref)
56
71
 
57
- # register a best-effort finalizer
58
- self._finalizer = weakref.finalize(self, self._finalize_silent)
59
-
60
- # Early debug
61
- self.logger.debug("Component %s initialized.", self.__class__.__name__)
72
+ if self.debug:
73
+ try:
74
+ self.logger.debug("Component %s initialized. %s", self.__class__.__name__, repr(self))
75
+ except Exception:
76
+ pass
62
77
 
63
78
  # ---------- Introspection ----------
64
79
  @property
@@ -72,10 +87,17 @@ class ManagedResource(abc.ABC):
72
87
  def __repr__(self) -> str:
73
88
  class_name = self.__class__.__name__
74
89
  logger_status = "own" if self._owns_logger else "external"
75
- fs_status = "none" if self.fs is None else ("own" if self._owns_fs else "external")
76
- return f"<{class_name} debug={self.debug} logger={logger_status} fs={fs_status}>"
90
+ if self.fs is None and self._fs_factory is not None:
91
+ fs_status = "own(lazy)"
92
+ elif self.fs is None:
93
+ fs_status = "none"
94
+ else:
95
+ fs_status = "own" if self._owns_fs else "external"
96
+ return (f"<{class_name} debug={self.debug} verbose={self.verbose} "
97
+ f"log_cleanup_errors={self._log_cleanup_errors} "
98
+ f"logger={logger_status} fs={fs_status}>")
77
99
 
78
- # ---------- Hooks for subclasses ----------
100
+ # ---------- Subclass hooks ----------
79
101
  def _cleanup(self) -> None:
80
102
  """Sync cleanup for resources created BY THE SUBCLASS."""
81
103
  return
@@ -84,82 +106,111 @@ class ManagedResource(abc.ABC):
84
106
  """Async cleanup for resources created BY THE SUBCLASS."""
85
107
  return
86
108
 
87
- # ---------- Owned resource shutdown ----------
88
- def _shutdown_logger(self) -> None:
89
- if not self._owns_logger:
90
- self.logger.debug("%s: skipping logger shutdown (not owned).", self.__class__.__name__)
91
- return
92
- self.logger.debug("%s: shutting down owned logger.", self.__class__.__name__)
93
- try:
94
- self.logger.shutdown()
95
- except Exception: # keep shutdown robust
96
- pass
97
-
98
- def _shutdown_owned_resources(self) -> None:
99
- # fsspec FS usually has no close; if it does, call it.
100
- if self._owns_fs and self.fs is not None:
101
- self.logger.debug("%s: releasing owned fsspec filesystem.", self.__class__.__name__)
102
- close = getattr(self.fs, "close", None)
103
- try:
104
- if callable(close):
105
- close()
106
- finally:
107
- self.fs = None
108
- else:
109
- self.logger.debug(
110
- "%s: skipping fs shutdown (not owned or none).", self.__class__.__name__
109
+ # ---------- FS helpers ----------
110
+ def _ensure_fs(self) -> Optional[fsspec.AbstractFileSystem]:
111
+ """Create the FS lazily if a factory was provided. Return fs (or None)."""
112
+ if self.fs is None and self._fs_factory is not None:
113
+ created = self._fs_factory()
114
+ if not isinstance(created, fsspec.AbstractFileSystem):
115
+ raise TypeError(f"fs_factory() must return fsspec.AbstractFileSystem, got {type(created)!r}")
116
+ self.fs = created
117
+ # _owns_fs already True when factory is present
118
+ return self.fs
119
+
120
+ def require_fs(self) -> fsspec.AbstractFileSystem:
121
+ """Return a filesystem or raise if not configured/creatable."""
122
+ fs = self._ensure_fs()
123
+ if fs is None:
124
+ raise RuntimeError(
125
+ f"{self.__class__.__name__}: filesystem is required but not configured"
111
126
  )
112
- self._shutdown_logger()
127
+ return fs
128
+
129
+ # ---------- Shared shutdown helpers (no logging; safe for late shutdown) ----------
130
+ def _release_owned_fs(self) -> None:
131
+ if self._owns_fs:
132
+ # ensure creation state is respected even if never used
133
+ _ = self.fs or None # no-op; if never created, nothing to close
134
+ if self.fs is not None:
135
+ close = getattr(self.fs, "close", None)
136
+ try:
137
+ if callable(close):
138
+ close()
139
+ finally:
140
+ self.fs = None
113
141
 
114
- async def _ashutdown_owned_resources(self) -> None:
115
- # No async close in fsspec by default, keep parity with sync
116
- if self._owns_fs and self.fs is not None:
117
- self.logger.debug("%s: releasing owned fsspec filesystem (async).", self.__class__.__name__)
118
- close = getattr(self.fs, "close", None)
142
+ def _shutdown_logger(self) -> None:
143
+ if self._owns_logger:
119
144
  try:
120
- if callable(close):
121
- close()
122
- finally:
123
- self.fs = None
145
+ self.logger.shutdown()
146
+ except Exception:
147
+ pass
148
+
149
+ def _shutdown_owned_resources(self) -> None:
150
+ self._release_owned_fs()
124
151
  self._shutdown_logger()
125
152
 
126
- # ---------- Public lifecycle ----------
153
+ # ---------- Public lifecycle (sync) ----------
127
154
  def close(self) -> None:
128
155
  with self._close_lock:
129
- if self._is_closed:
156
+ if self._is_closed or self._closing:
130
157
  return
131
- self.logger.debug("Closing component %s...", self.__class__.__name__)
132
- try:
133
- self._cleanup()
134
- except Exception:
135
- # log and propagate — callers need to know
136
- self.logger.error(
137
- "Error during %s._cleanup()", self.__class__.__name__, exc_info=True
138
- )
139
- raise
140
- finally:
141
- self._is_closed = True
142
- self._shutdown_owned_resources()
143
- self.logger.debug("Component %s closed.", self.__class__.__name__)
158
+ self._closing = True
144
159
 
160
+ try:
161
+ self._cleanup()
162
+ except Exception:
163
+ # Only include traceback when debug=True
164
+ if self._log_cleanup_errors:
165
+ try:
166
+ self.logger.error(
167
+ "Error during %s._cleanup()", self.__class__.__name__,
168
+ exc_info=self.debug
169
+ )
170
+ except Exception:
171
+ pass
172
+ raise
173
+ finally:
174
+ with self._close_lock:
175
+ self._is_closed = True
176
+ self._closing = False
177
+ self._shutdown_owned_resources()
178
+ if self.debug:
179
+ try:
180
+ self.logger.debug("Component %s closed.", self.__class__.__name__)
181
+ except Exception:
182
+ pass
183
+
184
+ # ---------- Public lifecycle (async) ----------
145
185
  async def aclose(self) -> None:
146
186
  with self._close_lock:
147
- if self._is_closed:
187
+ if self._is_closed or self._closing:
148
188
  return
149
- self.logger.debug("Asynchronously closing component %s...", self.__class__.__name__)
150
- # run subclass async cleanup outside of lock
189
+ self._closing = True
190
+
151
191
  try:
152
192
  await self._acleanup()
153
193
  except Exception:
154
- self.logger.error(
155
- "Error during %s._acleanup()", self.__class__.__name__, exc_info=True
156
- )
194
+ # Only include traceback when debug=True
195
+ if self._log_cleanup_errors:
196
+ try:
197
+ self.logger.error(
198
+ "Error during %s._acleanup()", self.__class__.__name__,
199
+ exc_info=self.debug
200
+ )
201
+ except Exception:
202
+ pass
157
203
  raise
158
204
  finally:
159
205
  with self._close_lock:
160
206
  self._is_closed = True
161
- await self._ashutdown_owned_resources()
162
- self.logger.debug("Async component %s closed.", self.__class__.__name__)
207
+ self._closing = False
208
+ self._shutdown_owned_resources()
209
+ if self.debug:
210
+ try:
211
+ self.logger.debug("Async component %s closed.", self.__class__.__name__)
212
+ except Exception:
213
+ pass
163
214
 
164
215
  # ---------- Context managers ----------
165
216
  def __enter__(self) -> Self:
@@ -176,148 +227,26 @@ class ManagedResource(abc.ABC):
176
227
  await self.aclose()
177
228
  return False
178
229
 
179
- # ---------- Finalizer ----------
180
- def _finalize_silent(self) -> None:
181
- # Best-effort, no logging (avoid noisy GC-time logs).
230
+ # ---------- Finalizer ( at Garbage Collection-time absolutely silent) ----------
231
+ @staticmethod
232
+ def _finalize_static(ref: "weakref.ReferenceType[ManagedResource]") -> None:
233
+ obj = ref()
234
+ if obj is None:
235
+ return
236
+ # No logging here; interpreter may be tearing down.
237
+ # Best-effort silent cleanup; avoid locks and context managers.
182
238
  try:
183
- if not self._is_closed:
184
- self.close()
239
+ if not obj._is_closed:
240
+ try:
241
+ obj._cleanup()
242
+ except Exception:
243
+ pass
244
+ obj._is_closed = True
245
+ try:
246
+ obj._shutdown_owned_resources()
247
+ except Exception:
248
+ pass
185
249
  except Exception:
186
- # absolutely swallow GC context
250
+ # do not show anything at garbage collection time
187
251
  pass
188
252
 
189
- # import abc
190
- # from typing import Self, Optional, Callable, Any
191
- #
192
- # import fsspec
193
- #
194
- # from sibi_dst.utils import Logger
195
- #
196
- #
197
- # class ManagedResource(abc.ABC):
198
- # """
199
- # A unified boilerplate ABC for creating manageable components.
200
- #
201
- # It provides integrated ownership and lifecycle management for a custom
202
- # logger and a fsspec filesystem client, with full async support.
203
- # """
204
- #
205
- # def __init__(
206
- # self,
207
- # *,
208
- # verbose: bool = False,
209
- # debug: bool = False,
210
- # logger: Optional[Logger] = None,
211
- # fs: Optional[fsspec.AbstractFileSystem] = None,
212
- # fs_factory: Optional[Callable[[], Any]] = None,
213
- # **kwargs: Any,
214
- # ) -> None:
215
- # self.debug = debug
216
- # self.verbose = verbose
217
- #
218
- # self._is_closed = False
219
- # self._owns_logger: bool
220
- # self.fs, self._owns_fs = (fs, False) if fs else (None, True)
221
- # if self._owns_fs and fs_factory:
222
- # self.fs = fs_factory
223
- # self.logger, self._owns_logger = (logger, False) if logger else (
224
- # Logger.default_logger(logger_name=f"{self.__class__.__name__}"), True)
225
- # self.logger.set_level(Logger.DEBUG if self.debug else Logger.INFO)
226
- # self.logger.debug(f"Component: {self.__class__.__name__} initialized.")
227
- #
228
- # @property
229
- # def is_closed(self) -> bool:
230
- # return self._is_closed
231
- #
232
- # # Private methods for cleanup in the subclass
233
- # def _cleanup(self) -> None:
234
- # """Cleanup for resources created BY THE SUBCLASS."""
235
- # pass
236
- #
237
- # async def _acleanup(self) -> None:
238
- # """Async cleanup for resources created BY THE SUBCLASS."""
239
- # pass
240
- #
241
- # # --- Private Shutdown Helpers ---
242
- # def _shutdown_logger(self) -> None:
243
- # # Your provided logger shutdown logic
244
- # if not self._owns_logger:
245
- # self.logger.debug(f"{self.__class__.__name__} is skipping logger shutdown (not owned).")
246
- # return
247
- # self.logger.debug(f"{self.__class__.__name__} is shutting down self-managed logger.")
248
- # self.logger.shutdown()
249
- #
250
- # def _shutdown_owned_resources(self) -> None:
251
- # if self._owns_fs and isinstance(self.fs, fsspec.AbstractFileSystem):
252
- # self.logger.debug(f"{self.__class__.__name__} is shutting down self-managed fsspec client synchronously.")
253
- # del self.fs
254
- # else:
255
- # self.logger.debug(
256
- # f"{self.__class__.__name__} is skipping fsspec client shutdown (not owned or not an fsspec client).")
257
- # self._shutdown_logger()
258
- #
259
- # async def _ashutdown_owned_resources(self) -> None:
260
- # """Internal method to shut down all owned resources ASYNCHRONOUSLY."""
261
- #
262
- # if self._owns_fs and isinstance(self.fs, fsspec.AbstractFileSystem):
263
- # self.logger.debug(f"{self.__class__.__name__} is shutting down self-managed fsspec client asynchronously.")
264
- # del self.fs
265
- #
266
- # self._shutdown_logger()
267
- #
268
- # # Methods for Cleanup ---
269
- # def close(self) -> None:
270
- # if self._is_closed: return
271
- # self.logger.debug(f"Closing component...{self.__class__.__name__}")
272
- # try:
273
- # self._cleanup()
274
- # except Exception as e:
275
- # self.logger.error(f"Error during subclass {self.__class__.__name__} cleanup: {e}", exc_info=True)
276
- # raise
277
- # finally:
278
- # self._is_closed = True
279
- # self._shutdown_owned_resources()
280
- # self.logger.debug(f"Component {self.__class__.__name__} closed successfully.")
281
- #
282
- # async def aclose(self) -> None:
283
- # if self._is_closed: return
284
- # self.logger.debug(f"Asynchronously closing component...{self.__class__.__name__}")
285
- # try:
286
- # await self._acleanup()
287
- # except Exception as e:
288
- # self.logger.error(f"Error during async subclass cleanup: {e}", exc_info=True)
289
- # raise
290
- # finally:
291
- # self._is_closed = True
292
- # await self._ashutdown_owned_resources()
293
- # self.logger.debug(f"Async Component {self.__class__.__name__} closed successfully.")
294
- #
295
- # def __repr__(self) -> str:
296
- # """Return a string representation of the ManagedResource."""
297
- # # Dynamically get the name of the class or subclass
298
- # class_name = self.__class__.__name__
299
- #
300
- # # Determine the status of the logger and filesystem
301
- # logger_status = "own" if self._owns_logger else "external"
302
- # fs_status = "own" if self._owns_fs else "external"
303
- # return (
304
- # f"<{class_name} debug={self.debug}, "
305
- # f"logger='{logger_status}', fs='{fs_status}'>"
306
- # )
307
- #
308
- # # --- Context Management and Destruction ---
309
- # def __enter__(self) -> Self:
310
- # return self
311
- #
312
- # def __exit__(self, *args) -> None:
313
- # self.close()
314
- #
315
- # async def __aenter__(self) -> Self:
316
- # return self
317
- #
318
- # async def __aexit__(self, *args) -> None:
319
- # await self.aclose()
320
- #
321
- # def __del__(self) -> None:
322
- # if not self._is_closed:
323
- # self.logger.critical(f"CRITICAL: Component {self!r} was not closed properly.")