omlish 0.0.0.dev242__py3-none-any.whl → 0.0.0.dev244__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/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev242'
2
- __revision__ = '0866b8cd6f11d42061b0949c01a15583a1a6b39d'
1
+ __version__ = '0.0.0.dev244'
2
+ __revision__ = 'e7768b1afd9f43a5f1534c2840691f48c0a89efb'
3
3
 
4
4
 
5
5
  #
@@ -0,0 +1,45 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import asyncio
4
+ import typing as ta
5
+
6
+ from ...lite.timeouts import Timeout
7
+ from ...lite.timeouts import TimeoutLike
8
+
9
+
10
+ async def asyncio_wait_until_can_connect(
11
+ host: ta.Any = None,
12
+ port: ta.Any = None,
13
+ *,
14
+ timeout: ta.Optional[TimeoutLike] = None,
15
+ on_fail: ta.Optional[ta.Callable[[BaseException], None]] = None,
16
+ sleep_s: float = .1,
17
+ exception: ta.Union[ta.Type[BaseException], ta.Tuple[ta.Type[BaseException], ...]] = (Exception,),
18
+ ) -> None:
19
+ timeout = Timeout.of(timeout)
20
+
21
+ async def inner():
22
+ while True:
23
+ timeout()
24
+
25
+ try:
26
+ reader, writer = await asyncio.open_connection(host, port)
27
+
28
+ except asyncio.CancelledError:
29
+ raise
30
+
31
+ except exception as e: # noqa
32
+ if on_fail is not None:
33
+ on_fail(e)
34
+
35
+ else:
36
+ writer.close()
37
+ await asyncio.wait_for(writer.wait_closed(), timeout=timeout.or_(None))
38
+ break
39
+
40
+ await asyncio.sleep(min(sleep_s, timeout.remaining()))
41
+
42
+ if timeout() != float('inf'):
43
+ await asyncio.wait_for(inner(), timeout=timeout())
44
+ else:
45
+ await inner()
omlish/daemons/daemon.py CHANGED
@@ -48,7 +48,9 @@ class Daemon:
48
48
 
49
49
  #
50
50
 
51
+ # TODO: None, defaults, figure out from spawn method
51
52
  reparent_process: bool = False
53
+
52
54
  launched_timeout_s: float = 5.
53
55
 
54
56
  #
@@ -126,7 +128,7 @@ class Daemon:
126
128
 
127
129
  #
128
130
 
129
- def launch_no_wait(self) -> None:
131
+ def launch_no_wait(self) -> bool:
130
132
  launcher = Launcher(
131
133
  target=self._target,
132
134
  spawning=check.not_none(self._config.spawning),
@@ -136,7 +138,7 @@ class Daemon:
136
138
  launched_timeout_s=self._config.launched_timeout_s,
137
139
  )
138
140
 
139
- launcher.launch()
141
+ return launcher.launch()
140
142
 
141
143
  def launch(self, timeout: lang.TimeoutLike = lang.Timeout.Default) -> None:
142
144
  self.launch_no_wait()
@@ -53,8 +53,10 @@ class Launcher:
53
53
  self,
54
54
  *,
55
55
  pidfile_manager: ta.ContextManager | None,
56
- launched_callback: ta.Callable[[], None] | None = None,
56
+ callback: ta.Callable[[], None] | None = None,
57
57
  ) -> None:
58
+ callback_called = False
59
+
58
60
  try:
59
61
  if self._reparent_process:
60
62
  log.info('Reparenting')
@@ -66,15 +68,16 @@ class Launcher:
66
68
  pidfile = check.isinstance(es.enter_context(pidfile_manager), Pidfile)
67
69
  pidfile.write()
68
70
 
69
- if launched_callback is not None:
70
- launched_callback()
71
+ if callback is not None:
72
+ callback_called = True
73
+ callback()
71
74
 
72
75
  runner = target_runner_for(self._target)
73
76
  runner.run()
74
77
 
75
78
  finally:
76
- if launched_callback is not None:
77
- launched_callback()
79
+ if callback is not None and not callback_called:
80
+ callback()
78
81
 
79
82
  def launch(self) -> bool:
80
83
  with contextlib.ExitStack() as es:
@@ -110,8 +113,9 @@ class Launcher:
110
113
  functools.partial(
111
114
  self._inner_launch,
112
115
  pidfile_manager=pidfile_manager,
113
- launched_callback=launched_event.set if launched_event is not None else None,
116
+ callback=launched_event.set if launched_event is not None else None,
114
117
  ),
118
+ target=self._target,
115
119
  inherit_fds=inherit_fds,
116
120
  ))
117
121
 
@@ -2,7 +2,10 @@ import os
2
2
  import sys
3
3
 
4
4
 
5
- def reparent_process() -> None:
5
+ def reparent_process(
6
+ *,
7
+ no_close_stdio: bool = False,
8
+ ) -> None:
6
9
  if (pid := os.fork()): # noqa
7
10
  sys.exit(0)
8
11
  raise RuntimeError('Unreachable') # noqa
@@ -12,5 +15,15 @@ def reparent_process() -> None:
12
15
  if (pid := os.fork()): # noqa
13
16
  sys.exit(0)
14
17
 
18
+ if not no_close_stdio:
19
+ rn_fd = os.open('/dev/null', os.O_RDONLY)
20
+ os.dup2(rn_fd, 0)
21
+ os.close(rn_fd)
22
+
23
+ wn_fd = os.open('/dev/null', os.O_WRONLY)
24
+ os.dup2(wn_fd, 1)
25
+ os.dup2(wn_fd, 2)
26
+ os.close(wn_fd)
27
+
15
28
  sys.stdout.flush()
16
29
  sys.stderr.flush()
@@ -1,4 +1,5 @@
1
1
  import abc
2
+ import threading
2
3
  import typing as ta
3
4
 
4
5
  from .. import cached
@@ -12,6 +13,7 @@ from .targets import TargetRunner
12
13
  from .targets import target_runner_for
13
14
 
14
15
 
16
+ ServiceT = ta.TypeVar('ServiceT', bound='Service')
15
17
  ServiceConfigT = ta.TypeVar('ServiceConfigT', bound='Service.Config')
16
18
 
17
19
 
@@ -82,27 +84,53 @@ def _(target: ServiceConfigTarget) -> ServiceConfigTargetRunner:
82
84
 
83
85
 
84
86
  @dc.dataclass(frozen=True)
