ominfra 0.0.0.dev403__py3-none-any.whl → 0.0.0.dev405__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.
@@ -79,9 +79,6 @@ CheckOnRaiseFn = ta.Callable[[Exception], None] # ta.TypeAlias
79
79
  CheckExceptionFactory = ta.Callable[..., Exception] # ta.TypeAlias
80
80
  CheckArgsRenderer = ta.Callable[..., ta.Optional[str]] # ta.TypeAlias
81
81
 
82
- # ../../../../omlish/configs/formats.py
83
- ConfigDataT = ta.TypeVar('ConfigDataT', bound='ConfigData')
84
-
85
82
  # ../../../../omlish/lite/contextmanagers.py
86
83
  ExitStackedT = ta.TypeVar('ExitStackedT', bound='ExitStacked')
87
84
  AsyncExitStackedT = ta.TypeVar('AsyncExitStackedT', bound='AsyncExitStacked')
@@ -89,6 +86,9 @@ AsyncExitStackedT = ta.TypeVar('AsyncExitStackedT', bound='AsyncExitStacked')
89
86
  # ../../../threadworkers.py
90
87
  ThreadWorkerT = ta.TypeVar('ThreadWorkerT', bound='ThreadWorker')
91
88
 
89
+ # ../../../../omlish/configs/formats.py
90
+ ConfigDataT = ta.TypeVar('ConfigDataT', bound='ConfigData')
91
+
92
92
 
93
93
  ########################################
94
94
  # ../../../../../omlish/configs/types.py
@@ -1730,6 +1730,201 @@ class Checks:
1730
1730
  check = Checks()
1731
1731
 
1732
1732
 
