omlish 0.0.0.dev102__py3-none-any.whl → 0.0.0.dev104__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.
@@ -0,0 +1,293 @@
1
+ """
2
+ TODO:
3
+ - https://docs.python.org/3/library/zlib.html#zlib.compressobj
4
+ """
5
+ import abc
6
+ import contextlib
7
+ import io
8
+ import typing as ta
9
+
10
+ from .. import check
11
+ from .. import lang
12
+ from ..concurrent import threadlets as tls
13
+ from ..sync import ConditionDeque
14
+
15
+
16
+ if ta.TYPE_CHECKING:
17
+ import threading
18
+
19
+ from . import pyio # noqa
20
+
21
+ else:
22
+ threading = lang.proxy_import('threading')
23
+
24
+ pyio = lang.proxy_import('.pyio', __package__)
25
+
26
+
27
+ T = ta.TypeVar('T')
28
+
29
+ BytesLike: ta.TypeAlias = ta.Any
30
+
31
+ BufferedReader = io.BufferedReader
32
+ # BufferedReader = pyio.BufferedReader
33
+
34
+
35
+ ##
36
+
37
+
38
+ class ProxyReadFile:
39
+ def __init__(self, read: ta.Callable[[int], bytes | BaseException]) -> None:
40
+ super().__init__()
41
+
42
+ self._read = read
43
+ self._eof = False
44
+ self._closed = False
45
+
46
+ #
47
+
48
+ def read(self, n: int, /) -> bytes:
49
+ if self._closed:
50
+ raise RuntimeError('Closed')
51
+ if self._eof:
52
+ return b''
53
+ d = self._read(n)
54
+ if isinstance(d, BaseException):
55
+ raise d
56
+ if not d:
57
+ self._eof = True
58
+ return d
59
+
60
+ def readall(self, *args, **kwargs):
61
+ raise TypeError # FIXME
62
+
63
+ def readinto(self, b: BytesLike) -> int | None:
64
+ d = self.read(len(b))
65
+ if d:
66
+ b[:len(d)] = d
67
+ return len(d)
68
+
69
+ #
70
+
71
+ def readable(self) -> bool:
72
+ return not (self._eof or self._closed)
73
+
74
+ @property
75
+ def closed(self) -> bool:
76
+ return self._closed
77
+
78
+ def close(self) -> None:
79
+ self._closed = True
80
+
81
+ #
82
+
83
+ def flush(self) -> None:
84
+ pass
85
+
86
+ def seekable(self) -> bool:
87
+ return False
88
+
89
+ def seek(self, n: int, /) -> ta.Any:
90
+ raise TypeError
91
+
92
+
93
+ ##
94
+
95
+
96
+ class NeedMore(lang.Marker):
97
+ pass
98
+
99
+
100
+ class Exited(lang.Marker):
101
+ pass
102
+
103
+
104
+ class Shutdown(BaseException): # noqa
105
+ pass
106
+
107
+
108
+ IoTrampolineTarget: ta.TypeAlias = ta.Callable[[io.BufferedReader], ta.ContextManager[ta.Callable[[], bytes]]]
109
+
110
+
111
+ class IoTrampoline(lang.Abstract):
112
+ def __init__(
113
+ self,
114
+ target: IoTrampolineTarget,
115
+ *,
116
+ buffer_size: int | None = None,
117
+ ) -> None:
118
+ super().__init__()
119
+
120
+ self._target = target
121
+ self._buffer_size = buffer_size
122
+
123
+ def _make_buffered_reader(self, raw: ta.Any) -> io.BufferedReader:
124
+ return io.BufferedReader(
125
+ raw,
126
+ **(dict(buffer_size=self._buffer_size) if self._buffer_size is not None else {}),
127
+ )
128
+
129
+ @abc.abstractmethod
130
+ def close(self, timeout: float | None = None) -> None:
131
+ raise NotImplementedError
132
+
133
+ @abc.abstractmethod
134
+ def __enter__(self) -> ta.Self:
135
+ raise NotImplementedError
136
+
137
+ @abc.abstractmethod
138
+ def __exit__(self, exc_type, exc_val, exc_tb):
139
+ raise NotImplementedError
140
+
141
+ @abc.abstractmethod
142
+ def feed(self, *data: bytes) -> ta.Iterable[bytes]:
143
+ raise NotImplementedError
144
+
145
+
146
+ #
147
+
148
+
149
+ class ThreadIoTrampoline(IoTrampoline):
150
+ def __init__(self, target: IoTrampolineTarget, **kwargs: ta.Any) -> None:
151
+ super().__init__(target, **kwargs)
152
+
153
+ self._in: ConditionDeque[bytes | BaseException] = ConditionDeque()
154
+ self._out: ConditionDeque[
155
+ bytes | # noqa
156
+ type[NeedMore] |
157
+ type[Exited] |
158
+ BaseException
159
+ ] = ConditionDeque()
160
+
161
+ self._proxy = ProxyReadFile(self._read)
162
+
163
+ self._thread = threading.Thread(target=self._thread_proc, daemon=True)
164
+
165
+ #
166
+
167
+ def close(self, timeout: float | None = None) -> None:
168
+ if not self._thread.is_alive():
169
+ return
170
+ self._out.push(Shutdown())
171
+ self._thread.join(timeout)
172
+ if self._thread.is_alive():
173
+ if timeout is not None:
174
+ raise TimeoutError
175
+ else:
176
+ raise RuntimeError('Failed to join thread')
177
+
178
+ #
179
+
180
+ def __enter__(self) -> ta.Self:
181
+ self._thread.start()
182
+ return self
183
+
184
+ def __exit__(self, exc_type, exc_val, exc_tb):
185
+ self.close()
186
+
187
+ #
188
+
189
+ def _read(self, n: int, /) -> bytes | BaseException:
190
+ return self._in.pop(if_empty=lambda: self._out.push(NeedMore))
191
+
192
+ def _thread_proc(self) -> None:
193
+ try:
194
+ with contextlib.closing(self._make_buffered_reader(self._proxy)) as bf: # noqa
195
+ with self._target(bf) as read:
196
+ while out := read():
197
+ self._out.push(out)
198
+ self._out.push(out)
199
+ except BaseException as e:
200
+ self._out.push(e)
201
+ raise
202
+ finally:
203
+ self._out.push(Exited)
204
+
205
+ def feed(self, *data: bytes) -> ta.Iterable[bytes]:
206
+ self._in.push(*data)
207
+ while True:
208
+ e = self._out.pop()
209
+ if e is NeedMore:
210
+ break
211
+ elif isinstance(e, BaseException):
212
+ raise e
213
+ elif e is Exited:
214
+ raise RuntimeError('IO thread exited')
215
+ elif isinstance(e, bytes):
216
+ yield e
217
+ if not e:
218
+ return
219
+ else:
220
+ raise TypeError(e)
221
+
222
+
223
+ #
224
+
225
+
226
+ class ThreadletIoTrampoline(IoTrampoline):
227
+ def __init__(
228
+ self,
229
+ target: IoTrampolineTarget,
230
+ threadlets: tls.Threadlets = tls.GREENLET_THREADLETS,
231
+ ** kwargs: ta.Any,
232
+ ) -> None:
233
+ super().__init__(target, **kwargs)
234
+
235
+ self._proxy = ProxyReadFile(self._read)
236
+ self._tl: tls.Threadlet = threadlets.spawn(self._g_proc)
237
+
238
+ #
239
+
240
+ def close(self, timeout: float | None = None) -> None:
241
+ if self._tl.dead:
242
+ return
243
+ out = self._tl.switch(Shutdown())
244
+ if out is not Exited or not self._tl.dead:
245
+ raise RuntimeError
246
+
247
+ #
248
+
249
+ def __enter__(self) -> ta.Self:
250
+ out = self._tl.switch()
251
+ if out is not NeedMore:
252
+ raise RuntimeError
253
+ return self
254
+
255
+ def __exit__(self, exc_type, exc_val, exc_tb):
256
+ self.close()
257
+
258
+ #
259
+
260
+ def _read(self, n: int, /) -> bytes:
261
+ out = check.not_none(self._tl.parent).switch(NeedMore)
262
+ return out
263
+
264
+ def _g_proc(self) -> ta.Any:
265
+ try:
266
+ with contextlib.closing(self._make_buffered_reader(self._proxy)) as bf: # noqa
267
+ with self._target(bf) as read:
268
+ while out := read():
269
+ e = check.not_none(self._tl.parent).switch(out)
270
+ if e is not NeedMore:
271
+ raise TypeError(e) # noqa
272
+ e = check.not_none(self._tl.parent).switch(out)
273
+ if not isinstance(e, Shutdown):
274
+ raise TypeError(e) # noqa
275
+ return Exited
276
+ except BaseException as e:
277
+ check.not_none(self._tl.parent).throw(e)
278
+ raise
279
+
280
+ def feed(self, *data: bytes) -> ta.Iterable[bytes]:
281
+ i: bytes | type[NeedMore]
282
+ for i in data:
283
+ while True:
284
+ e = self._tl.switch(i)
285
+ i = NeedMore
286
+ if e is NeedMore:
287
+ break
288
+ elif isinstance(e, bytes):
289
+ yield e
290
+ if not e:
291
+ return
292
+ else:
293
+ raise TypeError(e)
omlish/lite/cached.py CHANGED
@@ -1,7 +1,11 @@
1
1
  import functools