85
- class ServiceDaemon(lang.Final):
86
- service: Service | Service.Config
87
+ class ServiceDaemon(lang.Final, ta.Generic[ServiceT, ServiceConfigT]):
88
+ service: ServiceT | ServiceConfigT
87
89
 
88
90
  @cached.function
89
- def service_(self) -> Service:
90
- if isinstance(self.service, Service):
91
- return self.service
92
- elif isinstance(self.service, Service.Config):
93
- return Service.from_config(self.service)
94
- else:
95
- raise TypeError(self.service)
91
+ def service_config(self) -> ServiceConfigT:
92
+ with self._lock:
93
+ if isinstance(self.service, Service):
94
+ return self.service.config
95
+ elif isinstance(self.service, Service.Config):
96
+ return self.service
97
+ else:
98
+ raise TypeError(self.service)
99
+
100
+ @cached.function
101
+ def service_(self) -> ServiceT:
102
+ with self._lock:
103
+ if isinstance(self.service, Service):
104
+ return self.service # type: ignore[return-value]
105
+ elif isinstance(self.service, Service.Config):
106
+ return Service.from_config(self.service) # type: ignore[return-value]
107
+ else:
108
+ raise TypeError(self.service)
96
109
 
97
110
  #
98
111
 
99
112
  daemon: Daemon | Daemon.Config = Daemon.Config()
100
113
 
114
+ @cached.function
115
+ def daemon_config(self) -> Daemon.Config:
116
+ with self._lock:
117
+ if isinstance(self.daemon, Daemon):
118
+ return self.daemon.config
119
+ elif isinstance(self.daemon, Daemon.Config):
120
+ return self.daemon
121
+ else:
122
+ raise TypeError(self.daemon)
123
+
101
124
  @cached.function
102
125
  def daemon_(self) -> Daemon:
103
- if isinstance(self.daemon, Daemon):
104
- return self.daemon
105
- elif isinstance(self.daemon, Daemon.Config):
106
- return Daemon(Target.of(self.service_()), self.daemon)
107
- else:
108
- raise TypeError(self.daemon)
126
+ with self._lock:
127
+ if isinstance(self.daemon, Daemon):
128
+ return self.daemon
129
+ elif isinstance(self.daemon, Daemon.Config):
130
+ return Daemon(Target.of(self.service_()), self.daemon)
131
+ else:
132
+ raise TypeError(self.daemon)
133
+
134
+ #
135
+
136
+ _lock: threading.RLock = dc.field(default_factory=lambda: threading.RLock(), init=False)
@@ -1,4 +1,5 @@
1
1
  import abc
2
+ import enum
2
3
  import functools
3
4
  import os
4
5
  import sys
@@ -9,6 +10,7 @@ from .. import check
9
10
  from .. import dataclasses as dc
10
11
  from .. import lang
11
12
  from ..diag import pydevd
13
+ from .targets import Target
12
14
 
13
15
 
14
16
  if ta.TYPE_CHECKING:
@@ -37,6 +39,8 @@ class Spawn(dc.Frozen, final=True):
37
39
 
38
40
  _: dc.KW_ONLY
39
41
 
42
+ target: Target | None = None
43
+
40
44
  inherit_fds: ta.Collection[int] | None = None
41
45
 
42
46
 
@@ -59,10 +63,29 @@ def spawner_for(spawning: Spawning) -> Spawner:
59
63
 
60
64
 
61
65
  class MultiprocessingSpawning(Spawning, kw_only=True):
66
+ class StartMethod(enum.Enum):
67
+ SPAWN = enum.auto()
68
+ FORK = enum.auto()
69
+ # TODO: FORK_SERVER
70
+
62
71
  # Defaults to 'fork' if under pydevd, else 'spawn'
63
- start_method: str | None = None
72
+ start_method: StartMethod | None = None
73
+
74
+ #
75
+
76
+ # Note: Per multiprocessing docs, `no_linger=True` processes (corresponding to `Process(daemon=True)`) cannot spawn
77
+ # subprocesses, and thus will fail if `Daemon.Config.reparent_process` is set.
78
+ no_linger: bool = False
64
79
 
65
- non_daemon: bool = False
80
+ #
81
+
82
+ @dc.dataclass(frozen=True, kw_only=True)
83
+ class EntrypointArgs:
84
+ spawning: 'MultiprocessingSpawning'
85
+ spawn: Spawn
86
+ start_method: 'MultiprocessingSpawning.StartMethod'
87
+
88
+ entrypoint: ta.Callable[[EntrypointArgs], None] | None = None
66
89
 
67
90
 
68
91
  class MultiprocessingSpawner(Spawner):
@@ -72,20 +95,26 @@ class MultiprocessingSpawner(Spawner):
72
95
  self._spawning = spawning
73
96
  self._process: ta.Optional['mp.process.BaseProcess'] = None # noqa
74
97
 
98
+ @lang.cached_function
99
+ def _determine_start_method(self) -> 'MultiprocessingSpawning.StartMethod':
100
+ if (start_method := self._spawning.start_method) is not None:
101
+ return start_method
102
+
103
+ # Unfortunately, pydevd forces the use of the 'fork' start_method, which cannot be mixed with 'spawn':
104
+ # https://github.com/python/cpython/blob/a7427f2db937adb4c787754deb4c337f1894fe86/Lib/multiprocessing/spawn.py#L102 # noqa
105
+ if pydevd.is_running():
106
+ return MultiprocessingSpawning.StartMethod.FORK
107
+
108
+ return MultiprocessingSpawning.StartMethod.SPAWN
109
+
75
110
  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'
111
+ start_method = self._determine_start_method()
83
112
 
84
113
  ctx: 'mp.context.BaseContext' # noqa
85
- if start_method == 'fork':
86
- ctx = mp.get_context(check.non_empty_str(start_method))
114
+ if start_method == MultiprocessingSpawning.StartMethod.FORK:
115
+ ctx = mp.get_context(check.non_empty_str('fork'))
87
116
 
88
- elif start_method == 'spawn':
117
+ elif start_method == MultiprocessingSpawning.StartMethod.SPAWN:
89
118
  ctx = omp_spawn.ExtrasSpawnContext(omp_spawn.SpawnExtras(
90
119
  pass_fds=frozenset(spawn.inherit_fds) if spawn.inherit_fds is not None else None,
91
120
  ))
@@ -97,9 +126,20 @@ class MultiprocessingSpawner(Spawner):
97
126
 