1733
+ ########################################
1734
+ # ../../../../../omlish/lite/contextmanagers.py
1735
+
1736
+
1737
+ ##
1738
+
1739
+
1740
+ class ExitStacked:
1741
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
1742
+ super().__init_subclass__(**kwargs)
1743
+
1744
+ for a in ('__enter__', '__exit__'):
1745
+ for b in cls.__bases__:
1746
+ if b is ExitStacked:
1747
+ continue
1748
+ try:
1749
+ fn = getattr(b, a)
1750
+ except AttributeError:
1751
+ pass
1752
+ else:
1753
+ if fn is not getattr(ExitStacked, a):
1754
+ raise TypeError(f'ExitStacked subclass {cls} must not not override {a} via {b}')
1755
+
1756
+ _exit_stack: ta.Optional[contextlib.ExitStack] = None
1757
+
1758
+ @contextlib.contextmanager
1759
+ def _exit_stacked_init_wrapper(self) -> ta.Iterator[None]:
1760
+ """
1761
+ Overridable wrapper around __enter__ which deliberately does not have access to an _exit_stack yet. Intended for
1762
+ things like wrapping __enter__ in a lock.
1763
+ """
1764
+
1765
+ yield
1766
+
1767
+ @ta.final
1768
+ def __enter__(self: ExitStackedT) -> ExitStackedT:
1769
+ """
1770
+ Final because any contexts entered during this init must be exited if any exception is thrown, and user
1771
+ overriding would likely interfere with that. Override `_enter_contexts` for such init.
1772
+ """
1773
+
1774
+ with self._exit_stacked_init_wrapper():
1775
+ if self._exit_stack is not None:
1776
+ raise RuntimeError
1777
+ es = self._exit_stack = contextlib.ExitStack()
1778
+ es.__enter__()
1779
+ try:
1780
+ self._enter_contexts()
1781
+ except Exception: # noqa
1782
+ es.__exit__(*sys.exc_info())
1783
+ raise
1784
+ return self
1785
+
1786
+ @ta.final
1787
+ def __exit__(self, exc_type, exc_val, exc_tb):
1788
+ if (es := self._exit_stack) is None:
1789
+ return None
1790
+ try:
1791
+ self._exit_contexts()
1792
+ except Exception: # noqa
1793
+ es.__exit__(*sys.exc_info())
1794
+ raise
1795
+ return es.__exit__(exc_type, exc_val, exc_tb)
1796
+
1797
+ def _enter_contexts(self) -> None:
1798
+ pass
1799
+
1800
+ def _exit_contexts(self) -> None:
1801
+ pass
1802
+
1803
+ def _enter_context(self, cm: ta.ContextManager[T]) -> T:
1804
+ if (es := self._exit_stack) is None:
1805
+ raise RuntimeError
1806
+ return es.enter_context(cm)
1807
+
1808
+
1809
+ class AsyncExitStacked:
1810
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
1811
+ super().__init_subclass__(**kwargs)
1812
+
1813
+ for a in ('__aenter__', '__aexit__'):
1814
+ for b in cls.__bases__:
1815
+ if b is AsyncExitStacked:
1816
+ continue
1817
+ try:
1818
+ fn = getattr(b, a)
1819
+ except AttributeError:
1820
+ pass
1821
+ else:
1822
+ if fn is not getattr(AsyncExitStacked, a):
1823
+ raise TypeError(f'AsyncExitStacked subclass {cls} must not not override {a} via {b}')
1824
+
1825
+ _exit_stack: ta.Optional[contextlib.AsyncExitStack] = None
1826
+
1827
+ @contextlib.asynccontextmanager
1828
+ async def _async_exit_stacked_init_wrapper(self) -> ta.AsyncGenerator[None, None]:
1829
+ yield
1830
+
1831
+ @ta.final
1832
+ async def __aenter__(self: AsyncExitStackedT) -> AsyncExitStackedT:
1833
+ async with self._async_exit_stacked_init_wrapper():
1834
+ if self._exit_stack is not None:
1835
+ raise RuntimeError
1836
+ es = self._exit_stack = contextlib.AsyncExitStack()
1837
+ await es.__aenter__()
1838
+ try:
1839
+ await self._async_enter_contexts()
1840
+ except Exception: # noqa
1841
+ await es.__aexit__(*sys.exc_info())
1842
+ raise
1843
+ return self
1844
+
1845
+ @ta.final
1846
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
1847
+ if (es := self._exit_stack) is None:
1848
+ return None
1849
+ try:
1850
+ await self._async_exit_contexts()
1851
+ except Exception: # noqa
1852
+ await es.__aexit__(*sys.exc_info())
1853
+ raise
1854
+ return await es.__aexit__(exc_type, exc_val, exc_tb)
1855
+
1856
+ async def _async_enter_contexts(self) -> None:
1857
+ pass
1858
+
1859
+ async def _async_exit_contexts(self) -> None:
1860
+ pass
1861
+
1862
+ def _enter_context(self, cm: ta.ContextManager[T]) -> T:
1863
+ if (es := self._exit_stack) is None:
1864
+ raise RuntimeError
1865
+ return es.enter_context(cm)
1866
+
1867
+ async def _enter_async_context(self, cm: ta.AsyncContextManager[T]) -> T:
1868
+ if (es := self._exit_stack) is None:
1869
+ raise RuntimeError
1870
+ return await es.enter_async_context(cm)
1871
+
1872
+
1873
+ ##
1874
+
1875
+
1876
+ @contextlib.contextmanager
1877
+ def defer(fn: ta.Callable, *args: ta.Any, **kwargs: ta.Any) -> ta.Generator[ta.Callable, None, None]:
1878
+ if args or kwargs:
1879
+ fn = functools.partial(fn, *args, **kwargs)
1880
+ try:
1881
+ yield fn
1882
+ finally:
1883
+ fn()
1884
+
1885
+
1886
+ @contextlib.asynccontextmanager
1887
+ async def adefer(fn: ta.Awaitable) -> ta.AsyncGenerator[ta.Awaitable, None]:
1888
+ try:
1889
+ yield fn
1890
+ finally:
1891
+ await fn
1892
+
1893
+
1894
+ ##
1895
+
1896
+
1897
+ @contextlib.contextmanager
1898
+ def attr_setting(obj, attr, val, *, default=None): # noqa
1899
+ not_set = object()
1900
+ orig = getattr(obj, attr, not_set)
1901
+ try:
1902
+ setattr(obj, attr, val)
1903
+ if orig is not not_set:
1904
+ yield orig
1905
+ else:
1906
+ yield default
1907
+ finally:
1908
+ if orig is not_set:
1909
+ delattr(obj, attr)
1910
+ else:
1911
+ setattr(obj, attr, orig)
1912
+
1913
+
1914
+ ##
1915
+
1916
+
1917
+ class aclosing(contextlib.AbstractAsyncContextManager): # noqa
1918
+ def __init__(self, thing):
1919
+ self.thing = thing
1920
+
1921
+ async def __aenter__(self):
1922
+ return self.thing
1923
+
1924
+ async def __aexit__(self, *exc_info):
1925
+ await self.thing.aclose()
1926
+
1927
+
1733
1928
  ########################################
