omlish 0.0.0.dev230__py3-none-any.whl → 0.0.0.dev232__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.
omlish/.manifests.json CHANGED
@@ -307,6 +307,18 @@
307
307
  }
308
308
  }
309
309
  },
310
+ {
311
+ "module": ".os.pidfiles.__main__",
312
+ "attr": "_CLI_MODULE",
313
+ "file": "omlish/os/pidfiles/__main__.py",
314
+ "line": 1,
315
+ "value": {
316
+ "$omdev.cli.types.CliModule": {
317
+ "cmd_name": "pidfiles",
318
+ "mod_name": "omlish.os.pidfiles.__main__"
319
+ }
320
+ }
321
+ },
310
322
  {
311
323
  "module": ".secrets.pwgen",
312
324
  "attr": "_CLI_MODULE",
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev230'
2
- __revision__ = 'ed2672ad6a2fb4718c57e281c5ce91fdc7998a6f'
1
+ __version__ = '0.0.0.dev232'
2
+ __revision__ = '2f0435f277c869f9ecf426be2aeaf08eeb9792de'
3
3
 
4
4
 
5
5
  #
@@ -37,7 +37,7 @@ class Project(ProjectBase):
37
37
 
38
38
  'greenlet ~= 3.1',
39
39
 
40
- 'trio ~= 0.27',
40
+ 'trio ~= 0.29',
41
41
  'trio-asyncio ~= 0.15',
42
42
  ],
43
43
 
@@ -56,7 +56,7 @@ class Project(ProjectBase):
56
56
  'asttokens ~= 3.0',
57
57
  'executing ~= 2.2',
58
58
 
59
- 'psutil ~= 6.0',
59
+ 'psutil ~= 7.0',
60
60
  ],
61
61
 