2
+ import typing as ta
2
3
 
3
4
 
4
- class cached_nullary: # noqa
5
+ T = ta.TypeVar('T')
6
+
7
+
8
+ class _cached_nullary: # noqa
5
9
  def __init__(self, fn):
6
10
  super().__init__()
7
11
  self._fn = fn
@@ -16,3 +20,7 @@ class cached_nullary: # noqa
16
20
  def __get__(self, instance, owner): # noqa
17
21
  bound = instance.__dict__[self._fn.__name__] = self.__class__(self._fn.__get__(instance, owner))
18
22
  return bound
23
+
24
+
25
+ def cached_nullary(fn: ta.Callable[..., T]) -> ta.Callable[..., T]:
26
+ return _cached_nullary(fn)
@@ -1,4 +1,38 @@
1
+ # ruff: noqa: UP007
1
2
  import contextlib
3
+ import typing as ta
4
+
5
+ from .check import check_not_none
6
+ from .check import check_state
7
+
8
+
9
+ T = ta.TypeVar('T')
10
+ ExitStackedT = ta.TypeVar('ExitStackedT', bound='ExitStacked')
11
+
12
+
13
+ ##
14
+
15
+
16
+ class ExitStacked:
17
+ _exit_stack: ta.Optional[contextlib.ExitStack] = None
18
+
19
+ def __enter__(self: ExitStackedT) -> ExitStackedT:
20
+ check_state(self._exit_stack is None)
21
+ es = self._exit_stack = contextlib.ExitStack()
22
+ es.__enter__()
23
+ return self
24
+
25
+ def __exit__(self, exc_type, exc_val, exc_tb):
26
+ if (es := self._exit_stack) is None:
27
+ return None
28
+ return es.__exit__(exc_type, exc_val, exc_tb)
29
+
30
+ def _enter_context(self, cm: ta.ContextManager[T]) -> T:
31
+ es = check_not_none(self._exit_stack)
32
+ return es.enter_context(cm)
33
+
34
+
35
+ ##
2
36
 