1734
1929
  # ../../../../../omlish/lite/json.py
1735
1930
 
@@ -2728,52 +2923,239 @@ class JournalctlToAwsCursor:
2728
2923
 
2729
2924
 
2730
2925
  ########################################
2731
- # ../../../../../omlish/configs/formats.py
2926
+ # ../../../../threadworkers.py
2732
2927
  """
2733
- Notes:
2734
- - necessarily string-oriented
2735
- - single file, as this is intended to be amalg'd and thus all included anyway
2928
+ FIXME:
2929
+ - group is racy af - meditate on has_started, etc
2736
2930
 
2737
2931
  TODO:
2738
- - ConfigDataMapper? to_map -> ConfigMap?
2739
- - nginx ?
2740
- - raw ?
2932
+ - overhaul stop lol
2933
+ - group -> 'context'? :|
2934
+ - shared stop_event?
2741
2935
  """
2742
2936
 
2743
2937
 
2744
2938
  ##
2745
2939
 
2746
2940
 
2747
- @dc.dataclass(frozen=True)
2748
- class ConfigData(abc.ABC): # noqa
2749
- @abc.abstractmethod
2750
- def as_map(self) -> ConfigMap:
2751
- raise NotImplementedError
2941
+ class ThreadWorker(ExitStacked, abc.ABC):
2942
+ def __init__(
2943
+ self,
2944
+ *,
2945
+ stop_event: ta.Optional[threading.Event] = None,
2946
+ worker_groups: ta.Optional[ta.Iterable['ThreadWorkerGroup']] = None,
2947
+ ) -> None:
2948
+ super().__init__()
2752
2949
 
2950
+ if stop_event is None:
2951
+ stop_event = threading.Event()
2952
+ self._stop_event = stop_event
2753
2953
 
2754
- #
2954
+ self._lock = threading.RLock()
2955
+ self._thread: ta.Optional[threading.Thread] = None
2956
+ self._last_heartbeat: ta.Optional[float] = None
2755
2957
 
2958
+ for g in worker_groups or []:
2959
+ g.add(self)
2756
2960
 
2757
- class ConfigLoader(abc.ABC, ta.Generic[ConfigDataT]):
2758
- @property
2759
- def file_exts(self) -> ta.Sequence[str]:
2760
- return ()
2961
+ #
2761
2962
 
2762
- def match_file(self, n: str) -> bool:
2763
- return '.' in n and n.split('.')[-1] in check.not_isinstance(self.file_exts, str)
2963
+ @contextlib.contextmanager
2964
+ def _exit_stacked_init_wrapper(self) -> ta.Iterator[None]:
2965
+ with self._lock:
2966
+ yield
2764
2967
 
2765
2968
  #
2766
2969
 
2767
- def load_file(self, p: str) -> ConfigDataT:
2768
- with open(p) as f:
2769
- return self.load_str(f.read())
2770
-
2771
- @abc.abstractmethod
2772
- def load_str(self, s: str) -> ConfigDataT:
2773
- raise NotImplementedError
2970
+ def should_stop(self) -> bool:
2971
+ return self._stop_event.is_set()
2774
2972
 
2973
+ class Stopping(Exception): # noqa
2974
+ pass
2775
2975
 