62
62
  'formats': [
File without changes
@@ -0,0 +1,126 @@
1
+ """
2
+ TODO:
3
+ - OK.. this is useful for non-system-daemons too... which have no pidfile..
4
+ - split out pidfile concern to.. identity?
5
+ - async[io] support, really just waiting
6
+ - helpers for http, json, etc
7
+ - heartbeat? status checks? heartbeat file?
8
+ - zmq
9
+ - cli, cfg reload
10
+ - bootstrap
11
+ - rsrc limit
12
+ - logs
13
+ - https://github.com/Homebrew/homebrew-services
14
+ - deathpacts
15
+ - timebomb
16
+ - pickle protocol, revision / venv check, multiprocessing manager support
17
+ """
18
+ import logging
19
+ import os.path
20
+ import time
21
+
22
+ from .. import check
23
+ from .. import dataclasses as dc
24
+ from .. import lang
25
+ from ..os.pidfiles.pidfile import Pidfile
26
+ from .launching import Launcher
27
+ from .spawning import Spawning
28
+ from .targets import Target
29
+ from .waiting import Wait
30
+ from .waiting import waiter_for
31
+
32
+
33
+ log = logging.getLogger(__name__)
34
+
35
+
36
+ ##
37
+
38
+
39
+ class Daemon:
40
+ @dc.dataclass(frozen=True, kw_only=True)
41
+ class Config:
42
+ target: Target
43
+ spawning: Spawning
44
+
45
+ #
46
+
47
+ pid_file: str | None = None
48
+
49
+ #
50
+
51
+ reparent_process: bool = False
52
+ launched_timeout_s: float = 5.
53
+
54
+ #
55
+
56
+ wait: Wait | None = None
57
+
58
+ wait_timeout: lang.TimeoutLike = 10.
59
+ wait_sleep_s: float = .1
60
+
61
+ #
62
+
63
+ def __post_init__(self) -> None:
64
+ check.isinstance(self.pid_file, (str, None))
65
+
66
+ def __init__(self, config: Config) -> None:
67
+ super().__init__()
68
+
69
+ self._config = config
70
+
71
+ @property
72
+ def config(self) -> Config:
73
+ return self._config
74
+
75
+ #
76
+
77
+ @property
78
+ def has_pidfile(self) -> bool:
79
+ return self._config.pid_file is not None
80
+
81
+ def _non_inheritable_pidfile(self) -> Pidfile:
82
+ check.state(self.has_pidfile)
83
+ return Pidfile(
84
+ check.non_empty_str(self._config.pid_file),
85
+ inheritable=False,
86
+ )
87
+
88
+ def is_running(self) -> bool:
89
+ check.state(self.has_pidfile)
90
+
91
+ if not os.path.isfile(check.non_empty_str(self._config.pid_file)):
92
+ return False
93
+
94
+ with self._non_inheritable_pidfile() as pf:
95
+ return not pf.try_acquire_lock()
96
+
97
+ #
98
+
99
+ def wait_sync(self, timeout: lang.TimeoutLike = lang.Timeout.Default) -> None:
100
+ if self._config.wait is None:
101
+ return
102
+
103
+ timeout = lang.Timeout.of(timeout, self._config.wait_timeout)
104
+ waiter = waiter_for(self._config.wait)
105
+ while not waiter.do_wait():
106
+ timeout()
107
+ time.sleep(self._config.wait_sleep_s or 0.)
108
+
109
+ #
110
+
111
+ def launch_no_wait(self) -> None:
112
+ launcher = Launcher(
113
+ target=self._config.target,
114
+ spawning=self._config.spawning,
115
+
116
+ pid_file=self._config.pid_file,
117
+ reparent_process=self._config.reparent_process,
118
+ launched_timeout_s=self._config.launched_timeout_s,
119
+ )
120
+
121
+ launcher.launch()
122
+
123
+ def launch(self, timeout: lang.TimeoutLike = lang.Timeout.Default) -> None:
124
+ self.launch_no_wait()
125
+
126
+ self.wait_sync(timeout)
@@ -0,0 +1,121 @@
1
+ """
2
+ TODO:
3
+ - Config? dedupe defaults with Daemon
4
+ - ExitStacked? hold ref to Spawner, which holds refs to thread/proc - which will likely outlive it, but still
5
+ """
6
+ import contextlib
7
+ import functools
8
+ import logging
9
+ import threading
10
+ import typing as ta
11
+
12
+ from .. import check
13
+ from .. import lang
14
+ from ..os.pidfiles.manager import open_inheritable_pidfile
15
+ from ..os.pidfiles.pidfile import Pidfile
16
+ from .reparent import reparent_process
17
+ from .spawning import InProcessSpawner
18
+ from .spawning import Spawn
19
+ from .spawning import Spawner
20
+ from .spawning import Spawning
21
+ from .spawning import spawner_for
22
+ from .targets import Target
23
+ from .targets import target_runner_for
24
+
25
+
26
+ log = logging.getLogger(__name__)
27
+
28
+
29
+ ##
30
+
31
+
32
+ class Launcher:
33
+ def __init__(
34
+ self,
35
+ *,
36
+ target: Target,
37
+ spawning: Spawning,
38
+
39
+ pid_file: str | None = None,
40
+ reparent_process: bool = False, # noqa
41
+ launched_timeout_s: float = 5.,
42
+ ) -> None:
43
+ super().__init__()
44
+
45
+ self._target = target
46
+ self._spawning = spawning
47
+
48
+ self._pid_file = pid_file
49
+ self._reparent_process = reparent_process
50
+ self._launched_timeout_s = launched_timeout_s
51
+
52
+ def _inner_launch(
53
+ self,
54
+ *,
55
+ pidfile_manager: ta.ContextManager | None,
56
+ launched_callback: ta.Callable[[], None] | None = None,
57
+ ) -> None:
58
+ try:
59
+ if self._reparent_process:
60
+ log.info('Reparenting')
61
+ reparent_process()
62
+
63
+ with contextlib.ExitStack() as es:
64
+ pidfile: Pidfile | None = None # noqa
65
+ if pidfile_manager is not None:
66
+ pidfile = check.isinstance(es.enter_context(pidfile_manager), Pidfile)
67
+ pidfile.write()
68
+
69
+ if launched_callback is not None:
70
+ launched_callback()
71
+
72
+ runner = target_runner_for(self._target)
73
+ runner.run()
74
+
75
+ finally:
76
+ if launched_callback is not None:
77
+ launched_callback()
78
+
79
+ def launch(self) -> bool:
80
+ with contextlib.ExitStack() as es:
81
+ spawner: Spawner = es.enter_context(spawner_for(self._spawning))
82
+
83
+ #
84
+
85
+ inherit_fds: set[int] = set()
86
+ launched_event: threading.Event | None = None
87
+
88
+ pidfile: Pidfile | None = None # noqa
89
+ pidfile_manager: ta.ContextManager | None = None
90
+
91
+ if (pid_file := self._pid_file) is not None:
92
+ if not isinstance(spawner, InProcessSpawner):
93
+ pidfile = es.enter_context(open_inheritable_pidfile(pid_file))
94
+ pidfile_manager = lang.NopContextManager(pidfile)
95
+
96
+ else:
97
+ check.state(not self._reparent_process)
98
+ pidfile = es.enter_context(Pidfile(pid_file))
99
+ pidfile_manager = pidfile.dup()
100
+ launched_event = threading.Event()
101
+
102
+ if not pidfile.try_acquire_lock():
103
+ return False
104
+
105
+ inherit_fds.add(check.isinstance(pidfile.fileno(), int))
106
+
107
+ #
108
+
109
+ spawner.spawn(Spawn(
110
+ functools.partial(
111
+ self._inner_launch,
112
+ pidfile_manager=pidfile_manager,
113
+ launched_callback=launched_event.set if launched_event is not None else None,
114
+ ),
115
+ inherit_fds=inherit_fds,
116
+ ))
117
+
118
+ if launched_event is not None:
119
+ check.state(launched_event.wait(timeout=self._launched_timeout_s))
120
+
121
+ return True
@@ -0,0 +1,16 @@
1
+ import os
2
+ import sys
3
+
4
+
5
+ def reparent_process() -> None:
6
+ if (pid := os.fork()): # noqa
7
+ sys.exit(0)
8
+ raise RuntimeError('Unreachable') # noqa
9
+
10
+ os.setsid()
11
+
12
+ if (pid := os.fork()): # noqa
13
+ sys.exit(0)
14
+
15
+ sys.stdout.flush()
16
+ sys.stderr.flush()
@@ -0,0 +1,166 @@
1
+ import abc
2
+ import functools
3
+ import os
4
+ import sys
5
+ import threading
6
+ import typing as ta
7
+
8
+ from .. import check
9
+ from .. import dataclasses as dc
10
+ from .. import lang
11
+ from ..diag import pydevd
12
+
13
+
14
+ if ta.TYPE_CHECKING:
15
+ import multiprocessing as mp
16
+ import multiprocessing.context
17
+ import multiprocessing.process # noqa
18
+
19
+ from ..multiprocessing import spawn as omp_spawn
20
+
21
+ else:
22
+ mp = lang.proxy_import('multiprocessing', extras=['context', 'process'])
23
+ subprocess = lang.proxy_import('subprocess')
24
+
25
+ omp_spawn = lang.proxy_import('..multiprocessing.spawn', __package__)
26
+
27
+
28
+ ##
29
+
30
+
31
+ class Spawning(dc.Case):
32
+ pass
33
+
34
+
35
+ class Spawn(dc.Frozen, final=True):
36
+ fn: ta.Callable[[], None]
37
+
38
+ _: dc.KW_ONLY
39
+
40
+ inherit_fds: ta.Collection[int] | None = None
41
+
42
+
43
+ class Spawner(lang.ContextManaged, abc.ABC):
44
+ @abc.abstractmethod
45
+ def spawn(self, spawn: Spawn) -> None:
46
+ raise NotImplementedError
47
+
48
+
49
+ class InProcessSpawner(Spawner, abc.ABC):
50
+ pass
51
+
52
+
53
+ @functools.singledispatch
54
+ def spawner_for(spawning: Spawning) -> Spawner:
55
+ raise TypeError(spawning)
56
+
57
+
58
+ ##
59
+
60
+
61
+ class MultiprocessingSpawning(Spawning, kw_only=True):
62
+ # Defaults to 'fork' if under pydevd, else 'spawn'
63
+ start_method: str | None = None
64
+
65
+ non_daemon: bool = False
66
+
67
+
68
+ class MultiprocessingSpawner(Spawner):
69
+ def __init__(self, spawning: MultiprocessingSpawning) -> None:
70
+ super().__init__()
71
+
72
+ self._spawning = spawning
73
+ self._process: ta.Optional['mp.process.BaseProcess'] = None # noqa
74
+
75
+ def _process_cls(self, spawn: Spawn) -> type['mp.process.BaseProcess']:
76
+ if (start_method := self._spawning.start_method) is None:
77
+ # Unfortunately, pydevd forces the use of the 'fork' start_method, which cannot be mixed with 'spawn':
78
+ # https://github.com/python/cpython/blob/a7427f2db937adb4c787754deb4c337f1894fe86/Lib/multiprocessing/spawn.py#L102 # noqa
79
+ if pydevd.is_running():
80
+ start_method = 'fork'
81
+ else:
82
+ start_method = 'spawn'
83
+
84
+ ctx: 'mp.context.BaseContext' # noqa
85
+ if start_method == 'fork':
86
+ ctx = mp.get_context(check.non_empty_str(start_method))
87
+
88
+ elif start_method == 'spawn':
89
+ ctx = omp_spawn.ExtrasSpawnContext(omp_spawn.SpawnExtras(
90
+ pass_fds=frozenset(spawn.inherit_fds) if spawn.inherit_fds is not None else None,
91
+ ))
92
+
93
+ else:
94
+ raise ValueError(start_method)
95
+
96
+ return ctx.Process # type: ignore
97
+
98
+ def spawn(self, spawn: Spawn) -> None:
99
+ check.none(self._process)
100
+ self._process = self._process_cls(spawn)(
101
+ target=spawn.fn,
102
+ daemon=not self._spawning.non_daemon,
103
+ )
104
+ self._process.start()
105
+
106
+
107
+ @spawner_for.register
108
+ def _(spawning: MultiprocessingSpawning) -> MultiprocessingSpawner:
109
+ return MultiprocessingSpawner(spawning)
110
+
111
+
112
+ ##
113
+
114
+
115
+ class ForkSpawning(Spawning):
116
+ pass
117
+
118
+
119
+ class ForkSpawner(Spawner, dc.Frozen):
120
+ spawning: ForkSpawning
121
+
122
+ def spawn(self, spawn: Spawn) -> None:
123
+ if (pid := os.fork()): # noqa
124
+ return
125
+
126
+ try:
127
+ spawn.fn()
128
+ except BaseException: # noqa
129
+ sys.exit(1)
130
+ else:
131
+ sys.exit(0)
132
+
133
+ raise RuntimeError('Unreachable') # noqa
134
+
135
+
136
+ @spawner_for.register
137
+ def _(spawning: ForkSpawning) -> ForkSpawner:
138
+ return ForkSpawner(spawning)
139
+
140
+
141
+ ##
142
+
143
+
144
+ class ThreadSpawning(Spawning, kw_only=True):
145
+ non_daemon: bool = False
146
+
147
+
148
+ class ThreadSpawner(InProcessSpawner):
149
+ def __init__(self, spawning: ThreadSpawning) -> None:
150
+ super().__init__()
151
+
152
+ self._spawning = spawning
153
+ self._thread: threading.Thread | None = None
154
+
155
+ def spawn(self, spawn: Spawn) -> None:
156
+ check.none(self._thread)
157
+ self._thread = threading.Thread(
158
+ target=spawn.fn,
159
+ daemon=not self._spawning.non_daemon,
160
+ )
161
+ self._thread.start()
162
+
163
+
164
+ @spawner_for.register
165
+ def _(spawning: ThreadSpawning) -> ThreadSpawner:
166
+ return ThreadSpawner(spawning)
@@ -0,0 +1,89 @@
1
+ import abc
2
+ import functools
3
+ import os
4
+ import typing as ta
5
+
6
+ from .. import check
7
+ from .. import dataclasses as dc
8
+ from .. import lang
9
+
10
+
11
+ if ta.TYPE_CHECKING:
12
+ import runpy
13
+ else:
14
+ runpy = lang.proxy_import('runpy')
15
+
16
+
17
+ ##
18
+
19
+
20
+ class Target(dc.Case):
21
+ pass
22
+
23
+
24
+ class TargetRunner(abc.ABC):
25
+ @abc.abstractmethod
26
+ def run(self) -> None:
27
+ raise NotImplementedError
28
+
29
+
30
+ @functools.singledispatch
31
+ def target_runner_for(target: Target) -> TargetRunner:
32
+ raise TypeError(target)
33
+
34
+
35
+ ##
36
+
37
+
38
+ class FnTarget(Target):
39
+ fn: ta.Callable[[], None]
40
+
41
+
42
+ class FnTargetRunner(TargetRunner, dc.Frozen):
43
+ target: FnTarget
44
+
45
+ def run(self) -> None:
46
+ self.target.fn()
47
+
48
+
49
+ @target_runner_for.register
50
+ def _(target: FnTarget) -> FnTargetRunner:
51
+ return FnTargetRunner(target)
52
+
53
+
54
+ ##
55
+
56
+
57
+ class NameTarget(Target):
58
+ name: str
59
+
60
+
61
+ class NameTargetRunner(TargetRunner, dc.Frozen):
62
+ target: NameTarget
63
+
64
+ def run(self) -> None:
65
+ name = self.target.name
66
+ if lang.can_import(name):
67
+ runpy._run_module_as_main(name) # type: ignore # noqa
68
+ else:
69
+ obj = lang.import_attr(self.target.name)
70
+ obj()
71
+
72
+
73
+ ##
74
+
75
+
76
+ class ExecTarget(Target):
77
+ cmd: ta.Sequence[str] = dc.xfield(coerce=check.of_not_isinstance(str))
78
+
79
+
80
+ class ExecTargetRunner(TargetRunner, dc.Frozen):
81
+ target: ExecTarget
82
+
83
+ def run(self) -> None:
84
+ os.execl(*self.target.cmd)
85
+
86
+
87
+ @target_runner_for.register
88
+ def _(target: ExecTarget) -> ExecTargetRunner:
89
+ return ExecTargetRunner(target)
@@ -0,0 +1,95 @@
1
+ import abc
2
+ import functools
3
+ import socket
4
+ import typing as ta
5
+
6
+ from .. import dataclasses as dc
7
+
8
+
9
+ ##
10
+
11
+
12
+ class Wait(dc.Case):
13
+ pass
14
+
15
+
16
+ class Waiter(abc.ABC):
17
+ @abc.abstractmethod
18
+ def do_wait(self) -> bool:
19
+ raise NotImplementedError
20
+
21
+
22
+ @functools.singledispatch
23
+ def waiter_for(wait: Wait) -> Waiter:
24
+ raise TypeError(wait)
25
+
26
+
27
+ ##
28
+
29
+
30
+ class SequentialWait(Wait):
31
+ waits: ta.Sequence[Wait]
32
+
33
+
34
+ class SequentialWaiter(Waiter):
35
+ def __init__(self, waiters: ta.Sequence[Waiter]) -> None:
36
+ super().__init__()
37
+
38
+ self._waiters = waiters
39
+ self._idx = 0
40
+
41
+ def do_wait(self) -> bool:
42
+ while self._idx < len(self._waiters):
43
+ if not self._waiters[self._idx].do_wait():
44
+ return False
45
+ self._idx += 1
46
+ return True
47
+
48
+
49
+ @waiter_for.register
50
+ def _(wait: SequentialWait) -> SequentialWaiter:
51
+ return SequentialWaiter([waiter_for(c) for c in wait.waits])
52
+
53
+
54
+ ##
55
+
56
+
57
+ class FnWait(Wait):
58
+ fn: ta.Callable[[], bool]
59
+
60
+
61
+ class FnWaiter(Waiter, dc.Frozen):
62
+ wait: FnWait
63
+
64
+ def do_wait(self) -> bool:
65
+ return self.wait.fn()
66
+
67
+
68
+ @waiter_for.register
69
+ def _(wait: FnWait) -> FnWaiter:
70
+ return FnWaiter(wait)
71
+
72
+
73
+ ##
74
+
75
+
76
+ class ConnectWait(Wait):
77
+ address: ta.Any
78
+
79
+
80
+ class ConnectWaiter(Waiter, dc.Frozen):
81
+ wait: ConnectWait
82
+
83
+ def do_wait(self) -> bool:
84
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
85
+ try:
86
+ s.connect(self.wait.address)
87
+ except ConnectionRefusedError:
88
+ return False
89
+ else:
90
+ return True
91
+
92
+
93
+ @waiter_for.register
94
+ def _(wait: ConnectWait) -> ConnectWaiter:
95
+ return ConnectWaiter(wait)
omlish/lang/__init__.py CHANGED
@@ -61,9 +61,7 @@ from .contextmanagers import ( # noqa
61
61
  DefaultLockable,
62
62
  ExitStacked,
63
63
  Lockable,
64
- NOP_CONTEXT_MANAGED,
65
64
  NOP_CONTEXT_MANAGER,
66
- NopContextManaged,
67
65
  NopContextManager,
68
66
  Timer,
69
67
  a_defer,
@@ -18,6 +18,11 @@ T = ta.TypeVar('T')
18
18
  ##
19
19
 
20
20
 
21
+ class _NOT_SET: # noqa
22
+ def __new__(cls, *args, **kwargs): # noqa
23
+ raise TypeError
24
+
25
+
21
26
  class ContextManaged:
22
27
  def __enter__(self) -> ta.Self:
23
28
  return self
@@ -31,21 +36,21 @@ class ContextManaged:
31
36
  return None
32
37
 
33
38
 
34
- class NopContextManaged(ContextManaged):
35
- def __init_subclass__(cls, **kwargs: ta.Any) -> None:
36
- raise TypeError
37
-
39
+ class NopContextManager(ContextManaged):
40
+ def __init__(self, /, value: ta.Any = _NOT_SET) -> None:
41
+ super().__init__()
38
42
 
39
- NOP_CONTEXT_MANAGED = NopContextManaged()
43
+ self._value = value
40
44
 
45
+ def __enter__(self):
46
+ if (value := self._value) is _NOT_SET:
47
+ return self
48
+ else:
49
+ return value
41
50
 
42
- class NopContextManager:
43
51
  def __init_subclass__(cls, **kwargs: ta.Any) -> None:
44
52
  raise TypeError
45
53
 
46
- def __call__(self, *args, **kwargs):
47
- return NOP_CONTEXT_MANAGED
48
-
49
54
 
50
55
  NOP_CONTEXT_MANAGER = NopContextManager()
51
56
 
@@ -350,7 +355,7 @@ def default_lock(value: DefaultLockable, default: DefaultLockable = None) -> Loc
350
355
  return lambda: lock
351
356
 
352
357
  elif value is False or value is None:
353
- return NOP_CONTEXT_MANAGER
358
+ return lambda: NOP_CONTEXT_MANAGER
354
359
 
355
360
  elif callable(value):
356
361
  return value
omlish/lang/functions.py CHANGED
@@ -190,6 +190,16 @@ class Args:
190
190
  self.args = args
191
191
  self.kwargs = kwargs
192
192
 
193
+ def update(self, *args: ta.Any, **kwargs: ta.Any) -> 'Args':
194
+ return Args(
195
+ *self.args,
196
+ *args,
197
+ **{
198
+ **self.kwargs,
199
+ **kwargs,
200
+ },
201
+ )
202
+
193
203
  def __call__(self, fn: ta.Callable[..., T]) -> T:
194
204
  return fn(*self.args, **self.kwargs)
195
205
 
omlish/libc.py CHANGED
@@ -62,7 +62,6 @@ libc.free.argtypes = [ct.c_void_p]
62
62
 
63
63
 
64
64
  class Malloc:
65
-
66
65
  def __init__(self, sz: int) -> None:
67
66
  super().__init__()
68
67
 
@@ -174,7 +173,6 @@ if LINUX:
174
173
 
175
174
 
176
175
  class Mmap:
177
-
178
176
  def __init__(
179
177
  self,
180
178
  length: int,
@@ -537,14 +535,14 @@ elif DARWIN:
537
535
 
538
536
 
539
537
  if LINUX:
540
- def gettid():
538
+ def gettid() -> int:
541
539
  syscalls = {
542
540
  'i386': 224, # unistd_32.h: #define __NR_gettid 224
543
541
  'x86_64': 186, # unistd_64.h: #define __NR_gettid 186
544
542
  'aarch64': 178, # asm-generic/unistd.h: #define __NR_gettid 178
545
543
  }
546
544
  try:
547
- tid = ct.CDLL('libc.so.6').syscall(syscalls[platform.machine()])
545
+ tid = libc.syscall(syscalls[platform.machine()])
548
546
  except Exception: # noqa
549
547
  tid = -1
550
548
  return tid
omlish/lite/timeouts.py CHANGED
@@ -8,7 +8,7 @@ import time
8
8
  import typing as ta
9
9
 
10
10
 
11
- TimeoutLike = ta.Union['Timeout', 'Timeout.Default', ta.Iterable['TimeoutLike'], float] # ta.TypeAlias
11
+ TimeoutLike = ta.Union['Timeout', ta.Type['Timeout.Default'], ta.Iterable['TimeoutLike'], float] # ta.TypeAlias
12
12
 
13
13
 
14
14
  ##
@@ -0,0 +1,11 @@
1
+ # @omlish-manifest
2
+ _CLI_MODULE = {'$omdev.cli.types.CliModule': {
3
+ 'cmd_name': 'pidfiles',
4
+ 'mod_name': __name__,
5
+ }}
6
+
7
+
8
+ if __name__ == '__main__':
9
+ from .cli import _main # noqa
10
+
11
+ _main()
@@ -0,0 +1,88 @@
1
+ """
2
+ TODO:
3
+ - F_SETLK mode
4
+ """
5
+ import os
6
+ import typing as ta
7
+
8
+ from ... import lang
9
+ from ...argparse import all as ap
10
+ from ..pidfiles.pidfile import Pidfile
11
+ from ..pidfiles.pinning import PidfilePinner
12
+ from ..signals import parse_signal
13
+
14
+
15
+ class Cli(ap.Cli):
16
+ _PIDFILE_ARGS: ta.ClassVar[ta.Sequence[ap.Arg]] = [
17
+ ap.arg('pid-file'),
18
+ ap.arg('--create', action='store_true'),
19
+ ]
20
+
21
+ def _pidfile_args(self) -> lang.Args:
22
+ return lang.Args(
23
+ self.args.pid_file,
24
+ inheritable=False,
25
+ no_create=not self._args.create,
26
+ )
27
+
28
+ def _args_pidfile(self) -> Pidfile:
29
+ return self._pidfile_args()(Pidfile)
30
+
31
+ #
32
+
33
+ @ap.cmd(*_PIDFILE_ARGS)
34
+ def read_no_verify(self) -> None:
35
+ with self._args_pidfile() as pidfile:
36
+ print(pidfile.read())
37
+
38
+ @ap.cmd(*_PIDFILE_ARGS)
39
+ def lock(self) -> None:
40
+ with self._args_pidfile() as pidfile:
41
+ pidfile.acquire_lock()
42
+ print(os.getpid())
43
+ input()
44
+
45
+ #
46
+
47
+ _PIDFILE_PINNER_ARGS: ta.ClassVar[ta.Sequence[ap.Arg]] = [
48
+ *_PIDFILE_ARGS,
49
+ ap.arg('--timeout', type=float),
50
+ ]
51
+
52
+ def _pidfile_pinner_args(self) -> lang.Args:
53
+ return self._pidfile_args().update(
54
+ timeout=self._args.timeout,
55
+ )
56
+
57
+ def _args_pidfile_pinner(self) -> ta.ContextManager[int]:
58
+ return self._pidfile_pinner_args()(PidfilePinner.default_impl()().pin_pidfile_owner)
59
+
60
+ #
61
+
62
+ @ap.cmd(*_PIDFILE_PINNER_ARGS)
63
+ def read(self) -> None:
64
+ with self._args_pidfile_pinner() as pid:
65
+ print(pid)
66
+
67
+ @ap.cmd(*_PIDFILE_PINNER_ARGS)
68
+ def pin(self) -> None:
69
+ with self._args_pidfile_pinner() as pid:
70
+ print(pid)
71
+ input()
72
+
73
+ @ap.cmd(
74
+ *_PIDFILE_PINNER_ARGS,
75
+ ap.arg('signal'),
76
+ )
77
+ def kill(self) -> None:
78
+ sig = parse_signal(self._args.signal)
79
+ with self._args_pidfile_pinner() as pid:
80
+ os.kill(pid, sig)
81
+
82
+
83
+ def _main() -> None:
84
+ Cli().cli_run_and_exit()
85
+
86
+
87
+ if __name__ == '__main__':
88
+ _main()
@@ -27,11 +27,13 @@ class Pidfile:
27
27
  path: str,
28
28
  *,
29
29
  inheritable: bool = True,
30
+ no_create: bool = False,
30
31
  ) -> None:
31
32
  super().__init__()
32
33
 
33
34
  self._path = path
34
35
  self._inheritable = inheritable
36
+ self._no_create = no_create
35
37
 
36
38
  @property
37
39
  def path(self) -> str:
@@ -56,8 +58,28 @@ class Pidfile:
56
58
 
57
59
  #
58
60
 
61
+ _fd_to_dup: int
62
+
63
+ def dup(self) -> 'Pidfile':
64
+ fd = self._f.fileno()
65
+ dup = Pidfile(
66
+ self._path,
67
+ inheritable=self._inheritable,
68
+ )
69
+ dup._fd_to_dup = fd # noqa
70
+ return dup
71
+
72
+ #
73
+
59
74
  def __enter__(self) -> 'Pidfile':
60
- fd = os.open(self._path, os.O_RDWR | os.O_CREAT, 0o600)
75
+ if hasattr(self, '_fd_to_dup'):
76
+ fd = os.dup(self._fd_to_dup)
77
+ del self._fd_to_dup
78
+ else:
79
+ ofl = os.O_RDWR
80
+ if not self._no_create:
81
+ ofl |= os.O_CREAT
82
+ fd = os.open(self._path, ofl, 0o600)
61
83
 
62
84
  try:
63
85
  if self._inheritable:
@@ -27,6 +27,7 @@ import typing as ta
27
27
 
28
28
  from ...diag.lslocks import LslocksCommand
29
29
  from ...diag.lsof import LsofCommand
30
+ from ...lite.check import check
30
31
  from ...lite.timeouts import Timeout
31
32
  from ...lite.timeouts import TimeoutLike
32
33
  from ...subprocesses.sync import subprocesses # noqa
@@ -64,13 +65,21 @@ class PidfilePinner(abc.ABC):
64
65
  path: str,
65
66
  *,
66
67
  timeout: ta.Optional[TimeoutLike] = None,
68
+ inheritable: bool = False, # Present to match Pidfile kwargs for convenience, but enforced to be False.
69
+ **kwargs: ta.Any,
67
70
  ) -> ta.Iterator[int]:
71
+ check.arg(not inheritable)
72
+
68
73
  timeout = Timeout.of(timeout)
69
74
 
70
75
  if not os.path.isfile(path):
71
76
  raise self.NoOwnerError
72
77
 
73
- with Pidfile(path, inheritable=False) as pf:
78
+ with Pidfile(
79
+ path,
80
+ inheritable=False,
81
+ **kwargs,
82
+ ) as pf:
74
83
  try:
75
84
  with self._pin_pidfile_owner(pf, timeout) as pid:
76
85
  yield pid
omlish/os/signals.py ADDED
@@ -0,0 +1,15 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import signal
4
+
5
+
6
+ def parse_signal(s: str) -> int:
7
+ try:
8
+ return int(s)
9
+ except ValueError:
10
+ pass
11
+
12
+ s = s.upper()
13
+ if not s.startswith('SIG'):
14
+ s = 'SIG' + s
15
+ return signal.Signals[s] # noqa
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: omlish
3
- Version: 0.0.0.dev230
3
+ Version: 0.0.0.dev232
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -16,7 +16,7 @@ Provides-Extra: all
16
16
  Requires-Dist: anyio~=4.8; extra == "all"
17
17
  Requires-Dist: sniffio~=1.3; extra == "all"
18
18
  Requires-Dist: greenlet~=3.1; extra == "all"
19
- Requires-Dist: trio~=0.27; extra == "all"
19
+ Requires-Dist: trio~=0.29; extra == "all"
20
20
  Requires-Dist: trio-asyncio~=0.15; extra == "all"
21
21
  Requires-Dist: lz4~=4.4; extra == "all"
22
22
  Requires-Dist: python-snappy~=0.7; extra == "all"
@@ -24,7 +24,7 @@ Requires-Dist: zstandard~=0.23; extra == "all"
24
24
  Requires-Dist: brotli~=1.1; extra == "all"
25
25
  Requires-Dist: asttokens~=3.0; extra == "all"
26
26
  Requires-Dist: executing~=2.2; extra == "all"
27
- Requires-Dist: psutil~=6.0; extra == "all"
27
+ Requires-Dist: psutil~=7.0; extra == "all"
28
28
  Requires-Dist: orjson~=3.10; extra == "all"
29
29
  Requires-Dist: ujson~=5.10; extra == "all"
30
30
  Requires-Dist: pyyaml~=6.0; extra == "all"
@@ -54,7 +54,7 @@ Provides-Extra: async
54
54
  Requires-Dist: anyio~=4.8; extra == "async"
55
55
  Requires-Dist: sniffio~=1.3; extra == "async"
56
56
  Requires-Dist: greenlet~=3.1; extra == "async"
57
- Requires-Dist: trio~=0.27; extra == "async"
57
+ Requires-Dist: trio~=0.29; extra == "async"
58
58
  Requires-Dist: trio-asyncio~=0.15; extra == "async"
59
59
  Provides-Extra: compress
60
60
  Requires-Dist: lz4~=4.4; extra == "compress"
@@ -64,7 +64,7 @@ Requires-Dist: brotli~=1.1; extra == "compress"
64
64
  Provides-Extra: diag
65
65
  Requires-Dist: asttokens~=3.0; extra == "diag"
66
66
  Requires-Dist: executing~=2.2; extra == "diag"
67
- Requires-Dist: psutil~=6.0; extra == "diag"
67
+ Requires-Dist: psutil~=7.0; extra == "diag"
68
68
  Provides-Extra: formats
69
69
  Requires-Dist: orjson~=3.10; extra == "formats"
70
70
  Requires-Dist: ujson~=5.10; extra == "formats"
@@ -1,5 +1,5 @@
1
- omlish/.manifests.json,sha256=YGmAnUBszmosQQ_7Hh2wwtDiYdYZ4unNKYzOtALuels,7968
2
- omlish/__about__.py,sha256=5Ic1ejTV8SWSaVGgOg2_WDgpVTfT-KBLADFDeHHanA4,3380
1
+ omlish/.manifests.json,sha256=vQTAIvR8OblSq-uP2GUfnbei0RnmAnM5j0T1-OToh9E,8253
2
+ omlish/__about__.py,sha256=-wogzlxSt20e8KuTBCH4MRnaUQhIUXLRZdyQ82uUp4E,3380
3
3
  omlish/__init__.py,sha256=SsyiITTuK0v74XpKV8dqNaCmjOlan1JZKrHQv5rWKPA,253
4
4
  omlish/c3.py,sha256=ubu7lHwss5V4UznbejAI0qXhXahrU01MysuHOZI9C4U,8116
5
5
  omlish/cached.py,sha256=UI-XTFBwA6YXWJJJeBn-WkwBkfzDjLBBaZf4nIJA9y0,510
@@ -7,7 +7,7 @@ omlish/check.py,sha256=THqm6jD1a0skAO5EC8SOVg58yq96Vk5wcuruBkCYxyU,2016
7
7
  omlish/datetimes.py,sha256=HajeM1kBvwlTa-uR1TTZHmZ3zTPnnUr1uGGQhiO1XQ0,2152
8
8
  omlish/defs.py,sha256=9uUjJuVIbCBL3g14fyzAp-9gH935MFofvlfOGwcBIaM,4913
9
9
  omlish/dynamic.py,sha256=kIZokHHid8a0pIAPXMNiXrVJvJJyBnY49WP1a2m-HUQ,6525
10
- omlish/libc.py,sha256=20YdP3Sh45d-c13OMV0Od6FEAIxqKvU3d5kCOIW9xCQ,15650
10
+ omlish/libc.py,sha256=8K4c66YV1ziJerl5poAAYCmsV-VSsHkT3EHhPW04ufg,15639
11
11
  omlish/outcome.py,sha256=ABIE0zjjTyTNtn-ZqQ_9_mUzLiBQ3sDAyqc9JVD8N2k,7852
12
12
  omlish/runmodule.py,sha256=PWvuAaJ9wQQn6bx9ftEL3_d04DyotNn8dR_twm2pgw0,700
13
13
  omlish/shlex.py,sha256=bsW2XUD8GiMTUTDefJejZ5AyqT1pTgWMPD0BMoF02jE,248
@@ -175,6 +175,13 @@ omlish/configs/processing/matching.py,sha256=R64RxpPB1uX5Ztvvk2dQ2xi_xwlaxkxQgZw
175
175
  omlish/configs/processing/names.py,sha256=weHmaTclzgM9lUn3aBtw-kwZ3mc2N-CZlFg3Kd_UsKo,1093
176
176
  omlish/configs/processing/rewriting.py,sha256=v7PfHtuTn5v_5Y6Au7oMN2Z0nxAMy1iYyO5CXnTvZhs,4226
177
177
  omlish/configs/processing/strings.py,sha256=qFS2oh6z02IaM_q4lTKLdufzkJqAJ6J-Qjrz5S-QJoM,826
178
+ omlish/daemons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
179
+ omlish/daemons/daemon.py,sha256=lnMgGrPeZ3pjhuXPU-7DX2XGEdNG0kP6HAejCQPSGtk,2976
180
+ omlish/daemons/launching.py,sha256=mhtkuAO16STcznUl3rrX9pacfrKbPQRCP2AllKL4B70,3664
181
+ omlish/daemons/reparent.py,sha256=UaG2X6VJHJPOlUwHPNRH3aWGgF0Fg771jjO9IRPLlyY,280
182
+ omlish/daemons/spawning.py,sha256=cx00xeqSrfhlFbjCtKqaBHvMuHwB9hdjuKNHzAAo_dw,4030
183
+ omlish/daemons/targets.py,sha256=scq6BYgzi0H2apfgD74U0D8_msQuYAel3Qiij74YWVo,1501
184
+ omlish/daemons/waiting.py,sha256=RfgD1L33QQVbD2431dkKZGE4w6DUcGvYeRXXi8puAP4,1676
178
185
  omlish/dataclasses/__init__.py,sha256=D7I6ZJjEFeLN2re8oOZ_7JKWiG2plrPnaUFq3iEXYmQ,1553
179
186
  omlish/dataclasses/utils.py,sha256=N2seT8cJtfOv-41D7F3E-q4us-FCTQmnxxPv3dt1OcI,3796
180
187
  omlish/dataclasses/impl/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936
@@ -385,15 +392,15 @@ omlish/iterators/iterators.py,sha256=ghI4dO6WPyyFOLTIIMaHQ_IOy2xXaFpGPqveZ5YGIBU
385
392
  omlish/iterators/recipes.py,sha256=53mkexitMhkwXQZbL6DrhpT0WePQ_56uXd5Jaw3DfzI,467
386
393
  omlish/iterators/tools.py,sha256=Pi4ybXytUXVZ3xwK89xpPImQfYYId9p1vIFQvVqVLqA,2551
387
394
  omlish/iterators/unique.py,sha256=0jAX3kwzVfRNhe0Tmh7kVP_Q2WBIn8POo_O-rgFV0rQ,1390
388
- omlish/lang/__init__.py,sha256=_DddR5PNgjqr51AB5DPUq0iKVqJrKz55WMJnBm97LL0,4127
395
+ omlish/lang/__init__.py,sha256=bE8ra_QM5M4k8v3qFtreTjQswaZClIOaqXt9fE43MD0,4079
389
396
  omlish/lang/cached.py,sha256=tQaqMu1LID0q4NSTk5vPXsgxIBWSFAmjs5AhQoEHoCQ,7833
390
397
  omlish/lang/clsdct.py,sha256=sJYadm-fwzti-gsi98knR5qQUxriBmOqQE_qz3RopNk,1743
391
398
  omlish/lang/cmp.py,sha256=5vbzWWbqdzDmNKAGL19z6ZfUKe5Ci49e-Oegf9f4BsE,1346
392
- omlish/lang/contextmanagers.py,sha256=V-XRwj1Em17Pr92i9mSYeeElQs9pmfM_B4jTF732qIg,10400
399
+ omlish/lang/contextmanagers.py,sha256=Mrn8NJ3pP0Zxi-IoGqSjZDdWUctsyee2vrZ2FtZvNmo,10529
393
400
  omlish/lang/datetimes.py,sha256=ehI_DhQRM-bDxAavnp470XcekbbXc4Gdw9y1KpHDJT0,223
394
401
  omlish/lang/descriptors.py,sha256=njkYDS1gn5p4-3v1jr-s_srauC7tvvt571RjE7Q4LXE,6616
395
402
  omlish/lang/exceptions.py,sha256=qJBo3NU1mOWWm-NhQUHCY5feYXR3arZVyEHinLsmRH4,47
396
- omlish/lang/functions.py,sha256=t9nsnWwhsibG0w908VMx-_pRM5tZfruE3faPxrCWTbI,4160
403
+ omlish/lang/functions.py,sha256=0ql9EXA_gEEhvUVzMJCjVhEnVtHecsLKmfmAXuQqeGY,4388
397
404
  omlish/lang/generators.py,sha256=5LX17j-Ej3QXhwBgZvRTm_dq3n9veC4IOUcVmvSu2vU,5243
398
405
  omlish/lang/imports.py,sha256=FMPz1lIDmuoapM88uabXeZbeJX1uCT8pkHeukC3X4qc,10129
399
406
  omlish/lang/iterables.py,sha256=HOjcxOwyI5bBApDLsxRAGGhTTmw7fdZl2kEckxRVl-0,1994
@@ -434,7 +441,7 @@ omlish/lite/resources.py,sha256=YNSmX1Ohck1aoWRs55a-o5ChVbFJIQhtbqE-XwF55Oc,326
434
441
  omlish/lite/runtime.py,sha256=XQo408zxTdJdppUZqOWHyeUR50VlCpNIExNGHz4U6O4,459
435
442
  omlish/lite/secrets.py,sha256=3Mz3V2jf__XU9qNHcH56sBSw95L3U2UPL24bjvobG0c,816
436
443
  omlish/lite/strings.py,sha256=QGxT1Yh4oI8ycsfeobxnjEhvDob_GiAKLeIhZwo1j24,1986
437
- omlish/lite/timeouts.py,sha256=wZ6PP1D3B8jpAI7-cQm7wkGTh2sE366XIn6FSMH6DZ4,4959
444
+ omlish/lite/timeouts.py,sha256=lhXo0zwpLM7nr2-AliBRui2BO9jh7B8ALxetwOq6hYI,4968
438
445
  omlish/lite/timing.py,sha256=aVu3hEDB_jyTF_ryZI7iU-xg4q8CNwqpp9Apfru_iwY,196
439
446
  omlish/lite/types.py,sha256=fP5EMyBdEp2LmDxcHjUDtwAMdR06ISr9lKOL7smWfHM,140
440
447
  omlish/lite/typing.py,sha256=U3-JaEnkDSYxK4tsu_MzUn3RP6qALBe5FXQXpD-licE,1090
@@ -512,6 +519,7 @@ omlish/os/forkhooks.py,sha256=yjodOvs90ClXskv5oBIJbHn0Y7dzajLmZmOpRMKbyxM,5656
512
519
  omlish/os/journald.py,sha256=2nI8Res1poXkbLc31--MPUlzYMESnCcPUkIxDOCjZW0,3903
513
520
  omlish/os/linux.py,sha256=whJ6scwMKSFBdXiVhJW0BCpJV4jOGMr-a_a3Bhwz6Ls,18938
514
521
  omlish/os/paths.py,sha256=hqPiyg_eYaRoIVPdAeX4oeLEV4Kpln_XsH0tHvbOf8Q,844
522
+ omlish/os/signals.py,sha256=FtzkovLb58N3vNdfxflUeXWFCqqKzseCjk5kBdWT-ds,267
515
523
  omlish/os/sizes.py,sha256=ohkALLvqSqBX4iR-7DMKJ4pfOCRdZXV8htH4QywUNM0,152
516
524
  omlish/os/temp.py,sha256=P97KiVeNB7rfGn4tlgU5ro86JUxAsiphLMlxsjQgfB0,1198
517
525
  omlish/os/deathpacts/__init__.py,sha256=IFJkHVWff-VhBbQX38th1RlmjUF2ptKh5TPIzP9Ei2M,229
@@ -519,9 +527,11 @@ omlish/os/deathpacts/base.py,sha256=EGN3BWSXPv0s9kl_QLrWE31hTybDHCmsLc_w3U2VyHc,
519
527
  omlish/os/deathpacts/heartbeatfile.py,sha256=OybdvhM2kxBTuoJWOJJ5LcX-0lg3jTOvvD2HUunxDWU,1731
520
528
  omlish/os/deathpacts/pipe.py,sha256=ZH-l-fIKyurocCehqOgvaYRurxIEMWe8D7l2dsJeGws,3214
521
529
  omlish/os/pidfiles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
530
+ omlish/os/pidfiles/__main__.py,sha256=AF8TwjK4xgHVnoLAP9dIWgKvT0vGhHJlfDW0tKZ7tx4,200
531
+ omlish/os/pidfiles/cli.py,sha256=2SSsP4O3VdpsDIMAkWgWSjh_YNIPzCD9l5LNN2qrIjo,2074
522
532
  omlish/os/pidfiles/manager.py,sha256=qSEwNaWT1KOAnU0KxliwvU_uowme5jyf1FyIPsGwnTY,2391
523
- omlish/os/pidfiles/pidfile.py,sha256=WmZt_c8fvztPgZQnYHhcQCKWgHqAEsaI3Ggz6Wqgkc8,3748
524
- omlish/os/pidfiles/pinning.py,sha256=_AwYjJc1UGX7mdCOk4mItJJcsOJo3RW2ebBOm2noW5Y,6359
533
+ omlish/os/pidfiles/pidfile.py,sha256=yy14NCjvsCdXzlJjZzkw4vbzjt_FXEaUS5SbYT3dGY0,4277
534
+ omlish/os/pidfiles/pinning.py,sha256=v9RlJ4BnJZcaZZXiiRqbmzLluaSOkeeEb_WrbKEClBQ,6643
525
535
  omlish/reflect/__init__.py,sha256=Er2yBHibVO16hFNA1szQF2_f43Y3HRCBWtS-fjsOIYc,798
526
536
  omlish/reflect/inspect.py,sha256=WCo2YpBYauKw6k758FLlZ_H4Q05rgVPs96fEv9w6zHQ,1538
527
537
  omlish/reflect/ops.py,sha256=RJ6jzrM4ieFsXzWyNXWV43O_WgzEaUvlHSc5N2ezW2A,2044
@@ -695,9 +705,9 @@ omlish/text/indent.py,sha256=YjtJEBYWuk8--b9JU_T6q4yxV85_TR7VEVr5ViRCFwk,1336
695
705
  omlish/text/minja.py,sha256=jZC-fp3Xuhx48ppqsf2Sf1pHbC0t8XBB7UpUUoOk2Qw,5751
696
706
  omlish/text/parts.py,sha256=JkNZpyR2tv2CNcTaWJJhpQ9E4F0yPR8P_YfDbZfMtwQ,6182
697
707
  omlish/text/random.py,sha256=jNWpqiaKjKyTdMXC-pWAsSC10AAP-cmRRPVhm59ZWLk,194
698
- omlish-0.0.0.dev230.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
699
- omlish-0.0.0.dev230.dist-info/METADATA,sha256=9A2Cqst8XnecLQcMoWK5ElLcgfKuHa_CQqsM5YCaeUc,4176
700
- omlish-0.0.0.dev230.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
701
- omlish-0.0.0.dev230.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
702
- omlish-0.0.0.dev230.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
703
- omlish-0.0.0.dev230.dist-info/RECORD,,
708
+ omlish-0.0.0.dev232.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
709
+ omlish-0.0.0.dev232.dist-info/METADATA,sha256=q9xYAS00xDATKtVjZBhWNKwkSnMAIV0By4EIl_2vqFs,4176
710
+ omlish-0.0.0.dev232.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
711
+ omlish-0.0.0.dev232.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
712
+ omlish-0.0.0.dev232.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
713
+ omlish-0.0.0.dev232.dist-info/RECORD,,