ominfra 0.0.0.dev125__py3-none-any.whl → 0.0.0.dev127__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.
Files changed (34) hide show
  1. ominfra/clouds/aws/auth.py +1 -1
  2. ominfra/deploy/_executor.py +1 -1
  3. ominfra/deploy/poly/_main.py +1 -1
  4. ominfra/pyremote/_runcommands.py +1 -1
  5. ominfra/scripts/journald2aws.py +2 -2
  6. ominfra/scripts/supervisor.py +1825 -1217
  7. ominfra/supervisor/collections.py +52 -0
  8. ominfra/supervisor/context.py +2 -336
  9. ominfra/supervisor/datatypes.py +1 -63
  10. ominfra/supervisor/dispatchers.py +22 -338
  11. ominfra/supervisor/dispatchersimpl.py +342 -0
  12. ominfra/supervisor/groups.py +33 -110
  13. ominfra/supervisor/groupsimpl.py +86 -0
  14. ominfra/supervisor/inject.py +45 -13
  15. ominfra/supervisor/main.py +1 -1
  16. ominfra/supervisor/pipes.py +83 -0
  17. ominfra/supervisor/poller.py +6 -3
  18. ominfra/supervisor/privileges.py +65 -0
  19. ominfra/supervisor/processes.py +18 -0
  20. ominfra/supervisor/{process.py → processesimpl.py} +99 -317
  21. ominfra/supervisor/setup.py +38 -0
  22. ominfra/supervisor/setupimpl.py +261 -0
  23. ominfra/supervisor/signals.py +24 -16
  24. ominfra/supervisor/spawning.py +31 -0
  25. ominfra/supervisor/spawningimpl.py +347 -0
  26. ominfra/supervisor/supervisor.py +54 -78
  27. ominfra/supervisor/types.py +122 -39
  28. ominfra/supervisor/users.py +64 -0
  29. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/METADATA +3 -3
  30. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/RECORD +34 -23
  31. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/LICENSE +0 -0
  32. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/WHEEL +0 -0
  33. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/entry_points.txt +0 -0
  34. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/top_level.txt +0 -0
@@ -3,24 +3,27 @@ import signal
3
3
  import time
4
4
  import typing as ta
5
5
 
6
- from omlish.lite.cached import cached_nullary
6
+ from omlish.lite.check import check_isinstance
7
7
  from omlish.lite.check import check_not_none
8
8
  from omlish.lite.logs import log
9
- from omlish.lite.typing import Func
9
+ from omlish.lite.typing import Func1
10
10
 
11
11
  from .configs import ProcessGroupConfig
12
12
  from .context import ServerContextImpl
13
- from .dispatchers import Dispatcher
13
+ from .dispatchers import Dispatchers
14
14
  from .events import TICK_EVENTS
15
15
  from .events import EventCallbacks
16
16
  from .events import SupervisorRunningEvent
17
17
  from .events import SupervisorStoppingEvent
18
18
  from .groups import ProcessGroup
19
- from .groups import ProcessGroups
19
+ from .groups import ProcessGroupManager
20
20
  from .poller import Poller
21
+ from .processes import PidHistory
22
+ from .setup import SupervisorSetup
21
23
  from .signals import SignalReceiver
22
24
  from .signals import sig_name
23
25
  from .states import SupervisorState
26
+ from .types import OutputDispatcher
24
27
  from .types import Process
25
28
  from .utils import ExitNow
26
29
  from .utils import as_string
@@ -37,7 +40,7 @@ class SignalHandler:
37
40
  *,
38
41
  context: ServerContextImpl,
39
42
  signal_receiver: SignalReceiver,
40
- process_groups: ProcessGroups,
43
+ process_groups: ProcessGroupManager,
41
44
  ) -> None:
42
45
  super().__init__()
43
46
 
@@ -77,8 +80,10 @@ class SignalHandler:
77
80
  elif sig == signal.SIGUSR2:
78
81
  log.info('received %s indicating log reopen request', sig_name(sig))
79
82
 
80
- for group in self._process_groups:
81
- group.reopen_logs()
83
+ for p in self._process_groups.all_processes():
84
+ for d in p.get_dispatchers():
85
+ if isinstance(d, OutputDispatcher):
86
+ d.reopen_logs()
82
87
 
