thds.core 1.47.20251024053252__py3-none-any.whl → 1.47.20251027165848__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 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][0]
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
@@ -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] = logger.info,
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(thunks: ty.Mapping[H, ty.Callable[[], R]]) -> ty.Iterator[ty.Tuple[H, R]]:
141
- return failfast(yield_all(IteratorWithLen(len(thunks), thunks.items())))
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] = logger.info,
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(create_keys(thunks), executor_cm=executor_cm),
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
- logger.error( # should only use logger.exception from an except block
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
- logger.error(error_fmt(f"{len(excs)} tasks failed with exception: " + _type.__name__))
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
- logger.info("Raising one of the most common exception type.")
241
+ ERROR_LOGGER.info("Raising one of the most common exception type.")
227
242
  raise by_type[most_common_type][0] # type: ignore
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thds.core
3
- Version: 1.47.20251024053252
3
+ Version: 1.47.20251027165848
4
4
  Summary: Core utilities.
5
5
  Author-email: Trilliant Health <info@trillianthealth.com>
6
6
  License: MIT
@@ -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=IXzJYkc8XGx9W9Oym9Bus2fGMcWHLAtlzvmQ1PwDKmk,2371
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=FojyyUtJ3jrYcsPiHlotWqUUybP_HtVgAN59_ipYf2g,8597
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=bDbZvlxyymY6VrQzD8lCn0egniLEiA9hpNMAXZ7e7wY,1348
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=CyZVPnkUMtrUL2Lyk261AIEPmoP-buf_suFAhQlU1io,4063
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.20251024053252.dist-info/METADATA,sha256=yNcSdU59kVnfCMtS6Q3XwfMr6Ak3-PPnRYRS9ItJGc4,2216
78
- thds_core-1.47.20251024053252.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
79
- thds_core-1.47.20251024053252.dist-info/entry_points.txt,sha256=bOCOVhKZv7azF3FvaWX6uxE6yrjK6FcjqhtxXvLiFY8,161
80
- thds_core-1.47.20251024053252.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
81
- thds_core-1.47.20251024053252.dist-info/RECORD,,
77
+ thds_core-1.47.20251027165848.dist-info/METADATA,sha256=ss1jmPTCaNH9J_GlsxgBIqrvMzs8MXgSEvkJ7_tE0DM,2216
78
+ thds_core-1.47.20251027165848.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
79
+ thds_core-1.47.20251027165848.dist-info/entry_points.txt,sha256=bOCOVhKZv7azF3FvaWX6uxE6yrjK6FcjqhtxXvLiFY8,161
80
+ thds_core-1.47.20251027165848.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
81
+ thds_core-1.47.20251027165848.dist-info/RECORD,,