ominfra 0.0.0.dev125__py3-none-any.whl → 0.0.0.dev127__py3-none-any.whl

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