3
37
 
4
38
  @contextlib.contextmanager
omlish/lite/marshal.py CHANGED
@@ -12,6 +12,8 @@ import datetime
12
12
  import decimal
13
13
  import enum
14
14
  import fractions
15
+ import functools
16
+ import threading
15
17
  import typing as ta
16
18
  import uuid
17
19
  import weakref # noqa
@@ -150,7 +152,7 @@ class DataclassObjMarshaler(ObjMarshaler):
150
152
  return {k: m.marshal(getattr(o, k)) for k, m in self.fs.items()}
151
153
 
152
154
  def unmarshal(self, o: ta.Any) -> ta.Any:
153
- return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items() if self.nonstrict or k in self.fs})
155
+ return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items() if not self.nonstrict or k in self.fs})
154
156
 
155
157
 
156
158
  @dc.dataclass(frozen=True)
@@ -210,7 +212,10 @@ class UuidObjMarshaler(ObjMarshaler):
210
212
  return uuid.UUID(o)
211
213
 
212
214
 
213
- _OBJ_MARSHALERS: ta.Dict[ta.Any, ObjMarshaler] = {
215
+ ##
216
+
217
+
218
+ _DEFAULT_OBJ_MARSHALERS: ta.Dict[ta.Any, ObjMarshaler] = {
214
219
  **{t: NopObjMarshaler() for t in (type(None),)},
215
220
  **{t: CastObjMarshaler(t) for t in (int, float, str, bool)},
216
221
  **{t: Base64ObjMarshaler(t) for t in (bytes, bytearray)},
@@ -239,20 +244,19 @@ _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES: ta.Dict[ta.Any, type] = {
239
244
  }
240
245
 
241
246
 
242
- def register_opj_marshaler(ty: ta.Any, m: ObjMarshaler) -> None:
243
- if ty in _OBJ_MARSHALERS:
244
- raise KeyError(ty)
245
- _OBJ_MARSHALERS[ty] = m
246
-
247
-
248
- def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
247
+ def _make_obj_marshaler(
248
+ ty: ta.Any,
249
+ rec: ta.Callable[[ta.Any], ObjMarshaler],
250
+ *,
251
+ nonstrict_dataclasses: bool = False,
252
+ ) -> ObjMarshaler:
249
253
  if isinstance(ty, type):
250
254
  if abc.ABC in ty.__bases__:
251
255
  impls = [ # type: ignore
252
256
  PolymorphicObjMarshaler.Impl(
253
257
  ity,
254
258
  ity.__qualname__,
255
- get_obj_marshaler(ity),
259
+ rec(ity),
256
260
  )
257
261
  for ity in deep_subclasses(ty)
258
262
  if abc.ABC not in ity.__bases__
@@ -268,7 +272,8 @@ def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
268
272
  if dc.is_dataclass(ty):
269
273
  return DataclassObjMarshaler(
270
274
  ty,
271
- {f.name: get_obj_marshaler(f.type) for f in dc.fields(ty)},
275
+ {f.name: rec(f.type) for f in dc.fields(ty)},
276
+ nonstrict=nonstrict_dataclasses,
272
277
  )
273
278
 
274
279
  if is_generic_alias(ty):
@@ -278,7 +283,7 @@ def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
278
283
  pass
279
284
  else:
280
285
  k, v = ta.get_args(ty)
281
- return MappingObjMarshaler(mt, get_obj_marshaler(k), get_obj_marshaler(v))
286
+ return MappingObjMarshaler(mt, rec(k), rec(v))
282
287
 
283
288
  try:
284
289
  st = _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES[ta.get_origin(ty)]
@@ -286,33 +291,71 @@ def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
286
291
  pass
287
292
  else:
288
293
  [e] = ta.get_args(ty)
289
- return IterableObjMarshaler(st, get_obj_marshaler(e))
294
+ return IterableObjMarshaler(st, rec(e))
290
295
 
291
296
  if is_union_alias(ty):
292
- return OptionalObjMarshaler(get_obj_marshaler(get_optional_alias_arg(ty)))
297
+ return OptionalObjMarshaler(rec(get_optional_alias_arg(ty)))
293
298
 
294
299
  raise TypeError(ty)
295
300
 
296
301
 
297
- def get_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
298
- try:
299
- return _OBJ_MARSHALERS[ty]
300
- except KeyError:
301
- pass
302
-
303
- p = ProxyObjMarshaler()
304
- _OBJ_MARSHALERS[ty] = p
305
- try:
306
- m = _make_obj_marshaler(ty)
307
- except Exception:
308
- del _OBJ_MARSHALERS[ty]
309
- raise
310
- else:
311
- p.m = m
302
+ ##
303
+
304
+
305
+ _OBJ_MARSHALERS_LOCK = threading.RLock()
306
+
307
+ _OBJ_MARSHALERS: ta.Dict[ta.Any, ObjMarshaler] = dict(_DEFAULT_OBJ_MARSHALERS)
308
+
309
+ _OBJ_MARSHALER_PROXIES: ta.Dict[ta.Any, ProxyObjMarshaler] = {}
310
+
311
+
312
+ def register_opj_marshaler(ty: ta.Any, m: ObjMarshaler) -> None:
313
+ with _OBJ_MARSHALERS_LOCK:
314
+ if ty in _OBJ_MARSHALERS:
315
+ raise KeyError(ty)
312
316
  _OBJ_MARSHALERS[ty] = m
317
+
318
+
319
+ def get_obj_marshaler(
320
+ ty: ta.Any,
321
+ *,
322
+ no_cache: bool = False,
323
+ **kwargs: ta.Any,
324
+ ) -> ObjMarshaler:
325
+ with _OBJ_MARSHALERS_LOCK:
326
+ if not no_cache:
327
+ try:
328
+ return _OBJ_MARSHALERS[ty]
329
+ except KeyError:
330
+ pass
331
+
332
+ try:
333
+ return _OBJ_MARSHALER_PROXIES[ty]
334
+ except KeyError:
335
+ pass
336
+
337
+ rec = functools.partial(
338
+ get_obj_marshaler,
339
+ no_cache=no_cache,
340
+ **kwargs,
341
+ )
342
+
343
+ p = ProxyObjMarshaler()
344
+ _OBJ_MARSHALER_PROXIES[ty] = p
345
+ try:
346
+ m = _make_obj_marshaler(ty, rec, **kwargs)
347
+ finally:
348
+ del _OBJ_MARSHALER_PROXIES[ty]
349
+ p.m = m
350
+
351
+ if not no_cache:
352
+ _OBJ_MARSHALERS[ty] = m
313
353
  return m
314
354
 
315
355
 
356
+ ##
357
+
358
+
316
359
  def marshal_obj(o: ta.Any, ty: ta.Any = None) -> ta.Any:
317
360
  return get_obj_marshaler(ty if ty is not None else type(o)).marshal(o)
318
361
 
omlish/lite/pidfile.py CHANGED
@@ -30,7 +30,7 @@ class Pidfile:
30
30
  return self
31
31
 
32
32
  def __exit__(self, exc_type, exc_val, exc_tb):
33
- if self._f is not None:
33
+ if hasattr(self, '_f'):
34
34
  self._f.close()
35
35
  del self._f
36
36
 
@@ -110,3 +110,21 @@ def subprocess_try_output(
110
110
  def subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
111
111
  out = subprocess_try_output(*args, **kwargs)
112
112
  return out.decode().strip() if out is not None else None
113
+
114
+
115
+ ##
116
+
117
+
118
+ def subprocess_close(
119
+ proc: subprocess.Popen,
120
+ timeout: ta.Optional[float] = None,
121
+ ) -> None:
122
+ # TODO: terminate, sleep, kill
123
+ if proc.stdout:
124
+ proc.stdout.close()
125
+ if proc.stderr:
126
+ proc.stderr.close()
127
+ if proc.stdin:
128
+ proc.stdin.close()
129
+
130
+ proc.wait(timeout)
omlish/specs/__init__.py CHANGED
@@ -4,6 +4,8 @@ TODO:
4
4
  - iceberg
5
5
  - jsonnet
6
6
  - jsonpatch
7
+ - jsonpath
7
8
  - jsonpointer.py
8
9
  - protobuf
10
+ - irc? or in net?
9
11
  """
omlish/sync.py CHANGED
@@ -3,6 +3,7 @@ TODO:
3
3
  - sync (lol) w/ asyncs.anyio
4
4
  - atomics
5
5
  """
6
+ import collections
6
7
  import threading
7
8
  import typing as ta
8
9
 
@@ -68,3 +69,57 @@ class LazyFn(ta.Generic[T]):
68
69
  self._v = lang.just(self._fn())
69
70
  self._once.do(do)
70
71
  return self._v.must()
72
+
73
+
74
+ class ConditionDeque(ta.Generic[T]):
75
+ def __init__(
76
+ self,
77
+ *,
78
+ cond: ta.Optional['threading.Condition'] = None,
79
+ deque: collections.deque[T] | None = None,
80
+
81
+ lock: ta.Optional['threading.RLock'] = None,
82
+ maxlen: int | None = None,
83
+ init: ta.Iterable[T] | None = None,
84
+ ) -> None:
85
+ super().__init__()
86
+
87
+ if cond is None:
88
+ cond = threading.Condition(lock=lock)
89
+ if deque is None:
90
+ deque = collections.deque(maxlen=maxlen)
91
+ if init:
92
+ deque.extend(init)
93
+
94
+ self._cond = cond
95
+ self._deque = deque
96
+
97
+ @property
98
+ def cond(self) -> 'threading.Condition':
99
+ return self._cond
100
+
101
+ @property
102
+ def deque(self) -> collections.deque[T]:
103
+ return self._deque
104
+
105
+ def push(
106
+ self,
107
+ *items: T,
108
+ n: int = 1,
109
+ ) -> None:
110
+ with self.cond:
111
+ self.deque.extend(items)
112
+ self.cond.notify(n)
113
+
114
+ def pop(
115
+ self,
116
+ timeout: float | None = None,
117
+ *,
118
+ if_empty: ta.Callable[[], None] | None = None,
119
+ ) -> T:
120
+ with self.cond:
121
+ if not self.deque and if_empty is not None:
122
+ if_empty()
123
+ while not self.deque:
124
+ self.cond.wait(timeout)
125
+ return self.deque.popleft()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omlish
3
- Version: 0.0.0.dev102
3
+ Version: 0.0.0.dev104
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause