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.
Files changed (117) hide show
  1. py_misc_utils/__init__.py +0 -0
  2. py_misc_utils/abs_timeout.py +12 -0
  3. py_misc_utils/alog.py +311 -0
  4. py_misc_utils/app_main.py +179 -0
  5. py_misc_utils/archive_streamer.py +112 -0
  6. py_misc_utils/assert_checks.py +118 -0
  7. py_misc_utils/ast_utils.py +121 -0
  8. py_misc_utils/async_manager.py +189 -0
  9. py_misc_utils/break_control.py +63 -0
  10. py_misc_utils/buffered_iterator.py +35 -0
  11. py_misc_utils/cached_file.py +507 -0
  12. py_misc_utils/call_limiter.py +26 -0
  13. py_misc_utils/call_result_selector.py +13 -0
  14. py_misc_utils/cleanups.py +85 -0
  15. py_misc_utils/cmd.py +97 -0
  16. py_misc_utils/compression.py +116 -0
  17. py_misc_utils/cond_waiter.py +13 -0
  18. py_misc_utils/context_base.py +18 -0
  19. py_misc_utils/context_managers.py +67 -0
  20. py_misc_utils/core_utils.py +577 -0
  21. py_misc_utils/daemon_process.py +252 -0
  22. py_misc_utils/data_cache.py +46 -0
  23. py_misc_utils/date_utils.py +90 -0
  24. py_misc_utils/debug.py +24 -0
  25. py_misc_utils/dyn_modules.py +50 -0
  26. py_misc_utils/dynamod.py +103 -0
  27. py_misc_utils/env_config.py +35 -0
  28. py_misc_utils/executor.py +239 -0
  29. py_misc_utils/file_overwrite.py +29 -0
  30. py_misc_utils/fin_wrap.py +77 -0
  31. py_misc_utils/fp_utils.py +47 -0
  32. py_misc_utils/fs/__init__.py +0 -0
  33. py_misc_utils/fs/file_fs.py +127 -0
  34. py_misc_utils/fs/ftp_fs.py +242 -0
  35. py_misc_utils/fs/gcs_fs.py +196 -0
  36. py_misc_utils/fs/http_fs.py +241 -0
  37. py_misc_utils/fs/s3_fs.py +417 -0
  38. py_misc_utils/fs_base.py +133 -0
  39. py_misc_utils/fs_utils.py +207 -0
  40. py_misc_utils/gcs_fs.py +169 -0
  41. py_misc_utils/gen_indices.py +54 -0
  42. py_misc_utils/gfs.py +371 -0
  43. py_misc_utils/git_repo.py +77 -0
  44. py_misc_utils/global_namespace.py +110 -0
  45. py_misc_utils/http_async_fetcher.py +139 -0
  46. py_misc_utils/http_server.py +196 -0
  47. py_misc_utils/http_utils.py +143 -0
  48. py_misc_utils/img_utils.py +20 -0
  49. py_misc_utils/infix_op.py +20 -0
  50. py_misc_utils/inspect_utils.py +205 -0
  51. py_misc_utils/iostream.py +21 -0
  52. py_misc_utils/iter_file.py +117 -0
  53. py_misc_utils/key_wrap.py +46 -0
  54. py_misc_utils/lazy_import.py +25 -0
  55. py_misc_utils/lockfile.py +164 -0
  56. py_misc_utils/mem_size.py +64 -0
  57. py_misc_utils/mirror_from.py +72 -0
  58. py_misc_utils/mmap.py +16 -0
  59. py_misc_utils/module_utils.py +196 -0
  60. py_misc_utils/moving_average.py +19 -0
  61. py_misc_utils/msgpack_streamer.py +26 -0
  62. py_misc_utils/multi_wait.py +24 -0
  63. py_misc_utils/multiprocessing.py +102 -0
  64. py_misc_utils/named_array.py +224 -0
  65. py_misc_utils/no_break.py +46 -0
  66. py_misc_utils/no_except.py +32 -0
  67. py_misc_utils/np_ml_framework.py +184 -0
  68. py_misc_utils/np_utils.py +346 -0
  69. py_misc_utils/ntuple_utils.py +38 -0
  70. py_misc_utils/num_utils.py +54 -0
  71. py_misc_utils/obj.py +73 -0
  72. py_misc_utils/object_cache.py +100 -0
  73. py_misc_utils/object_tracker.py +88 -0
  74. py_misc_utils/ordered_set.py +71 -0
  75. py_misc_utils/osfd.py +27 -0
  76. py_misc_utils/packet.py +22 -0
  77. py_misc_utils/parquet_streamer.py +69 -0
  78. py_misc_utils/pd_utils.py +254 -0
  79. py_misc_utils/periodic_task.py +61 -0
  80. py_misc_utils/pickle_wrap.py +121 -0
  81. py_misc_utils/pipeline.py +98 -0
  82. py_misc_utils/remap_pickle.py +50 -0
  83. py_misc_utils/resource_manager.py +155 -0
  84. py_misc_utils/rnd_utils.py +56 -0
  85. py_misc_utils/run_once.py +19 -0
  86. py_misc_utils/scheduler.py +135 -0
  87. py_misc_utils/select_params.py +300 -0
  88. py_misc_utils/signal.py +141 -0
  89. py_misc_utils/skl_utils.py +270 -0
  90. py_misc_utils/split.py +147 -0
  91. py_misc_utils/state.py +53 -0
  92. py_misc_utils/std_module.py +56 -0
  93. py_misc_utils/stream_dataframe.py +176 -0
  94. py_misc_utils/streamed_file.py +144 -0
  95. py_misc_utils/tempdir.py +79 -0
  96. py_misc_utils/template_replace.py +51 -0
  97. py_misc_utils/tensor_stream.py +269 -0
  98. py_misc_utils/thread_context.py +33 -0
  99. py_misc_utils/throttle.py +30 -0
  100. py_misc_utils/time_trigger.py +18 -0
  101. py_misc_utils/timegen.py +11 -0
  102. py_misc_utils/traceback.py +49 -0
  103. py_misc_utils/tracking_executor.py +91 -0
  104. py_misc_utils/transform_array.py +42 -0
  105. py_misc_utils/uncompress.py +35 -0
  106. py_misc_utils/url_fetcher.py +157 -0
  107. py_misc_utils/utils.py +538 -0
  108. py_misc_utils/varint.py +50 -0
  109. py_misc_utils/virt_array.py +52 -0
  110. py_misc_utils/weak_call.py +33 -0
  111. py_misc_utils/work_results.py +100 -0
  112. py_misc_utils/writeback_file.py +43 -0
  113. python_misc_utils-0.2.dist-info/METADATA +36 -0
  114. python_misc_utils-0.2.dist-info/RECORD +117 -0
  115. python_misc_utils-0.2.dist-info/WHEEL +5 -0
  116. python_misc_utils-0.2.dist-info/licenses/LICENSE +13 -0
  117. 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
+
@@ -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
+