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/df_helper/__init__.py +3 -2
- sibi_dst/df_helper/_artifact_updater_async.py +238 -0
- sibi_dst/df_helper/_artifact_updater_threaded.py +195 -0
- sibi_dst/df_helper/_df_helper.py +1 -1
- sibi_dst/df_helper/_parquet_artifact.py +24 -4
- sibi_dst/df_helper/_parquet_reader.py +9 -10
- sibi_dst/utils/__init__.py +2 -0
- sibi_dst/utils/base.py +153 -224
- sibi_dst/utils/business_days.py +248 -0
- sibi_dst/utils/data_wrapper.py +166 -106
- sibi_dst/utils/date_utils.py +711 -394
- sibi_dst/utils/file_age_checker.py +301 -0
- sibi_dst/utils/periods.py +42 -0
- sibi_dst/utils/update_planner.py +2 -2
- {sibi_dst-2025.8.1.dist-info → sibi_dst-2025.8.2.dist-info}/METADATA +1 -1
- {sibi_dst-2025.8.1.dist-info → sibi_dst-2025.8.2.dist-info}/RECORD +17 -13
- sibi_dst/df_helper/_artifact_updater_multi_wrapper.py +0 -315
- {sibi_dst-2025.8.1.dist-info → sibi_dst-2025.8.2.dist-info}/WHEEL +0 -0
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
|
-
#
|
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.
|
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
|
-
#
|
41
|
-
self._owns_fs = fs is None
|
53
|
+
# ---- FS ownership & lazy creation
|
42
54
|
if fs is not None:
|
43
|
-
self.fs
|
55
|
+
self.fs = fs
|
56
|
+
self._owns_fs = False
|
57
|
+
self._fs_factory = None
|
44
58
|
elif fs_factory is not None:
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
64
|
+
self.fs = None
|
65
|
+
self._owns_fs = False
|
66
|
+
self._fs_factory = None
|
53
67
|
|
54
|
-
|
55
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
76
|
-
|
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
|
-
# ----------
|
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
|
-
# ----------
|
88
|
-
def
|
89
|
-
if
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
self.
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
def
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
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
|
-
|
115
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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.
|
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.
|
150
|
-
|
189
|
+
self._closing = True
|
190
|
+
|
151
191
|
try:
|
152
192
|
await self._acleanup()
|
153
193
|
except Exception:
|
154
|
-
|
155
|
-
|
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
|
-
|
162
|
-
self.
|
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
|
-
|
181
|
-
|
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
|
184
|
-
|
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
|
-
#
|
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.")
|