2776
- #
2976
+ #
2977
+
2978
+ @property
2979
+ def last_heartbeat(self) -> ta.Optional[float]:
2980
+ return self._last_heartbeat
2981
+
2982
+ def _heartbeat(
2983
+ self,
2984
+ *,
2985
+ no_stop_check: bool = False,
2986
+ ) -> None:
2987
+ self._last_heartbeat = time.time()
2988
+
2989
+ if not no_stop_check and self.should_stop():
2990
+ log.info('Stopping: %s', self)
2991
+ raise ThreadWorker.Stopping
2992
+
2993
+ #
2994
+
2995
+ def has_started(self) -> bool:
2996
+ return self._thread is not None
2997
+
2998
+ def is_alive(self) -> bool:
2999
+ return (thr := self._thread) is not None and thr.is_alive()
3000
+
3001
+ def start(self) -> None:
3002
+ with self._lock:
3003
+ if self._thread is not None:
3004
+ raise RuntimeError('Thread already started: %r', self)
3005
+
3006
+ thr = threading.Thread(target=self.__thread_main)
3007
+ self._thread = thr
3008
+ thr.start()
3009
+
3010
+ #
3011
+
3012
+ def __thread_main(self) -> None:
3013
+ try:
3014
+ self._run()
3015
+ except ThreadWorker.Stopping:
3016
+ log.exception('Thread worker stopped: %r', self)
3017
+ except Exception: # noqa
3018
+ log.exception('Error in worker thread: %r', self)
3019
+ raise
3020
+
3021
+ @abc.abstractmethod
3022
+ def _run(self) -> None:
3023
+ raise NotImplementedError
3024
+
3025
+ #
3026
+
3027
+ def stop(self) -> None:
3028
+ self._stop_event.set()
3029
+
3030
+ def join(
3031
+ self,
3032
+ timeout: ta.Optional[float] = None,
3033
+ *,
3034
+ unless_not_started: bool = False,
3035
+ ) -> None:
3036
+ with self._lock:
3037
+ if self._thread is None:
3038
+ if not unless_not_started:
3039
+ raise RuntimeError('Thread not started: %r', self)
3040
+ return
3041
+ self._thread.join(timeout)
3042
+
3043
+
3044
+ ##
3045
+
3046
+
3047
+ class ThreadWorkerGroup:
3048
+ @dc.dataclass()
3049
+ class _State:
3050
+ worker: ThreadWorker
3051
+
3052
+ last_heartbeat: ta.Optional[float] = None
3053
+
3054
+ def __init__(self) -> None:
3055
+ super().__init__()
3056
+
3057
+ self._lock = threading.RLock()
3058
+ self._states: ta.Dict[ThreadWorker, ThreadWorkerGroup._State] = {}
3059
+ self._last_heartbeat_check: ta.Optional[float] = None
3060
+
3061
+ #
3062
+
3063
+ def add(self, *workers: ThreadWorker) -> 'ThreadWorkerGroup':
3064
+ with self._lock:
3065
+ for w in workers:
3066
+ if w in self._states:
3067
+ raise KeyError(w)
3068
+ self._states[w] = ThreadWorkerGroup._State(w)
3069
+
3070
+ return self
3071
+
3072
+ #
3073
+
3074
+ def start_all(self) -> None:
3075
+ thrs = list(self._states)
3076
+ with self._lock:
3077
+ for thr in thrs:
3078
+ if not thr.has_started():
3079
+ thr.start()
3080
+
3081
+ def stop_all(self) -> None:
3082
+ for w in reversed(list(self._states)):
3083
+ if w.has_started():
3084
+ w.stop()
3085
+
3086
+ def join_all(self, timeout: ta.Optional[float] = None) -> None:
3087
+ for w in reversed(list(self._states)):
3088
+ if w.has_started():
3089
+ w.join(timeout, unless_not_started=True)
3090
+
3091
+ #
3092
+
3093
+ def get_dead(self) -> ta.List[ThreadWorker]:
3094
+ with self._lock:
3095
+ return [thr for thr in self._states if not thr.is_alive()]
3096
+
3097
+ def check_heartbeats(self) -> ta.Dict[ThreadWorker, float]:
3098
+ with self._lock:
3099
+ dct: ta.Dict[ThreadWorker, float] = {}
3100
+ for thr, st in self._states.items():
3101
+ if not thr.has_started():
3102
+ continue
3103
+ hb = thr.last_heartbeat
3104
+ if hb is None:
3105
+ hb = time.time()
3106
+ st.last_heartbeat = hb
3107
+ dct[st.worker] = time.time() - hb
3108
+ self._last_heartbeat_check = time.time()
3109
+ return dct
3110
+
3111
+
3112
+ ########################################
3113
+ # ../../../../../omlish/configs/formats.py
3114
+ """
3115
+ Notes:
3116
+ - necessarily string-oriented
3117
+ - single file, as this is intended to be amalg'd and thus all included anyway
3118
+
3119
+ TODO:
3120
+ - ConfigDataMapper? to_map -> ConfigMap?
3121
+ - nginx ?
3122
+ - raw ?
3123
+ """
3124
+
3125
+
3126
+ ##
3127
+
3128
+
3129
+ @dc.dataclass(frozen=True)
3130
+ class ConfigData(abc.ABC): # noqa
3131
+ @abc.abstractmethod
3132
+ def as_map(self) -> ConfigMap:
3133
+ raise NotImplementedError
3134
+
3135
+
3136
+ #
3137
+
3138
+
3139
+ class ConfigLoader(abc.ABC, ta.Generic[ConfigDataT]):
3140
+ @property
3141
+ def file_exts(self) -> ta.Sequence[str]:
3142
+ return ()
3143
+
3144
+ def match_file(self, n: str) -> bool:
3145
+ return '.' in n and n.split('.')[-1] in check.not_isinstance(self.file_exts, str)
3146
+
3147
+ #
3148
+
3149
+ def load_file(self, p: str) -> ConfigDataT:
3150
+ with open(p) as f:
3151
+ return self.load_str(f.read())
3152
+
3153
+ @abc.abstractmethod
3154
+ def load_str(self, s: str) -> ConfigDataT:
3155
+ raise NotImplementedError
3156
+
3157
+
3158
+ #
2777
3159
 
