thds.core 1.47.20251024053252__py3-none-any.whl → 1.47.20251024204437__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.
Potentially problematic release.
This version of thds.core might be problematic. Click here for more details.
- thds/core/inspect.py +26 -1
- thds/core/log/__init__.py +1 -1
- thds/core/log/kw_logger.py +10 -0
- thds/core/parallel.py +38 -23
- {thds_core-1.47.20251024053252.dist-info → thds_core-1.47.20251024204437.dist-info}/METADATA +1 -1
- {thds_core-1.47.20251024053252.dist-info → thds_core-1.47.20251024204437.dist-info}/RECORD +9 -9
- {thds_core-1.47.20251024053252.dist-info → thds_core-1.47.20251024204437.dist-info}/WHEEL +0 -0
- {thds_core-1.47.20251024053252.dist-info → thds_core-1.47.20251024204437.dist-info}/entry_points.txt +0 -0
- {thds_core-1.47.20251024053252.dist-info → thds_core-1.47.20251024204437.dist-info}/top_level.txt +0 -0
thds/core/inspect.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import typing as ty
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from types import ModuleType
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
@dataclass(frozen=True)
|
|
@@ -32,7 +33,7 @@ def get_caller_info(skip: int = 2) -> CallerInfo:
|
|
|
32
33
|
start = 0 + skip
|
|
33
34
|
if len(stack) < start + 1:
|
|
34
35
|
raise RuntimeError(f"The stack has less than f{skip} + 1 frames in it.")
|
|
35
|
-
parentframe = stack[start]
|
|
36
|
+
parentframe = stack[start].frame
|
|
36
37
|
|
|
37
38
|
# full dotted name of caller module
|
|
38
39
|
module_info = inspect.getmodule(parentframe)
|
|
@@ -68,3 +69,27 @@ def bind_arguments(
|
|
|
68
69
|
|
|
69
70
|
def get_argument(arg_name: str, bound_arguments: inspect.BoundArguments) -> ty.Any:
|
|
70
71
|
return bound_arguments.arguments[arg_name]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def yield_caller_modules_and_frames(*skip: str) -> ty.Iterator[tuple[ModuleType, inspect.FrameInfo]]:
|
|
75
|
+
"""Yields caller modules and their frame info, skipping any modules in the skip list."""
|
|
76
|
+
stack = inspect.stack()
|
|
77
|
+
skip = set(skip) | {__name__} # type: ignore
|
|
78
|
+
for frame_info in stack[1:]: # don't bother with the current frame, obviously
|
|
79
|
+
module = inspect.getmodule(frame_info.frame)
|
|
80
|
+
if module:
|
|
81
|
+
module_name = module.__name__
|
|
82
|
+
if module_name not in skip:
|
|
83
|
+
yield module, frame_info
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def caller_module_name(*skip: str) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Find the first caller module that is not in the skip list.
|
|
89
|
+
:param skip: module names to skip
|
|
90
|
+
:return: the first caller module name not in skip, or empty string if no module can be found
|
|
91
|
+
"""
|
|
92
|
+
for module, _frame in yield_caller_modules_and_frames(*skip):
|
|
93
|
+
return module.__name__
|
|
94
|
+
|
|
95
|
+
return "" # this is trivially distinguishable from a module name, so no need to force people to handle None
|
thds/core/log/__init__.py
CHANGED
|
@@ -26,4 +26,4 @@ logger.info("testing 5")
|
|
|
26
26
|
|
|
27
27
|
from .basic_config import DuplicateFilter, set_logger_to_console_level # noqa: F401
|
|
28
28
|
from .kw_formatter import ThdsCompactFormatter # noqa: F401
|
|
29
|
-
from .kw_logger import KwLogger, getLogger, logger_context, make_th_formatters_safe # noqa: F401
|
|
29
|
+
from .kw_logger import KwLogger, auto, getLogger, logger_context, make_th_formatters_safe # noqa: F401
|
thds/core/log/kw_logger.py
CHANGED
|
@@ -80,6 +80,16 @@ def getLogger(name: Optional[str] = None) -> logging.LoggerAdapter:
|
|
|
80
80
|
return KwLogger(logging.getLogger(name), dict())
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def auto(*skip: str) -> logging.LoggerAdapter:
|
|
84
|
+
from .. import inspect
|
|
85
|
+
|
|
86
|
+
module_name = inspect.caller_module_name(__name__, *skip)
|
|
87
|
+
if not module_name:
|
|
88
|
+
raise ValueError("Cannot automatically determine caller module name for logger.")
|
|
89
|
+
|
|
90
|
+
return getLogger(module_name)
|
|
91
|
+
|
|
92
|
+
|
|
83
93
|
def make_th_formatters_safe(logger: logging.Logger):
|
|
84
94
|
"""Non-adapted loggers may still run into our root format string,
|
|
85
95
|
which expects _TH_REC_CTXT to be present on every LogRecord.
|
thds/core/parallel.py
CHANGED
|
@@ -8,7 +8,7 @@ from collections import defaultdict
|
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from uuid import uuid4
|
|
10
10
|
|
|
11
|
-
from thds.core import concurrency, config, files, log
|
|
11
|
+
from thds.core import concurrency, config, files, inspect, log
|
|
12
12
|
|
|
13
13
|
PARALLEL_OFF = config.item("off", default=False, parse=config.tobool)
|
|
14
14
|
# if you want to simplify a stack trace, this may be your friend
|
|
@@ -17,9 +17,6 @@ R = ty.TypeVar("R")
|
|
|
17
17
|
T_co = ty.TypeVar("T_co", covariant=True)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
logger = log.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
20
|
class IterableWithLen(ty.Protocol[T_co]):
|
|
24
21
|
def __iter__(self) -> ty.Iterator[T_co]: ... # pragma: no cover
|
|
25
22
|
|
|
@@ -69,13 +66,21 @@ class Error:
|
|
|
69
66
|
H = ty.TypeVar("H", bound=ty.Hashable)
|
|
70
67
|
|
|
71
68
|
|
|
69
|
+
def _get_caller_logger(named: str) -> ty.Callable[[str], ty.Any]:
|
|
70
|
+
module_name = inspect.caller_module_name(__name__)
|
|
71
|
+
if module_name:
|
|
72
|
+
return log.getLogger(module_name).info if named else log.getLogger(module_name).debug
|
|
73
|
+
return log.getLogger(__name__).debug # if not named, we default to debug level
|
|
74
|
+
|
|
75
|
+
|
|
72
76
|
def yield_all(
|
|
73
77
|
thunks: ty.Iterable[ty.Tuple[H, ty.Callable[[], R]]],
|
|
74
78
|
*,
|
|
75
79
|
executor_cm: ty.Optional[ty.ContextManager[concurrent.futures.Executor]] = None,
|
|
76
80
|
fmt: ty.Callable[[str], str] = lambda x: x,
|
|
81
|
+
error_fmt: ty.Callable[[str], str] = lambda x: x,
|
|
77
82
|
named: str = "",
|
|
78
|
-
progress_logger: ty.Callable[[str], ty.Any] =
|
|
83
|
+
progress_logger: ty.Optional[ty.Callable[[str], ty.Any]] = None,
|
|
79
84
|
) -> ty.Iterator[ty.Tuple[H, ty.Union[R, Error]]]:
|
|
80
85
|
"""Stream your results so that you don't have to load them all into memory at the same
|
|
81
86
|
time (necessarily). Also, yield (rather than raise) Exceptions, wrapped as Errors.
|
|
@@ -104,6 +109,8 @@ def yield_all(
|
|
|
104
109
|
yield key, Error(e)
|
|
105
110
|
return # we're done here
|
|
106
111
|
|
|
112
|
+
progress_logger = progress_logger or _get_caller_logger(named)
|
|
113
|
+
|
|
107
114
|
executor_cm = executor_cm or concurrent.futures.ThreadPoolExecutor(
|
|
108
115
|
max_workers=len_or_none or None, **concurrency.initcontext()
|
|
109
116
|
) # if len_or_none turns out to be zero, swap in a None which won't kill the executor
|
|
@@ -113,8 +120,12 @@ def yield_all(
|
|
|
113
120
|
# While concurrent.futures.as_completed accepts an iterable as input, it
|
|
114
121
|
# does not yield any completed futures until the input iterable is
|
|
115
122
|
# exhausted.
|
|
123
|
+
num_exceptions = 0
|
|
116
124
|
for i, future in enumerate(concurrent.futures.as_completed(keys_onto_futures.values()), start=1):
|
|
117
125
|
thunk_key = future_ids_onto_keys[id(future)]
|
|
126
|
+
error_suffix = (
|
|
127
|
+
error_fmt(f"; {num_exceptions} tasks have raised exceptions") if num_exceptions else ""
|
|
128
|
+
)
|
|
118
129
|
try:
|
|
119
130
|
result = future.result()
|
|
120
131
|
yielder: tuple[H, ty.Union[R, Error]] = thunk_key, ty.cast(R, result)
|
|
@@ -123,7 +134,7 @@ def yield_all(
|
|
|
123
134
|
yielder = thunk_key, Error(e)
|
|
124
135
|
name = named or e.__class__.__name__
|
|
125
136
|
finally:
|
|
126
|
-
progress_logger(fmt(f"Yielding {name} {i}{num_tasks_log}"))
|
|
137
|
+
progress_logger(fmt(f"Yielding {name} {i}{num_tasks_log}") + error_suffix)
|
|
127
138
|
yield yielder
|
|
128
139
|
|
|
129
140
|
|
|
@@ -137,8 +148,10 @@ def failfast(results: ty.Iterable[ty.Tuple[H, ty.Union[R, Error]]]) -> ty.Iterat
|
|
|
137
148
|
yield key, res
|
|
138
149
|
|
|
139
150
|
|
|
140
|
-
def xf_mapping(
|
|
141
|
-
|
|
151
|
+
def xf_mapping(
|
|
152
|
+
thunks: ty.Mapping[H, ty.Callable[[], R]], named: str = ""
|
|
153
|
+
) -> ty.Iterator[ty.Tuple[H, R]]:
|
|
154
|
+
return failfast(yield_all(IteratorWithLen(len(thunks), thunks.items()), named=named))
|
|
142
155
|
|
|
143
156
|
|
|
144
157
|
def create_keys(iterable: ty.Iterable[R]) -> ty.Iterator[ty.Tuple[str, R]]:
|
|
@@ -153,6 +166,9 @@ def create_keys(iterable: ty.Iterable[R]) -> ty.Iterator[ty.Tuple[str, R]]:
|
|
|
153
166
|
return iter(with_keys)
|
|
154
167
|
|
|
155
168
|
|
|
169
|
+
ERROR_LOGGER = log.getLogger(__name__)
|
|
170
|
+
|
|
171
|
+
|
|
156
172
|
def yield_results(
|
|
157
173
|
thunks: ty.Iterable[ty.Callable[[], R]],
|
|
158
174
|
*,
|
|
@@ -160,9 +176,9 @@ def yield_results(
|
|
|
160
176
|
error_fmt: ty.Callable[[str], str] = lambda x: x,
|
|
161
177
|
success_fmt: ty.Callable[[str], str] = lambda x: x,
|
|
162
178
|
named: str = "",
|
|
163
|
-
progress_logger: ty.Callable[[str], ty.Any] =
|
|
179
|
+
progress_logger: ty.Optional[ty.Callable[[str], ty.Any]] = None,
|
|
164
180
|
) -> ty.Iterator[R]:
|
|
165
|
-
"""Yield only the successful results of your Callables/Thunks.
|
|
181
|
+
"""Yield only the successful results of your Callables/Thunks. Continue despite errors.
|
|
166
182
|
|
|
167
183
|
If your iterable has a length, we will be able to log progress
|
|
168
184
|
information. In most cases, this will be advantageous for you.
|
|
@@ -177,27 +193,26 @@ def yield_results(
|
|
|
177
193
|
|
|
178
194
|
exceptions: ty.List[Exception] = list()
|
|
179
195
|
|
|
180
|
-
num_tasks = try_len(thunks)
|
|
181
|
-
num_tasks_log = "" if not num_tasks else f" of {num_tasks}"
|
|
182
|
-
named = f" {named} " if named else " result "
|
|
183
|
-
|
|
184
196
|
for i, (_key, res) in enumerate(
|
|
185
|
-
yield_all(
|
|
197
|
+
yield_all(
|
|
198
|
+
create_keys(thunks),
|
|
199
|
+
executor_cm=executor_cm,
|
|
200
|
+
named=named,
|
|
201
|
+
progress_logger=progress_logger,
|
|
202
|
+
fmt=success_fmt,
|
|
203
|
+
error_fmt=error_fmt,
|
|
204
|
+
),
|
|
186
205
|
start=1,
|
|
187
206
|
):
|
|
188
207
|
if not isinstance(res, Error):
|
|
189
|
-
errors = error_fmt(f"; {len(exceptions)} tasks have raised exceptions") if exceptions else ""
|
|
190
|
-
progress_logger(success_fmt(f"Yielding{named}{i}{num_tasks_log} {errors}"))
|
|
191
208
|
yield res
|
|
192
209
|
else:
|
|
193
210
|
exceptions.append(res.error)
|
|
194
211
|
# print tracebacks as we go, so as not to defer potentially-helpful
|
|
195
212
|
# debugging information while a long run is ongoing.
|
|
196
213
|
traceback.print_exception(type(res.error), res.error, res.error.__traceback__)
|
|
197
|
-
|
|
198
|
-
error_fmt(
|
|
199
|
-
f"Task {i}{num_tasks_log} errored with {type(res.error).__name__}({res.error})"
|
|
200
|
-
)
|
|
214
|
+
ERROR_LOGGER.error( # should only use logger.exception from an except block
|
|
215
|
+
error_fmt(f"Task {i} errored with {type(res.error).__name__}({res.error})")
|
|
201
216
|
)
|
|
202
217
|
|
|
203
218
|
summarize_exceptions(error_fmt, exceptions)
|
|
@@ -218,10 +233,10 @@ def summarize_exceptions(
|
|
|
218
233
|
most_common_type = None
|
|
219
234
|
max_count = 0
|
|
220
235
|
for _type, excs in by_type.items():
|
|
221
|
-
|
|
236
|
+
ERROR_LOGGER.error(error_fmt(f"{len(excs)} tasks failed with exception: " + _type.__name__))
|
|
222
237
|
if len(excs) > max_count:
|
|
223
238
|
max_count = len(excs)
|
|
224
239
|
most_common_type = _type
|
|
225
240
|
|
|
226
|
-
|
|
241
|
+
ERROR_LOGGER.info("Raising one of the most common exception type.")
|
|
227
242
|
raise by_type[most_common_type][0] # type: ignore
|
|
@@ -22,14 +22,14 @@ thds/core/hashing.py,sha256=dR4HEWcAdU8O-9ASGkl8naKs6I1Sd7aps4EcVefvVLQ,4246
|
|
|
22
22
|
thds/core/home.py,sha256=tTClL_AarIKeri1aNCpuIC6evD7qr83ESGD173B81hU,470
|
|
23
23
|
thds/core/hostname.py,sha256=canFGr-JaaG7nUfsQlyL0JT-2tnZoT1BvXzyaOMK1vA,208
|
|
24
24
|
thds/core/imports.py,sha256=0LVegY8I8_XKZPcqiIp2OVVzEDtyqYA3JETf9OAKNKs,568
|
|
25
|
-
thds/core/inspect.py,sha256=
|
|
25
|
+
thds/core/inspect.py,sha256=vBuVJ9aKR_WT0W8SZ59UfZ3NXVS330WCeef1uUe546w,3426
|
|
26
26
|
thds/core/iterators.py,sha256=h0JBu2-rYhKMfJTDlZWfyHQWzgtIO8vp_Sp0gENFo7g,645
|
|
27
27
|
thds/core/lazy.py,sha256=e1WvG4LsbEydV0igEr_Vl1cq05zlQNIE8MFYT90yglE,3289
|
|
28
28
|
thds/core/link.py,sha256=4-9d22l_oSkKoSzlYEO-rwxO1hvvj6VETY7LwvGcX6M,5534
|
|
29
29
|
thds/core/logical_root.py,sha256=gWkIYRv9kNQfzbpxJaYiwNXVz1neZ2NvnvProtOn9d8,1399
|
|
30
30
|
thds/core/merge_args.py,sha256=7oj7dtO1-XVkfTM3aBlq3QlZbo8tb6X7E3EVIR-60t8,5781
|
|
31
31
|
thds/core/meta.py,sha256=Df0DxV5UzHcEsu5UCYaE1BWipMPTEXycn9Ug4cdquMk,12114
|
|
32
|
-
thds/core/parallel.py,sha256=
|
|
32
|
+
thds/core/parallel.py,sha256=is12mjELQwueUfrpTyu9d4Ow20641nGozw0Qib5bmWA,9108
|
|
33
33
|
thds/core/pickle_visit.py,sha256=QNMWIi5buvk2zsvx1-D-FKL7tkrFUFDs387vxgGebgU,833
|
|
34
34
|
thds/core/prof.py,sha256=5ViolfPsAPwUTHuhAe-bon7IArPGXydpGoB5uZmObDk,8264
|
|
35
35
|
thds/core/progress.py,sha256=tY8tc_6CMnu_O8DVisnsRoDpFJOw5vqyYzLhQDxsLn8,4361
|
|
@@ -46,11 +46,11 @@ thds/core/thunks.py,sha256=LxwqUsu3YPVDleGbNk5JWZIncDYwvM8wUBNOS2L09zs,1056
|
|
|
46
46
|
thds/core/timer.py,sha256=aOpNP-wHKaKs6ONK5fOtIOgx00FChVZquG4PeaEYH_k,5376
|
|
47
47
|
thds/core/tmp.py,sha256=jA8FwDbXo3hx8o4kRjAlkwpcI77X86GY4Sktkps29ho,3166
|
|
48
48
|
thds/core/types.py,sha256=sFqI_8BsB1u85PSizjBZw8PBtplC7U54E19wZZWCEvI,152
|
|
49
|
-
thds/core/log/__init__.py,sha256=
|
|
49
|
+
thds/core/log/__init__.py,sha256=wbHgzqHln74wfqM7gLzRURgOn9V_-2dOnoOBi37xczU,1354
|
|
50
50
|
thds/core/log/basic_config.py,sha256=2Y9U_c4PTrIsCmaN7Ps6Xr90AhJPzdYjeUzUMqO7oFU,6704
|
|
51
51
|
thds/core/log/json_formatter.py,sha256=C5bRsSbAqaQqfTm88jc3mYe3vwKZZLAxET8s7_u7aN0,1757
|
|
52
52
|
thds/core/log/kw_formatter.py,sha256=9-MVOd2r5NEkYNne9qWyFMeR5lac3w7mjHXsDa681i0,3379
|
|
53
|
-
thds/core/log/kw_logger.py,sha256=
|
|
53
|
+
thds/core/log/kw_logger.py,sha256=bXajphFJXOtiIOix7a4uOs_gzqTiPu-76uVGIZK7A5E,4351
|
|
54
54
|
thds/core/log/logfmt.py,sha256=i66zoG2oERnE1P_0TVXdlfJ1YgUmvtMjqRtdV5u2SvU,10366
|
|
55
55
|
thds/core/source/__init__.py,sha256=e-cRoLl1HKY3YrDjpV5p_i7zvr1L4q51-t1ISTxdig4,543
|
|
56
56
|
thds/core/source/_construct.py,sha256=jtsh0Du67TslWjCLASZ3pAMeaiowfgm7Bt50zIhwx7k,4330
|
|
@@ -74,8 +74,8 @@ thds/core/sqlite/structured.py,sha256=8t1B6XbM5NnudKEeBLsdjRVbSXXSr6iHOW0HwEAqtX
|
|
|
74
74
|
thds/core/sqlite/types.py,sha256=oq8m0UrvSn1IqWWcQ4FPptfAhdj6DllnCe7puVqSHlY,1297
|
|
75
75
|
thds/core/sqlite/upsert.py,sha256=BmKK6fsGVedt43iY-Lp7dnAu8aJ1e9CYlPVEQR2pMj4,5827
|
|
76
76
|
thds/core/sqlite/write.py,sha256=z0219vDkQDCnsV0WLvsj94keItr7H4j7Y_evbcoBrWU,3458
|
|
77
|
-
thds_core-1.47.
|
|
78
|
-
thds_core-1.47.
|
|
79
|
-
thds_core-1.47.
|
|
80
|
-
thds_core-1.47.
|
|
81
|
-
thds_core-1.47.
|
|
77
|
+
thds_core-1.47.20251024204437.dist-info/METADATA,sha256=2bk7jxHjsXiKVXmQD_x-FSSUbw3UZRDNdRG2skxndrY,2216
|
|
78
|
+
thds_core-1.47.20251024204437.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
79
|
+
thds_core-1.47.20251024204437.dist-info/entry_points.txt,sha256=bOCOVhKZv7azF3FvaWX6uxE6yrjK6FcjqhtxXvLiFY8,161
|
|
80
|
+
thds_core-1.47.20251024204437.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
|
|
81
|
+
thds_core-1.47.20251024204437.dist-info/RECORD,,
|
|
File without changes
|
{thds_core-1.47.20251024053252.dist-info → thds_core-1.47.20251024204437.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{thds_core-1.47.20251024053252.dist-info → thds_core-1.47.20251024204437.dist-info}/top_level.txt
RENAMED
|
File without changes
|