83
88
  else:
84
89
  log.debug('received %s indicating nothing', sig_name(sig))
@@ -87,7 +92,8 @@ class SignalHandler:
87
92
  ##
88
93
 
89
94
 
90
- ProcessGroupFactory = ta.NewType('ProcessGroupFactory', Func[ProcessGroup]) # (config: ProcessGroupConfig)
95
+ class ProcessGroupFactory(Func1[ProcessGroupConfig, ProcessGroup]):
96
+ pass
91
97
 
92
98
 
93
99
  class Supervisor:
@@ -96,10 +102,12 @@ class Supervisor:
96
102
  *,
97
103
  context: ServerContextImpl,
98
104
  poller: Poller,
99
- process_groups: ProcessGroups,
105
+ process_groups: ProcessGroupManager,
100
106
  signal_handler: SignalHandler,
101
107
  event_callbacks: EventCallbacks,
102
108
  process_group_factory: ProcessGroupFactory,
109
+ pid_history: PidHistory,
110
+ setup: SupervisorSetup,
103
111
  ) -> None:
104
112
  super().__init__()
105
113
 
@@ -109,6 +117,8 @@ class Supervisor:
109
117
  self._signal_handler = signal_handler
110
118
  self._event_callbacks = event_callbacks
111
119
  self._process_group_factory = process_group_factory
120
+ self._pid_history = pid_history
121
+ self._setup = setup
112
122
 
113
123
  self._ticks: ta.Dict[int, float] = {}
114
124
  self._stop_groups: ta.Optional[ta.List[ProcessGroup]] = None # list used for priority ordered shutdown
@@ -126,31 +136,13 @@ class Supervisor:
126
136
 
127
137
  #
128
138
 
129
- class DiffToActive(ta.NamedTuple):
130
- added: ta.List[ProcessGroupConfig]
131
- changed: ta.List[ProcessGroupConfig]
132
- removed: ta.List[ProcessGroupConfig]
133
-
134
- def diff_to_active(self) -> DiffToActive:
135
- new = self._context.config.groups or []
136
- cur = [group.config for group in self._process_groups]
137
-
138
- curdict = dict(zip([cfg.name for cfg in cur], cur))
139
- newdict = dict(zip([cfg.name for cfg in new], new))
140
-
141
- added = [cand for cand in new if cand.name not in curdict]
142
- removed = [cand for cand in cur if cand.name not in newdict]
143
-
144
- changed = [cand for cand in new if cand != curdict.get(cand.name, cand)]
145
-
146
- return Supervisor.DiffToActive(added, changed, removed)
147
-
148
139
  def add_process_group(self, config: ProcessGroupConfig) -> bool:
149
140
  if self._process_groups.get(config.name) is not None:
150
141
  return False
151
142
 
152
- group = self._process_group_factory(config)
153
- group.after_setuid()
143
+ group = check_isinstance(self._process_group_factory(config), ProcessGroup)
144
+ for process in group:
145
+ process.after_setuid()
154
146
 
155
147
  self._process_groups.add(group)
156
148
 
@@ -164,11 +156,7 @@ class Supervisor:
164
156
 
165
157
  return True
166
158
 
167
- def get_process_map(self) -> ta.Dict[int, Dispatcher]:
168
- process_map: ta.Dict[int, Dispatcher] = {}
169
- for group in self._process_groups:
170
- process_map.update(group.get_dispatchers())
171
- return process_map
159
+ #
172
160
 
173
161
  def shutdown_report(self) -> ta.List[Process]:
174
162
  unstopped: ta.List[Process] = []
@@ -191,25 +179,12 @@ class Supervisor:
191
179
 
192
180
  #
193
181
 
194
- def main(self) -> None:
195
- self.setup()
196
- self.run()
197
-
198
- @cached_nullary
199
- def setup(self) -> None:
200
- if not self._context.first:
201
- # prevent crash on libdispatch-based systems, at least for the first request
202
- self._context.cleanup_fds()
203
-
204
- self._context.set_uid_or_exit()
205
-
206
- if self._context.first:
207
- self._context.set_rlimits_or_exit()
208
-
209
- # this sets the options.logger object delay logger instantiation until after setuid
210
- if not self._context.config.nocleanup:
211
- # clean up old automatic logs
212
- self._context.clear_auto_child_logdir()
182
+ def main(self, **kwargs: ta.Any) -> None:
183
+ self._setup.setup()
184
+ try:
185
+ self.run(**kwargs)
186
+ finally:
187
+ self._setup.cleanup()
213
188
 