2778
3160
 
2779
3161
  class ConfigRenderer(abc.ABC, ta.Generic[ConfigDataT]):
@@ -3193,249 +3575,59 @@ class ReadableListBuffer:
3193
3575
  while i < len(lst):
3194
3576
  if (p := lst[i].find(delim)) >= 0:
3195
3577
  return self._chop(i, p + len(delim))
3196
- i += 1
3197
-
3198
- return i
3199
-
3200
- def read_until(self, delim: bytes = b'\n') -> ta.Optional[bytes]:
3201
- r = self.read_until_(delim)
3202
- return r if isinstance(r, bytes) else None
3203
-
3204
-
3205
- ##
3206
-
3207
-
3208
- class IncrementalWriteBuffer:
3209
- def __init__(
3210
- self,
3211
- data: bytes,
3212
- *,
3213
- write_size: int = 0x10000,
3214
- ) -> None:
3215
- super().__init__()
3216
-
3217
- check.not_empty(data)
3218
- self._len = len(data)
3219
- self._write_size = write_size
3220
-
3221
- self._lst = [
3222
- data[i:i + write_size]
3223
- for i in range(0, len(data), write_size)
3224
- ]
3225
- self._pos = 0
3226
-
3227
- @property
3228
- def rem(self) -> int:
3229
- return self._len - self._pos
3230
-
3231
- def write(self, fn: ta.Callable[[bytes], int]) -> int:
3232
- lst = check.not_empty(self._lst)
3233
-
3234
- t = 0
3235
- for i, d in enumerate(lst): # noqa
3236
- n = fn(check.not_empty(d))
3237
- if not n:
3238
- break
3239
- t += n
3240
-
3241
- if t:
3242
- self._lst = [
3243
- *([d[n:]] if n < len(d) else []),
3244
- *lst[i + 1:],
3245
- ]
3246
- self._pos += t
3247
-
3248
- return t
3249
-
3250
-
3251
- ########################################
3252
- # ../../../../../omlish/lite/contextmanagers.py
3253
-
3254
-
3255
- ##
3256
-
3257
-
3258
- class ExitStacked:
3259
- def __init_subclass__(cls, **kwargs: ta.Any) -> None:
3260
- super().__init_subclass__(**kwargs)
3261
-
3262
- for a in ('__enter__', '__exit__'):
3263
- for b in cls.__bases__:
3264
- if b is ExitStacked:
3265
- continue
3266
- try:
3267
- fn = getattr(b, a)
3268
- except AttributeError:
3269
- pass
3270
- else:
3271
- if fn is not getattr(ExitStacked, a):
3272
- raise TypeError(f'ExitStacked subclass {cls} must not not override {a} via {b}')
3273
-
3274
- _exit_stack: ta.Optional[contextlib.ExitStack] = None
3275
-
3276
- @contextlib.contextmanager
3277
- def _exit_stacked_init_wrapper(self) -> ta.Iterator[None]:
3278
- """
3279
- Overridable wrapper around __enter__ which deliberately does not have access to an _exit_stack yet. Intended for
3280
- things like wrapping __enter__ in a lock.
3281
- """
3282
-
3283
- yield
3284
-
3285
- @ta.final
3286
- def __enter__(self: ExitStackedT) -> ExitStackedT:
3287
- """
3288
- Final because any contexts entered during this init must be exited if any exception is thrown, and user
3289
- overriding would likely interfere with that. Override `_enter_contexts` for such init.
3290
- """
3291
-
3292
- with self._exit_stacked_init_wrapper():
3293
- check.state(self._exit_stack is None)
3294
- es = self._exit_stack = contextlib.ExitStack()
3295
- es.__enter__()
3296
- try:
3297
- self._enter_contexts()
3298
- except Exception: # noqa
3299
- es.__exit__(*sys.exc_info())
3300
- raise
3301
- return self
3302
-
3303
- @ta.final
3304
- def __exit__(self, exc_type, exc_val, exc_tb):
3305
- if (es := self._exit_stack) is None:
3306
- return None
3307
- try:
3308
- self._exit_contexts()
3309
- except Exception: # noqa
3310
- es.__exit__(*sys.exc_info())
3311
- raise
3312
- return es.__exit__(exc_type, exc_val, exc_tb)
3313
-
3314
- def _enter_contexts(self) -> None:
3315
- pass
3316
-
3317
- def _exit_contexts(self) -> None:
3318
- pass
3319
-
3320
- def _enter_context(self, cm: ta.ContextManager[T]) -> T:
3321
- es = check.not_none(self._exit_stack)
3322
- return es.enter_context(cm)
3323
-
3324
-
3325
- class AsyncExitStacked:
3326
- def __init_subclass__(cls, **kwargs: ta.Any) -> None:
3327
- super().__init_subclass__(**kwargs)
3328
-
3329
- for a in ('__aenter__', '__aexit__'):
3330
- for b in cls.__bases__:
3331
- if b is AsyncExitStacked:
3332
- continue
3333
- try:
3334
- fn = getattr(b, a)
3335
- except AttributeError:
3336
- pass
3337
- else:
3338
- if fn is not getattr(AsyncExitStacked, a):
3339
- raise TypeError(f'AsyncExitStacked subclass {cls} must not not override {a} via {b}')
3340
-
3341
- _exit_stack: ta.Optional[contextlib.AsyncExitStack] = None
3342
-
3343
- @contextlib.asynccontextmanager
3344
- async def _async_exit_stacked_init_wrapper(self) -> ta.AsyncGenerator[None, None]:
3345
- yield
3346
-
3347
- @ta.final
3348
- async def __aenter__(self: AsyncExitStackedT) -> AsyncExitStackedT:
3349
- async with self._async_exit_stacked_init_wrapper():
3350
- check.state(self._exit_stack is None)
3351
- es = self._exit_stack = contextlib.AsyncExitStack()
3352
- await es.__aenter__()
3353
- try:
3354
- await self._async_enter_contexts()
3355
- except Exception: # noqa
3356
- await es.__aexit__(*sys.exc_info())
3357
- raise
3358
- return self
3359
-
3360
- @ta.final
3361
- async def __aexit__(self, exc_type, exc_val, exc_tb):
3362
- if (es := self._exit_stack) is None:
3363
- return None
3364
- try:
3365
- await self._async_exit_contexts()
3366
- except Exception: # noqa
3367
- await es.__aexit__(*sys.exc_info())
3368
- raise
3369
- return await es.__aexit__(exc_type, exc_val, exc_tb)
3370
-
3371
- async def _async_enter_contexts(self) -> None:
3372
- pass
3373
-
3374
- async def _async_exit_contexts(self) -> None:
3375
- pass
3376
-
3377
- def _enter_context(self, cm: ta.ContextManager[T]) -> T:
3378
- es = check.not_none(self._exit_stack)
3379
- return es.enter_context(cm)
3380
-
3381
- async def _enter_async_context(self, cm: ta.AsyncContextManager[T]) -> T:
3382
- es = check.not_none(self._exit_stack)
3383
- return await es.enter_async_context(cm)
3384
-
3385
-
3386
- ##
3387
-
3388
-
3389
- @contextlib.contextmanager
3390
- def defer(fn: ta.Callable, *args: ta.Any, **kwargs: ta.Any) -> ta.Generator[ta.Callable, None, None]:
3391
- if args or kwargs:
3392
- fn = functools.partial(fn, *args, **kwargs)
3393
- try:
3394
- yield fn
3395
- finally:
3396
- fn()
3578
+ i += 1
3397
3579
 