98
127
  def spawn(self, spawn: Spawn) -> None:
99
128
  check.none(self._process)
129
+
130
+ target: ta.Callable[[], None]
131
+ if (ep := self._spawning.entrypoint) is not None:
132
+ target = functools.partial(ep, MultiprocessingSpawning.EntrypointArgs(
133
+ spawning=self._spawning,
134
+ spawn=spawn,
135
+ start_method=self._determine_start_method(),
136
+ ))
137
+ else:
138
+ target = spawn.fn
139
+
100
140
  self._process = self._process_cls(spawn)(
101
- target=spawn.fn,
102
- daemon=not self._spawning.non_daemon,
141
+ target=target,
142
+ daemon=self._spawning.no_linger,
103
143
  )
104
144
  self._process.start()
105
145
 
@@ -142,7 +182,7 @@ def _(spawning: ForkSpawning) -> ForkSpawner:
142
182
 
143
183
 
144
184
  class ThreadSpawning(Spawning, kw_only=True):
145
- non_daemon: bool = False
185
+ linger: bool = False
146
186
 
147
187
 
148
188
  class ThreadSpawner(InProcessSpawner):
@@ -156,7 +196,7 @@ class ThreadSpawner(InProcessSpawner):
156
196
  check.none(self._thread)
157
197
  self._thread = threading.Thread(
158
198
  target=spawn.fn,
159
- daemon=not self._spawning.non_daemon,
199
+ daemon=not self._spawning.linger,
160
200
  )
161
201
  self._thread.start()
162
202
 
@@ -43,6 +43,7 @@ def make_simple_http_server(
43
43
  ignore_ssl_errors: bool = False,
44
44
  executor: ta.Optional[cf.Executor] = None,
45
45
  use_threads: bool = False,
46
+ **kwargs: ta.Any,
46
47
  ) -> ta.Iterator[SocketServer]:
47
48
  check.arg(not (executor is not None and use_threads))
48
49
 