214
189
  def run(
215
190
  self,
@@ -227,12 +202,6 @@ class Supervisor:
227
202
 
228
203
  self._signal_handler.set_signals()
229
204
 
230
- if not self._context.config.nodaemon and self._context.first:
231
- self._context.daemonize()
232
-
233
- # writing pid file needs to come *after* daemonizing or pid will be wrong
234
- self._context.write_pidfile()
235
-
236
205
  self._event_callbacks.notify(SupervisorRunningEvent())
237
206
 
238
207
  while True:
@@ -242,7 +211,7 @@ class Supervisor:
242
211
  self._run_once()
243
212
 
244
213
  finally:
245
- self._context.cleanup()
214
+ self._poller.close()
246
215
 
247
216
  #
248
217
 
@@ -271,18 +240,24 @@ class Supervisor:
271
240
  # down, so push it back on to the end of the stop group queue
272
241
  self._stop_groups.append(group)
273
242
 
243
+ def get_dispatchers(self) -> Dispatchers:
244
+ return Dispatchers(
245
+ d
246
+ for p in self._process_groups.all_processes()
247
+ for d in p.get_dispatchers()
248
+ )
249
+
274
250
  def _poll(self) -> None:
275
- combined_map = {}
276
- combined_map.update(self.get_process_map())
251
+ dispatchers = self.get_dispatchers()
277
252
 
278
- pgroups = list(self._process_groups)
279
- pgroups.sort()
253
+ sorted_groups = list(self._process_groups)
254
+ sorted_groups.sort()
280
255
 
281
256
  if self._context.state < SupervisorState.RUNNING:
282
257
  if not self._stopping:
283
258
  # first time, set the stopping flag, do a notification and set stop_groups
284
259
  self._stopping = True
285
- self._stop_groups = pgroups[:]
260
+ self._stop_groups = sorted_groups[:]
286
261
  self._event_callbacks.notify(SupervisorStoppingEvent())
287
262
 
288
263
  self._ordered_stop_groups_phase_1()
@@ -291,7 +266,7 @@ class Supervisor:
291
266
  # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
292
267
  raise ExitNow
293
268
 
294
- for fd, dispatcher in combined_map.items():
269
+ for fd, dispatcher in dispatchers.items():
295
270
  if dispatcher.readable():
296
271
  self._poller.register_readable(fd)
297
272
  if dispatcher.writable():
@@ -301,9 +276,9 @@ class Supervisor:
301
276
  r, w = self._poller.poll(timeout)
302
277
 
303
278
  for fd in r:
304
- if fd in combined_map:
279
+ if fd in dispatchers:
305
280
  try:
306
- dispatcher = combined_map[fd]
281
+ dispatcher = dispatchers[fd]
307
282
  log.debug('read event caused by %r', dispatcher)
308
283
  dispatcher.handle_read_event()
309
284
  if not dispatcher.readable():
@@ -311,9 +286,9 @@ class Supervisor:
311
286
  except ExitNow:
312
287
  raise
313
288
  except Exception: # noqa
314
- combined_map[fd].handle_error()
289
+ dispatchers[fd].handle_error()
315
290
  else:
316
- # if the fd is not in combined_map, we should unregister it. otherwise, it will be polled every
291
+ # if the fd is not in combined map, we should unregister it. otherwise, it will be polled every
317
292
  # time, which may cause 100% cpu usage
318
293
  log.debug('unexpected read event from fd %r', fd)
319
294
  try:
@@ -322,9 +297,9 @@ class Supervisor:
322
297
  pass
323
298
 
324
299
  for fd in w:
325
- if fd in combined_map:
300
+ if fd in dispatchers:
326
301
  try:
327
- dispatcher = combined_map[fd]
302
+ dispatcher = dispatchers[fd]
328
303
  log.debug('write event caused by %r', dispatcher)
329
304
  dispatcher.handle_write_event()
330
305
  if not dispatcher.writable():
@@ -332,7 +307,7 @@ class Supervisor:
332
307
  except ExitNow:
333
308
  raise
334
309
  except Exception: # noqa
335
- combined_map[fd].handle_error()
310
+ dispatchers[fd].handle_error()
336
311
  else:
337
312
  log.debug('unexpected write event from fd %r', fd)
338
313
  try:
@@ -340,8 +315,9 @@ class Supervisor:
340
315
  except Exception: # noqa
341
316
  pass
342
317
 
343
- for group in pgroups:
344
- group.transition()
318
+ for group in sorted_groups:
319
+ for process in group:
320
+ process.transition()
345
321
 
346
322
  def _reap(self, *, once: bool = False, depth: int = 0) -> None:
347
323
  if depth >= 100:
@@ -351,13 +327,13 @@ class Supervisor:
351
327
  if not pid:
352
328
  return
353
329
 
354
- process = self._context.pid_history.get(pid, None)
330
+ process = self._pid_history.get(pid, None)
355
331
  if process is None:
356
332
  _, msg = decode_wait_status(check_not_none(sts))
357
333
  log.info('reaped unknown pid %s (%s)', pid, msg)
358
334
  else:
359
335
  process.finish(check_not_none(sts))
360
- del self._context.pid_history[pid]
336
+ del self._pid_history[pid]
361
337
 
362
338
  if not once:
363
339
  # keep reaping until no more kids to reap, but don't recurse infinitely
@@ -3,6 +3,7 @@ import abc
3
3
  import functools
4
4
  import typing as ta
5
5
 
6
+ from .collections import KeyedCollectionAccessors
6
7
  from .configs import ProcessConfig
7
8
  from .configs import ProcessGroupConfig
8
9
  from .configs import ServerConfig
@@ -10,6 +11,36 @@ from .states import ProcessState
10
11
  from .states import SupervisorState
11
12
 
12
13
 
14
+ if ta.TYPE_CHECKING:
15
+ from .dispatchers import Dispatchers
16
+
17
+
18
+ ##
19
+
20
+
21
+ ServerEpoch = ta.NewType('ServerEpoch', int)
22
+
23
+
24
+ ##
25
+
26
+
27
+ @functools.total_ordering
28
+ class ConfigPriorityOrdered(abc.ABC):
29
+ @property
30
+ @abc.abstractmethod
31
+ def config(self) -> ta.Any:
32
+ raise NotImplementedError
33
+
34
+ def __lt__(self, other):
35
+ return self.config.priority < other.config.priority
36
+
37
+ def __eq__(self, other):
38
+ return self.config.priority == other.config.priority
39
+
40
+
41
+ ##
42
+
43
+
13
44
  class ServerContext(abc.ABC):
14
45
  @property
15
46
  @abc.abstractmethod
@@ -31,45 +62,60 @@ class ServerContext(abc.ABC):
31
62
  raise NotImplementedError
32
63
 
33
64
 
34
- # class Dispatcher(abc.ABC):
35
- # pass
36
- #
37
- #
38
- # class OutputDispatcher(Dispatcher, abc.ABC):
39
- # pass
40
- #
41
- #
42
- # class InputDispatcher(Dispatcher, abc.ABC):
43
- # pass
65
+ ##
44
66
 
45
67
 
46
- @functools.total_ordering
47
- class Process(abc.ABC):
68
+ class Dispatcher(abc.ABC):
48
69
  @property
49
70
  @abc.abstractmethod
50
- def pid(self) -> int:
71
+ def process(self) -> 'Process':
51
72
  raise NotImplementedError
52
73
 
53
74
  @property
54
75
  @abc.abstractmethod
55
- def config(self) -> ProcessConfig:
76
+ def channel(self) -> str:
56
77
  raise NotImplementedError
57
78
 
58
- def __lt__(self, other):
59
- return self.config.priority < other.config.priority
60
-
61
- def __eq__(self, other):
62
- return self.config.priority == other.config.priority
79
+ @property
80
+ @abc.abstractmethod
81
+ def fd(self) -> int:
82
+ raise NotImplementedError
63
83
 
64
84
  @property
65
85
  @abc.abstractmethod
66
- def context(self) -> ServerContext:
86
+ def closed(self) -> bool:
67
87
  raise NotImplementedError
68
88
 
89
+ #
90
+
69
91
  @abc.abstractmethod
70
- def finish(self, sts: int) -> None:
92
+ def close(self) -> None:
93
+ raise NotImplementedError
94
+
95
+ @abc.abstractmethod
96
+ def handle_error(self) -> None:
97
+ raise NotImplementedError
98
+
99
+ #
100
+
101
+ @abc.abstractmethod
102
+ def readable(self) -> bool:
71
103
  raise NotImplementedError
72
104
 
105
+ @abc.abstractmethod
106
+ def writable(self) -> bool:
107
+ raise NotImplementedError
108
+
109
+ #
110
+
111
+ def handle_read_event(self) -> None:
112
+ raise TypeError
113
+
114
+ def handle_write_event(self) -> None:
115
+ raise TypeError
116
+
117
+
118
+ class OutputDispatcher(Dispatcher, abc.ABC):
73
119
  @abc.abstractmethod
74
120
  def remove_logs(self) -> None:
75
121
  raise NotImplementedError
@@ -78,67 +124,104 @@ class Process(abc.ABC):
78
124
  def reopen_logs(self) -> None:
79
125
  raise NotImplementedError
80
126
 
127
+
128
+ class InputDispatcher(Dispatcher, abc.ABC):
81
129
  @abc.abstractmethod
82
- def stop(self) -> ta.Optional[str]:
130
+ def write(self, chars: ta.Union[bytes, str]) -> None:
83
131
  raise NotImplementedError
84
132
 
85
133
  @abc.abstractmethod
86
- def give_up(self) -> None:
134
+ def flush(self) -> None:
87
135
  raise NotImplementedError
88
136
 
137
+
138
+ ##
139
+
140
+
141
+ class Process(ConfigPriorityOrdered, abc.ABC):
142
+ @property
89
143
  @abc.abstractmethod
90
- def transition(self) -> None:
144
+ def name(self) -> str:
91
145
  raise NotImplementedError
92
146
 
147
+ @property
93
148
  @abc.abstractmethod
94
- def get_state(self) -> ProcessState:
149
+ def config(self) -> ProcessConfig:
95
150
  raise NotImplementedError
96
151
 
152
+ @property
97
153
  @abc.abstractmethod
98
- def create_auto_child_logs(self) -> None:
154
+ def group(self) -> 'ProcessGroup':
99
155
  raise NotImplementedError
100
156
 
157
+ @property
101
158
  @abc.abstractmethod
102
- def get_dispatchers(self) -> ta.Mapping[int, ta.Any]: # Dispatcher]:
159
+ def pid(self) -> int:
103
160
  raise NotImplementedError
104
161
 
162
+ #
105
163
 
106
- @functools.total_ordering
107
- class ProcessGroup(abc.ABC):
108
164
  @property
109
165
  @abc.abstractmethod
110
- def config(self) -> ProcessGroupConfig:
166
+ def context(self) -> ServerContext:
111
167
  raise NotImplementedError
112
168
 
113
- def __lt__(self, other):
114
- return self.config.priority < other.config.priority
169
+ @abc.abstractmethod
170
+ def finish(self, sts: int) -> None:
171
+ raise NotImplementedError
115
172
 
116
- def __eq__(self, other):
117
- return self.config.priority == other.config.priority
173
+ @abc.abstractmethod
174
+ def stop(self) -> ta.Optional[str]:
175
+ raise NotImplementedError
176
+
177
+ @abc.abstractmethod
178
+ def give_up(self) -> None:
179
+ raise NotImplementedError
118
180
 
119
181
  @abc.abstractmethod
120
182
  def transition(self) -> None:
121
183
  raise NotImplementedError
122
184
 
123
185
  @abc.abstractmethod
124
- def stop_all(self) -> None:
186
+ def get_state(self) -> ProcessState:
187
+ raise NotImplementedError
188
+
189
+ @abc.abstractmethod
190
+ def after_setuid(self) -> None:
191
+ raise NotImplementedError
192
+
193
+ @abc.abstractmethod
194
+ def get_dispatchers(self) -> 'Dispatchers':
125
195
  raise NotImplementedError
126
196
 
197
+
198
+ ##
199
+
200
+
201
+ class ProcessGroup(
202
+ ConfigPriorityOrdered,
203
+ KeyedCollectionAccessors[str, Process],
204
+ abc.ABC,
205
+ ):
127
206
  @property
128
207
  @abc.abstractmethod
129
208
  def name(self) -> str:
130
209
  raise NotImplementedError
131
210
 
211
+ @property
132
212
  @abc.abstractmethod
133
- def before_remove(self) -> None:
213
+ def config(self) -> ProcessGroupConfig:
134
214
  raise NotImplementedError
135
215
 
216
+ @property
136
217
  @abc.abstractmethod
137
- def get_dispatchers(self) -> ta.Mapping[int, ta.Any]: # Dispatcher]:
218
+ def by_name(self) -> ta.Mapping[str, Process]:
138
219
  raise NotImplementedError
139
220
 
221
+ #
222
+
140
223
  @abc.abstractmethod
141
- def reopen_logs(self) -> None:
224
+ def stop_all(self) -> None:
142
225
  raise NotImplementedError
143
226
 
144
227
  @abc.abstractmethod
@@ -146,5 +229,5 @@ class ProcessGroup(abc.ABC):
146
229
  raise NotImplementedError
147
230
 
148
231
  @abc.abstractmethod
149
- def after_setuid(self) -> None:
232
+ def before_remove(self) -> None:
150
233
  raise NotImplementedError
@@ -0,0 +1,64 @@
1
+ # ruff: noqa: UP007
2
+ import dataclasses as dc
3
+ import grp
4
+ import pwd
5
+
6
+
7
+ ##
8
+
9
+
10
+ def name_to_uid(name: str) -> int:
11
+ try:
12
+ uid = int(name)
13
+ except ValueError:
14
+ try:
15
+ pwdrec = pwd.getpwnam(name)
16
+ except KeyError:
17
+ raise ValueError(f'Invalid user name {name}') # noqa
18
+ uid = pwdrec[2]
19
+ else:
20
+ try:
21
+ pwd.getpwuid(uid) # check if uid is valid
22
+ except KeyError:
23
+ raise ValueError(f'Invalid user id {name}') # noqa
24
+ return uid
25
+
26
+
27
+ def name_to_gid(name: str) -> int:
28
+ try:
29
+ gid = int(name)
30
+ except ValueError:
31
+ try:
32
+ grprec = grp.getgrnam(name)
33
+ except KeyError:
34
+ raise ValueError(f'Invalid group name {name}') # noqa
35
+ gid = grprec[2]
36
+ else:
37
+ try:
38
+ grp.getgrgid(gid) # check if gid is valid
39
+ except KeyError:
40
+ raise ValueError(f'Invalid group id {name}') # noqa
41
+ return gid
42
+
43
+
44
+ def gid_for_uid(uid: int) -> int:
45
+ pwrec = pwd.getpwuid(uid)
46
+ return pwrec[3]
47
+
48
+
49
+ ##
50
+
51
+
52
+ @dc.dataclass(frozen=True)
53
+ class User:
54
+ name: str
55
+ uid: int
56
+ gid: int
57
+
58
+
59
+ def get_user(name: str) -> User:
60
+ return User(
61
+ name=name,
62
+ uid=(uid := name_to_uid(name)),
63
+ gid=gid_for_uid(uid),
64
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ominfra
3
- Version: 0.0.0.dev125
3
+ Version: 0.0.0.dev127
4
4
  Summary: ominfra
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -12,8 +12,8 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Operating System :: POSIX
13
13
  Requires-Python: >=3.12
14
14
  License-File: LICENSE
15
- Requires-Dist: omdev==0.0.0.dev125
16
- Requires-Dist: omlish==0.0.0.dev125
15
+ Requires-Dist: omdev==0.0.0.dev127
16
+ Requires-Dist: omlish==0.0.0.dev127
17
17
  Provides-Extra: all
18
18
  Requires-Dist: paramiko~=3.5; extra == "all"
19
19
  Requires-Dist: asyncssh~=2.18; extra == "all"