3580
+ return i
3398
3581
 
3399
- @contextlib.asynccontextmanager
3400
- async def adefer(fn: ta.Awaitable) -> ta.AsyncGenerator[ta.Awaitable, None]:
3401
- try:
3402
- yield fn
3403
- finally:
3404
- await fn
3582
+ def read_until(self, delim: bytes = b'\n') -> ta.Optional[bytes]:
3583
+ r = self.read_until_(delim)
3584
+ return r if isinstance(r, bytes) else None
3405
3585
 
3406
3586
 
3407
3587
  ##
3408
3588
 
3409
3589
 
3410
- @contextlib.contextmanager
3411
- def attr_setting(obj, attr, val, *, default=None): # noqa
3412
- not_set = object()
3413
- orig = getattr(obj, attr, not_set)
3414
- try:
3415
- setattr(obj, attr, val)
3416
- if orig is not not_set:
3417
- yield orig
3418
- else:
3419
- yield default
3420
- finally:
3421
- if orig is not_set:
3422
- delattr(obj, attr)
3423
- else:
3424
- setattr(obj, attr, orig)
3590
+ class IncrementalWriteBuffer:
3591
+ def __init__(
3592
+ self,
3593
+ data: bytes,
3594
+ *,
3595
+ write_size: int = 0x10000,
3596
+ ) -> None:
3597
+ super().__init__()
3425
3598
 
