thds.core 0.0.1__py3-none-any.whl → 1.31.20250123022540__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.

Files changed (70) hide show
  1. thds/core/__init__.py +48 -0
  2. thds/core/ansi_esc.py +46 -0
  3. thds/core/cache.py +201 -0
  4. thds/core/calgitver.py +82 -0
  5. thds/core/concurrency.py +100 -0
  6. thds/core/config.py +250 -0
  7. thds/core/decos.py +55 -0
  8. thds/core/dict_utils.py +188 -0
  9. thds/core/env.py +40 -0
  10. thds/core/exit_after.py +121 -0
  11. thds/core/files.py +125 -0
  12. thds/core/fretry.py +115 -0
  13. thds/core/generators.py +56 -0
  14. thds/core/git.py +81 -0
  15. thds/core/hash_cache.py +86 -0
  16. thds/core/hashing.py +106 -0
  17. thds/core/home.py +15 -0
  18. thds/core/hostname.py +10 -0
  19. thds/core/imports.py +17 -0
  20. thds/core/inspect.py +58 -0
  21. thds/core/iterators.py +9 -0
  22. thds/core/lazy.py +83 -0
  23. thds/core/link.py +153 -0
  24. thds/core/log/__init__.py +29 -0
  25. thds/core/log/basic_config.py +171 -0
  26. thds/core/log/json_formatter.py +43 -0
  27. thds/core/log/kw_formatter.py +84 -0
  28. thds/core/log/kw_logger.py +93 -0
  29. thds/core/log/logfmt.py +302 -0
  30. thds/core/merge_args.py +168 -0
  31. thds/core/meta.json +8 -0
  32. thds/core/meta.py +518 -0
  33. thds/core/parallel.py +200 -0
  34. thds/core/pickle_visit.py +24 -0
  35. thds/core/prof.py +276 -0
  36. thds/core/progress.py +112 -0
  37. thds/core/protocols.py +17 -0
  38. thds/core/py.typed +0 -0
  39. thds/core/scaling.py +39 -0
  40. thds/core/scope.py +199 -0
  41. thds/core/source.py +238 -0
  42. thds/core/source_serde.py +104 -0
  43. thds/core/sqlite/__init__.py +21 -0
  44. thds/core/sqlite/connect.py +33 -0
  45. thds/core/sqlite/copy.py +35 -0
  46. thds/core/sqlite/ddl.py +4 -0
  47. thds/core/sqlite/functions.py +63 -0
  48. thds/core/sqlite/index.py +22 -0
  49. thds/core/sqlite/insert_utils.py +23 -0
  50. thds/core/sqlite/merge.py +84 -0
  51. thds/core/sqlite/meta.py +190 -0
  52. thds/core/sqlite/read.py +66 -0
  53. thds/core/sqlite/sqlmap.py +179 -0
  54. thds/core/sqlite/structured.py +138 -0
  55. thds/core/sqlite/types.py +64 -0
  56. thds/core/sqlite/upsert.py +139 -0
  57. thds/core/sqlite/write.py +99 -0
  58. thds/core/stack_context.py +41 -0
  59. thds/core/thunks.py +40 -0
  60. thds/core/timer.py +214 -0
  61. thds/core/tmp.py +85 -0
  62. thds/core/types.py +4 -0
  63. thds.core-1.31.20250123022540.dist-info/METADATA +68 -0
  64. thds.core-1.31.20250123022540.dist-info/RECORD +67 -0
  65. {thds.core-0.0.1.dist-info → thds.core-1.31.20250123022540.dist-info}/WHEEL +1 -1
  66. thds.core-1.31.20250123022540.dist-info/entry_points.txt +4 -0
  67. thds.core-1.31.20250123022540.dist-info/top_level.txt +1 -0
  68. thds.core-0.0.1.dist-info/METADATA +0 -8
  69. thds.core-0.0.1.dist-info/RECORD +0 -4
  70. thds.core-0.0.1.dist-info/top_level.txt +0 -1
