python-misc-utils 0.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.
- py_misc_utils/__init__.py +0 -0
- py_misc_utils/abs_timeout.py +12 -0
- py_misc_utils/alog.py +311 -0
- py_misc_utils/app_main.py +179 -0
- py_misc_utils/archive_streamer.py +112 -0
- py_misc_utils/assert_checks.py +118 -0
- py_misc_utils/ast_utils.py +121 -0
- py_misc_utils/async_manager.py +189 -0
- py_misc_utils/break_control.py +63 -0
- py_misc_utils/buffered_iterator.py +35 -0
- py_misc_utils/cached_file.py +507 -0
- py_misc_utils/call_limiter.py +26 -0
- py_misc_utils/call_result_selector.py +13 -0
- py_misc_utils/cleanups.py +85 -0
- py_misc_utils/cmd.py +97 -0
- py_misc_utils/compression.py +116 -0
- py_misc_utils/cond_waiter.py +13 -0
- py_misc_utils/context_base.py +18 -0
- py_misc_utils/context_managers.py +67 -0
- py_misc_utils/core_utils.py +577 -0
- py_misc_utils/daemon_process.py +252 -0
- py_misc_utils/data_cache.py +46 -0
- py_misc_utils/date_utils.py +90 -0
- py_misc_utils/debug.py +24 -0
- py_misc_utils/dyn_modules.py +50 -0
- py_misc_utils/dynamod.py +103 -0
- py_misc_utils/env_config.py +35 -0
- py_misc_utils/executor.py +239 -0
- py_misc_utils/file_overwrite.py +29 -0
- py_misc_utils/fin_wrap.py +77 -0
- py_misc_utils/fp_utils.py +47 -0
- py_misc_utils/fs/__init__.py +0 -0
- py_misc_utils/fs/file_fs.py +127 -0
- py_misc_utils/fs/ftp_fs.py +242 -0
- py_misc_utils/fs/gcs_fs.py +196 -0
- py_misc_utils/fs/http_fs.py +241 -0
- py_misc_utils/fs/s3_fs.py +417 -0
- py_misc_utils/fs_base.py +133 -0
- py_misc_utils/fs_utils.py +207 -0
- py_misc_utils/gcs_fs.py +169 -0
- py_misc_utils/gen_indices.py +54 -0
- py_misc_utils/gfs.py +371 -0
- py_misc_utils/git_repo.py +77 -0
- py_misc_utils/global_namespace.py +110 -0
- py_misc_utils/http_async_fetcher.py +139 -0
- py_misc_utils/http_server.py +196 -0
- py_misc_utils/http_utils.py +143 -0
- py_misc_utils/img_utils.py +20 -0
- py_misc_utils/infix_op.py +20 -0
- py_misc_utils/inspect_utils.py +205 -0
- py_misc_utils/iostream.py +21 -0
- py_misc_utils/iter_file.py +117 -0
- py_misc_utils/key_wrap.py +46 -0
- py_misc_utils/lazy_import.py +25 -0
- py_misc_utils/lockfile.py +164 -0
- py_misc_utils/mem_size.py +64 -0
- py_misc_utils/mirror_from.py +72 -0
- py_misc_utils/mmap.py +16 -0
- py_misc_utils/module_utils.py +196 -0
- py_misc_utils/moving_average.py +19 -0
- py_misc_utils/msgpack_streamer.py +26 -0
- py_misc_utils/multi_wait.py +24 -0
- py_misc_utils/multiprocessing.py +102 -0
- py_misc_utils/named_array.py +224 -0
- py_misc_utils/no_break.py +46 -0
- py_misc_utils/no_except.py +32 -0
- py_misc_utils/np_ml_framework.py +184 -0
- py_misc_utils/np_utils.py +346 -0
- py_misc_utils/ntuple_utils.py +38 -0
- py_misc_utils/num_utils.py +54 -0
- py_misc_utils/obj.py +73 -0
- py_misc_utils/object_cache.py +100 -0
- py_misc_utils/object_tracker.py +88 -0
- py_misc_utils/ordered_set.py +71 -0
- py_misc_utils/osfd.py +27 -0
- py_misc_utils/packet.py +22 -0
- py_misc_utils/parquet_streamer.py +69 -0
- py_misc_utils/pd_utils.py +254 -0
- py_misc_utils/periodic_task.py +61 -0
- py_misc_utils/pickle_wrap.py +121 -0
- py_misc_utils/pipeline.py +98 -0
- py_misc_utils/remap_pickle.py +50 -0
- py_misc_utils/resource_manager.py +155 -0
- py_misc_utils/rnd_utils.py +56 -0
- py_misc_utils/run_once.py +19 -0
- py_misc_utils/scheduler.py +135 -0
- py_misc_utils/select_params.py +300 -0
- py_misc_utils/signal.py +141 -0
- py_misc_utils/skl_utils.py +270 -0
- py_misc_utils/split.py +147 -0
- py_misc_utils/state.py +53 -0
- py_misc_utils/std_module.py +56 -0
- py_misc_utils/stream_dataframe.py +176 -0
- py_misc_utils/streamed_file.py +144 -0
- py_misc_utils/tempdir.py +79 -0
- py_misc_utils/template_replace.py +51 -0
- py_misc_utils/tensor_stream.py +269 -0
- py_misc_utils/thread_context.py +33 -0
- py_misc_utils/throttle.py +30 -0
- py_misc_utils/time_trigger.py +18 -0
- py_misc_utils/timegen.py +11 -0
- py_misc_utils/traceback.py +49 -0
- py_misc_utils/tracking_executor.py +91 -0
- py_misc_utils/transform_array.py +42 -0
- py_misc_utils/uncompress.py +35 -0
- py_misc_utils/url_fetcher.py +157 -0
- py_misc_utils/utils.py +538 -0
- py_misc_utils/varint.py +50 -0
- py_misc_utils/virt_array.py +52 -0
- py_misc_utils/weak_call.py +33 -0
- py_misc_utils/work_results.py +100 -0
- py_misc_utils/writeback_file.py +43 -0
- python_misc_utils-0.2.dist-info/METADATA +36 -0
- python_misc_utils-0.2.dist-info/RECORD +117 -0
- python_misc_utils-0.2.dist-info/WHEEL +5 -0
- python_misc_utils-0.2.dist-info/licenses/LICENSE +13 -0
- python_misc_utils-0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import collections
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
import multiprocessing
|
|
6
|
+
import os
|
|
7
|
+
import pickle
|
|
8
|
+
import psutil
|
|
9
|
+
import signal
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from . import fs_utils as fsu
|
|
15
|
+
from . import lockfile as lockf
|
|
16
|
+
from . import osfd
|
|
17
|
+
from . import packet as pkt
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
DaemonResult = collections.namedtuple(
|
|
21
|
+
'DaemonResult',
|
|
22
|
+
'pid, msg, exclass',
|
|
23
|
+
defaults=(-1, None, None),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def _get_pids_dir():
|
|
27
|
+
pidsdir = os.path.join(tempfile.gettempdir(), '.pids')
|
|
28
|
+
os.makedirs(pidsdir, exist_ok=True)
|
|
29
|
+
|
|
30
|
+
return pidsdir
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_PIDS_DIR = _get_pids_dir()
|
|
34
|
+
|
|
35
|
+
def _get_pidfile(name):
|
|
36
|
+
return os.path.join(_PIDS_DIR, f'{name}.pid')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _term_handler(sig, frame):
|
|
40
|
+
sys.exit(sig)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DaemonBase:
|
|
44
|
+
|
|
45
|
+
def __init__(self, name):
|
|
46
|
+
self._name = name
|
|
47
|
+
self._pidfile = _get_pidfile(name)
|
|
48
|
+
|
|
49
|
+
def _write_result(self, wpipe, **kwargs):
|
|
50
|
+
dres = DaemonResult(**kwargs)
|
|
51
|
+
pkt.write_packet(wpipe, pickle.dumps(dres))
|
|
52
|
+
|
|
53
|
+
def _read_result(self, rpipe):
|
|
54
|
+
return pickle.loads(pkt.read_packet(rpipe))
|
|
55
|
+
|
|
56
|
+
def _delpid(self, pid=None):
|
|
57
|
+
with self._lockfile():
|
|
58
|
+
if pid is None or (xpid := self._readpid()) == pid:
|
|
59
|
+
try:
|
|
60
|
+
os.remove(self._pidfile)
|
|
61
|
+
|
|
62
|
+
return True
|
|
63
|
+
except OSError:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def _writepid(self, pid):
|
|
69
|
+
with self._lockfile():
|
|
70
|
+
xpid = self._readpid()
|
|
71
|
+
if xpid is not None:
|
|
72
|
+
if self._runnning_pid(xpid):
|
|
73
|
+
raise FileExistsError(f'Daemon already running with PID {xpid}')
|
|
74
|
+
|
|
75
|
+
os.remove(self._pidfile)
|
|
76
|
+
|
|
77
|
+
# Use mode=0o660 to make sure only allowed users can access the PID file.
|
|
78
|
+
with osfd.OsFd(self._pidfile, os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode=0o660) as fd:
|
|
79
|
+
os.write(fd, f'{pid}\n'.encode())
|
|
80
|
+
|
|
81
|
+
def _readpid(self):
|
|
82
|
+
try:
|
|
83
|
+
with osfd.OsFd(self._pidfile, os.O_RDONLY) as fd:
|
|
84
|
+
return int(fsu.readall(fd).strip())
|
|
85
|
+
except IOError:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def getpid(self):
|
|
89
|
+
with self._lockfile():
|
|
90
|
+
pid = self._readpid()
|
|
91
|
+
|
|
92
|
+
return pid if pid is None or self._runnning_pid(pid) else None
|
|
93
|
+
|
|
94
|
+
def _lockfile(self):
|
|
95
|
+
return lockf.LockFile(self._pidfile)
|
|
96
|
+
|
|
97
|
+
def _runnning_pid(self, pid):
|
|
98
|
+
try:
|
|
99
|
+
proc = psutil.Process(pid)
|
|
100
|
+
|
|
101
|
+
return proc.status() not in {psutil.STATUS_DEAD, psutil.STATUS_ZOMBIE}
|
|
102
|
+
except psutil.NoSuchProcess:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def _killpid(self, pid, kill_timeout=None):
|
|
106
|
+
try:
|
|
107
|
+
proc = psutil.Process(pid)
|
|
108
|
+
proc.terminate()
|
|
109
|
+
time.sleep(kill_timeout or 1.0)
|
|
110
|
+
proc.kill()
|
|
111
|
+
except psutil.NoSuchProcess:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def _setup_daemon(self, wpipe):
|
|
115
|
+
# This 2nd os.setsid() makes the daemon a process group, so with can kill the
|
|
116
|
+
# whole group, if required. We do this only on systems that supports it.
|
|
117
|
+
if os.name == 'posix':
|
|
118
|
+
os.setsid()
|
|
119
|
+
|
|
120
|
+
sys.stdout.flush()
|
|
121
|
+
sys.stderr.flush()
|
|
122
|
+
|
|
123
|
+
infd = os.open(os.devnull, os.O_RDONLY)
|
|
124
|
+
outfd = os.open(os.devnull, os.O_WRONLY | os.O_APPEND)
|
|
125
|
+
errfd = os.open(os.devnull, os.O_WRONLY | os.O_APPEND)
|
|
126
|
+
|
|
127
|
+
os.dup2(infd, sys.stdin.fileno())
|
|
128
|
+
os.dup2(outfd, sys.stdout.fileno())
|
|
129
|
+
os.dup2(errfd, sys.stderr.fileno())
|
|
130
|
+
|
|
131
|
+
pid = os.getpid()
|
|
132
|
+
self._writepid(pid)
|
|
133
|
+
|
|
134
|
+
# Register the signal handlers otherwise atexit callbacks will not get
|
|
135
|
+
# called in case a signal terminates the daemon process.
|
|
136
|
+
signal.signal(signal.SIGINT, _term_handler)
|
|
137
|
+
signal.signal(signal.SIGTERM, _term_handler)
|
|
138
|
+
atexit.register(functools.partial(self._delpid, pid=pid))
|
|
139
|
+
|
|
140
|
+
self._write_result(wpipe, pid=pid)
|
|
141
|
+
|
|
142
|
+
def stop(self, kill_timeout=None):
|
|
143
|
+
with self._lockfile():
|
|
144
|
+
pid = self._readpid()
|
|
145
|
+
if pid is not None:
|
|
146
|
+
self._killpid(pid, kill_timeout=kill_timeout)
|
|
147
|
+
|
|
148
|
+
self._delpid(pid=pid)
|
|
149
|
+
|
|
150
|
+
return pid is not None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class DaemonPosix(DaemonBase):
|
|
154
|
+
|
|
155
|
+
def _daemonize(self):
|
|
156
|
+
rpipe, wpipe = os.pipe()
|
|
157
|
+
os.set_inheritable(wpipe, True)
|
|
158
|
+
|
|
159
|
+
pid = os.fork()
|
|
160
|
+
if pid > 0:
|
|
161
|
+
dres = self._read_result(rpipe)
|
|
162
|
+
if dres.pid < 0:
|
|
163
|
+
raise dres.exclass(dres.msg)
|
|
164
|
+
|
|
165
|
+
return dres.pid
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
os.chdir('/')
|
|
169
|
+
os.setsid()
|
|
170
|
+
os.umask(0)
|
|
171
|
+
|
|
172
|
+
pid = os.fork()
|
|
173
|
+
if pid > 0:
|
|
174
|
+
sys.exit(0)
|
|
175
|
+
|
|
176
|
+
self._setup_daemon(wpipe)
|
|
177
|
+
|
|
178
|
+
return 0
|
|
179
|
+
except Exception as ex:
|
|
180
|
+
self._write_result(wpipe, exclass=ex.__class__, msg=f'Daemonize failed: {ex}')
|
|
181
|
+
sys.exit(1)
|
|
182
|
+
|
|
183
|
+
def start(self, target):
|
|
184
|
+
pid = self.getpid()
|
|
185
|
+
if pid is None:
|
|
186
|
+
if (pid := self._daemonize()) == 0:
|
|
187
|
+
target()
|
|
188
|
+
sys.exit(0)
|
|
189
|
+
|
|
190
|
+
return pid
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class DaemonCompat(DaemonBase):
|
|
194
|
+
|
|
195
|
+
def _write_result(self, wpipe, **kwargs):
|
|
196
|
+
dres = DaemonResult(**kwargs)
|
|
197
|
+
wpipe.send(pickle.dumps(dres))
|
|
198
|
+
|
|
199
|
+
def _read_result(self, rpipe):
|
|
200
|
+
return pickle.loads(rpipe.recv())
|
|
201
|
+
|
|
202
|
+
def _boostrap(self, target, wpipe):
|
|
203
|
+
try:
|
|
204
|
+
os.chdir('/')
|
|
205
|
+
if os.name == 'posix':
|
|
206
|
+
os.umask(0)
|
|
207
|
+
|
|
208
|
+
self._setup_daemon(wpipe)
|
|
209
|
+
except Exception as ex:
|
|
210
|
+
self._write_result(wpipe, exclass=ex.__class__, msg=f'Daemonize failed: {ex}')
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
target()
|
|
214
|
+
sys.exit(0)
|
|
215
|
+
|
|
216
|
+
def _start_daemon(self, target):
|
|
217
|
+
mps = multiprocessing.get_context(method=os.getenv('DAEMON_MP_CONTEXT'))
|
|
218
|
+
rpipe, wpipe = mps.Pipe()
|
|
219
|
+
proc = mps.Process(target=self._boostrap, args=(target, wpipe))
|
|
220
|
+
proc.start()
|
|
221
|
+
|
|
222
|
+
dres = self._read_result(rpipe)
|
|
223
|
+
if dres.pid < 0:
|
|
224
|
+
raise dres.exclass(dres.msg)
|
|
225
|
+
|
|
226
|
+
assert dres.pid == proc.pid
|
|
227
|
+
|
|
228
|
+
# HACK!
|
|
229
|
+
# This removes the daemon process from the list of known child processes, preventing
|
|
230
|
+
# it to be forcibly killed at exit.
|
|
231
|
+
multiprocessing.process._children.discard(proc)
|
|
232
|
+
|
|
233
|
+
return dres.pid
|
|
234
|
+
|
|
235
|
+
def start(self, target):
|
|
236
|
+
pid = self.getpid()
|
|
237
|
+
if pid is None:
|
|
238
|
+
pid = self._start_daemon(target)
|
|
239
|
+
|
|
240
|
+
return pid
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
HAS_MP_CHILDREN = isinstance(multiprocessing.process._children, set)
|
|
245
|
+
except:
|
|
246
|
+
HAS_MP_CHILDREN = False
|
|
247
|
+
|
|
248
|
+
if HAS_MP_CHILDREN and os.getenv('DAEMON_MODE') != 'posix':
|
|
249
|
+
Daemon = DaemonCompat
|
|
250
|
+
else:
|
|
251
|
+
Daemon = DaemonPosix
|
|
252
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
import pickle
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from . import gfs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_MAX_AGE = int(os.getenv('DATACACHE_MAX_AGE', 3600 * 24))
|
|
10
|
+
|
|
11
|
+
class DataCache:
|
|
12
|
+
|
|
13
|
+
def __init__(self, data_id, max_age=_MAX_AGE):
|
|
14
|
+
fsid = hashlib.sha1(data_id.encode()).hexdigest()
|
|
15
|
+
|
|
16
|
+
cache_path = os.path.join(gfs.cache_dir(), 'data_cache')
|
|
17
|
+
gfs.makedirs(cache_path, exist_ok=True)
|
|
18
|
+
|
|
19
|
+
self._data_path = os.path.join(cache_path, fsid)
|
|
20
|
+
self._orig_data = None
|
|
21
|
+
try:
|
|
22
|
+
sres = gfs.stat(self._data_path)
|
|
23
|
+
if max_age is None or (sres.st_ctime + max_age) > time.time():
|
|
24
|
+
with gfs.open(self._data_path, mode='rb') as fd:
|
|
25
|
+
self._orig_data = pickle.load(fd)
|
|
26
|
+
except:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
self._data = self._orig_data
|
|
30
|
+
|
|
31
|
+
def data(self):
|
|
32
|
+
return self._data
|
|
33
|
+
|
|
34
|
+
def store(self, data):
|
|
35
|
+
self._data = data
|
|
36
|
+
|
|
37
|
+
def __enter__(self):
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def __exit__(self, *exc):
|
|
41
|
+
if self._data is not None and self._data is not self._orig_data:
|
|
42
|
+
with gfs.open(self._data_path, mode='wb') as fd:
|
|
43
|
+
pickle.dump(self._data, fd)
|
|
44
|
+
|
|
45
|
+
return False
|
|
46
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import os
|
|
3
|
+
import pytz
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import dateutil
|
|
7
|
+
import dateutil.parser
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from . import assert_checks as tas
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Default timezone so code shows/works with such timezone.
|
|
15
|
+
DEFAULT_TZ = pytz.timezone(os.getenv('DEFAULT_TZ', 'America/New_York'))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ny_market_timezone():
|
|
19
|
+
return pytz.timezone('America/New_York')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def now(tz=None):
|
|
23
|
+
return datetime.datetime.now(tz=tz or DEFAULT_TZ)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def from_timestamp(ts, tz=None):
|
|
27
|
+
return datetime.datetime.fromtimestamp(ts, tz=tz or DEFAULT_TZ)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def make_datetime_from_epoch(s, tz=None):
|
|
31
|
+
ds = pd.to_datetime(s, unit='s', origin='unix', utc=True)
|
|
32
|
+
|
|
33
|
+
if tz is not None:
|
|
34
|
+
return ds.dt.tz_convert(tz) if isinstance(ds, pd.Series) else ds.tz_convert(tz)
|
|
35
|
+
|
|
36
|
+
return ds
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_date(dstr, tz=None):
|
|
40
|
+
# Accept EPOCH values starting with @.
|
|
41
|
+
m = re.match(r'@((\d+)(\.\d*)?)$', dstr)
|
|
42
|
+
if m:
|
|
43
|
+
return from_timestamp(float(m.group(1)), tz=tz)
|
|
44
|
+
|
|
45
|
+
# ISO Format: 2011-11-17T00:05:23-04:00
|
|
46
|
+
dt = dateutil.parser.isoparse(dstr)
|
|
47
|
+
if dt.tzinfo is None and tz is not None:
|
|
48
|
+
dt = tz.localize(dt)
|
|
49
|
+
|
|
50
|
+
return dt
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def day_offset(dt):
|
|
54
|
+
ddt = dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
55
|
+
|
|
56
|
+
return dt.timestamp() - ddt.timestamp(), ddt
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def np_datetime_to_epoch(dt, dtype=np.float64):
|
|
60
|
+
tas.check_fn(np.issubdtype, dt.dtype, np.datetime64)
|
|
61
|
+
u, c = np.datetime_data(dt.dtype)
|
|
62
|
+
if u == 's':
|
|
63
|
+
return dt.astype(dtype)
|
|
64
|
+
|
|
65
|
+
dt = dt.astype(np.float64)
|
|
66
|
+
if u == 'ns':
|
|
67
|
+
dt = dt / 1e9
|
|
68
|
+
elif u == 'us':
|
|
69
|
+
dt = dt / 1e6
|
|
70
|
+
elif u == 'ms':
|
|
71
|
+
dt = dt / 1e3
|
|
72
|
+
else:
|
|
73
|
+
alog.xraise(RuntimeError, f'Unknown NumPy datetime64 unit: {u}')
|
|
74
|
+
|
|
75
|
+
return dt if dtype == np.float64 else dt.astype(dtype)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def align(dt, step, ceil=False):
|
|
79
|
+
secs = step.total_seconds() if isinstance(step, datetime.timedelta) else step
|
|
80
|
+
fp, ip = np.modf(dt.timestamp() / secs)
|
|
81
|
+
|
|
82
|
+
if ceil and not np.isclose(fp, 0):
|
|
83
|
+
ip += 1
|
|
84
|
+
|
|
85
|
+
return from_timestamp(ip * secs, tz=dt.tzinfo)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def at_time(dt, hour=0, minute=0, second=0, microsecond=0):
|
|
89
|
+
return dt.replace(hour=hour, minute=minute, second=second, microsecond=microsecond)
|
|
90
|
+
|
py_misc_utils/debug.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import pdb
|
|
2
|
+
import signal
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _debug(signum, frame):
|
|
7
|
+
signame = signal.strsignal(signum)
|
|
8
|
+
sys.stderr.write(f'** {signame} received, entering debugger\n' \
|
|
9
|
+
f'** Type "c" to continue or "q" to stop the process\n' \
|
|
10
|
+
f'** Or {signame} again to quit (and possibly dump core)\n')
|
|
11
|
+
sys.stderr.flush()
|
|
12
|
+
|
|
13
|
+
signal.signal(signum, signal.SIG_DFL)
|
|
14
|
+
try:
|
|
15
|
+
pdb.set_trace()
|
|
16
|
+
finally:
|
|
17
|
+
signal.signal(signum, _debug)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def install_pdb_hook(signum):
|
|
21
|
+
signum = getattr(signal, signum) if isinstance(signum, str) else signum
|
|
22
|
+
|
|
23
|
+
signal.signal(signum, _debug)
|
|
24
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import importlib
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from . import alog
|
|
6
|
+
from . import assert_checks as tas
|
|
7
|
+
from . import gfs
|
|
8
|
+
from . import module_utils as mu
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DynLoader:
|
|
12
|
+
|
|
13
|
+
def __init__(self, modname=None, path=None, postfix=''):
|
|
14
|
+
if modname is not None:
|
|
15
|
+
tas.check_is_none(path, msg=f'Cannot specify path="{path}" when specified modname="{modname}"')
|
|
16
|
+
|
|
17
|
+
parent_mod = importlib.import_module(modname)
|
|
18
|
+
mpath = os.path.dirname(parent_mod.__file__)
|
|
19
|
+
else:
|
|
20
|
+
tas.check_is_not_none(path, msg=f'Path must be specified if "modname" is missing')
|
|
21
|
+
mpath = path
|
|
22
|
+
|
|
23
|
+
module_names, matcher = [], gfs.RegexMatcher(r'(.*)' + postfix + r'\.py$')
|
|
24
|
+
for fname in gfs.enumerate_files(mpath, matcher=matcher):
|
|
25
|
+
module_names.append((matcher.match.group(1), os.path.join(mpath, fname)))
|
|
26
|
+
|
|
27
|
+
self._modules = collections.OrderedDict()
|
|
28
|
+
for (imod_name, imod_path) in sorted(module_names):
|
|
29
|
+
if modname is not None:
|
|
30
|
+
imod = importlib.import_module(f'{modname}.{imod_name}{postfix}')
|
|
31
|
+
else:
|
|
32
|
+
imod = mu.load_module(imod_path, modname=f'{imod_name}{postfix}')
|
|
33
|
+
mname = getattr(imod, 'MODULE_NAME', imod_name)
|
|
34
|
+
self._modules[mname] = imod
|
|
35
|
+
|
|
36
|
+
def module_names(self):
|
|
37
|
+
return tuple(self._modules.keys())
|
|
38
|
+
|
|
39
|
+
def modules(self):
|
|
40
|
+
return tuple(self._modules.values())
|
|
41
|
+
|
|
42
|
+
def get(self, name):
|
|
43
|
+
return self._modules.get(name)
|
|
44
|
+
|
|
45
|
+
def __getitem__(self, name):
|
|
46
|
+
return self._modules[name]
|
|
47
|
+
|
|
48
|
+
def __len__(self):
|
|
49
|
+
return len(self._modules)
|
|
50
|
+
|
py_misc_utils/dynamod.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import importlib
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
from . import global_namespace as gns
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_MODROOT = 'pym'
|
|
12
|
+
|
|
13
|
+
def _create_root():
|
|
14
|
+
from . import tempdir as tmpd
|
|
15
|
+
|
|
16
|
+
path = os.path.join(tmpd.get_temp_root(), _MODROOT)
|
|
17
|
+
os.mkdir(path)
|
|
18
|
+
|
|
19
|
+
return path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_MODNAME = '_dynamod'
|
|
23
|
+
|
|
24
|
+
def _create_mod_folder():
|
|
25
|
+
path = _create_root()
|
|
26
|
+
dpath = os.path.join(path, _MODNAME)
|
|
27
|
+
os.mkdir(dpath)
|
|
28
|
+
|
|
29
|
+
with open(os.path.join(dpath, '__init__.py'), mode='w') as fd:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
sys.path.append(path)
|
|
33
|
+
|
|
34
|
+
return dpath
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _clone_mod_folder(src_path):
|
|
38
|
+
# This copies the source folder into a new temporary one for the new process,
|
|
39
|
+
# which will be in turn deleted once this exists.
|
|
40
|
+
# We cannot point directly to the source folder since it will be removed once
|
|
41
|
+
# the parent exists.
|
|
42
|
+
path = _create_root()
|
|
43
|
+
shutil.copytree(src_path, os.path.join(path, _MODNAME))
|
|
44
|
+
|
|
45
|
+
src_root = os.path.dirname(src_path)
|
|
46
|
+
if src_root in sys.path:
|
|
47
|
+
sys.path.remove(src_root)
|
|
48
|
+
sys.path.append(path)
|
|
49
|
+
|
|
50
|
+
return path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_LOCK = threading.RLock()
|
|
54
|
+
_MOD_FOLDER = gns.Var('dynamod.MOD_FOLDER',
|
|
55
|
+
child_fn=_clone_mod_folder,
|
|
56
|
+
defval=_create_mod_folder)
|
|
57
|
+
|
|
58
|
+
_HASHNAME_LEN = int(os.getenv('DYNAMOD_HASHNAME_LEN', 12))
|
|
59
|
+
|
|
60
|
+
def make_code_name(code):
|
|
61
|
+
chash = hashlib.sha1(code.encode()).hexdigest()[: _HASHNAME_LEN]
|
|
62
|
+
|
|
63
|
+
return f'_hashed._{chash}'
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def create_module(name, code, overwrite=None):
|
|
67
|
+
path = gns.get(_MOD_FOLDER)
|
|
68
|
+
mpath = os.path.join(path, *name.split('.')) + '.py'
|
|
69
|
+
|
|
70
|
+
with _LOCK:
|
|
71
|
+
reload = False
|
|
72
|
+
if os.path.exists(mpath):
|
|
73
|
+
if overwrite in (None, False):
|
|
74
|
+
raise RuntimeError(f'Dynamic module "{name}" already exists: {mpath}')
|
|
75
|
+
else:
|
|
76
|
+
# Note that there exist an issue with Python import subsystem:
|
|
77
|
+
#
|
|
78
|
+
# https://bugs.python.org/issue31772
|
|
79
|
+
#
|
|
80
|
+
# Such issue causes a module to not be reloaded if the size of the source file
|
|
81
|
+
# has not changed.
|
|
82
|
+
# So BIG HACK here to add an headline comment if that's the case!
|
|
83
|
+
reload = True
|
|
84
|
+
if os.stat(mpath).st_size == len(code):
|
|
85
|
+
code = f'# Note: Added due to https://bugs.python.org/issue31772\n\n{code}'
|
|
86
|
+
|
|
87
|
+
os.makedirs(os.path.dirname(mpath), exist_ok=True)
|
|
88
|
+
with open(mpath, mode='w') as f:
|
|
89
|
+
f.write(code)
|
|
90
|
+
|
|
91
|
+
module = get_module(name)
|
|
92
|
+
|
|
93
|
+
return importlib.reload(module) if reload else module
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def module_name(name):
|
|
97
|
+
return f'{_MODNAME}.{name}'
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_module(name):
|
|
101
|
+
with _LOCK:
|
|
102
|
+
return importlib.import_module(module_name(name))
|
|
103
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from . import utils as ut
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EnvConfig:
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
parser = argparse.ArgumentParser()
|
|
10
|
+
state = dict()
|
|
11
|
+
for name in dir(self):
|
|
12
|
+
if not name.startswith('_'):
|
|
13
|
+
value = getattr(self, name)
|
|
14
|
+
# Do not try to override functions (even though there really should not
|
|
15
|
+
# be in an EnvConfig derived object).
|
|
16
|
+
if not callable(value):
|
|
17
|
+
state[name] = ut.getenv(name, dtype=type(value))
|
|
18
|
+
parser.add_argument(f'--{name}', type=type(value))
|
|
19
|
+
|
|
20
|
+
args, _ = parser.parse_known_args()
|
|
21
|
+
for name, value in state.items():
|
|
22
|
+
avalue = getattr(args, name, None)
|
|
23
|
+
avalue = value if avalue is None else avalue
|
|
24
|
+
if avalue is not None:
|
|
25
|
+
setattr(self, name, avalue)
|
|
26
|
+
|
|
27
|
+
def __repr__(self):
|
|
28
|
+
cvars = dict()
|
|
29
|
+
for name in dir(self):
|
|
30
|
+
if not name.startswith('_'):
|
|
31
|
+
cvars[name] = getattr(self, name)
|
|
32
|
+
|
|
33
|
+
return ut.stri(cvars)
|
|
34
|
+
|
|
35
|
+
|