3599
+ check.not_empty(data)
3600
+ self._len = len(data)
3601
+ self._write_size = write_size
3426
3602
 
3427
- ##
3603
+ self._lst = [
3604
+ data[i:i + write_size]
3605
+ for i in range(0, len(data), write_size)
3606
+ ]
3607
+ self._pos = 0
3428
3608
 
3609
+ @property
3610
+ def rem(self) -> int:
3611
+ return self._len - self._pos
3429
3612
 
3430
- class aclosing(contextlib.AbstractAsyncContextManager): # noqa
3431
- def __init__(self, thing):
3432
- self.thing = thing
3613
+ def write(self, fn: ta.Callable[[bytes], int]) -> int:
3614
+ lst = check.not_empty(self._lst)
3433
3615
 
3434
- async def __aenter__(self):
3435
- return self.thing
3616
+ t = 0
3617
+ for i, d in enumerate(lst): # noqa
3618
+ n = fn(check.not_empty(d))
3619
+ if not n:
3620
+ break
3621
+ t += n
3436
3622
 
3437
- async def __aexit__(self, *exc_info):
3438
- await self.thing.aclose()
3623
+ if t:
3624
+ self._lst = [
3625
+ *([d[n:]] if n < len(d) else []),
3626
+ *lst[i + 1:],
3627
+ ]
3628
+ self._pos += t
3629
+
3630
+ return t
3439
3631
 
3440
3632
 
3441
3633
  ########################################
@@ -4420,193 +4612,6 @@ class JournalctlMessageBuilder:
4420
4612
  return ret
4421
4613
 
4422
4614
 
