omlish 0.0.0.dev229__py3-none-any.whl → 0.0.0.dev231__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. omlish/__about__.py +2 -2
  2. omlish/collections/__init__.py +15 -15
  3. omlish/collections/frozen.py +0 -2
  4. omlish/collections/identity.py +0 -3
  5. omlish/collections/indexed.py +0 -2
  6. omlish/collections/mappings.py +0 -3
  7. omlish/collections/ordered.py +0 -2
  8. omlish/collections/persistent/__init__.py +0 -0
  9. omlish/collections/sorted/__init__.py +0 -0
  10. omlish/collections/{skiplist.py → sorted/skiplist.py} +0 -1
  11. omlish/collections/{sorted.py → sorted/sorted.py} +2 -5
  12. omlish/collections/unmodifiable.py +0 -2
  13. omlish/daemons/__init__.py +0 -0
  14. omlish/daemons/daemon.py +200 -0
  15. omlish/daemons/reparent.py +16 -0
  16. omlish/daemons/spawning.py +166 -0
  17. omlish/daemons/targets.py +89 -0
  18. omlish/daemons/waiting.py +95 -0
  19. omlish/dispatch/dispatch.py +4 -1
  20. omlish/dispatch/methods.py +4 -0
  21. omlish/formats/json/__init__.py +5 -0
  22. omlish/io/compress/brotli.py +3 -3
  23. omlish/io/fdio/__init__.py +3 -0
  24. omlish/lang/__init__.py +0 -2
  25. omlish/lang/contextmanagers.py +15 -10
  26. omlish/libc.py +2 -4
  27. omlish/lite/timeouts.py +1 -1
  28. omlish/marshal/__init__.py +10 -10
  29. omlish/marshal/base.py +1 -1
  30. omlish/marshal/standard.py +2 -2
  31. omlish/marshal/trivial/__init__.py +0 -0
  32. omlish/marshal/{forbidden.py → trivial/forbidden.py} +7 -7
  33. omlish/marshal/{nop.py → trivial/nop.py} +5 -5
  34. omlish/os/deathpacts/__init__.py +15 -0
  35. omlish/os/deathpacts/base.py +76 -0
  36. omlish/os/deathpacts/heartbeatfile.py +85 -0
  37. omlish/os/{death.py → deathpacts/pipe.py} +20 -90
  38. omlish/os/forkhooks.py +55 -31
  39. omlish/os/pidfiles/manager.py +11 -44
  40. omlish/os/pidfiles/pidfile.py +18 -1
  41. omlish/reflect/__init__.py +1 -0
  42. omlish/reflect/inspect.py +43 -0
  43. omlish/sql/queries/__init__.py +4 -4
  44. omlish/sql/queries/rendering2.py +248 -0
  45. omlish/text/parts.py +26 -23
  46. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/METADATA +1 -1
  47. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/RECORD +57 -45
  48. omlish/formats/json/delimted.py +0 -4
  49. /omlish/collections/{persistent.py → persistent/persistent.py} +0 -0
  50. /omlish/collections/{treap.py → persistent/treap.py} +0 -0
  51. /omlish/collections/{treapmap.py → persistent/treapmap.py} +0 -0
  52. /omlish/marshal/{utils.py → proxy.py} +0 -0
  53. /omlish/marshal/{singular → trivial}/any.py +0 -0
  54. /omlish/sql/queries/{building.py → std.py} +0 -0
  55. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/LICENSE +0 -0
  56. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/WHEEL +0 -0
  57. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/entry_points.txt +0 -0
  58. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/top_level.txt +0 -0
@@ -1,84 +1,11 @@
1
- import abc
2
1
  import os
3
- import signal
4
- import sys
5
- import time
6
2
  import typing as ta
7
3
  import weakref
8
4
 
