GJDutils 0.2.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.
- gjdutils/__init__.py +12 -0
- gjdutils/audios.py +39 -0
- gjdutils/cacheing.py +237 -0
- gjdutils/cmd.py +149 -0
- gjdutils/colab.py +39 -0
- gjdutils/collections.py +36 -0
- gjdutils/decorators.py +34 -0
- gjdutils/dicts.py +216 -0
- gjdutils/dsci.py +202 -0
- gjdutils/dt.py +296 -0
- gjdutils/env.py +64 -0
- gjdutils/errors.py +12 -0
- gjdutils/files.py +140 -0
- gjdutils/functions.py +6 -0
- gjdutils/google_translate.py +80 -0
- gjdutils/hashing.py +32 -0
- gjdutils/html.py +87 -0
- gjdutils/indexing.py +97 -0
- gjdutils/iterfunc.py +99 -0
- gjdutils/jsons.py +70 -0
- gjdutils/lists.py +13 -0
- gjdutils/llm_utils.py +167 -0
- gjdutils/llms_claude.py +131 -0
- gjdutils/llms_openai.py +299 -0
- gjdutils/misc.py +30 -0
- gjdutils/num.py +77 -0
- gjdutils/obsolete/google_text_to_speech.py +46 -0
- gjdutils/obsolete/llms_obsolete.py +298 -0
- gjdutils/outloud_text_to_speech.py +230 -0
- gjdutils/prompt_templates.py +20 -0
- gjdutils/pypi_build.py +112 -0
- gjdutils/pytest_utils.py +24 -0
- gjdutils/rand.py +65 -0
- gjdutils/regex.py +78 -0
- gjdutils/requirements_dev.txt +2 -0
- gjdutils/runtime.py +19 -0
- gjdutils/sets.py +5 -0
- gjdutils/shell.py +69 -0
- gjdutils/sorteddict.py +34 -0
- gjdutils/stopwatch.py +79 -0
- gjdutils/strings.py +218 -0
- gjdutils/todo/convert_parquet.py +28 -0
- gjdutils/typ.py +37 -0
- gjdutils/voice_speechrecognition.py +29 -0
- gjdutils/web.py +68 -0
- gjdutils-0.2.2.dist-info/METADATA +101 -0
- gjdutils-0.2.2.dist-info/RECORD +49 -0
- gjdutils-0.2.2.dist-info/WHEEL +4 -0
- gjdutils-0.2.2.dist-info/licenses/LICENSE +21 -0
gjdutils/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# Add the root directory to Python path to import __VERSION__
|
|
5
|
+
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
6
|
+
if root_dir not in sys.path:
|
|
7
|
+
sys.path.append(root_dir)
|
|
8
|
+
|
|
9
|
+
from __VERSION__ import __version__
|
|
10
|
+
|
|
11
|
+
# Export version at package level
|
|
12
|
+
__version__ = __version__
|
gjdutils/audios.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def play_mp3(mp3_filen: str, prog: str = "cli", speed: Optional[float] = None):
|
|
6
|
+
prog = prog.lower().strip()
|
|
7
|
+
full_mp3_filen = os.path.abspath(os.path.expanduser(mp3_filen))
|
|
8
|
+
if prog == "vlc":
|
|
9
|
+
# pip install python-vlc
|
|
10
|
+
import vlc
|
|
11
|
+
|
|
12
|
+
vlc_mp3_filen = os.path.join("file://", full_mp3_filen)
|
|
13
|
+
p = vlc.MediaPlayer(vlc_mp3_filen)
|
|
14
|
+
if speed is not None:
|
|
15
|
+
p.set_rate(speed) # type: ignore
|
|
16
|
+
p.play() # type: ignore
|
|
17
|
+
elif prog == "pygame":
|
|
18
|
+
assert speed is None, "Not implemented speed for pygame"
|
|
19
|
+
import pygame
|
|
20
|
+
|
|
21
|
+
pygame.init()
|
|
22
|
+
pygame.mixer.init()
|
|
23
|
+
pygame.mixer.music.load(full_mp3_filen)
|
|
24
|
+
pygame.mixer.music.play()
|
|
25
|
+
pygame.event.wait()
|
|
26
|
+
elif prog == "playsound":
|
|
27
|
+
# maybe set to 1.2.2 if you're having trouble installing
|
|
28
|
+
from playsound import playsound
|
|
29
|
+
|
|
30
|
+
assert speed is None, "Playsound doesn't support changing speed"
|
|
31
|
+
# https://stackoverflow.com/a/63147250/230523
|
|
32
|
+
playsound(mp3_filen)
|
|
33
|
+
elif prog == "cli":
|
|
34
|
+
cmd = f"afplay -r {speed} '{full_mp3_filen}'"
|
|
35
|
+
# print(cmd)
|
|
36
|
+
os.system(cmd)
|
|
37
|
+
else:
|
|
38
|
+
raise Exception(f"Unknown PROG '{prog}'")
|
|
39
|
+
return full_mp3_filen
|
gjdutils/cacheing.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from functools import update_wrapper
|
|
2
|
+
import inspect
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# from abracadjabra.utils
|
|
7
|
+
def generate_mckey(prefix, d):
|
|
8
|
+
"""
|
|
9
|
+
Should generate a legal, human-readable, unique and
|
|
10
|
+
predictable string Memcached key from dictionary
|
|
11
|
+
D. N.B. we're using 'mckey' to distinguish Memcached
|
|
12
|
+
'keys' from dictionary 'keys'.
|
|
13
|
+
|
|
14
|
+
Designed to be called at the top of your function with
|
|
15
|
+
locals(), so it ignores any REQUEST and SELF keys - but
|
|
16
|
+
you can always add {'user': request.user} if needed.
|
|
17
|
+
|
|
18
|
+
Notes:
|
|
19
|
+
|
|
20
|
+
- Prepends PREFIX + '__' to the MCKEY generated from D.
|
|
21
|
+
|
|
22
|
+
- For dictionaries, concatenates the keys+values, sorted
|
|
23
|
+
by key.
|
|
24
|
+
|
|
25
|
+
- Converts lists and querysets to lists of IDs, hashing
|
|
26
|
+
the result if too long.
|
|
27
|
+
|
|
28
|
+
- Hashes the MCKEY if it's non-ascii or too long. Removes spaces etc.
|
|
29
|
+
|
|
30
|
+
Keys are separated from their value by '::', and the
|
|
31
|
+
key/value pairs are separated from one another by '__'.
|
|
32
|
+
|
|
33
|
+
e.g.
|
|
34
|
+
|
|
35
|
+
{'a': 100, 'b': 'blah'} -> u'a::100__b::blah'
|
|
36
|
+
|
|
37
|
+
xxx - should be moved to utils.caching, along with Evan's cmcd
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def sorted_dict_by_keys(d):
|
|
41
|
+
"""
|
|
42
|
+
Returns a SortedDict, with the keys sorted alphabetically.
|
|
43
|
+
|
|
44
|
+
This might not be necessary, since I think the order
|
|
45
|
+
of a python dict's keys() is deterministic, but by
|
|
46
|
+
sorting by dictionary keys, it's easier to know in
|
|
47
|
+
advance what the generated MCKEY should look like.
|
|
48
|
+
The idea is to ensure that no matter how D was
|
|
49
|
+
created, you'll know what the key should be.
|
|
50
|
+
"""
|
|
51
|
+
sorted_d = SortedDict()
|
|
52
|
+
for k in sorted(d.keys()):
|
|
53
|
+
sorted_d[k] = d[k]
|
|
54
|
+
return sorted_d
|
|
55
|
+
|
|
56
|
+
def to_str_or_hash(s):
|
|
57
|
+
"""
|
|
58
|
+
Tries to convert S to a STR. If it doesn't work,
|
|
59
|
+
just return the hash.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
s = str(s)
|
|
63
|
+
except UnicodeEncodeError:
|
|
64
|
+
s = str(hash(s))
|
|
65
|
+
return s
|
|
66
|
+
|
|
67
|
+
def hash_if_too_long(s):
|
|
68
|
+
"""
|
|
69
|
+
Return the HASH of S rather than S if it's too long
|
|
70
|
+
(since the hash is only 10 characters).
|
|
71
|
+
|
|
72
|
+
We call this on each component and then once more at
|
|
73
|
+
the end because we want to keep the overall result
|
|
74
|
+
as human-readable as possible, while still being
|
|
75
|
+
unique.
|
|
76
|
+
"""
|
|
77
|
+
if len(s) > MAX_MEMCACHED_KEY_LEN:
|
|
78
|
+
s = str(hash(s))
|
|
79
|
+
return s
|
|
80
|
+
|
|
81
|
+
def iterable_to_string(seq):
|
|
82
|
+
"""
|
|
83
|
+
If the items in SEQ are Django Models,
|
|
84
|
+
store a comma-separated list of ids.
|
|
85
|
+
|
|
86
|
+
Otherwise, just join the items in SEQ.
|
|
87
|
+
|
|
88
|
+
e.g. [Thing.objects.get(id=1), Thing.objects.get(id=2)] -> 'Thing:1,2'
|
|
89
|
+
"""
|
|
90
|
+
if not seq:
|
|
91
|
+
return ""
|
|
92
|
+
pieces = [to_str_or_hash(x) for x in seq]
|
|
93
|
+
model_prefix = ""
|
|
94
|
+
s = model_prefix + ",".join(pieces)
|
|
95
|
+
s = hash_if_too_long(s)
|
|
96
|
+
return s
|
|
97
|
+
|
|
98
|
+
def sanitize_val(v):
|
|
99
|
+
"""
|
|
100
|
+
If V is an iterable, turn it into a comma-separated
|
|
101
|
+
string (of IDs, if Models).
|
|
102
|
+
|
|
103
|
+
Even though (empirically) it appears that Django's
|
|
104
|
+
cache.set and cache.get use Memcached's binary
|
|
105
|
+
protocol (so they can deal with non-ascii keys), it
|
|
106
|
+
seems safer to require the key to be ascii.
|
|
107
|
+
"""
|
|
108
|
+
if isinstance(v, str):
|
|
109
|
+
pass
|
|
110
|
+
elif isinstance(v, unicode):
|
|
111
|
+
v = to_str_or_hash(v)
|
|
112
|
+
elif hasattr(v, "pk"):
|
|
113
|
+
# for instances, i decided not to separate the
|
|
114
|
+
# modelname from the id with a colon to
|
|
115
|
+
# distinguish them from querysets
|
|
116
|
+
v = v._meta.object_name + str(v.pk)
|
|
117
|
+
elif isinstance(v, dict):
|
|
118
|
+
# we might decide that even if we *can* deal
|
|
119
|
+
# with dicts like this, it's too crazy to be worth it...
|
|
120
|
+
v = generate_mckey("", v)
|
|
121
|
+
elif isiterable(v):
|
|
122
|
+
v = iterable_to_string(v)
|
|
123
|
+
else:
|
|
124
|
+
v = to_str_or_hash(v)
|
|
125
|
+
v = v.strip()
|
|
126
|
+
return hash_if_too_long(v)
|
|
127
|
+
|
|
128
|
+
# 250 bytes, minus global KEY_PREFIX, plus leave extra room in case
|
|
129
|
+
MAX_MEMCACHED_KEY_LEN = 200
|
|
130
|
+
|
|
131
|
+
prefix = to_str_or_hash(prefix).upper()
|
|
132
|
+
|
|
133
|
+
assert isinstance(d, dict)
|
|
134
|
+
# ignore REQUEST and SELF, so you can easily pass in locals() for D
|
|
135
|
+
if "request" in d:
|
|
136
|
+
del d["request"]
|
|
137
|
+
if "self" in d:
|
|
138
|
+
del d["self"]
|
|
139
|
+
d = sorted_dict_by_keys(d)
|
|
140
|
+
|
|
141
|
+
# require everything to be a nice ascii string
|
|
142
|
+
pieces = "__".join(
|
|
143
|
+
[
|
|
144
|
+
"%s::%s"
|
|
145
|
+
% (
|
|
146
|
+
sanitize_val(k),
|
|
147
|
+
sanitize_val(v),
|
|
148
|
+
)
|
|
149
|
+
for k, v in d.items()
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
prefix_pieces = prefix + "__" + pieces
|
|
154
|
+
# replace all whitespace with underscores
|
|
155
|
+
prefix_pieces = re.sub("[ \t\r\n]+", "_", prefix_pieces)
|
|
156
|
+
# not too long
|
|
157
|
+
prefix_pieces = hash_if_too_long(prefix_pieces)
|
|
158
|
+
# make sure it's ascii-friendly
|
|
159
|
+
prefix_pieces = str(prefix_pieces)
|
|
160
|
+
return prefix_pieces
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def cmcd(prefix=None, arg_names=(), expiry=None):
|
|
164
|
+
"""Caches the return value of func based on the cache key generated by
|
|
165
|
+
generate_mckey. The prefix argument to the `generate_mckey` is
|
|
166
|
+
determined from the module and the name of the function if `prefix` is
|
|
167
|
+
`None`. `arg_names` should be a sequence of strings that will be
|
|
168
|
+
pulled from the kwargs dict and passed to `generate_mckey` to generate
|
|
169
|
+
a key.
|
|
170
|
+
|
|
171
|
+
Unfortunately we don't have access to the same locals() as the
|
|
172
|
+
function itself, so the functions we're wrapping with this need to
|
|
173
|
+
take keyword arguments, and the arguments we're generating the cache
|
|
174
|
+
from must be specified.
|
|
175
|
+
|
|
176
|
+
NOTE: prefix must be defined in settings.CACHE_EXPIRY, OR set expiry=EXPIRY_TIME, e.g.
|
|
177
|
+
|
|
178
|
+
See utils.tests for usage.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def dec(func, prefix=prefix, arg_names=arg_names, expiry=expiry):
|
|
182
|
+
if expiry is None:
|
|
183
|
+
if prefix == None:
|
|
184
|
+
prefix = ".".join((func.__module__, func.__name__))
|
|
185
|
+
|
|
186
|
+
prefix = prefix.upper()
|
|
187
|
+
if prefix not in sett.CACHE_EXPIRY:
|
|
188
|
+
raise Exception(
|
|
189
|
+
"Prefix %s must be defined in settings.CACHE_EXPIRY if expiry is not specified"
|
|
190
|
+
% prefix
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
expiry = sett.CACHE_EXPIRY[prefix]
|
|
194
|
+
|
|
195
|
+
fspec = inspect.getargspec(func)
|
|
196
|
+
pos_args = fspec.args
|
|
197
|
+
defaults = fspec.defaults
|
|
198
|
+
defaults = defaults if defaults else ()
|
|
199
|
+
|
|
200
|
+
if pos_args or defaults:
|
|
201
|
+
if not arg_names:
|
|
202
|
+
raise Exception(
|
|
203
|
+
"arg_names must be specified for functions that take arguments."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
default_args = dict(
|
|
207
|
+
(k, v) for k, v in zip(reversed(pos_args), reversed(defaults))
|
|
208
|
+
)
|
|
209
|
+
noargs = False
|
|
210
|
+
else:
|
|
211
|
+
noargs = True
|
|
212
|
+
|
|
213
|
+
def f(*args, **kwargs):
|
|
214
|
+
if not noargs:
|
|
215
|
+
all_args = dict(default_args)
|
|
216
|
+
all_args.update(dict((n, v) for n, v in zip(pos_args, args)))
|
|
217
|
+
all_args.update(kwargs)
|
|
218
|
+
d = dict((k, all_args.get(k, None)) for k in arg_names)
|
|
219
|
+
else:
|
|
220
|
+
all_args = {}
|
|
221
|
+
d = {}
|
|
222
|
+
|
|
223
|
+
mckey = generate_mckey(prefix, d)
|
|
224
|
+
cached = cache.get(mckey)
|
|
225
|
+
|
|
226
|
+
if cached:
|
|
227
|
+
return cached
|
|
228
|
+
|
|
229
|
+
val = func(*args, **kwargs)
|
|
230
|
+
|
|
231
|
+
cache.set(mckey, val, expiry)
|
|
232
|
+
|
|
233
|
+
return val
|
|
234
|
+
|
|
235
|
+
return update_wrapper(f, func)
|
|
236
|
+
|
|
237
|
+
return dec
|
gjdutils/cmd.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from typing import Union, Optional, Dict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from gjdutils.shell import fatal_error_msg
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_cmd(
|
|
12
|
+
cmd: Union[str, list[str]],
|
|
13
|
+
before_msg: Optional[str] = None,
|
|
14
|
+
fatal_msg: Optional[str] = None,
|
|
15
|
+
verbose: int = 2,
|
|
16
|
+
replace_sys_python_executable: bool = True,
|
|
17
|
+
dry_run: bool = False,
|
|
18
|
+
**subprocess_kwargs,
|
|
19
|
+
) -> tuple[int, str, Dict]:
|
|
20
|
+
"""Run a shell command with enhanced output and error handling.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
cmd: Command to run as string (shell=True) or list of strings (shell=False)
|
|
24
|
+
before_msg: Optional message to display before running command (green)
|
|
25
|
+
fatal_msg: Optional message to use if command fails (calls fatal_error_msg)
|
|
26
|
+
verbose: Output verbosity level:
|
|
27
|
+
0 = silent
|
|
28
|
+
1 = show before_msg if provided
|
|
29
|
+
2 = also show command being run (default)
|
|
30
|
+
3 = also show working directory and duration
|
|
31
|
+
4 = also show command stdout output
|
|
32
|
+
replace_sys_python_executable: Replace 'python ' with sys.executable
|
|
33
|
+
dry_run: If True, only print what would be run
|
|
34
|
+
**subprocess_kwargs: Additional arguments passed to subprocess.run
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple of (returncode, stdout, extra) where extra is a dict containing:
|
|
38
|
+
- stderr: Standard error output
|
|
39
|
+
- duration: Time taken to run command
|
|
40
|
+
- cmd_str: Final command string that was run
|
|
41
|
+
- cwd: Working directory
|
|
42
|
+
- input_args: Original function arguments
|
|
43
|
+
- subprocess_result: Full subprocess.CompletedProcess object
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
Simple usage with string command:
|
|
47
|
+
>>> retcode, out, _ = run_cmd4("ls -l", before_msg="Listing files...")
|
|
48
|
+
Listing files...
|
|
49
|
+
$ ls -l
|
|
50
|
+
>>> print(out)
|
|
51
|
+
total 8
|
|
52
|
+
-rw-r--r-- 1 user user 2048 Mar 15 10:00 example.txt
|
|
53
|
+
|
|
54
|
+
Complex usage with list command and error handling:
|
|
55
|
+
>>> cmd = ["pytest", "tests/", "-v", "--cov"]
|
|
56
|
+
>>> retcode, out, extra = run_cmd4(
|
|
57
|
+
... cmd,
|
|
58
|
+
... before_msg="Running tests with coverage...",
|
|
59
|
+
... fatal_msg="Tests failed!",
|
|
60
|
+
... verbose=2,
|
|
61
|
+
... timeout=300,
|
|
62
|
+
... check=True
|
|
63
|
+
... )
|
|
64
|
+
Running tests with coverage...
|
|
65
|
+
$ pytest tests/ -v --cov
|
|
66
|
+
=== test session starts ===
|
|
67
|
+
...
|
|
68
|
+
"""
|
|
69
|
+
input_args = locals()
|
|
70
|
+
|
|
71
|
+
console = Console()
|
|
72
|
+
|
|
73
|
+
start_time = time.time()
|
|
74
|
+
|
|
75
|
+
# Convert list command to string if needed
|
|
76
|
+
cmd_str = " ".join(cmd) if isinstance(cmd, list) else cmd
|
|
77
|
+
|
|
78
|
+
# Replace python executable if requested
|
|
79
|
+
if replace_sys_python_executable and cmd_str.startswith("python "):
|
|
80
|
+
cmd_str = f"{sys.executable} {cmd_str[7:]}"
|
|
81
|
+
|
|
82
|
+
# Handle verbosity
|
|
83
|
+
if verbose >= 1 and before_msg:
|
|
84
|
+
console.print(f"[green]{before_msg}[/green]")
|
|
85
|
+
if verbose >= 2:
|
|
86
|
+
console.print(f"[white]$ {cmd_str}[/white]")
|
|
87
|
+
|
|
88
|
+
# Handle dry run
|
|
89
|
+
if dry_run:
|
|
90
|
+
return (
|
|
91
|
+
0,
|
|
92
|
+
"",
|
|
93
|
+
{
|
|
94
|
+
"stderr": "",
|
|
95
|
+
"duration": 0,
|
|
96
|
+
"cmd_str": cmd_str,
|
|
97
|
+
"cwd": str(Path.cwd()),
|
|
98
|
+
"input_args": input_args,
|
|
99
|
+
"subprocess_result": None,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Set defaults for subprocess
|
|
104
|
+
subprocess_kwargs.setdefault("shell", isinstance(cmd, str))
|
|
105
|
+
subprocess_kwargs.setdefault("capture_output", True)
|
|
106
|
+
subprocess_kwargs.setdefault("text", True)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
result = subprocess.run(
|
|
110
|
+
cmd if isinstance(cmd, list) else cmd_str,
|
|
111
|
+
**subprocess_kwargs,
|
|
112
|
+
)
|
|
113
|
+
except subprocess.TimeoutExpired as e:
|
|
114
|
+
if fatal_msg:
|
|
115
|
+
fatal_error_msg(
|
|
116
|
+
fatal_msg,
|
|
117
|
+
f"Command timed out after {subprocess_kwargs.get('timeout', '?')}s",
|
|
118
|
+
)
|
|
119
|
+
raise
|
|
120
|
+
|
|
121
|
+
duration = time.time() - start_time
|
|
122
|
+
|
|
123
|
+
# Show additional info at verbose level 3
|
|
124
|
+
if verbose >= 3:
|
|
125
|
+
console.print(f"[blue]Working directory: {Path.cwd()}[/blue]")
|
|
126
|
+
console.print(f"[blue]Duration: {duration:.2f}s[/blue]")
|
|
127
|
+
if verbose >= 4:
|
|
128
|
+
console.print(f"[blue]Command output:[/blue]\n{result.stdout}")
|
|
129
|
+
|
|
130
|
+
# Handle errors
|
|
131
|
+
if result.returncode != 0:
|
|
132
|
+
# Show both stdout and stderr for failed commands
|
|
133
|
+
if result.stdout:
|
|
134
|
+
console.print(f"[red]Command output:[/red]\n{result.stdout}")
|
|
135
|
+
if result.stderr:
|
|
136
|
+
console.print(f"[red]Command error output:[/red]\n{result.stderr}")
|
|
137
|
+
if fatal_msg:
|
|
138
|
+
fatal_error_msg(fatal_msg)
|
|
139
|
+
|
|
140
|
+
extra = {
|
|
141
|
+
"stderr": result.stderr,
|
|
142
|
+
"duration": duration,
|
|
143
|
+
"cmd_str": cmd_str,
|
|
144
|
+
"cwd": str(Path.cwd()),
|
|
145
|
+
"input_args": input_args,
|
|
146
|
+
"subprocess_result": result,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result.returncode, result.stdout.strip(), extra
|
gjdutils/colab.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from .runtime import in_colab
|
|
4
|
+
|
|
5
|
+
# https://stackoverflow.com/a/53586419/230523
|
|
6
|
+
IN_COLAB = in_colab()
|
|
7
|
+
# also specified in authortools_demo.ipynb
|
|
8
|
+
GOOGLE_DRIVE_MOUNT_PATH = "/content/drive"
|
|
9
|
+
GOOGLE_DRIVE_OUTPUT_PATH = os.path.join(
|
|
10
|
+
GOOGLE_DRIVE_MOUNT_PATH,
|
|
11
|
+
"Shareddrives",
|
|
12
|
+
"Blah", # TODO
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def colab_path_if_needed(filen: str):
|
|
17
|
+
"""
|
|
18
|
+
Prepend the Google Drive mount path for Colab if IN_COLAB is True.
|
|
19
|
+
"""
|
|
20
|
+
if IN_COLAB:
|
|
21
|
+
filen = os.path.join(GOOGLE_DRIVE_OUTPUT_PATH, filen)
|
|
22
|
+
return filen
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_css_for_colab():
|
|
26
|
+
from IPython.display import HTML, display
|
|
27
|
+
|
|
28
|
+
# from https://stackoverflow.com/a/61401455/230523
|
|
29
|
+
display(
|
|
30
|
+
HTML(
|
|
31
|
+
"""
|
|
32
|
+
<style>
|
|
33
|
+
pre {
|
|
34
|
+
white-space: pre-wrap;
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
37
|
+
"""
|
|
38
|
+
)
|
|
39
|
+
)
|
gjdutils/collections.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
from typing import Any, Callable, Literal, Sequence, TypeVar
|
|
3
|
+
|
|
4
|
+
T = TypeVar("T")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def found_one(lst: Sequence[T]) -> T | Literal[False]:
|
|
8
|
+
if len(lst) == 0:
|
|
9
|
+
return False
|
|
10
|
+
elif len(lst) == 1:
|
|
11
|
+
found = lst[0]
|
|
12
|
+
assert found is not False, "Too confusing - we found something, but it's False"
|
|
13
|
+
return found
|
|
14
|
+
else:
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_duplicates(lst: Sequence[T]) -> list[T]:
|
|
19
|
+
return [item for item in lst if lst.count(item) > 1]
|
|
20
|
+
|
|
21
|
+
from collections import OrderedDict
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# def uniquify(items: Sequence[T], key: Callable[[T], Any] | None = None) -> list[T]:
|
|
25
|
+
# this would be useful if you wanted to uniquify something non-hashable, but I couldn't get it to work
|
|
26
|
+
# https://www.perplexity.ai/search/in-python-unique-version-of-a-5r0iCRlBSjm2Dv6HGLu_6g
|
|
27
|
+
# return list(OrderedDict.fromkeys(map(key, items) if key else items))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def uniquify(items: Sequence[T]) -> list[T]:
|
|
31
|
+
# https://www.perplexity.ai/search/unique-version-of-a-list-prese-qYpae.JBRDedvHdmEyOqfA
|
|
32
|
+
# seen = set()
|
|
33
|
+
# return [x for x in lst if not (x in seen or seen.add(x))]
|
|
34
|
+
|
|
35
|
+
# https://www.w3resource.com/python-exercises/list-advanced/python-list-advanced-exercise-8.php
|
|
36
|
+
return list(dict.fromkeys(items))
|
gjdutils/decorators.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from typing import Callable, TypeVar, Any, cast
|
|
4
|
+
|
|
5
|
+
console = Console()
|
|
6
|
+
|
|
7
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def console_print_doc(color: str = "blue") -> Callable[[F], F]:
|
|
11
|
+
"""
|
|
12
|
+
A decorator that prints the docstring of a function when it starts running.
|
|
13
|
+
The entire docstring will be printed in the specified color.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
color (str): Color for the docstring text. Defaults to "blue".
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
@console_print_doc(color="green")
|
|
20
|
+
def my_function():
|
|
21
|
+
"This entire docstring will be green"
|
|
22
|
+
pass
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def decorator(func: F) -> F:
|
|
26
|
+
@wraps(func)
|
|
27
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
28
|
+
if func.__doc__:
|
|
29
|
+
console.print(func.__doc__.strip(), style=color)
|
|
30
|
+
return func(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
return cast(F, wrapper)
|
|
33
|
+
|
|
34
|
+
return decorator
|