4423
- ########################################
4424
- # ../../../../threadworkers.py
4425
- """
4426
- FIXME:
4427
- - group is racy af - meditate on has_started, etc
4428
-
4429
- TODO:
4430
- - overhaul stop lol
4431
- - group -> 'context'? :|
4432
- - shared stop_event?
4433
- """
4434
-
4435
-
4436
- ##
4437
-
4438
-
4439
- class ThreadWorker(ExitStacked, abc.ABC):
4440
- def __init__(
4441
- self,
4442
- *,
4443
- stop_event: ta.Optional[threading.Event] = None,
4444
- worker_groups: ta.Optional[ta.Iterable['ThreadWorkerGroup']] = None,
4445
- ) -> None:
4446
- super().__init__()
4447
-
4448
- if stop_event is None:
4449
- stop_event = threading.Event()
4450
- self._stop_event = stop_event
4451
-
4452
- self._lock = threading.RLock()
4453
- self._thread: ta.Optional[threading.Thread] = None
4454
- self._last_heartbeat: ta.Optional[float] = None
4455
-
4456
- for g in worker_groups or []:
4457
- g.add(self)
4458
-
4459
- #
4460
-
4461
- @contextlib.contextmanager
4462
- def _exit_stacked_init_wrapper(self) -> ta.Iterator[None]:
4463
- with self._lock:
4464
- yield
4465
-
4466
- #
4467
-
4468
- def should_stop(self) -> bool:
4469
- return self._stop_event.is_set()
4470
-
4471
- class Stopping(Exception): # noqa
4472
- pass
4473
-
4474
- #
4475
-
4476
- @property
4477
- def last_heartbeat(self) -> ta.Optional[float]:
4478
- return self._last_heartbeat
4479
-
4480
- def _heartbeat(
4481
- self,
4482
- *,
4483
- no_stop_check: bool = False,
4484
- ) -> None:
4485
- self._last_heartbeat = time.time()
4486
-
4487
- if not no_stop_check and self.should_stop():
4488
- log.info('Stopping: %s', self)
4489
- raise ThreadWorker.Stopping
4490
-
4491
- #
4492
-
4493
- def has_started(self) -> bool:
4494
- return self._thread is not None
4495
-
4496
- def is_alive(self) -> bool:
4497
- return (thr := self._thread) is not None and thr.is_alive()
4498
-
4499
- def start(self) -> None:
4500
- with self._lock:
4501
- if self._thread is not None:
4502
- raise RuntimeError('Thread already started: %r', self)
4503
-
4504
- thr = threading.Thread(target=self.__thread_main)
4505
- self._thread = thr
4506
- thr.start()
4507
-
4508
- #
4509
-
4510
- def __thread_main(self) -> None:
4511
- try:
4512
- self._run()
4513
- except ThreadWorker.Stopping:
4514
- log.exception('Thread worker stopped: %r', self)
4515
- except Exception: # noqa
4516
- log.exception('Error in worker thread: %r', self)
4517
- raise
4518
-
4519
- @abc.abstractmethod
4520
- def _run(self) -> None:
4521
- raise NotImplementedError
4522
-
4523
- #
4524
-
4525
- def stop(self) -> None:
4526
- self._stop_event.set()
4527
-
4528
- def join(
4529
- self,
4530
- timeout: ta.Optional[float] = None,
4531
- *,
4532
- unless_not_started: bool = False,
4533
- ) -> None:
4534
- with self._lock:
4535
- if self._thread is None:
4536
- if not unless_not_started:
4537
- raise RuntimeError('Thread not started: %r', self)
4538
- return
4539
- self._thread.join(timeout)
4540
-
4541
-
4542
- ##
4543
-
4544
-
4545
- class ThreadWorkerGroup:
4546
- @dc.dataclass()
4547
- class _State:
4548
- worker: ThreadWorker
4549
-
4550
- last_heartbeat: ta.Optional[float] = None
4551
-
4552
- def __init__(self) -> None:
4553
- super().__init__()
4554
-
4555
- self._lock = threading.RLock()
4556
- self._states: ta.Dict[ThreadWorker, ThreadWorkerGroup._State] = {}
4557
- self._last_heartbeat_check: ta.Optional[float] = None
4558
-
4559
- #
4560
-
4561
- def add(self, *workers: ThreadWorker) -> 'ThreadWorkerGroup':
4562
- with self._lock:
4563
- for w in workers:
4564
- if w in self._states:
4565
- raise KeyError(w)
4566
- self._states[w] = ThreadWorkerGroup._State(w)
4567
-
4568
- return self
4569
-
4570
- #
4571
-
4572
- def start_all(self) -> None:
4573
- thrs = list(self._states)
4574
- with self._lock:
4575
- for thr in thrs:
4576
- if not thr.has_started():
4577
- thr.start()
4578
-
4579
- def stop_all(self) -> None:
4580
- for w in reversed(list(self._states)):
4581
- if w.has_started():
4582
- w.stop()
4583
-
4584
- def join_all(self, timeout: ta.Optional[float] = None) -> None:
4585
- for w in reversed(list(self._states)):
4586
- if w.has_started():
4587
- w.join(timeout, unless_not_started=True)
4588
-
4589
- #
4590
-
4591
- def get_dead(self) -> ta.List[ThreadWorker]:
4592
- with self._lock:
4593
- return [thr for thr in self._states if not thr.is_alive()]
4594
-
4595
- def check_heartbeats(self) -> ta.Dict[ThreadWorker, float]:
4596
- with self._lock:
4597
- dct: ta.Dict[ThreadWorker, float] = {}
4598
- for thr, st in self._states.items():
4599
- if not thr.has_started():
4600
- continue
4601
- hb = thr.last_heartbeat
4602
- if hb is None:
4603
- hb = time.time()
4604
- st.last_heartbeat = hb
4605
- dct[st.worker] = time.time() - hb
4606
- self._last_heartbeat_check = time.time()
4607
- return dct
4608
-
4609
-
4610
4615
  ########################################
4611
4616
  # ../../../../../omlish/lite/configs.py
4612
4617