@@ -107,6 +108,7 @@ def make_simple_http_server(
107
108
  server = es.enter_context(SocketServer(
108
109
  SocketBinder.of(bind),
109
110
  server_handler,
111
+ **kwargs,
110
112
  ))
111
113
 
112
114
  yield server
omlish/lang/__init__.py CHANGED
@@ -54,18 +54,15 @@ from .cmp import ( # noqa
54
54
 
55
55
  from .contextmanagers import ( # noqa
56
56
  AsyncContextManager,
57
- AsyncExitStacked,
58
57
  ContextManaged,
59
58
  ContextManager,
60
59
  ContextWrapped,
61
60
  DefaultLockable,
62
- ExitStacked,
63
61
  Lockable,
64
62
  NOP_CONTEXT_MANAGER,
65
63
  NopContextManager,
66
64
  Timer,
67
65
  a_defer,
68
- attr_setting,
69
66
  breakpoint_on_exception,
70
67
  context_var_setting,
71
68
  context_wrapped,
@@ -232,6 +229,12 @@ from .typing import ( # noqa
232
229
 
233
230
  ##
234
231
 
232
+ from ..lite.contextmanagers import ( # noqa
233
+ attr_setting,
234
+ AsyncExitStacked,
235
+ ExitStacked,
236
+ )
237
+
235
238
  from ..lite.imports import ( # noqa
236
239
  import_attr,
237
240
  import_module,
@@ -170,103 +170,6 @@ def context_var_setting(var: contextvars.ContextVar[T], val: T) -> ta.Iterator[T
170
170
  var.reset(token)
171
171
 
172
172
 
173
- @contextlib.contextmanager
174
- def attr_setting(obj, attr, val, *, default=None): # noqa
175
- not_set = object()
176
- orig = getattr(obj, attr, not_set)
177
- try:
178
- setattr(obj, attr, val)
179
- if orig is not not_set:
180
- yield orig
181
- else:
182
- yield default
183
- finally:
184
- if orig is not_set:
185
- delattr(obj, attr)
186
- else:
187
- setattr(obj, attr, orig)
188
-
189
-
190
- ##
191
-
192
-
193
- class ExitStacked:
194
- @property
195
- def _exit_stack(self) -> contextlib.ExitStack:
196
- try:
197
- return self.__exit_stack # type: ignore
198
- except AttributeError:
199
- es = self.__exit_stack = contextlib.ExitStack()
200
- return es
201
-
202
- def _enter_context(self, context_manager: ta.ContextManager[T]) -> T:
203
- return self._exit_stack.enter_context(ta.cast(ta.ContextManager, context_manager))
204
-
205
- def __enter__(self) -> ta.Self:
206
- try:
207
- superfn = super().__enter__ # type: ignore
208
- except AttributeError:
209
- ret = self
210
- else:
211
- ret = superfn()
212
- self._exit_stack.__enter__()
213
- return ret
214
-
215
- def __exit__(
216
- self,
217
- exc_type: type[BaseException] | None,
218
- exc_val: BaseException | None,
219
- exc_tb: types.TracebackType | None,
220
- ) -> bool | None:
221
- self._exit_stack.__exit__(exc_type, exc_val, exc_tb)
222
- try:
223
- superfn = super().__exit__ # type: ignore
224
- except AttributeError:
225
- return None
226
- else:
227
- return superfn(exc_type, exc_val, exc_tb)
228
-
229
-
230
- class AsyncExitStacked:
231
- @property
232
- def _exit_stack(self) -> contextlib.AsyncExitStack:
233
- try:
234
- return self.__exit_stack # type: ignore
235
- except AttributeError:
236
- es = self.__exit_stack = contextlib.AsyncExitStack()
237
- return es
238
-
239
- async def _enter_async_context(self, context_manager: ta.AsyncContextManager[T]) -> T:
240
- return await self._exit_stack.enter_async_context(ta.cast(ta.AsyncContextManager, context_manager))
241
-
242
- def _enter_context(self, context_manager: ta.ContextManager[T]) -> T:
243
- return self._exit_stack.enter_context(ta.cast(ta.ContextManager, context_manager))
244
-
245
- async def __aenter__(self) -> ta.Self:
246
- try:
247
- superfn = super().__aenter__ # type: ignore
248
- except AttributeError:
249
- ret = self
250
- else:
251
- ret = await superfn()
252
- await self._exit_stack.__aenter__()
253
- return ret
254
-
255
- async def __aexit__(
256
- self,
257
- exc_type: type[BaseException] | None,
258
- exc_val: BaseException | None,
259
- exc_tb: types.TracebackType | None,
260
- ) -> bool | None:
261
- await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
262
- try:
263
- superfn = super().__aexit__ # type: ignore
264
- except AttributeError:
265
- return None
266
- else:
267
- return await superfn(exc_type, exc_val, exc_tb)
268
-
269
-
270
173
  ##
271
174
 
272
175
 
@@ -1,5 +1,6 @@
1
1
  # ruff: noqa: UP007
2
2
  import contextlib
3
+ import sys
3
4
  import typing as ta
4
5
 
5
6
  from .check import check
@@ -14,20 +15,64 @@ AsyncExitStackedT = ta.TypeVar('AsyncExitStackedT', bound='AsyncExitStacked')
14
15
 
15
16
 
16
17
  class ExitStacked:
18
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
19
+ super().__init_subclass__(**kwargs)
20
+
21
+ for a in ('__enter__', '__exit__'):
22
+ for b in cls.__bases__:
23
+ if b is ExitStacked:
24
+ continue
25
+ try:
26
+ fn = getattr(b, a)
27
+ except AttributeError:
28
+ pass
29
+ else:
30
+ if fn is not getattr(ExitStacked, a):
31
+ raise TypeError(f'ExitStacked subclass {cls} must not not override {a} via {b}')
32
+
17
33
  _exit_stack: ta.Optional[contextlib.ExitStack] = None
18
34
 
19
- def __enter__(self: ExitStackedT) -> ExitStackedT:
20
- check.state(self._exit_stack is None)
21
- es = self._exit_stack = contextlib.ExitStack()
22
- es.__enter__()
23
- return self
35
+ @contextlib.contextmanager
36
+ def _exit_stacked_init_wrapper(self) -> ta.Iterator[None]:
37
+ """
38
+ Overridable wrapper around __enter__ which deliberately does not have access to an _exit_stack yet. Intended for
39
+ things like wrapping __enter__ in a lock.
40
+ """
41
+
42
+ yield
24
43
 
44
+ @ta.final
45
+ def __enter__(self: ExitStackedT) -> ExitStackedT:
46
+ """
47
+ Final because any contexts entered during this init must be exited if any exception is thrown, and user
48
+ overriding would likely interfere with that. Override `_enter_contexts` for such init.
49
+ """
50
+
51
+ with self._exit_stacked_init_wrapper():
52
+ check.state(self._exit_stack is None)
53
+ es = self._exit_stack = contextlib.ExitStack()
54
+ es.__enter__()
55
+ try:
56
+ self._enter_contexts()
57
+ except Exception: # noqa
58
+ es.__exit__(*sys.exc_info())
59
+ raise
60
+ return self
61
+
62
+ @ta.final
25
63
  def __exit__(self, exc_type, exc_val, exc_tb):
26
64
  if (es := self._exit_stack) is None:
27
65
  return None
28
- self._exit_contexts()
66
+ try:
67
+ self._exit_contexts()
68
+ except Exception: # noqa
69
+ es.__exit__(*sys.exc_info())
70
+ raise
29
71
  return es.__exit__(exc_type, exc_val, exc_tb)
30
72
 
73
+ def _enter_contexts(self) -> None:
74
+ pass
75
+
31
76
  def _exit_contexts(self) -> None:
32
77
  pass
33
78
 
@@ -37,20 +82,54 @@ class ExitStacked:
37
82
 
38
83
 
39
84
  class AsyncExitStacked:
85
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
86
+ super().__init_subclass__(**kwargs)
87
+
88
+ for a in ('__aenter__', '__aexit__'):
89
+ for b in cls.__bases__:
90
+ if b is AsyncExitStacked:
91
+ continue
92
+ try:
93
+ fn = getattr(b, a)
94
+ except AttributeError:
95
+ pass
96
+ else:
97
+ if fn is not getattr(AsyncExitStacked, a):
98
+ raise TypeError(f'AsyncExitStacked subclass {cls} must not not override {a} via {b}')
99
+
40
100
  _exit_stack: ta.Optional[contextlib.AsyncExitStack] = None
41
101
 
42
- async def __aenter__(self: AsyncExitStackedT) -> AsyncExitStackedT:
43
- check.state(self._exit_stack is None)
44
- es = self._exit_stack = contextlib.AsyncExitStack()
45
- await es.__aenter__()
46
- return self
102
+ @contextlib.asynccontextmanager
103
+ async def _async_exit_stacked_init_wrapper(self) -> ta.AsyncGenerator[None, None]:
104
+ yield
47
105
 
106
+ @ta.final
107
+ async def __aenter__(self: AsyncExitStackedT) -> AsyncExitStackedT:
108
+ async with self._async_exit_stacked_init_wrapper():
109
+ check.state(self._exit_stack is None)
110
+ es = self._exit_stack = contextlib.AsyncExitStack()
111
+ await es.__aenter__()
112
+ try:
113
+ await self._async_enter_contexts()
114
+ except Exception: # noqa
115
+ await es.__aexit__(*sys.exc_info())
116
+ raise
117
+ return self
118
+
119
+ @ta.final
48
120
  async def __aexit__(self, exc_type, exc_val, exc_tb):
49
121
  if (es := self._exit_stack) is None:
50
122
  return None
51
- await self._async_exit_contexts()
123
+ try:
124
+ await self._async_exit_contexts()
125
+ except Exception: # noqa
126
+ await es.__aexit__(*sys.exc_info())
127
+ raise
52
128
  return await es.__aexit__(exc_type, exc_val, exc_tb)
53
129
 
130
+ async def _async_enter_contexts(self) -> None:
131
+ pass
132
+
54
133
  async def _async_exit_contexts(self) -> None:
55
134
  pass
56
135
 
omlish/logs/protocol.py CHANGED
@@ -23,25 +23,25 @@ class Logging(ta.Protocol):
23
23
 
24
24
  #
25
25
 
26
- def debug(self, msg: str, *args, **kwargs) -> None:
26
+ def debug(self, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
27
27
  ...
28
28
 
29
- def info(self, msg: str, *args, **kwargs) -> None:
29
+ def info(self, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
30
30
  ...
31
31
 
32
- def warning(self, msg: str, *args, **kwargs) -> None:
32
+ def warning(self, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
33
33
  ...
34
34
 
35
- def error(self, msg: str, *args, **kwargs) -> None:
35
+ def error(self, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
36
36
  ...
37
37
 
38
- def exception(self, msg: str, *args, exc_info=True, **kwargs) -> None:
38
+ def exception(self, msg: str, *args: ta.Any, exc_info: bool = True, **kwargs) -> None:
39
39
  ...
40
40
 
41
- def critical(self, msg: str, *args, **kwargs) -> None:
41
+ def critical(self, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
42
42
  ...
43
43
 
44
- def log(self, level: LogLevel, msg: str, *args, **kwargs) -> None:
44
+ def log(self, level: LogLevel, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
45
45
  ...
46
46
 
47
47
 
@@ -66,30 +66,30 @@ class AbstractLogging(abc.ABC):
66
66
 
67
67
  #
68
68
 
69
- def debug(self, msg: str, *args, **kwargs) -> None:
69
+ def debug(self, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
70
70
  if self.is_enabled_for(logging.DEBUG):
71
71
  self.log(logging.DEBUG, msg, args, **kwargs)
72
72
 
73
- def info(self, msg: str, *args, **kwargs) -> None:
73
+ def info(self, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
74
74
  if self.is_enabled_for(logging.INFO):
75
75
  self.log(logging.INFO, msg, args, **kwargs)
76
76
 
77
- def warning(self, msg: str, *args, **kwargs) -> None:
77
+ def warning(self, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
78
78
  if self.is_enabled_for(logging.WARNING):
79
79
  self.log(logging.WARNING, msg, args, **kwargs)
80
80
 
81
- def error(self, msg: str, *args, **kwargs) -> None:
81
+ def error(self, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
82
82
  if self.is_enabled_for(logging.ERROR):
83
83
  self.log(logging.ERROR, msg, args, **kwargs)
84
84
 
85
- def exception(self, msg: str, *args, exc_info=True, **kwargs) -> None:
85
+ def exception(self, msg: str, *args: ta.Any, exc_info: bool = True, **kwargs: ta.Any) -> None:
86
86
  self.error(msg, *args, exc_info=exc_info, **kwargs)
87
87
 
88
- def critical(self, msg: str, *args, **kwargs) -> None:
88
+ def critical(self, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
89
89
  if self.is_enabled_for(logging.CRITICAL):
90
90
  self.log(logging.CRITICAL, msg, args, **kwargs)
91
91
 
92
- def log(self, level: LogLevel, msg: str, *args, **kwargs) -> None:
92
+ def log(self, level: LogLevel, msg: str, *args: ta.Any, **kwargs: ta.Any) -> None:
93
93
  if not isinstance(level, int):
94
94
  raise TypeError('Level must be an integer.')
95
95
  if self.is_enabled_for(level):
@@ -2,11 +2,14 @@
2
2
  # ruff: noqa: UP006 UP007
3
3
  import abc
4
4
  import contextlib
5
+ import enum
5
6
  import logging
6
7
  import selectors
7
8
  import threading
8
9
  import typing as ta
9
10
 
11
+ from ...lite.contextmanagers import ExitStacked
12
+ from ...lite.contextmanagers import defer
10
13
  from ..addresses import SocketAndAddress
11
14
  from ..bind import SocketBinder
12
15
  from ..io import close_socket_immediately
@@ -76,58 +79,88 @@ class SocketServer(abc.ABC):
76
79
 
77
80
  #
78
81
 
79
- @contextlib.contextmanager
80
- def _listen_context(self) -> ta.Iterator[SelectorProtocol]:
81
- with contextlib.ExitStack() as es:
82
- es.enter_context(self._lock)
83
- es.enter_context(self._binder)
82
+ class PollResult(enum.Enum):
83
+ TIMEOUT = enum.auto()
84
+ CONNECTION = enum.auto()
85
+ ERROR = enum.auto()
86
+ SHUTDOWN = enum.auto()
84
87
 
85
- self._binder.listen()
88
+ class PollContext(ExitStacked, abc.ABC):
89
+ @abc.abstractmethod
90
+ def poll(self, timeout: ta.Optional[float] = None) -> 'SocketServer.PollResult':
91
+ raise NotImplementedError
86
92
 
87
- self._is_shutdown.clear()
88
- try:
89
- # XXX: Consider using another file descriptor or connecting to the socket to wake this up instead of
90
- # polling. Polling reduces our responsiveness to a shutdown request and wastes cpu at all other times.
91
- with self.Selector() as selector:
92
- selector.register(self._binder.fileno(), selectors.EVENT_READ)
93
+ class _PollContext(PollContext):
94
+ def __init__(self, server: 'SocketServer') -> None:
95
+ super().__init__()
93
96
 
94
- yield selector
97
+ self._server = server
95
98
 
96
- finally:
97
- self._is_shutdown.set()
99
+ _selector: ta.Any = None
98
100
 
99
- @contextlib.contextmanager
100
- def loop_context(self, poll_interval: ta.Optional[float] = None) -> ta.Iterator[ta.Iterator[bool]]:
101
- if poll_interval is None:
102
- poll_interval = self._poll_interval
101
+ def _enter_contexts(self) -> None:
102
+ self._enter_context(self._server._lock) # noqa: SLF001
103
+ self._enter_context(self._server._binder) # noqa: SLF001
103
104
 
104
- with self._listen_context() as selector:
105
- def loop():
106
- while not self._should_shutdown:
107
- ready = selector.select(poll_interval)
105
+ self._server._binder.listen() # noqa: SLF001
106
+
107
+ self._server._is_shutdown.clear() # noqa: SLF001
108
+ self._enter_context(defer(self._server._is_shutdown.set)) # noqa
109
+
110
+ # XXX: Consider using another file descriptor or connecting to the socket to wake this up instead of
111
+ # polling. Polling reduces our responsiveness to a shutdown request and wastes cpu at all other times.
112
+ self._selector = self._enter_context(self._server.Selector())
113
+ self._selector.register(self._server._binder.fileno(), selectors.EVENT_READ) # noqa: SLF001
108
114
 
109
- # bpo-35017: shutdown() called during select(), exit immediately.
110
- if self._should_shutdown:
111
- break # type: ignore[unreachable]
115
+ def poll(self, timeout: ta.Optional[float] = None) -> 'SocketServer.PollResult':
116
+ if self._server._should_shutdown: # noqa: SLF001
117
+ return SocketServer.PollResult.SHUTDOWN
112
118
 
113
- if ready:
114
- try:
115
- conn = self._binder.accept()
119
+ ready = self._selector.select(timeout)
116
120
 
117
- except OSError as exc:
118
- self._handle_error(exc)
121
+ # bpo-35017: shutdown() called during select(), exit immediately.
122
+ if self._server._should_shutdown: # noqa: SLF001
123
+ return SocketServer.PollResult.SHUTDOWN # type: ignore[unreachable]
119
124
 
120
- return
125
+ if not ready:
126
+ return SocketServer.PollResult.TIMEOUT
121
127
 
122
- try:
123
- self._handler(conn)
128
+ try:
129
+ conn = self._server._binder.accept() # noqa: SLF001
130
+
131
+ except OSError as exc:
132
+ self._server._handle_error(exc) # noqa: SLF001
124
133
 
125
- except Exception as exc: # noqa
126
- self._handle_error(exc, conn)
134
+ return SocketServer.PollResult.ERROR
127
135
 
128
- close_socket_immediately(conn.socket)
136
+ try:
137
+ self._server._handler(conn) # noqa: SLF001
129
138
 
130
- yield bool(ready)
139
+ except Exception as exc: # noqa
140
+ self._server._handle_error(exc, conn) # noqa: SLF001
141
+
142
+ close_socket_immediately(conn.socket)
143
+
144
+ return SocketServer.PollResult.CONNECTION
145
+
146
+ def poll_context(self) -> PollContext:
147
+ return self._PollContext(self)
148
+
149
+ #
150
+
151
+ @contextlib.contextmanager
152
+ def loop_context(self, poll_interval: ta.Optional[float] = None) -> ta.Iterator[ta.Iterator[bool]]:
153
+ if poll_interval is None:
154
+ poll_interval = self._poll_interval
155
+
156
+ with self.poll_context() as pc:
157
+ def loop():
158
+ while True:
159
+ res = pc.poll(poll_interval)
160
+ if res in (SocketServer.PollResult.ERROR, SocketServer.PollResult.SHUTDOWN):
161
+ return
162
+ else:
163
+ yield res == SocketServer.PollResult.CONNECTION
131
164
 
132
165
  yield loop()
133
166
 
omlish/sockets/wait.py ADDED
@@ -0,0 +1,61 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import socket
4
+ import threading
5
+ import typing as ta
6
+
7
+ from ..lite.timeouts import Timeout
8
+ from ..lite.timeouts import TimeoutLike
9
+
10
+
11
+ ##
12
+
13
+
14
+ def socket_can_connect(
15
+ address: ta.Any,
16
+ *,
17
+ timeout: ta.Optional[TimeoutLike] = None,
18
+ on_fail: ta.Optional[ta.Callable[[BaseException], None]] = None,
19
+ exception: ta.Union[ta.Type[BaseException], ta.Tuple[ta.Type[BaseException], ...]] = (ConnectionRefusedError,),
20
+ ) -> bool:
21
+ timeout = Timeout.of(timeout)
22
+
23
+ try:
24
+ conn = socket.create_connection(address, timeout=timeout.or_(None))
25
+
26
+ except exception as e: # noqa
27
+ if on_fail is not None:
28
+ on_fail(e)
29
+ return False
30
+
31
+ else:
32
+ conn.close()
33
+ return True
34
+
35
+
36
+ def socket_wait_until_can_connect(
37
+ address: ta.Any,
38
+ *,
39
+ timeout: ta.Optional[TimeoutLike] = None,
40
+ on_fail: ta.Optional[ta.Callable[[BaseException], None]] = None,
41
+ sleep_s: float = .1,
42
+ exception: ta.Union[ta.Type[BaseException], ta.Tuple[ta.Type[BaseException], ...]] = (ConnectionRefusedError,),
43
+ cancel_event: ta.Optional[threading.Event] = None,
44
+ ) -> None:
45
+ timeout = Timeout.of(timeout)
46
+
47
+ if cancel_event is None:
48
+ cancel_event = threading.Event()
49
+
50
+ while not cancel_event.is_set():
51
+ timeout()
52
+
53
+ if socket_can_connect(
54
+ address,
55
+ timeout=timeout,
56
+ on_fail=on_fail,
57
+ exception=exception,
58
+ ):
59
+ break
60
+
61
+ cancel_event.wait(min(sleep_s, timeout.remaining()))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: omlish
3
- Version: 0.0.0.dev242
3
+ Version: 0.0.0.dev244
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -1,5 +1,5 @@
1
1
  omlish/.manifests.json,sha256=vQTAIvR8OblSq-uP2GUfnbei0RnmAnM5j0T1-OToh9E,8253
2
- omlish/__about__.py,sha256=v3KofXu74aLHjSY2AMiIiv61EfsLsoedesWKEUsNxWA,3380
2
+ omlish/__about__.py,sha256=I6B5uZQiJsqsRd0Vxt8abjKC5YXkiwOJFtBfeWlVYAM,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
@@ -101,6 +101,7 @@ omlish/asyncs/asyncio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
101
101
  omlish/asyncs/asyncio/all.py,sha256=EksCHjRQKobiGrxuDW72IaH53WJMs7rdj_ZDBI3iKcg,315
102
102
  omlish/asyncs/asyncio/asyncio.py,sha256=mDjYNm1cylUhQ8slWXwdPoXasuWfafjzu78GHt2Mdig,2437
103
103
  omlish/asyncs/asyncio/channels.py,sha256=ZbmsEmdK1fV96liHdcVpRqA2dAMkXJt4Q3rFAg3YOIw,1074
104
+ omlish/asyncs/asyncio/sockets.py,sha256=Ni5O80fNAccSMAGrlSkZ923Nawxi3FHoXrKj4nr0xlU,1266
104
105
  omlish/asyncs/asyncio/streams.py,sha256=Uc9PCWSfBqrK2kdVtfjjQU1eaYTWYmZm8QISDj2xiuw,1004
105
106
  omlish/asyncs/asyncio/subprocesses.py,sha256=f30-wi-3n9R5dftm4CMrzp23EEa4GX283bORixm1_UU,6931
106
107
  omlish/asyncs/asyncio/timeouts.py,sha256=hokhi7jZSAtBv0ME3qL1cO5eshNA9ViEH7BLafNCBpQ,454
@@ -179,11 +180,11 @@ omlish/configs/processing/names.py,sha256=weHmaTclzgM9lUn3aBtw-kwZ3mc2N-CZlFg3Kd
179
180
  omlish/configs/processing/rewriting.py,sha256=v7PfHtuTn5v_5Y6Au7oMN2Z0nxAMy1iYyO5CXnTvZhs,4226
180
181
  omlish/configs/processing/strings.py,sha256=qFS2oh6z02IaM_q4lTKLdufzkJqAJ6J-Qjrz5S-QJoM,826
181
182
  omlish/daemons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
182
- omlish/daemons/daemon.py,sha256=ykdbCPbpxKrdZVZc892SnedTdftTAYt_YdDpYchKcUE,3410
183
- omlish/daemons/launching.py,sha256=mhtkuAO16STcznUl3rrX9pacfrKbPQRCP2AllKL4B70,3664
184
- omlish/daemons/reparent.py,sha256=UaG2X6VJHJPOlUwHPNRH3aWGgF0Fg771jjO9IRPLlyY,280
185
- omlish/daemons/services.py,sha256=UAzzdP4jG0-piVzz6CsSTPIjTGt4VFXtbzP7KczMCho,2354
186
- omlish/daemons/spawning.py,sha256=cx00xeqSrfhlFbjCtKqaBHvMuHwB9hdjuKNHzAAo_dw,4030
183
+ omlish/daemons/daemon.py,sha256=3Wkvu8M_EaCKSpKI5UN5OayRXV0oVdF62tBss9_hlr0,3479
184
+ omlish/daemons/launching.py,sha256=sNOYW939IGI4ZlLQ0bKxzXj6EyeOiwV7Upqhd5XfoHc,3747
185
+ omlish/daemons/reparent.py,sha256=7uJ9oPGt9Ud7uA8bDl_SHcuqjcsmXa3kkjp9jf29wOw,585
186
+ omlish/daemons/services.py,sha256=jIlGWhiWoqQlm_OFeffkSs9jjr-icDF1-I-SNMEgg9Y,3406
187
+ omlish/daemons/spawning.py,sha256=psR73zOYjMKTqNpx1bMib8uU9wAZz62tw5TaWHrTdyY,5337
187
188
  omlish/daemons/targets.py,sha256=00KmtlknMhQ5PyyVAhWl3rpeTMPym0GxvHHq6mYPZ7c,3051
188
189
  omlish/daemons/waiting.py,sha256=RfgD1L33QQVbD2431dkKZGE4w6DUcGvYeRXXi8puAP4,1676
189
190
  omlish/dataclasses/__init__.py,sha256=b7EZCIfHnEHCHWwgD3YXxkdsU-uYd9iD4hM36RgpI1g,1598
@@ -332,7 +333,7 @@ omlish/http/wsgi.py,sha256=czZsVUX-l2YTlMrUjKN49wRoP4rVpS0qpeBn4O5BoMY,948
332
333
  omlish/http/coro/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
333
334
  omlish/http/coro/fdio.py,sha256=bd9K4EYVWbXV3e3npDPXI9DuDAruJiyDmrgFpgNcjzY,4035
334
335
  omlish/http/coro/server.py,sha256=30FTcJG8kuFeThf0HJYpTzMZN-giLTBP7wr5Wl3b9X0,18285
335
- omlish/http/coro/simple.py,sha256=_ZKFlfLda9Gatd3bNBNGJpITNQl4tuTAbL3P-Mr5j5w,3152
336
+ omlish/http/coro/simple.py,sha256=inWA_ss6Nz5Rqmy4dL9_SGah4anYoDecDTRQqVIGYeY,3200
336
337
  omlish/http/coro/sockets.py,sha256=rtpZZ-XCOfC5tXr4Fmo1HSn-8f5nxfIOlJaPUkQeDyU,1654
337
338
  omlish/inject/__init__.py,sha256=n0RC9UDGsBQQ39cST39-XJqJPq2M0tnnh9yJubW9azo,1891
338
339
  omlish/inject/binder.py,sha256=DAbc8TZi5w8Mna0TUtq0mT4jeDVA7i7SlBtOFrh2swc,4185
@@ -398,11 +399,11 @@ omlish/iterators/iterators.py,sha256=ghI4dO6WPyyFOLTIIMaHQ_IOy2xXaFpGPqveZ5YGIBU
398
399
  omlish/iterators/recipes.py,sha256=53mkexitMhkwXQZbL6DrhpT0WePQ_56uXd5Jaw3DfzI,467
399
400
  omlish/iterators/tools.py,sha256=Pi4ybXytUXVZ3xwK89xpPImQfYYId9p1vIFQvVqVLqA,2551
400
401
  omlish/iterators/unique.py,sha256=0jAX3kwzVfRNhe0Tmh7kVP_Q2WBIn8POo_O-rgFV0rQ,1390
401
- omlish/lang/__init__.py,sha256=2jxO7QWT0uOvdYdmBFgnqmVh-6I7DofsapgUl3wu1fY,4145
402
+ omlish/lang/__init__.py,sha256=T_hx_ygi-IqmeDPQ-uto2U4ZuAUDs-agOSAsF6WWLVI,4193
402
403
  omlish/lang/cached.py,sha256=tQaqMu1LID0q4NSTk5vPXsgxIBWSFAmjs5AhQoEHoCQ,7833
403
404
  omlish/lang/clsdct.py,sha256=sJYadm-fwzti-gsi98knR5qQUxriBmOqQE_qz3RopNk,1743
404
405
  omlish/lang/cmp.py,sha256=5vbzWWbqdzDmNKAGL19z6ZfUKe5Ci49e-Oegf9f4BsE,1346
405
- omlish/lang/contextmanagers.py,sha256=Mrn8NJ3pP0Zxi-IoGqSjZDdWUctsyee2vrZ2FtZvNmo,10529
406
+ omlish/lang/contextmanagers.py,sha256=UPH6daYwSP9cH5AfSVsJyEHk1UURMGhVPM5ZRhp_Hvw,7576
406
407
  omlish/lang/datetimes.py,sha256=ehI_DhQRM-bDxAavnp470XcekbbXc4Gdw9y1KpHDJT0,223
407
408
  omlish/lang/descriptors.py,sha256=mZ2h9zJ__MMpw8hByjRbAiONcwfVb6GD0btNnVi8C5w,6573
408
409
  omlish/lang/exceptions.py,sha256=qJBo3NU1mOWWm-NhQUHCY5feYXR3arZVyEHinLsmRH4,47
@@ -434,7 +435,7 @@ omlish/lite/__init__.py,sha256=ISLhM4q0LR1XXTCaHdZOZxBRyIsoZqYm4u0bf1BPcVk,148
434
435
  omlish/lite/cached.py,sha256=O7ozcoDNFm1Hg2wtpHEqYSp_i_nCLNOP6Ueq_Uk-7mU,1300
435
436
  omlish/lite/check.py,sha256=OLwtE2x6nlbGx4vS3Rda7zMHpgqzDSLJminTAX2lqLA,13529
436
437
  omlish/lite/configs.py,sha256=Ev_19sbII67pTWzInYjYqa9VyTiZBvyjhZqyG8TtufE,908
437
- omlish/lite/contextmanagers.py,sha256=ciaMl0D3QDHToM7M28-kwZ-Q48LtwgCxiud3nekgutA,2863
438
+ omlish/lite/contextmanagers.py,sha256=XSCwr9GpPBJxXR9Vr07M4A_BH3uLpZettyoSE5KqJu8,5566
438
439
  omlish/lite/dataclasses.py,sha256=t1G5-xOuvE6o6w9RyqHzLT9wHD0HkqBh5P8HUZWxGzs,1912
439
440
  omlish/lite/imports.py,sha256=o9WWrNrWg0hKeMvaj91giaovED_9VFanN2MyEHBGekY,1346
440
441
  omlish/lite/inject.py,sha256=-tTsOqqef-Ix5Tgl2DP_JAsNWJQDFUptERl3lk14Uzs,29007
@@ -462,7 +463,7 @@ omlish/logs/filters.py,sha256=2noFRyBez3y519fpfsDSt1vo8wX-85b8sMXZi5o_xyE,208
462
463
  omlish/logs/handlers.py,sha256=zgSnKQA5q9Fu7T0Nkd7twog9H1Wg9-bDCzz4_F1TOBo,319
463
464
  omlish/logs/json.py,sha256=zyqMWpZY3lk4WRk4wgmataBomGX9S3iDsydiM1sS-lI,1366
464
465
  omlish/logs/noisy.py,sha256=Ubc-eTH6ZbGYsLfUUi69JAotwuUwzb-SJBeGo_0dIZI,348
465
- omlish/logs/protocol.py,sha256=NzyCeNBN-fyKpJinhECfjUQSd5MxZLiYbuLCTtW6QUU,4500
466
+ omlish/logs/protocol.py,sha256=dfAR0_5kLEAkx0nhuWBhWMTVjWjhEl2uL-MxejrW1lk,4732
466
467
  omlish/logs/proxy.py,sha256=A-ROPUUAlF397qTbEqhel6YhQMstNuXL3Xmts7w9dAo,2347
467
468
  omlish/logs/standard.py,sha256=FbKdF2Z4Na5i2TNwKn0avLJXyICe2JKsPufjvKCHGn0,3162
468
469
  omlish/logs/timing.py,sha256=XrFUHIPT4EHDujLKbGs9fGFMmoM3NEP8xPRaESJr7bQ,1513
@@ -565,9 +566,10 @@ omlish/sockets/bind.py,sha256=J1SfFFFnVf3H5nqESDX2NGEY8DmjyIMUXZciZM33zQY,8003
565
566
  omlish/sockets/handlers.py,sha256=Gj6xZoo4vommge8XvkehYw3B7O4aql2P4qzZIIa0p24,462
566
567
  omlish/sockets/io.py,sha256=lfhTkB7NnAIx9kuQhAkwgsEUXY78Mp1_WtYrIQNS_k8,1408
567
568
  omlish/sockets/ports.py,sha256=Wm4mRFFz5MdD8KbdaEfT1c4PbJnsuK_iyJlZJE_-8jo,1402
569
+ omlish/sockets/wait.py,sha256=aznyOzGa9oNBc31xnyk1S7TylO8hGx7vFyOVsdY3zFE,1585
568
570
  omlish/sockets/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
569
571
  omlish/sockets/server/handlers.py,sha256=PPsb1X5oU9dN8jfztaMGsRiqWTyEANT-1aSLbS6bUVg,3867
570
- omlish/sockets/server/server.py,sha256=mZmHPkCRPitous56_7FJdAsDLZag2wDqjj-LaYM8_Fg,4943
572
+ omlish/sockets/server/server.py,sha256=FkaishIxJuU4it9tTI7wzlGqJYzFGXzDrd_HgV0jAmU,6253
571
573
  omlish/sockets/server/ssl.py,sha256=VE0GpdA-gYsN2m9_uvfDwWmXtIbRQqJomVdpGJO8o2M,1061
572
574
  omlish/sockets/server/threading.py,sha256=YmW3Ym_p5j_F4SIH9BgRHIObywjq1HS39j9CGWIcMAY,2856
573
575
  omlish/specs/__init__.py,sha256=zZwF8yXTEkSstYtORkDhVLK-_hWU8WOJCuBpognb_NY,118
@@ -727,9 +729,9 @@ omlish/text/mangle.py,sha256=kfzFLfvepH-chl1P89_mdc5vC4FSqyPA2aVtgzuB8IY,1133
727
729
  omlish/text/minja.py,sha256=jZC-fp3Xuhx48ppqsf2Sf1pHbC0t8XBB7UpUUoOk2Qw,5751
728
730
  omlish/text/parts.py,sha256=JkNZpyR2tv2CNcTaWJJhpQ9E4F0yPR8P_YfDbZfMtwQ,6182
729
731
  omlish/text/random.py,sha256=jNWpqiaKjKyTdMXC-pWAsSC10AAP-cmRRPVhm59ZWLk,194
730
- omlish-0.0.0.dev242.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
731
- omlish-0.0.0.dev242.dist-info/METADATA,sha256=f11RfSbCLw8fvLQKc--nQePc_WBXEXJ_ljN3zB8QuvM,4176
732
- omlish-0.0.0.dev242.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
733
- omlish-0.0.0.dev242.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
734
- omlish-0.0.0.dev242.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
735
- omlish-0.0.0.dev242.dist-info/RECORD,,
732
+ omlish-0.0.0.dev244.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
733
+ omlish-0.0.0.dev244.dist-info/METADATA,sha256=7T41OvUEW2aOLkXNGyFicYfwqbooZHctPFTfjF0XpnQ,4176
734
+ omlish-0.0.0.dev244.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
735
+ omlish-0.0.0.dev244.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
736
+ omlish-0.0.0.dev244.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
737
+ omlish-0.0.0.dev244.dist-info/RECORD,,