9
- from .. import check
10
- from .forkhooks import ForkHook
11
- from .forkhooks import get_fork_depth
12
-
13
-
14
- ##
15
-
16
-
17
- class Deathpact(abc.ABC):
18
- @abc.abstractmethod
19
- def poll(self) -> None:
20
- raise NotImplementedError
21
-
22
-
23
- class NopDeathpact(Deathpact):
24
- def poll(self) -> None:
25
- pass
26
-
27
-
28
- ##
29
-
30
-
31
- class BaseDeathpact(Deathpact, abc.ABC):
32
- def __init__(
33
- self,
34
- *,
35
- interval_s: float = .5,
36
- signal: int | None = signal.SIGTERM,
37
- output: ta.Literal['stdout', 'stderr'] | None = 'stderr',
38
- on_die: ta.Callable[[], None] | None = None,
39
- ) -> None:
40
- super().__init__()
41
-
42
- self._interval_s = interval_s
43
- self._signal = signal
44
- self._output = output
45
- self._on_die = on_die
46
-
47
- self._last_check_t: float | None = None
48
-
49
- def _print(self, msg: str) -> None:
50
- match self._output:
51
- case 'stdout':
52
- f = sys.stdout
53
- case 'stderr':
54
- f = sys.stderr
55
- case _:
56
- return
57
- print(f'{self} pid={os.getpid()}: {msg}', file=f)
58
-
59
- def die(self) -> None:
60
- self._print('Triggered! Process terminating!')
61
-
62
- if self._on_die is not None:
63
- self._on_die()
64
-
65
- if self._signal is not None:
66
- os.kill(os.getpid(), self._signal)
67
-
68
- sys.exit(1)
69
-
70
- @abc.abstractmethod
71
- def should_die(self) -> bool:
72
- raise NotImplementedError
73
-
74
- def maybe_die(self) -> None:
75
- if self.should_die():
76
- self.die()
77
-
78
- def poll(self) -> None:
79
- if self._last_check_t is None or (time.monotonic() - self._last_check_t) >= self._interval_s:
80
- self.maybe_die()
81
- self._last_check_t = time.monotonic()
5
+ from ... import check
6
+ from ..forkhooks import ForkHook
7
+ from ..forkhooks import ProcessOriginTracker
8
+ from .base import BaseDeathpact
82
9
 
83
10
 
84
11
  ##
@@ -92,16 +19,13 @@ class PipeDeathpact(BaseDeathpact):
92
19
  handle such cases.
