omlish 0.0.0.dev230__py3-none-any.whl → 0.0.0.dev232__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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,,