@@ -0,0 +1,302 @@
1
+ # MIT License
2
+
3
+ # Copyright (c) 2022 Joshua Taylor Eppinette
4
+
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ # flake8: noqa
24
+
25
+ import io
26
+ import logging
27
+ import numbers
28
+ import traceback
29
+ from contextlib import closing
30
+ from types import TracebackType
31
+ from typing import Dict, List, Optional, Tuple, Type, cast
32
+
33
+ ExcInfo = Tuple[Type[BaseException], BaseException, TracebackType]
34
+
35
+ # Reserved log record attributes cannot be overwritten. They
36
+ # will not be included in the formatted log.
37
+ #
38
+ # https://docs.python.org/3/library/logging.html#logrecord-attributes
39
+ RESERVED: Tuple[str, ...] = (
40
+ "args",
41
+ "asctime",
42
+ "created",
43
+ "exc_info",
44
+ "exc_text",
45
+ "filename",
46
+ "funcName",
47
+ "levelname",
48
+ "levelno",
49
+ "lineno",
50
+ "message",
51
+ "module",
52
+ "msecs",
53
+ "msg",
54
+ "name",
55
+ "pathname",
56
+ "process",
57
+ "processName",
58
+ "relativeCreated",
59
+ "stack_info",
60
+ "taskName",
61
+ "thread",
62
+ "threadName",
63
+ )
64
+
65
+
66
+ class Logfmter(logging.Formatter):
67
+ @classmethod
68
+ def format_string(cls, value: str) -> str:
69
+ """
70
+ Process the provided string with any necessary quoting and/or escaping.
71
+ """
72
+ needs_dquote_escaping = '"' in value
73
+ needs_newline_escaping = "\n" in value
74
+ needs_quoting = " " in value or "=" in value
75
+
76
+ if needs_dquote_escaping:
77
+ value = value.replace('"', '\\"')
78
+
79
+ if needs_newline_escaping:
80
+ value = value.replace("\n", "\\n")
81
+
82
+ if needs_quoting:
83
+ value = '"{}"'.format(value)
84
+
85
+ return value if value else '""'
86
+
87
+ @classmethod
88
+ def format_value(cls, value) -> str:
89
+ """
90
+ Map the provided value to the proper logfmt formatted string.
91
+ """
92
+ if value is None:
93
+ return ""
94
+ elif isinstance(value, bool):
95
+ return "true" if value else "false"
96
+ elif isinstance(value, numbers.Number):
97
+ return str(value)
98
+
99
+ return cls.format_string(str(value))
100
+
101
+ @classmethod
102
+ def format_exc_info(cls, exc_info: ExcInfo) -> str:
103
+ """
104
+ Format the provided exc_info into a logfmt formatted string.
105
+
106
+ This function should only be used to format exceptions which are
107
+ currently being handled. Not with those exceptions which are
108
+ manually passed into the logger. For example:
109
+
110
+ try:
111
+ raise Exception()
112
+ except Exception:
113
+ logging.exception()
114
+ """
115
+ _type, exc, tb = exc_info
116
+
117
+ with closing(io.StringIO()) as sio:
118
+ traceback.print_exception(_type, exc, tb, None, sio)
119
+ value = sio.getvalue()
120
+
121
+ # Tracebacks have a single trailing newline that we don't need.
122
+ value = value.rstrip("\n")
123
+
124
+ return cls.format_string(value)
125
+
126
+ @classmethod
127
+ def format_params(cls, params: dict) -> str:
128
+ """
129
+ Return a string representing the logfmt formatted parameters.
130
+ """
131
+ return " ".join(["{}={}".format(key, cls.format_value(value)) for key, value in params.items()])
132
+
133
+ @classmethod
134
+ def normalize_key(cls, key: str) -> str:
135
+ """
136
+ Return a string whereby any spaces are converted to underscores and
137
+ newlines are escaped.
138
+
139
+ If the provided key is empty, then return a single underscore. This
140
+ function is used to prevent any logfmt parameters from having invalid keys.
141
+
142
+ As a choice of implementation, we normalize any keys instead of raising an
143
+ exception to prevent raising exceptions during logging. The goal is to never
144
+ impede logging. This is especially important when logging in exception handlers.
145
+ """
146
+ if not key:
147
+ return "_"
148
+
149
+ return key.replace(" ", "_").replace("\n", "\\n")
150
+
151
+ @classmethod
152
+ def get_extra(cls, record: logging.LogRecord) -> dict:
153
+ """
154
+ Return a dictionary of logger extra parameters by filtering any reserved keys.
155
+ """
156
+ return {
157
+ cls.normalize_key(key): value
158
+ for key, value in record.__dict__.items()
159
+ if key not in RESERVED
160
+ }
161
+
162
+ def __init__(
163
+ self,
164
+ keys: List[str] = ["at"],
165
+ mapping: Dict[str, str] = {"at": "levelname"},
166
+ datefmt: Optional[str] = None,
167
+ ):
168
+ self.keys = [self.normalize_key(key) for key in keys]
169
+ self.mapping = {self.normalize_key(key): value for key, value in mapping.items()}
170
+ self.datefmt = datefmt
171
+
172
+ def format_key_value(self, record: logging.LogRecord, key: str, value) -> str:
173
+ """This is net-new code that makes the formatter more extensible."""
174
+ return f"{key}={self.format_value(value)}"
175
+
176
+ def format(self, record: logging.LogRecord) -> str:
177
+ # If the 'asctime' attribute will be used, then generate it.
178
+ if "asctime" in self.keys or "asctime" in self.mapping.values():
179
+ record.asctime = self.formatTime(record, self.datefmt)
180
+
181
+ if isinstance(record.msg, dict):
182
+ params = {self.normalize_key(key): value for key, value in record.msg.items()}
183
+ else:
184
+ params = {"msg": record.getMessage()}
185
+
186
+ params.update(self.get_extra(record))
187
+
188
+ tokens = []
189
+
190
+ # Add the initial tokens from the provided list of default keys.
191
+ #
192
+ # This supports parameters which should be included in every log message. The
193
+ # values for these keys must already exist on the log record. If they are
194
+ # available under a different attribute name, then the formatter's mapping will
195
+ # be used to lookup these attributes. e.g. 'at' from 'levelname'
196
+ for key in self.keys:
197
+
198
+ attribute = key
199
+
200
+ # If there is a mapping for this key's attribute, then use it to lookup
201
+ # the key's value.
202
+ if key in self.mapping:
203
+ attribute = self.mapping[key]
204
+
205
+ # If this key is in params, then skip it, because it was manually passed in
206
+ # will be added via the params system.
207
+ if attribute in params:
208
+ continue
209
+
210
+ # If the attribute doesn't exist on the log record, then skip it.
211
+ if not hasattr(record, attribute):
212
+ continue
213
+
214
+ value = getattr(record, attribute)
215
+
216
+ tokens.append(self.format_key_value(record, key, value))
217
+
218
+ formatted_params = self.format_params(params)
219
+ if formatted_params:
220
+ tokens.append(formatted_params)
221
+
222
+ if record.exc_info:
223
+ # Cast exc_info to its not null variant to make mypy happy.
224
+ exc_info = cast(ExcInfo, record.exc_info)
225
+
226
+ tokens.append("exc_info={}".format(self.format_exc_info(exc_info)))
227
+
228
+ return " ".join(tokens)
229
+
230
+
231
+ # all of the above is _almost_ identical (minus format_key_value) to the raw source from
232
+ # https://github.com/jteppinette/python-logfmter
233
+ #
234
+ # what follows is our slight modifications
235
+
236
+ from .. import ansi_esc
237
+ from .kw_formatter import ThdsCompactFormatter
238
+ from .kw_logger import TH_REC_CTXT
239
+
240
+ # similar to what's in kw_formatter, but does not use background colors
241
+ # since those don't seem to translate well in k8s/Grafana:
242
+ _COLOR_LEVEL_MAP = {
243
+ "low": f"{ansi_esc.fg.BLUE}{{}}{ansi_esc.fg.RESET}",
244
+ "info": f"{ansi_esc.fg.GREEN}{{}}{ansi_esc.fg.RESET}",
245
+ "warning": (
246
+ f"{ansi_esc.fg.YELLOW}{ansi_esc.style.BRIGHT}" "{}" f"{ansi_esc.style.NORMAL}{ansi_esc.fg.RESET}"
247
+ ),
248
+ "error": (
249
+ f"{ansi_esc.fg.RED}{ansi_esc.style.BRIGHT}" "{}" f"{ansi_esc.style.NORMAL}{ansi_esc.fg.RESET}"
250
+ ),
251
+ "critical": (
252
+ f"{ansi_esc.fg.MAGENTA}{ansi_esc.style.BRIGHT}"
253
+ "{}"
254
+ f"{ansi_esc.fg.RESET}{ansi_esc.style.NORMAL}"
255
+ ),
256
+ }
257
+
258
+
259
+ def log_level_caps(levelno: int, levelname: str) -> str:
260
+ if levelno < logging.WARNING:
261
+ return levelname.lower()
262
+ return levelname
263
+
264
+
265
+ def log_level_color(levelno: int, base_levelname: str) -> str:
266
+ if levelno < logging.INFO:
267
+ return _COLOR_LEVEL_MAP["low"].format(base_levelname)
268
+ elif levelno < logging.WARNING:
269
+ return _COLOR_LEVEL_MAP["info"].format(base_levelname)
270
+ elif levelno < logging.ERROR:
271
+ return _COLOR_LEVEL_MAP["warning"].format(base_levelname)
272
+ elif levelno < logging.CRITICAL:
273
+ return _COLOR_LEVEL_MAP["error"].format(base_levelname)
274
+ return _COLOR_LEVEL_MAP["critical"].format(base_levelname)
275
+
276
+
277
+ class ThdsLogfmter(Logfmter):
278
+ @classmethod
279
+ def get_extra(cls, record: logging.LogRecord) -> dict:
280
+ """
281
+ Return a dictionary of logger extra parameters by filtering any reserved keys.
282
+ """
283
+ extra = dict(super().get_extra(record))
284
+ th_ctx = extra.pop(TH_REC_CTXT, None)
285
+ if th_ctx:
286
+ extra.update(th_ctx)
287
+ return extra
288
+
289
+ def format_key_value(self, record: logging.LogRecord, key: str, value) -> str:
290
+ if key == "mod":
291
+ return f"mod={ThdsCompactFormatter.format_module_name(value)}"
292
+ if key == "lvl":
293
+ core_str = log_level_color(record.levelno, f"lvl={log_level_caps(record.levelno, value)}")
294
+ return core_str + " " * (7 - len(record.levelname))
295
+ return super().format_key_value(record, key, value)
296
+
297
+
298
+ def mk_default_logfmter() -> ThdsLogfmter:
299
+ return ThdsLogfmter(
300
+ keys=["lvl", "mod"],
301
+ mapping={"lvl": "levelname", "mod": "name"},
302
+ )
@@ -0,0 +1,168 @@
1
+ # Merge signatures of two functions with Advanced Hackery.
2
+ # Copyright © 2018-2023, Chris Warrick.
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are
7
+ # met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions, and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions, and the following disclaimer in the
14
+ # documentation and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the author of this software nor the names of
17
+ # contributors to this software may be used to endorse or promote
18
+ # products derived from this software without specific prior written
19
+ # consent.
20
+ #
21
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+
33
+ import functools
34
+ import inspect
35
+ import itertools
36
+ import sys
37
+ import types
38
+
39
+ PY38 = sys.version_info >= (3, 8)
40
+ PY310 = sys.version_info >= (3, 10)
41
+ PY311 = sys.version_info >= (3, 11)
42
+
43
+
44
+ def _blank(): # pragma: no cover
45
+ pass
46
+
47
+
48
+ def _merge(source, dest): # noqa: C901
49
+ """Merge the signatures of ``source`` and ``dest``.
50
+ ``dest`` args go before ``source`` args in all three categories
51
+ (positional, keyword-maybe, keyword-only).
52
+ """
53
+ source_spec = inspect.getfullargspec(source)
54
+ dest_spec = inspect.getfullargspec(dest)
55
+
56
+ if source_spec.varargs or source_spec.varkw:
57
+ raise ValueError("The source function may not take variable arguments.")
58
+
59
+ source_all = source_spec.args
60
+ dest_all = dest_spec.args
61
+
62
+ if source_spec.defaults:
63
+ source_pos = source_all[: -len(source_spec.defaults)]
64
+ source_kw = source_all[-len(source_spec.defaults) :]
65
+ else:
66
+ source_pos = source_all
67
+ source_kw = []
68
+
69
+ if dest_spec.defaults:
70
+ dest_pos = dest_all[: -len(dest_spec.defaults)]
71
+ dest_kw = dest_all[-len(dest_spec.defaults) :]
72
+ else:
73
+ dest_pos = dest_all
74
+ dest_kw = []
75
+
76
+ args_merged = dest_pos
77
+ for a in source_pos:
78
+ if a not in args_merged:
79
+ args_merged.append(a)
80
+
81
+ defaults_merged = []
82
+ for a, default in itertools.chain(
83
+ zip(dest_kw, dest_spec.defaults or []), zip(source_kw, source_spec.defaults or [])
84
+ ):
85
+ if a not in args_merged:
86
+ args_merged.append(a)
87
+ defaults_merged.append(default)
88
+
89
+ kwonlyargs_merged = dest_spec.kwonlyargs
90
+ for a in source_spec.kwonlyargs:
91
+ if a not in kwonlyargs_merged:
92
+ kwonlyargs_merged.append(a)
93
+
94
+ args_all = tuple(args_merged + kwonlyargs_merged)
95
+
96
+ if PY38:
97
+ replace_kwargs = {
98
+ "co_argcount": len(args_merged),
99
+ "co_kwonlyargcount": len(kwonlyargs_merged),
100
+ "co_posonlyargcount": dest.__code__.co_posonlyargcount,
101
+ "co_nlocals": len(args_all),
102
+ "co_flags": source.__code__.co_flags,
103
+ "co_varnames": args_all,
104
+ "co_filename": dest.__code__.co_filename,
105
+ "co_name": dest.__code__.co_name,
106
+ "co_firstlineno": dest.__code__.co_firstlineno,
107
+ }
108
+
109
+ if PY310:
110
+ replace_kwargs["co_linetable"] = dest.__code__.co_linetable
111
+ else:
112
+ replace_kwargs["co_lnotab"] = dest.__code__.co_lnotab
113
+
114
+ if PY311:
115
+ replace_kwargs["co_exceptiontable"] = dest.__code__.co_exceptiontable
116
+ replace_kwargs["co_qualname"] = dest.__code__.co_qualname
117
+
118
+ passer_code = _blank.__code__.replace(**replace_kwargs)
119
+ else:
120
+ passer_args = [
121
+ len(args_merged),
122
+ len(kwonlyargs_merged),
123
+ _blank.__code__.co_nlocals,
124
+ _blank.__code__.co_stacksize,
125
+ source.__code__.co_flags,
126
+ _blank.__code__.co_code,
127
+ (),
128
+ (),
129
+ args_all,
130
+ dest.__code__.co_filename,
131
+ dest.__code__.co_name,
132
+ dest.__code__.co_firstlineno,
133
+ dest.__code__.co_lnotab,
134
+ ]
135
+ passer_code = types.CodeType(*passer_args)
136
+
137
+ passer = types.FunctionType(passer_code, globals())
138
+ dest.__wrapped__ = passer
139
+
140
+ # annotations
141
+
142
+ # ensure we take destination’s return annotation
143
+ has_dest_ret = "return" in dest.__annotations__
144
+ if has_dest_ret:
145
+ dest_ret = dest.__annotations__["return"]
146
+
147
+ for v in ("__kwdefaults__", "__annotations__"):
148
+ out = getattr(source, v)
149
+ if out is None:
150
+ out = {}
151
+ if getattr(dest, v) is not None:
152
+ out = out.copy()
153
+ out.update(getattr(dest, v))
154
+ setattr(passer, v, out)
155
+
156
+ if has_dest_ret:
157
+ passer.__annotations__["return"] = dest_ret
158
+ dest.__annotations__ = passer.__annotations__
159
+
160
+ passer.__defaults__ = tuple(defaults_merged)
161
+ if not dest.__doc__:
162
+ dest.__doc__ = source.__doc__
163
+ return dest
164
+
165
+
166
+ def merge_args(source):
167
+ """Merge the signatures of two functions."""
168
+ return functools.partial(_merge, source)
thds/core/meta.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "git_commit": "b374dd32673467abb1c76990d4aaade7f6562eba",
3
+ "git_branch": "main",
4
+ "git_is_clean": true,
5
+ "pyproject_version": "1.31.20250123022540",
6
+ "thds_user": "runner",
7
+ "misc": {}
8
+ }