93
20
  """
94
21
 
95
- _COOKIE: ta.ClassVar[bytes] = os.urandom(16)
96
-
97
22
  def __init__(self, **kwargs: ta.Any) -> None:
98
23
  super().__init__(**kwargs)
99
24
 
100
25
  self._rfd: int | None = None
101
26
  self._wfd: int | None = None
102
27
 
103
- self._cookie: bytes | None = self._COOKIE
104
- self._fork_depth: int | None = get_fork_depth()
28
+ self._process_origin = ProcessOriginTracker()
105
29
 
106
30
  def __repr__(self) -> str:
107
31
  return f'{self.__class__.__name__}(rfd={self._rfd}, wfd={self._wfd})'
@@ -111,11 +35,13 @@ class PipeDeathpact(BaseDeathpact):
111
35
  return check.not_none(self._rfd)
112
36
 
113
37
  def is_parent(self) -> bool:
114
- return (self._COOKIE, get_fork_depth()) == (self._cookie, self._fork_depth)
38
+ return self._process_origin.is_in_origin_process()
115
39
 
116
40
  #
117
41
 
118
42
  def __enter__(self) -> ta.Self:
43
+ check.state(self.is_parent())
44
+
119
45
  check.none(self._rfd)
120
46
  check.none(self._wfd)
121
47
 
@@ -125,18 +51,24 @@ class PipeDeathpact(BaseDeathpact):
125
51
 
126
52
  return self
127
53
 
128
- def _close_wfd_if_not_parent(self) -> None:
54
+ def close(self) -> None:
55
+ if self._rfd is not None:
56
+ os.close(self._rfd)
57
+ self._rfd = None
58
+
129
59
  if self._wfd is not None:
130
- if not self.is_parent():
60
+ if self.is_parent():
131
61
  os.close(check.not_none(self._wfd))
132
62
  self._wfd = None
133
63
 
134
64
  def __exit__(self, exc_type, exc_val, exc_tb):
135
- if self._rfd is not None:
136
- os.close(self._rfd)
137
- self._rfd = None
65
+ self.close()
138
66
 
139
- self._close_wfd_if_not_parent()
67
+ def _close_wfd_if_not_parent(self) -> None:
68
+ if self._wfd is not None:
69
+ if not self.is_parent():
70
+ os.close(check.not_none(self._wfd))
71
+ self._wfd = None
140
72
 
141
73
  #
142
74
 
@@ -145,8 +77,6 @@ class PipeDeathpact(BaseDeathpact):
145
77
  **self.__dict__,
146
78
  **dict(
147
79
  _wfd=None,
148
- _cookie=None,
149
- _fork_depth=None,
150
80
  ),
151
81
  }
152
82
 
omlish/os/forkhooks.py CHANGED
@@ -1,9 +1,5 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  # @omlish-lite
3
- """
4
- TODO:
5
- - ForkHook base class? all classmethods? prevents pickling
6
- """
7
3
  import abc
8
4
  import os
9
5
  import threading
@@ -29,31 +25,6 @@ class _ForkHookManager:
29
25
 
30
26
  #
31
27
 
32
- _installed: ta.ClassVar[bool] = False
33
-
34
- @classmethod
35
- def _install(cls) -> bool:
36
- if cls._installed:
37
- return False
38
-
39
- check.state(not cls._installed)
40
-
41
- os.register_at_fork(
42
- before=cls._before_fork,
43
- after_in_parent=cls._after_fork_in_parent,
44
- after_in_child=cls._after_fork_in_child,
45
- )
46
-
47
- cls._installed = True
48
- return True
49
-
50
- @classmethod
51
- def install(cls) -> bool:
52
- with cls._lock:
53
- return cls._install()
54
-
55
- #
56
-
57
28
  class Hook(ta.NamedTuple):
58
29
  key: ta.Any
59
30
  priority: int
@@ -74,8 +45,8 @@ class _ForkHookManager:
74
45
  def _rebuild_hook_collections(cls) -> None:
75
46
  cls._hook_keys = frozenset(cls._hooks_by_key)
76
47
 
77
- # Uses on dict order preservation for insertion-order of hooks of equal priority (although that shouldn't be
78
- # depended upon for usecase correctness.
48
+ # Uses dict order preservation to retain insertion-order of hooks of equal priority (although that shouldn't be
49
+ # depended upon for usecase correctness).
79
50
  cls._priority_ordered_hooks = sorted(cls._hooks_by_key.values(), key=lambda h: h.priority)
80
51
 
81
52
  #
@@ -121,6 +92,31 @@ class _ForkHookManager:
121
92
 
122
93
  #
123
94
 
95
+ _installed: ta.ClassVar[bool] = False
96
+
97
+ @classmethod
98
+ def _install(cls) -> bool:
99
+ if cls._installed:
100
+ return False
101
+
102
+ check.state(not cls._installed)
103
+
104
+ os.register_at_fork(
105
+ before=cls._before_fork,
106
+ after_in_parent=cls._after_fork_in_parent,
107
+ after_in_child=cls._after_fork_in_child,
108
+ )
109
+
110
+ cls._installed = True
111
+ return True
112
+
113
+ @classmethod
114
+ def install(cls) -> bool:
115
+ with cls._lock:
116
+ return cls._install()
117
+
118
+ #
119
+
124
120
  @classmethod
125
121
  def _before_fork(cls) -> None:
126
122
  cls._lock.acquire()
@@ -213,3 +209,31 @@ class _ForkDepthTracker(ForkHook):
213
209
 
214
210
  def get_fork_depth() -> int:
215
211
  return _ForkDepthTracker.get_fork_depth()
212
+
213
+
214
+ ##
215
+
216
+
217
+ class ProcessOriginTracker:
218
+ _PROCESS_COOKIE: ta.ClassVar[bytes] = os.urandom(16)
219
+
220
+ def __init__(self, **kwargs: ta.Any) -> None:
221
+ super().__init__(**kwargs)
222
+
223
+ self._process_cookie: bytes | None = self._PROCESS_COOKIE
224
+ self._fork_depth: int | None = get_fork_depth()
225
+
226
+ def is_in_origin_process(self) -> bool:
227
+ return (self._PROCESS_COOKIE, get_fork_depth()) == (self._process_cookie, self._fork_depth)
228
+
229
+ def __getstate__(self):
230
+ return {
231
+ **self.__dict__,
232
+ **dict(
233
+ _cookie=None,
234
+ _fork_depth=None,
235
+ ),
236
+ }
237
+
238
+ def __setstate__(self, state):
239
+ self.__dict__.update(state)
@@ -7,71 +7,38 @@ import typing as ta
7
7
  import weakref
8
8
 
9
9
  from ...lite.check import check
10
+ from ..forkhooks import ForkHook
10
11
  from .pidfile import Pidfile
11
12
 
12
13
 
13
14
  ##
14
15
 
15
16
 
16
- class _PidfileManager:
17
+ class _PidfileManager(ForkHook):
17
18
  """
