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,346 @@
1
+ import numpy as np
2
+
3
+ from . import alog
4
+ from . import assert_checks as tas
5
+ from . import core_utils as cu
6
+
7
+
8
+ def diff_split(data, mask_fn, sort=True):
9
+ if sort:
10
+ indices = np.argsort(data)
11
+ sdata = np.take_along_axis(data, indices, axis=None)
12
+ else:
13
+ indices, sdata = None, data
14
+
15
+ diff = np.diff(sdata)
16
+ mask = mask_fn(diff)
17
+ msteps = np.flatnonzero(np.asarray(mask))
18
+
19
+ sindices = np.arange(len(data))
20
+ splits = np.split(sindices, msteps + 1)
21
+
22
+ # Caller should use data[result[i]] to fetch split data.
23
+ return [indices[s] for s in splits] if indices is not None else splits
24
+
25
+
26
+ def group_splits(data, mask_fn):
27
+ diff = np.diff(data)
28
+ mask = mask_fn(diff)
29
+
30
+ return np.flatnonzero(np.asarray(mask))
31
+
32
+
33
+ def group_by_delta(data, mask_fn):
34
+ return np.split(data, group_splits(data, mask_fn) + 1)
35
+
36
+
37
+ def fillna(data, copy=False, defval=0):
38
+ if copy:
39
+ data = data.copy()
40
+ fdata = data.flatten()
41
+ mask = np.where(np.isnan(fdata))[0]
42
+ for r in group_by_delta(mask, lambda x: x != 1):
43
+ if r.size == 0:
44
+ continue
45
+ vi = r[-1] + 1
46
+ if vi < len(fdata):
47
+ rv = fdata[vi]
48
+ else:
49
+ vi = r[0] - 1
50
+ if vi >= 0:
51
+ rv = fdata[vi]
52
+ else:
53
+ rv = defval
54
+
55
+ fdata[r] = rv
56
+
57
+ return fdata.reshape(data.shape)
58
+
59
+
60
+ def infer_np_dtype(dtypes):
61
+ dtype = None
62
+ for t in dtypes:
63
+ if dtype is None or t == np.float64:
64
+ dtype = t
65
+ elif t == np.float32:
66
+ if dtype.itemsize > t.itemsize:
67
+ dtype = np.float64
68
+ else:
69
+ dtype = t
70
+ elif dtype != np.float64:
71
+ if dtype == np.float32 and t.itemsize > dtype.itemsize:
72
+ dtype = np.float64
73
+ elif t.itemsize > dtype.itemsize:
74
+ dtype = t
75
+
76
+ return dtype if dtype is not None else np.float32
77
+
78
+
79
+ def maybe_stack_slices(slices, axis=0):
80
+ if slices and isinstance(slices[0], np.ndarray):
81
+ return np.stack(slices, axis=axis)
82
+
83
+ return slices
84
+
85
+
86
+ def to_numpy(data):
87
+ if isinstance(data, np.ndarray):
88
+ return data
89
+
90
+ npfn = getattr(data, 'to_numpy', None)
91
+ if callable(npfn):
92
+ return npfn()
93
+
94
+ # This is PyTorch ...
95
+ npfn = getattr(data, 'numpy', None)
96
+ if callable(npfn):
97
+ return npfn(force=True)
98
+
99
+ return np.array(data)
100
+
101
+
102
+ def is_numeric(dtype):
103
+ return np.issubdtype(dtype, np.number)
104
+
105
+
106
+ def is_integer(dtype):
107
+ return np.issubdtype(dtype, np.integer)
108
+
109
+
110
+ def is_floating(dtype):
111
+ return np.issubdtype(dtype, np.floating)
112
+
113
+
114
+ def is_numpy(v):
115
+ return type(v).__module__ == np.__name__
116
+
117
+
118
+ def is_sorted(data, descending=False):
119
+ if not isinstance(data, np.ndarray):
120
+ data = np.array(data)
121
+
122
+ if descending:
123
+ return np.all(data[:-1] >= data[1:])
124
+
125
+ return np.all(data[:-1] <= data[1:])
126
+
127
+
128
+ def astype(data, col, dtype):
129
+ if cu.isdict(dtype):
130
+ cdtype = dtype.get(col)
131
+ elif is_numeric(data.dtype):
132
+ cdtype = dtype
133
+ else:
134
+ cdtype = None
135
+
136
+ return data if cdtype is None else data.astype(cdtype)
137
+
138
+
139
+ def softmax(x):
140
+ e_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
141
+
142
+ return e_x / np.sum(e_x, axis=-1, keepdims=True)
143
+
144
+
145
+ def categorical(un_probs, n=None):
146
+ probs = softmax(un_probs)
147
+ if probs.ndim == 1:
148
+ values = np.random.choice(len(probs), size=n or 1, p=probs)
149
+
150
+ return values[0] if n is None else values
151
+ else:
152
+ fprobs = np.reshape(probs, (-1, probs.shape[-1]))
153
+ values = []
154
+ for p in fprobs:
155
+ values.append(np.random.choice(len(p), size=n or 1, p=p))
156
+
157
+ catv = np.vstack(values)
158
+
159
+ return np.reshape(catv, tuple(probs.shape[: -1]) + (catv.shape[-1],))
160
+
161
+
162
+ def onehot(values, num_categories=None):
163
+ if num_categories is None:
164
+ num_categories = np.max(values) + 1
165
+ else:
166
+ tas.check_lt(np.max(values), num_categories)
167
+
168
+ return np.eye(num_categories)[values]
169
+
170
+
171
+ def normalize(data, axis=None):
172
+ mean = np.mean(data, axis=axis)
173
+ std = np.std(data, axis=axis)
174
+
175
+ if std.ndim > 0:
176
+ std[np.where(std == 0.0)] = 1.0
177
+ elif std == 0.0:
178
+ std = 1.0
179
+
180
+ return (data - mean) / std
181
+
182
+
183
+ def moving_average(data, window, include_current=True):
184
+ weights = np.ones(window, dtype=data.dtype) / window
185
+ pdata = np.pad(data, (window, window), mode='edge')
186
+ cdata = np.convolve(pdata, weights, mode='valid')
187
+ base = 1 if include_current else 0
188
+
189
+ return cdata[base: base + len(data)]
190
+
191
+
192
+ def rolling_window(a, window):
193
+ shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
194
+ strides = a.strides + (a.strides[-1],)
195
+
196
+ return np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)
197
+
198
+
199
+ def shift(data, pos=1):
200
+ result = np.empty_like(data)
201
+ if pos > 0:
202
+ result[: pos] = data[0]
203
+ result[pos:] = data[: -pos]
204
+ elif pos < 0:
205
+ result[pos:] = data[-1]
206
+ result[: pos] = data[-pos:]
207
+ else:
208
+ result[:] = data
209
+
210
+ return result
211
+
212
+
213
+ def complement_indices(indices, size):
214
+ all_indices = np.full(size, 1, dtype=np.int8)
215
+ all_indices[indices] = 0
216
+
217
+ return np.flatnonzero(all_indices)
218
+
219
+
220
+ def polyfit_std(yv, xv=None, deg=1):
221
+ xv = np.arange(len(yv), dtype=np.float32) if xv is None else np.array(xv)
222
+ yv = yv if isinstance(yv, np.ndarray) else np.array(yv)
223
+
224
+ yfit = np.polynomial.Polynomial.fit(xv, yv, deg)
225
+ fyv = yfit(xv)
226
+
227
+ return np.std(yv - fyv), fyv, yfit
228
+
229
+
230
+ def npdict_clone(npd):
231
+ cloned = type(npd)()
232
+ for k, v in npd.items():
233
+ cloned[k] = np.array(v)
234
+
235
+ return cloned
236
+
237
+
238
+ def is_ordered(v, reverse=False):
239
+ npv = to_numpy(v)
240
+
241
+ return np.all(npv[:-1] >= npv[1:]) if reverse else np.all(npv[:-1] <= npv[1:])
242
+
243
+
244
+ class RingBuffer:
245
+
246
+ def __init__(self, capacity, dtype, vshape):
247
+ self.capacity = capacity
248
+ self.dtype = dtype
249
+ self._vshape = tuple(vshape)
250
+ self._count = 0
251
+ self._data = np.zeros((capacity,) + self._vshape, dtype=dtype)
252
+
253
+ @property
254
+ def shape(self):
255
+ return (len(self),) + self._vshape
256
+
257
+ def resize(self, capacity):
258
+ self._data = np.resize(self._data, (capacity,) + self._vshape)
259
+ self._count = min(self._count, self.capacity, capacity)
260
+ self.capacity = capacity
261
+
262
+ def select(self, indices):
263
+ indices = indices[indices < len(self)]
264
+ self._count = len(indices)
265
+ self._data[: self._count] = self._data[indices]
266
+
267
+ def append(self, v):
268
+ self._data[self._count % self.capacity] = v
269
+ self._count += 1
270
+
271
+ def extend(self, v):
272
+ arr = np.asarray(v, dtype=self.dtype)
273
+ if self._vshape:
274
+ arr = arr.reshape((-1,) + self._vshape)
275
+
276
+ pos = self._count % self.capacity
277
+ front = min(self.capacity - pos, len(arr))
278
+
279
+ self._data[pos: pos + front] = arr[: front]
280
+
281
+ back = min(pos, len(arr) - front)
282
+ if back > 0:
283
+ self._data[: back] = arr[front: front + back]
284
+
285
+ self._count += front + back
286
+
287
+ def to_numpy(self):
288
+ return np.concatenate(tuple(self.iter_views()))
289
+
290
+ def data(self, dtype=None):
291
+ return np.array(self._data[: len(self)], dtype=dtype)
292
+
293
+ def iter_views(self):
294
+ if self._count <= self.capacity:
295
+ yield self._data[: self._count]
296
+ else:
297
+ pos = self._count % self.capacity
298
+
299
+ yield self._data[pos:]
300
+
301
+ if pos > 0:
302
+ yield self._data[: pos]
303
+
304
+ def iter_indices(self):
305
+ if self._count <= self.capacity:
306
+ return np.arange(0, self._count)
307
+
308
+ return np.arange(self._count, self._count + self.capacity) % self.capacity
309
+
310
+ def __len__(self):
311
+ return min(self.capacity, self._count)
312
+
313
+ def __getitem__(self, i):
314
+ if isinstance(i, int):
315
+ idx = (max(self._count, self.capacity) + i) % self.capacity
316
+ else:
317
+ # Allow seamless slicing in case of non-integer indexing.
318
+ idx = i
319
+
320
+ return self._data[idx]
321
+
322
+ def __array__(self, dtype=None):
323
+ arr = self.to_numpy()
324
+
325
+ return arr if dtype is None else arr.astype(dtype)
326
+
327
+
328
+ _NP_ARRAY_TYPECODES = {
329
+ bool: 'B',
330
+ int: 'q',
331
+ np.int8: 'b',
332
+ np.uint8: 'B',
333
+ np.int16: 'h',
334
+ np.uint16: 'H',
335
+ np.int32: 'l',
336
+ np.uint32: 'L',
337
+ np.int64: 'q',
338
+ np.uint64: 'Q',
339
+ np.float16: 'f',
340
+ np.float32: 'f',
341
+ np.float64: 'd',
342
+ }
343
+
344
+ def array_typecode(dtype):
345
+ return _NP_ARRAY_TYPECODES.get(dtype.type)
346
+
@@ -0,0 +1,38 @@
1
+ import collections
2
+ import re
3
+
4
+ from . import assert_checks as tas
5
+
6
+
7
+ def extend(base_nt, name, fields, defaults=None):
8
+ if isinstance(fields, str):
9
+ fields = re.split(r'\s*[ ,]\s*', fields)
10
+
11
+ ext_fields, ext_def_fields, ext_defaults = [], [], []
12
+ missing = object()
13
+ for field in base_nt._fields:
14
+ defval = base_nt._field_defaults.get(field, missing)
15
+ if defval is missing:
16
+ ext_fields.append(field)
17
+ else:
18
+ ext_def_fields.append(field)
19
+ ext_defaults.append(defval)
20
+
21
+ defaults = defaults or ()
22
+ for i, field in enumerate(fields):
23
+ defidx = i - (len(fields) - len(defaults))
24
+ if defidx >= 0:
25
+ tas.check(field not in ext_def_fields,
26
+ msg=f'Field already exists: {field} in {ext_def_fields}')
27
+ tas.check(field not in ext_fields,
28
+ msg=f'Field already exists: {field} in {ext_fields}')
29
+ ext_def_fields.append(field)
30
+ ext_defaults.append(defaults[defidx])
31
+ else:
32
+ tas.check(field not in ext_fields,
33
+ msg=f'Field already exists: {field} in {ext_fields}')
34
+ ext_fields.append(field)
35
+
36
+ return collections.namedtuple(name, tuple(ext_fields + ext_def_fields),
37
+ defaults=tuple(ext_defaults))
38
+
@@ -0,0 +1,54 @@
1
+
2
+ def prime_factors(n):
3
+ i = 2
4
+ while i * i <= n:
5
+ q, r = divmod(n, i)
6
+ if r == 0:
7
+ n = q
8
+ yield i
9
+ else:
10
+ i += 1
11
+
12
+ if n > 1:
13
+ yield n
14
+
15
+
16
+ def nearest_divisor(value, n):
17
+ nmin, nmax = n, n
18
+
19
+ while nmin > 1:
20
+ if value % nmin == 0:
21
+ break
22
+ nmin -= 1
23
+
24
+ dmin = n - nmin
25
+ top_n = min(n + dmin, value // 2)
26
+
27
+ while nmax <= top_n:
28
+ if value % nmax == 0:
29
+ break
30
+ nmax += 1
31
+
32
+ if value % nmax != 0:
33
+ return nmin
34
+
35
+ return nmin if dmin < (nmax - n) else nmax
36
+
37
+
38
+ def sign_extend(value, nbits):
39
+ sign = 1 << (nbits - 1)
40
+
41
+ return (value & (sign - 1)) - (value & sign)
42
+
43
+
44
+ def round_up(v, step):
45
+ return ((v + step - 1) // step) * step
46
+
47
+
48
+ def round_down(v, step):
49
+ return (v // step) * step
50
+
51
+
52
+ def mix(a, b, gamma):
53
+ return a * gamma + b * (1.0 - gamma)
54
+
py_misc_utils/obj.py ADDED
@@ -0,0 +1,73 @@
1
+ import copy
2
+
3
+ from . import core_utils as cu
4
+
5
+
6
+ class Obj:
7
+
8
+ def __init__(self, **kwargs):
9
+ self.update(**kwargs)
10
+
11
+ def update(self, **kwargs):
12
+ vars(self).update(kwargs)
13
+
14
+ return self
15
+
16
+ def update_from(self, obj):
17
+ vars(self).update(vars(obj))
18
+
19
+ return self
20
+
21
+ def clone(self, **kwargs):
22
+ nobj = copy.copy(self)
23
+ nobj.update(**kwargs)
24
+
25
+ return nobj
26
+
27
+ def as_dict(self):
28
+ ad = dict()
29
+ for k, v in vars(self).items():
30
+ if isinstance(v, Obj):
31
+ v = v.as_dict()
32
+ elif isinstance(v, (list, tuple)):
33
+ vals = []
34
+ for x in v:
35
+ if isinstance(x, Obj):
36
+ x = x.as_dict()
37
+ vals.append(x)
38
+
39
+ v = type(v)(vals)
40
+ elif cu.isdict(v):
41
+ vd = type(v)()
42
+ for z, x in v.items():
43
+ if isinstance(x, Obj):
44
+ x = x.as_dict()
45
+ vd[z] = x
46
+
47
+ v = vd
48
+
49
+ ad[k] = v
50
+
51
+ return ad
52
+
53
+ def __eq__(self, other):
54
+ missing = object()
55
+ for k, v in vars(self).items():
56
+ ov = getattr(other, k, missing)
57
+ if ov is missing or v != ov:
58
+ return False
59
+ for k in vars(other).keys():
60
+ if not hasattr(self, k):
61
+ return False
62
+
63
+ return True
64
+
65
+ def __repr__(self):
66
+ values = ', '.join(f'{k}={str_value(v)}' for k, v in vars(self).items())
67
+
68
+ return f'{type(self).__name__}({values})'
69
+
70
+
71
+ def str_value(v):
72
+ return '"' + v.replace('"', '\\"') + '"' if isinstance(v, str) else str(v)
73
+
@@ -0,0 +1,100 @@
1
+ import abc
2
+ import collections
3
+ import functools
4
+ import os
5
+ import threading
6
+ import time
7
+
8
+ from . import alog
9
+ from . import fin_wrap as fw
10
+ from . import periodic_task as ptsk
11
+
12
+
13
+ _Entry = collections.namedtuple('Entry', 'name, obj, handler, time')
14
+
15
+
16
+ class Handler(abc.ABC):
17
+
18
+ @abc.abstractmethod
19
+ def create(self):
20
+ ...
21
+
22
+ def max_age(self):
23
+ return 60
24
+
25
+ def is_alive(self, obj):
26
+ return True
27
+
28
+ def close(self, obj):
29
+ pass
30
+
31
+
32
+ class Cache:
33
+
34
+ def __init__(self, clean_timeo=None):
35
+ self._lock = threading.Lock()
36
+ self._cond = threading.Condition(lock=self._lock)
37
+ self._cache = collections.defaultdict(collections.deque)
38
+ self._cleaner = ptsk.PeriodicTask(
39
+ 'CacheCleaner',
40
+ self._try_cleanup,
41
+ clean_timeo or int(os.getenv('CACHE_CLEAN_TIMEO', 8)),
42
+ stop_on_error=False,
43
+ )
44
+ self._cleaner.start()
45
+
46
+ def _try_cleanup(self):
47
+ alog.verbose(f'Object cache cleanup running')
48
+ cleans = []
49
+ with self._lock:
50
+ new_cache = collections.defaultdict(collections.deque)
51
+ for name, cqueue in self._cache.items():
52
+ for entry in cqueue:
53
+ age = time.time() - entry.time
54
+ if age > entry.handler.max_age():
55
+ cleans.append(entry)
56
+ else:
57
+ new_cache[name].append(entry)
58
+
59
+ self._cache = new_cache
60
+
61
+ for entry in cleans:
62
+ alog.debug(f'Cache Clean: name={entry.name} obj={entry.obj}')
63
+ entry.handler.close(entry.obj)
64
+
65
+ def shutdown(self):
66
+ self._cleaner.stop()
67
+
68
+ def _release(self, name, handler, obj):
69
+ alog.debug(f'Cache Release: name={name} obj={obj}')
70
+ with self._lock:
71
+ self._cache[name].append(_Entry(name=name,
72
+ obj=obj,
73
+ handler=handler,
74
+ time=time.time()))
75
+
76
+ def get(self, name, handler):
77
+ with self._lock:
78
+ cqueue, obj = self._cache[name], None
79
+ if cqueue:
80
+ entry = cqueue.popleft()
81
+ if not entry.handler.is_alive(entry.obj):
82
+ entry.handler.close(obj)
83
+ obj = None
84
+ else:
85
+ obj = entry.obj
86
+ alog.debug(f'Cache Hit: name={name} obj={obj}')
87
+
88
+ if obj is None:
89
+ obj = handler.create()
90
+
91
+ finfn = functools.partial(self._release, name, handler, obj)
92
+
93
+ return fw.FinWrapper(obj, finfn)
94
+
95
+
96
+ _CACHE = Cache()
97
+
98
+ def cache():
99
+ return _CACHE
100
+
@@ -0,0 +1,88 @@
1
+ import collections
2
+ import gc
3
+
4
+ from . import alog
5
+ from . import core_utils as cu
6
+ from . import inspect_utils as iu
7
+
8
+
9
+ def _get_referred(obj):
10
+ referred = []
11
+ if cu.isdict(obj):
12
+ for name, value in obj.items():
13
+ referred.append((name, value))
14
+ elif isinstance(obj, (list, tuple)):
15
+ for i, value in enumerate(obj):
16
+ referred.append((f'[{i}]', value))
17
+ elif hasattr(obj, '__dict__'):
18
+ for name, value in vars(obj).items():
19
+ referred.append((name, value))
20
+
21
+ return tuple(referred)
22
+
23
+
24
+ def _get_tracking_references(obj, tracked_by, max_references=None):
25
+ max_references = max_references or 8
26
+
27
+ references = []
28
+ to_track = [(obj, None)]
29
+ while to_track:
30
+ tobj, tname = to_track.pop()
31
+ ntrack = 0
32
+ for rname, robj in tracked_by.get(id(tobj), ()):
33
+ suffix = ''
34
+ if tname is not None:
35
+ sep = '' if tname.startswith('[') else '.'
36
+ suffix = f'{sep}{tname}'
37
+
38
+ to_track.append((robj, f'{rname}{suffix}'))
39
+ ntrack += 1
40
+
41
+ if ntrack == 0:
42
+ references.append((iu.qual_name(tobj), tname))
43
+ if len(references) >= max_references:
44
+ break
45
+
46
+ return tuple(references)
47
+
48
+
49
+ def track_objects(tracker, max_references=None):
50
+ gc.collect()
51
+
52
+ gc_objs = gc.get_objects()
53
+
54
+ all_objs = dict()
55
+ tracking = collections.defaultdict(list)
56
+ tracked_by = collections.defaultdict(list)
57
+ for obj in gc_objs:
58
+ all_objs[id(obj)] = obj
59
+
60
+ referred = _get_referred(obj)
61
+ for rname, robj in referred:
62
+ tracking[id(obj)].append((rname, robj))
63
+ tracked_by[id(robj)].append((rname, obj))
64
+ all_objs[id(robj)] = robj
65
+
66
+ report = []
67
+ for obj in all_objs.values():
68
+ try:
69
+ if (trackres := tracker.track(obj)) is not None:
70
+ prio, info = trackres
71
+
72
+ refs = _get_tracking_references(obj, tracked_by,
73
+ max_references=max_references)
74
+
75
+ treport = [info]
76
+ for r in refs:
77
+ treport.append(f' refby = {r[0]} ({r[1]})')
78
+
79
+ report.append((prio, treport))
80
+ except Exception as ex:
81
+ alog.warning(f'Exception while tracking objects: {ex}')
82
+
83
+ sreport = []
84
+ for r in sorted(report, key=lambda r: r[0]):
85
+ sreport.extend(r[1])
86
+
87
+ return '\n'.join(sreport)
88
+