18
19
  Manager for controlled inheritance of Pidfiles across forks in the presence of multiple threads. There is of course
19
20
  no safe or correct way to mix the use of fork and multiple active threads, and one should never write code which
20
21
  does so, but in the Real World one may still find oneself in such a situation outside of their control (such as when
21
22
  running under Pycharm's debugger which forces the use of forked multiprocessing).
22
-
23
- Not implemented as an instantiated class as there is no way to unregister at_fork listeners, and because Pidfiles
24
- may be pickled and there must be no possibility of accidentally unpickling and instantiating a new instance of the
25
- manager.
26
23
  """
27
24
 
28
- def __new__(cls, *args, **kwargs): # noqa
29
- raise TypeError
30
-
31
25
  _lock: ta.ClassVar[threading.Lock] = threading.Lock()
32
- _installed: ta.ClassVar[bool] = False
33
26
  _pidfile_threads: ta.ClassVar[ta.MutableMapping[Pidfile, threading.Thread]] = weakref.WeakKeyDictionary()
27
+ _num_kills: ta.ClassVar[int] = 0
34
28
 
35
29
  @classmethod
36
- def _before_fork(cls) -> None:
37
- cls._lock.acquire()
38
-
39
- @classmethod
40
- def _after_fork_in_parent(cls) -> None:
41
- cls._lock.release()
30
+ def num_kills(cls) -> int:
31
+ return cls._num_kills
42
32
 
43
33
  @classmethod
44
34
  def _after_fork_in_child(cls) -> None:
45
- th = threading.current_thread()
46
- for pf, pf_th in list(cls._pidfile_threads.items()):
47
- if pf_th is not th:
48
- pf.close()
49
- del cls._pidfile_threads[pf]
50
-
51
- cls._lock.release()
52
-
53
- #
54
-
55
- @classmethod
56
- def _install(cls) -> None:
57
- check.state(not cls._installed)
58
-
59
- os.register_at_fork(
60
- before=cls._before_fork,
61
- after_in_parent=cls._after_fork_in_parent,
62
- after_in_child=cls._after_fork_in_child,
63
- )
64
-
65
- cls._installed = True
66
-
67
- @classmethod
68
- def install(cls) -> bool:
69
35
  with cls._lock:
70
- if cls._installed:
71
- return False
72
-
73
- cls._install()
74
- return True
36
+ th = threading.current_thread()
37
+ for pf, pf_th in list(cls._pidfile_threads.items()):
38
+ if pf_th is not th:
39
+ pf.close()
40
+ del cls._pidfile_threads[pf]
41
+ cls._num_kills += 1
75
42
 
76
43
  @classmethod
77
44
  @contextlib.contextmanager
@@ -56,8 +56,25 @@ class Pidfile:
56
56
 
57
57
  #
58
58
 
59
+ _fd_to_dup: int
60
+
61
+ def dup(self) -> 'Pidfile':
62
+ fd = self._f.fileno()
63
+ dup = Pidfile(
64
+ self._path,
65
+ inheritable=self._inheritable,
66
+ )
67
+ dup._fd_to_dup = fd # noqa
68
+ return dup
69
+
70
+ #
71
+
59
72
  def __enter__(self) -> 'Pidfile':
60
- fd = os.open(self._path, os.O_RDWR | os.O_CREAT, 0o600)
73
+ if hasattr(self, '_fd_to_dup'):
74
+ fd = os.dup(self._fd_to_dup)
75
+ del self._fd_to_dup
76
+ else:
77
+ fd = os.open(self._path, os.O_RDWR | os.O_CREAT, 0o600)
61
78
 
62
79
  try:
63
80
  if self._inheritable:
@@ -1,5 +1,6 @@
1
1
  from .inspect import ( # noqa
2
2
  get_annotations,
3
+ get_filtered_type_hints,
3
4
  )
4
5
 
5
6
  from .ops import ( # noqa
omlish/reflect/inspect.py CHANGED
@@ -4,6 +4,7 @@ TODO:
4
4
  """
5
5
  import typing as ta
6
6
 
7
+ from .. import check
7
8
  from .. import lang
8
9
 
9
10
 
@@ -16,8 +17,50 @@ else:
16
17
  annotationlib = lang.lazy_import('annotationlib', optional=True, cache_failure=True)
17
18
 
18
19
 
20
+ ##
21
+
22
+
19
23
  def get_annotations(obj: ta.Any) -> ta.Mapping[str, ta.Any]:
20
24
  if (al := annotationlib()) is not None:
21
25
  return al.get_annotations(obj, format=al.Format.FORWARDREF) # noqa
22
26
  else:
23
27
  return inspect.get_annotations(obj)
28
+
29
+
30
+ ##
31
+
32
+
33
+ class _TypeHintsTarget:
34
+ def __init__(self, obj, annotations):
35
+ super().__init__()
36
+
37
+ self.obj = obj
38
+ self.__annotations__ = annotations
39
+ self.__globals__ = obj.__globals__
40
+ self.__type_params__ = obj.__type_params__
41
+
42
+
43
+ def get_filtered_type_hints(
44
+ obj: ta.Any,
45
+ *,
46
+ include: ta.Container[str] | None = None,
47
+ exclude: ta.Container[str] | None = None,
48
+ ) -> ta.Mapping[str, ta.Any]:
49
+ """
50
+ Gross hack: get_type_hints doesn't support recursive types, but it's nice to support functions with, at least,
51
+ recursive return types.
52
+
53
+ This is consciously constrained in what it supports - basically just functions.
54
+ """
55
+
56
+ check.not_isinstance(include, str)
57
+ check.not_isinstance(exclude, str)
58
+
59
+ anns = {
60
+ k: v
61
+ for k, v in obj.__annotations__.items()
62
+ if (include is None or k in include)
63
+ and not (exclude is not None and k in exclude)
64
+ }
65
+
66
+ return ta.get_type_hints(_TypeHintsTarget(obj, anns))
@@ -11,10 +11,6 @@ from .binary import ( # noqa
11
11
  BinaryOps,
12
12
  )
13
13
 
14
- from .building import ( # noqa
15
- StdBuilder,
16
- )
17
-
18
14
  from .exprs import ( # noqa
19
15
  CanExpr,
20
16
  CanLiteral,
@@ -76,6 +72,10 @@ from .selects import ( # noqa
76
72
  SelectItem,
77
73
  )
78
74
 
75
+ from .std import ( # noqa
76
+ StdBuilder,
77
+ )
78
+
79
79
  from .stmts import ( # noqa
80
80
  CanExpr,
81
81
  ExprStmt,