omlish 0.0.0.dev240__py3-none-any.whl → 0.0.0.dev242__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.dev240'
2
- __revision__ = '971f90a65c71e29b2ef80c9a63301bac68a526e8'
1
+ __version__ = '0.0.0.dev242'
2
+ __revision__ = '0866b8cd6f11d42061b0949c01a15583a1a6b39d'
3
3
 
4
4
 
5
5
  #
@@ -1,7 +1,12 @@
1
1
  # ruff: noqa: I001
2
2
  from .asyncio import ( # noqa
3
+ asyncio_ensure_task as ensure_task,
4
+
3
5
  asyncio_once as once,
4
- asyncio_wait_concurrent as wait_concurrent,
6
+
5
7
  drain_asyncio_tasks as drain_tasks,
6
8
  draining_asyncio_tasks as draining_tasks,
9
+
10
+ asyncio_wait_concurrent as wait_concurrent,
11
+ asyncio_wait_maybe_concurrent as wait_maybe_concurrent,
7
12
  )
@@ -1,5 +1,9 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  # @omlish-lite
3
+ """
4
+ TODO:
5
+ - split module
6
+ """
3
7
  import asyncio
4
8
  import contextlib
5
9
  import functools
@@ -11,19 +15,37 @@ T = ta.TypeVar('T')
11
15
  CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
12
16
 
13
17
 
18
+ ##
19
+
20
+
21
+ def asyncio_ensure_task(obj: ta.Awaitable) -> asyncio.Task:
22
+ if isinstance(obj, asyncio.Task):
23
+ return obj
24
+ elif isinstance(obj, ta.Coroutine):
25
+ return asyncio.create_task(obj)
26
+ else:
27
+ raise TypeError(obj)
28
+
29
+
30
+ ##
31
+
32
+
14
33
  def asyncio_once(fn: CallableT) -> CallableT:
15
- future = None
34
+ task = None
16
35
 
17
36
  @functools.wraps(fn)
18
37
  async def inner(*args, **kwargs):
19
- nonlocal future
20
- if not future:
21
- future = asyncio.create_task(fn(*args, **kwargs))
22
- return await future
38
+ nonlocal task
39
+ if not task:
40
+ task = asyncio.create_task(fn(*args, **kwargs))
41
+ return await task
23
42
 
24
43
  return ta.cast(CallableT, inner)
25
44
 
26
45
 
46
+ ##
47
+
48
+
27
49
  def drain_asyncio_tasks(loop=None):
28
50
  if loop is None:
29
51
  loop = asyncio.get_running_loop()
@@ -42,8 +64,11 @@ def draining_asyncio_tasks() -> ta.Iterator[None]:
42
64
  drain_asyncio_tasks(loop) # noqa
43
65
 
44
66
 
67
+ ##
68
+
69
+
45
70
  async def asyncio_wait_concurrent(
46
- coros: ta.Iterable[ta.Awaitable[T]],
71
+ awaitables: ta.Iterable[ta.Awaitable[T]],
47
72
  concurrency: ta.Union[int, asyncio.Semaphore],
48
73
  *,
49
74
  return_when: ta.Any = asyncio.FIRST_EXCEPTION,
@@ -55,18 +80,30 @@ async def asyncio_wait_concurrent(
55
80
  else:
56
81
  raise TypeError(concurrency)
57
82
 
58
- async def limited_task(coro):
83
+ async def limited_task(a):
59
84
  async with semaphore:
60
- return await coro
85
+ return await a
86
+
87
+ futs = [asyncio.create_task(limited_task(a)) for a in awaitables]
88
+ done, pending = await asyncio.wait(futs, return_when=return_when)
61
89
 
62
- tasks = [asyncio.create_task(limited_task(coro)) for coro in coros]
63
- done, pending = await asyncio.wait(tasks, return_when=return_when)
90
+ for fut in pending:
91
+ fut.cancel()
64
92
 
65
- for task in pending:
66
- task.cancel()
93
+ for fut in done:
94
+ if fut.exception():
95
+ raise fut.exception() # type: ignore
67
96
 
68
- for task in done:
69
- if task.exception():
70
- raise task.exception() # type: ignore
97
+ return [fut.result() for fut in done]
71
98
 
72
- return [task.result() for task in done]
99
+
100
+ async def asyncio_wait_maybe_concurrent(
101
+ awaitables: ta.Iterable[ta.Awaitable[T]],
102
+ concurrency: ta.Union[int, asyncio.Semaphore, None],
103
+ ) -> ta.List[T]:
104
+ # Note: Only supports return_when=asyncio.FIRST_EXCEPTION
105
+ if concurrency is None:
106
+ return [await a for a in awaitables]
107
+
108
+ else:
109
+ return await asyncio_wait_concurrent(awaitables, concurrency)
omlish/docker/all.py CHANGED
@@ -27,5 +27,5 @@ from .hub import ( # noqa
27
27
  )
28
28
 
29
29
  from .timebomb import ( # noqa
30
- timebomb_payload,
30
+ docker_timebomb_payload as timebomb_payload,
31
31
  )
omlish/docker/ns1.py ADDED
@@ -0,0 +1,98 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import dataclasses as dc
4
+ import re
5
+ import typing as ta
6
+
7
+ from ..lite.check import check
8
+ from ..subprocesses.run import SubprocessRun
9
+ from ..subprocesses.run import SubprocessRunnable
10
+ from ..subprocesses.run import SubprocessRunOutput
11
+
12
+
13
+ ##
14
+
15
+
16
+ DEFAULT_DOCKER_NS1_RUN_IMAGE: str = 'debian'
17
+
18
+
19
+ def build_docker_ns1_run_args(
20
+ *cmd: str,
21
+ image: ta.Optional[str] = None,
22
+ nsenter: str = 'nsenter',
23
+ ) -> ta.List[str]:
24
+ """
25
+ - https://gist.github.com/BretFisher/5e1a0c7bcca4c735e716abf62afad389
26
+ - https://github.com/justincormack/nsenter1/blob/8d3ba504b2c14d73c70cf34f1ec6943c093f1b02/nsenter1.c
27
+
28
+ alt:
29
+ - nc -U ~/Library/Containers/com.docker.docker/Data/debug-shell.sock
30
+ """
31
+
32
+ return [
33
+ '--privileged',
34
+ '--pid=host',
35
+ (image if image is not None else DEFAULT_DOCKER_NS1_RUN_IMAGE),
36
+
37
+ nsenter,
38
+ '-t', '1',
39
+
40
+ '-m', # mount
41
+ '-u', # uts
42
+ '-i', # ipc
43
+ '-n', # net
44
+ '-p', # pid
45
+ '-C', # cgroup
46
+ # '-U', # user
47
+ '-T', # time
48
+
49
+ *cmd,
50
+ ]
51
+
52
+
53
+ def build_docker_ns1_run_cmd(
54
+ *cmd: str,
55
+ exe: str = 'docker',
56
+ run_args: ta.Optional[ta.Sequence[str]] = None,
57
+ **kwargs: ta.Any,
58
+ ) -> ta.List[str]:
59
+ if run_args is not None:
60
+ check.not_isinstance(run_args, str)
61
+
62
+ return [
63
+ exe,
64
+ 'run',
65
+ '--rm',
66
+ *(run_args if run_args is not None else []),
67
+ '-i',
68
+ *build_docker_ns1_run_args(*cmd, **kwargs),
69
+ ]
70
+
71
+
72
+ ##
73
+
74
+
75
+ @dc.dataclass(frozen=True)
76
+ class DockerNs1ListUsedTcpPortsCommand(SubprocessRunnable[ta.List[int]]):
77
+ kwargs: ta.Optional[ta.Mapping[str, str]] = None
78
+
79
+ def make_run(self) -> SubprocessRun:
80
+ return SubprocessRun.of(
81
+ *build_docker_ns1_run_cmd(
82
+ 'netstat', '-tan',
83
+ ),
84
+ stdout='pipe',
85
+ stderr='devnull',
86
+ check=True,
87
+ )
88
+
89
+ _NETSTAT_LINE_PAT: ta.ClassVar[re.Pattern] = re.compile(r'\d{1,3}(.(\d{1,3})){3}:(?P<port>\d+)')
90
+
91
+ def handle_run_output(self, output: SubprocessRunOutput) -> ta.List[int]:
92
+ lines = [s for l in check.not_none(output.stdout).decode().splitlines() if (s := l.strip())]
93
+ return [
94
+ int(m.groupdict()['port'])
95
+ for l in lines
96
+ if len(ps := l.split(maxsplit=4)) > 3
97
+ if (m := self._NETSTAT_LINE_PAT.fullmatch(ps[3])) is not None
98
+ ]
@@ -1,13 +1,33 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  # @omlish-lite
3
+ """
4
+ TODO:
5
+ - docstring
6
+ - timebomb
7
+ - auto-discover available ports
8
+ """
3
9
  import dataclasses as dc
4
10
  import os
5
11
  import typing as ta
6
12
 
7
13
 
14
+ ##
15
+
16
+
8
17
  @dc.dataclass(frozen=True)
9
18
  class DockerPortRelay:
10
- docker_port: int
19
+ """
20
+ Uses roughly the following command to forward connections from inside docker-for-mac's vm to the mac host:
21
+
22
+ docker run --rm -i -p 5001:5000 alpine/socat -d -d TCP-LISTEN:5000,fork,reuseaddr TCP:host.docker.internal:5021
23
+
24
+ This allows requests made by the docker daemon running inside the vm to `host.docker.internal:5001` to be forwarded
25
+ to the mac host on port 5021. The reason for this is to be able to use a docker registry running locally directly on
26
+ the host mac - specifically to be able to do so with ssl certificate checking disabled (which docker will only do on
27
+ localhost, which on a mac in the vm isn't actually the mac host - hence the necessity of the relay).
28
+ """
29
+
30
+ docker_port: int # port
11
31
  host_port: int
12
32
 
13
33
  name: ta.Optional[str] = None
@@ -30,7 +50,7 @@ class DockerPortRelay:
30
50
 
31
51
  def run_args(self) -> ta.List[str]:
32
52
  if (name := self.name) is None:
33
- name = f'docker_port_relay-{os.getpid()}'
53
+ name = f'docker_port_relay-{os.getpid()}-{self.docker_port}-{self.intermediate_port}-{self.host_port}'
34
54
 
35
55
  return [
36
56
  '--name', name,
omlish/docker/timebomb.py CHANGED
@@ -1,10 +1,14 @@
1
+ # @omlish-lite
1
2
  import shlex
2
3
 
3
4
 
4
- _DEFAULT_TIMEBOMB_NAME = '-'.join([*__name__.split('.'), 'timebomb'])
5
+ ##
5
6
 
6
7
 
7
- def timebomb_payload(delay_s: float, name: str = _DEFAULT_TIMEBOMB_NAME) -> str:
8
+ _DEFAULT_DOCKER_TIMEBOMB_NAME = 'omlish-timebomb'
9
+
10
+
11
+ def docker_timebomb_payload(delay_s: float, name: str = _DEFAULT_DOCKER_TIMEBOMB_NAME) -> str:
8
12
  return (
9
13
  '('
10
14
  f'echo {shlex.quote(name)} && '
@@ -1,5 +1,9 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  # @omlish-lite
3
+ """
4
+ TODO:
5
+ - logging
6
+ """
3
7
  import concurrent.futures as cf
4
8
  import contextlib
5
9
  import functools
@@ -15,6 +19,7 @@ from ...sockets.server.handlers import SocketServerHandler
15
19
  from ...sockets.server.handlers import SocketWrappingSocketServerHandler
16
20
  from ...sockets.server.handlers import StandardSocketServerHandler
17
21
  from ...sockets.server.server import SocketServer
22
+ from ...sockets.server.ssl import SslErrorHandlingSocketServerHandler
18
23
  from ...sockets.server.threading import ThreadingSocketServerHandler
19
24
  from ..handlers import HttpHandler
20
25
  from ..parsing import HttpRequestParser
@@ -35,6 +40,7 @@ def make_simple_http_server(
35
40
  *,
36
41
  server_version: HttpProtocolVersion = HttpProtocolVersions.HTTP_1_1,
37
42
  ssl_context: ta.Optional['ssl.SSLContext'] = None,
43
+ ignore_ssl_errors: bool = False,
38
44
  executor: ta.Optional[cf.Executor] = None,
39
45
  use_threads: bool = False,
40
46
  ) -> ta.Iterator[SocketServer]:
@@ -72,6 +78,11 @@ def make_simple_http_server(
72
78
  )),
73
79
  )
74
80
 
81
+ if ignore_ssl_errors:
82
+ server_handler = SslErrorHandlingSocketServerHandler(
83
+ server_handler,
84
+ )
85
+
75
86
  #
76
87
 
77
88
  server_handler = StandardSocketServerHandler(
omlish/lite/inject.py CHANGED
@@ -510,14 +510,6 @@ _INJECTION_INSPECTION_CACHE: ta.MutableMapping[ta.Any, _InjectionInspection] = w
510
510
 
511
511
  def _do_injection_inspect(obj: ta.Any) -> _InjectionInspection:
512
512
  tgt = obj
513
- if isinstance(tgt, type) and tgt.__init__ is not object.__init__: # type: ignore[misc]
514
- # Python 3.8's inspect.signature can't handle subclasses overriding __new__, always generating *args/**kwargs.
515
- # - https://bugs.python.org/issue40897
516
- # - https://github.com/python/cpython/commit/df7c62980d15acd3125dfbd81546dad359f7add7
517
- tgt = tgt.__init__ # type: ignore[misc]
518
- has_generic_base = True
519
- else:
520
- has_generic_base = False
521
513
 
522
514
  # inspect.signature(eval_str=True) was added in 3.10 and we have to support 3.8, so we have to get_type_hints to
523
515
  # eval str annotations *in addition to* getting the signature for parameter information.
@@ -525,23 +517,40 @@ def _do_injection_inspect(obj: ta.Any) -> _InjectionInspection:
525
517
  has_partial = False
526
518
  while True:
527
519
  if isinstance(uw, functools.partial):
528
- has_partial = True
529
520
  uw = uw.func
521
+ has_partial = True
530
522
  else:
531
523
  if (uw2 := inspect.unwrap(uw)) is uw:
532
524
  break
533
525
  uw = uw2
534
526
 
535
- if has_generic_base and has_partial:
527
+ has_args_offset = False
528
+
529
+ if isinstance(tgt, type) and tgt.__new__ is not object.__new__:
530
+ # Python 3.8's inspect.signature can't handle subclasses overriding __new__, always generating *args/**kwargs.
531
+ # - https://bugs.python.org/issue40897
532
+ # - https://github.com/python/cpython/commit/df7c62980d15acd3125dfbd81546dad359f7add7
533
+ tgt = tgt.__init__ # type: ignore[misc]
534
+ has_args_offset = True
535
+
536
+ if tgt in (object.__init__, object.__new__):
537
+ # inspect strips self for types but not the underlying methods.
538
+ def dummy(self):
539
+ pass
540
+ tgt = dummy
541
+ has_args_offset = True
542
+
543
+ if has_partial and has_args_offset:
544
+ # TODO: unwrap partials masking parameters like modern python
536
545
  raise InjectorError(
537
- 'Injector inspection does not currently support both a typing.Generic base and a functools.partial: '
546
+ 'Injector inspection does not currently support both an args offset and a functools.partial: '
538
547
  f'{obj}',
539
548
  )
540
549
 
541
550
  return _InjectionInspection(
542
551
  inspect.signature(tgt),
543
552
  ta.get_type_hints(uw),
544
- 1 if has_generic_base else 0,
553
+ 1 if has_args_offset else 0,
545
554
  )
546
555
 
547
556
 
omlish/marshal/base.py CHANGED
@@ -13,6 +13,7 @@ See:
13
13
  - https://github.com/python-attrs/cattrs
14
14
  - https://github.com/jcrist/msgspec
15
15
  - https://github.com/Fatal1ty/mashumaro
16
+ - https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#custom-serializers
16
17
 
17
18
  cattrs:
18
19
  *
omlish/metadata.py CHANGED
@@ -8,6 +8,7 @@ TODO:
8
8
  - merge mro?
9
9
  - are these better left up to callers? too usecase-specific to favor either way?
10
10
  """
11
+ import threading
11
12
  import types
12
13
  import typing as ta
13
14
 
@@ -60,6 +61,8 @@ def _unwrap_object_metadata_target(obj: ta.Any) -> ta.Any:
60
61
  ##
61
62
 
62
63
 
64
+ _OBJECT_METADATA_LOCK = threading.RLock()
65
+
63
66
  _OBJECT_METADATA_ATTR = '__' + __name__.replace('.', '_') + '__metadata__'
64
67
 
65
68
 
@@ -71,15 +74,14 @@ def append_object_metadata(obj: T, *mds: ObjectMetadata) -> T:
71
74
  dct = tgt.__dict__
72
75
 
73
76
  if isinstance(dct, types.MappingProxyType):
74
- for _ in range(2):
75
- try:
76
- lst = dct[_OBJECT_METADATA_ATTR]
77
- except KeyError:
78
- setattr(tgt, _OBJECT_METADATA_ATTR, [])
79
- else:
80
- break
81
- else:
82
- raise RuntimeError
77
+ try:
78
+ lst = dct[_OBJECT_METADATA_ATTR]
79
+ except KeyError:
80
+ with _OBJECT_METADATA_LOCK:
81
+ try:
82
+ lst = dct[_OBJECT_METADATA_ATTR]
83
+ except KeyError:
84
+ setattr(tgt, _OBJECT_METADATA_ATTR, lst := [])
83
85
 
84
86
  else:
85
87
  lst = dct.setdefault(_OBJECT_METADATA_ATTR, [])
omlish/os/mangle.py CHANGED
@@ -1,5 +1,9 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  # @omlish-lite
3
+ """
4
+ TODO:
5
+ - use ..text.mangle
6
+ """
3
7
 
4
8
 
5
9
  #
omlish/sockets/bind.py CHANGED
@@ -175,11 +175,11 @@ class SocketBinder(abc.ABC, ta.Generic[SocketBinderConfigT]):
175
175
  if hasattr(self, '_socket'):
176
176
  raise self.AlreadyBoundError
177
177
 
178
- socket = socket_.socket(self.address_family, socket_.SOCK_STREAM)
179
- self._socket = socket
178
+ sock = socket_.socket(self.address_family, socket_.SOCK_STREAM)
179
+ self._socket = sock
180
180
 
181
181
  if self._config.allow_reuse_address and hasattr(socket_, 'SO_REUSEADDR'):
182
- socket.setsockopt(socket_.SOL_SOCKET, socket_.SO_REUSEADDR, 1)
182
+ sock.setsockopt(socket_.SOL_SOCKET, socket_.SO_REUSEADDR, 1)
183
183
 
184
184
  # Since Linux 6.12.9, SO_REUSEPORT is not allowed on other address families than AF_INET/AF_INET6.
185
185
  if (
@@ -187,13 +187,13 @@ class SocketBinder(abc.ABC, ta.Generic[SocketBinderConfigT]):
187
187
  self.address_family in (socket_.AF_INET, socket_.AF_INET6)
188
188
  ):
189
189
  try:
190
- socket.setsockopt(socket_.SOL_SOCKET, socket_.SO_REUSEPORT, 1)
190
+ sock.setsockopt(socket_.SOL_SOCKET, socket_.SO_REUSEPORT, 1)
191
191
  except OSError as err:
192
192
  if err.errno not in (errno.ENOPROTOOPT, errno.EINVAL):
193
193
  raise
194
194
 
195
- if self._config.set_inheritable and hasattr(socket, 'set_inheritable'):
196
- socket.set_inheritable(True)
195
+ if self._config.set_inheritable and hasattr(sock, 'set_inheritable'):
196
+ sock.set_inheritable(True)
197
197
 
198
198
  def _pre_bind(self) -> None:
199
199
  pass
@@ -224,7 +224,7 @@ class SocketBinder(abc.ABC, ta.Generic[SocketBinderConfigT]):
224
224
  self.socket.listen(self._config.listen_backlog)
225
225
 
226
226
  @abc.abstractmethod
227
- def accept(self, socket: ta.Optional[socket_.socket] = None) -> SocketAndAddress:
227
+ def accept(self, sock: ta.Optional[socket_.socket] = None) -> SocketAndAddress:
228
228
  raise NotImplementedError
229
229
 
230
230
 
@@ -270,11 +270,11 @@ class TcpSocketBinder(SocketBinder):
270
270
 
271
271
  #
272
272
 
273
- def accept(self, socket: ta.Optional[socket_.socket] = None) -> SocketAndAddress:
274
- if socket is None:
275
- socket = self.socket
273
+ def accept(self, sock: ta.Optional[socket_.socket] = None) -> SocketAndAddress:
274
+ if sock is None:
275
+ sock = self.socket
276
276
 
277
- conn, client_address = socket.accept()
277
+ conn, client_address = sock.accept()
278
278
  return SocketAndAddress(conn, client_address)
279
279
 
280
280
 
@@ -0,0 +1,62 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import contextlib
4
+ import socket
5
+ import typing as ta
6
+
7
+ from ..lite.check import check
8
+ from ..lite.timeouts import Timeout
9
+ from ..lite.timeouts import TimeoutLike
10
+
11
+
12
+ ##
13
+
14
+
15
+ DEFAULT_AVAILABLE_PORT_HOST: str = '127.0.0.1'
16
+
17
+
18
+ @contextlib.contextmanager
19
+ def get_available_port_context(host: ta.Optional[str] = None) -> ta.Iterator[int]:
20
+ if host is None:
21
+ host = DEFAULT_AVAILABLE_PORT_HOST
22
+
23
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
24
+ sock.bind((host, 0))
25
+ sock.listen(1)
26
+ port = sock.getsockname()[1]
27
+ yield port
28
+
29
+
30
+ def get_available_port(host: ta.Optional[str] = None) -> int:
31
+ with get_available_port_context(host) as port:
32
+ pass
33
+ return port
34
+
35
+
36
+ def get_available_ports(
37
+ n: int,
38
+ *,
39
+ host: ta.Optional[str] = None,
40
+ exclude: ta.Optional[ta.Iterable[int]] = None,
41
+ timeout: ta.Optional[TimeoutLike] = None,
42
+ ) -> ta.List[int]:
43
+ exclude = set(exclude or [])
44
+
45
+ seen: ta.Set[int] = set()
46
+ ret: ta.List[int] = []
47
+
48
+ timeout = Timeout.of(timeout)
49
+
50
+ with contextlib.ExitStack() as es:
51
+ while len(ret) < n:
52
+ timeout()
53
+
54
+ cur = es.enter_context(get_available_port_context(host))
55
+
56
+ check.not_in(cur, seen)
57
+ seen.add(cur)
58
+
59
+ if cur not in exclude:
60
+ ret.append(cur)
61
+
62
+ return ret
@@ -13,7 +13,7 @@ from ..io import SocketIoPair
13
13
  from ..io import close_socket_immediately
14
14
 
15
15
 
16
- SocketServerHandler = ta.Callable[[SocketAndAddress], None] # ta.TypeAlias
16
+ SocketServerHandler = ta.Callable[['SocketAndAddress'], None] # ta.TypeAlias
17
17
 
18
18
 
19
19
  ##
@@ -0,0 +1,39 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import dataclasses as dc
4
+ import logging
5
+ import typing as ta
6
+
7
+ from ..addresses import SocketAndAddress
8
+ from ..io import close_socket_immediately
9
+ from .handlers import SocketServerHandler
10
+ from .handlers import SocketServerHandler_
11
+
12
+
13
+ ##
14
+
15
+
16
+ @dc.dataclass(frozen=True)
17
+ class SslErrorHandlingSocketServerHandler(SocketServerHandler_):
18
+ handler: SocketServerHandler
19
+
20
+ log: ta.Optional[logging.Logger] = None
21
+
22
+ #
23
+
24
+ _error_cls: ta.ClassVar[ta.Optional[ta.Type[BaseException]]] = None
25
+
26
+ @classmethod
27
+ def _get_error_cls(cls) -> ta.Type[BaseException]:
28
+ if (error_cls := cls._error_cls) is None:
29
+ import ssl
30
+ error_cls = cls._error_cls = ssl.SSLError
31
+ return error_cls
32
+
33
+ def __call__(self, conn: SocketAndAddress) -> None:
34
+ try:
35
+ self.handler(conn)
36
+ except self._get_error_cls(): # noqa
37
+ if (log := self.log) is not None:
38
+ log.exception('SSL Error in connection %r', conn)
39
+ close_socket_immediately(conn.socket)
omlish/sql/abc.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """
2
2
  https://peps.python.org/pep-0249/
3
3
  """
4
+ import enum
4
5
  import typing as ta
5
6
 
6
7
 
@@ -83,3 +84,88 @@ class DbapiCursor(ta.Protocol):
83
84
  def setinputsizes(self, sizes: ta.Sequence[DbapiTypeCode | int | None]) -> object: ...
84
85
 
85
86
  def setoutputsize(self, size: int, column: int = ...) -> object: ...
87
+
88
+
89
+ class DbapiThreadSafety(enum.IntEnum):
90
+ NONE = 0
91
+ MODULE = 1
92
+ CONNECTION = 2
93
+ CURSOR = 3
94
+
95
+
96
+ class DbapiModule(ta.Protocol):
97
+ def connect(self, *args: ta.Any, **kwargs: ta.Any) -> DbapiConnection: ...
98
+
99
+ #
100
+
101
+ @property
102
+ def apilevel(self) -> str: ...
103
+
104
+ @property
105
+ def threadsafety(self) -> int: ...
106
+
107
+ @property
108
+ def paramstyle(self) -> str: ...
109
+
110
+ #
111
+
112
+ @property
113
+ def Warning(self) -> type[Exception]: ... # noqa
114
+
115
+ @property
116
+ def Error(self) -> type[Exception]: ... # noqa
117
+
118
+ @property
119
+ def InterfaceError(self) -> type[Exception]: ... # noqa
120
+
121
+ @property
122
+ def DatabaseError(self) -> type[Exception]: ... # noqa
123
+
124
+ @property
125
+ def DataError(self) -> type[Exception]: ... # noqa
126
+
127
+ @property
128
+ def OperationalError(self) -> type[Exception]: ... # noqa
129
+
130
+ @property
131
+ def IntegrityError(self) -> type[Exception]: ... # noqa
132
+
133
+ @property
134
+ def InternalError(self) -> type[Exception]: ... # noqa
135
+
136
+ @property
137
+ def ProgrammingError(self) -> type[Exception]: ... # noqa
138
+
139
+ @property
140
+ def NotSupportedError(self) -> type[Exception]: ... # noqa
141
+
142
+ #
143
+
144
+ def Date(self, year: ta.Any, month: ta.Any, day: ta.Any) -> ta.Any: ... # noqa
145
+
146
+ def Time(self, hour: ta.Any, minute: ta.Any, second: ta.Any) -> ta.Any: ... # noqa
147
+
148
+ def Timestamp(self, year, month, day, hour, minute, second) -> ta.Any: ... # noqa
149
+
150
+ def DateFromTicks(self, ticks: ta.Any) -> ta.Any: ... # noqa
151
+
152
+ def TimeFromTicks(self, ticks: ta.Any) -> ta.Any: ... # noqa
153
+
154
+ def TimestampFromTicks(self, ticks: ta.Any) -> ta.Any: ... # noqa
155
+
156
+ def Binary(self, string: ta.Any) -> ta.Any: ... # noqa
157
+
158
+ @property
159
+ def STRING(self) -> type: ... # noqa
160
+
161
+ @property
162
+ def BINARY(self) -> type: ... # noqa
163
+
164
+ @property
165
+ def NUMBER(self) -> type: ... # noqa
166
+
167
+ @property
168
+ def DATETIME(self) -> type: ... # noqa
169
+
170
+ @property
171
+ def ROWID(self) -> type: ... # noqa
omlish/sql/api/base.py CHANGED
@@ -20,7 +20,7 @@ class Closer(lang.Abstract):
20
20
  pass
21
21
 
22
22
 
23
- class SelfCloser(Closer):
23
+ class ContextCloser(Closer):
24
24
  def __enter__(self) -> ta.Self:
25
25
  return self
26
26
 
@@ -31,7 +31,7 @@ class SelfCloser(Closer):
31
31
  ##
32
32
 
33
33
 
34
- class Querier(SelfCloser, lang.Abstract):
34
+ class Querier(ContextCloser, lang.Abstract):
35
35
  @property
36
36
  @abc.abstractmethod
37
37
  def adapter(self) -> 'Adapter':
@@ -45,7 +45,7 @@ class Querier(SelfCloser, lang.Abstract):
45
45
  ##
46
46
 
47
47
 
48
- class Rows(SelfCloser, lang.Abstract):
48
+ class Rows(ContextCloser, lang.Abstract):
49
49
  @property
50
50
  @abc.abstractmethod
51
51
  def columns(self) -> Columns:
omlish/sql/dbapi.py CHANGED
@@ -5,12 +5,6 @@ https://peps.python.org/pep-0249/
5
5
 
6
6
  apilevel = '2.0'
7
7
 
8
- threadsafety:
9
- 0 - Threads may not share the module.
10
- 1 - Threads may share the module, but not connections.
11
- 2 - Threads may share the module and connections.
12
- 3 - Threads may share the module, connections and cursors.
13
-
14
8
  paramstyle:
15
9
  qmark - Question mark style, e.g. ...WHERE name=?
16
10
  numeric - Numeric, positional style, e.g. ...WHERE name=:1
@@ -20,6 +20,10 @@ def if_python_version_less_than(num: ta.Sequence[int]):
20
20
  return pytest.mark.skipif(sys.version_info < tuple(num), reason=f'python version {tuple(sys.version_info)} < {tuple(num)}') # noqa
21
21
 
22
22
 
23
+ def if_not_platform(*platforms: str):
24
+ return pytest.mark.skipif(sys.platform not in platforms, reason=f'requires platform in {platforms}')
25
+
26
+
23
27
  def if_not_single():
24
28
  # FIXME
25
29
  # [resolve_collection_argument(a) for a in session.config.args]
omlish/text/mangle.py ADDED
@@ -0,0 +1,38 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import dataclasses as dc
4
+ import typing as ta
5
+
6
+ from ..lite.cached import cached_nullary
7
+ from ..lite.check import check
8
+
9
+
10
+ @dc.dataclass(frozen=True)
11
+ class StringMangler:
12
+ escape: str
13
+ escaped: ta.Sequence[str]
14
+
15
+ @classmethod
16
+ def of(cls, escape: str, escaped: ta.Iterable[str]) -> 'StringMangler':
17
+ check.arg(len(escape) == 1)
18
+ return StringMangler(escape, sorted(set(escaped) - {escape}))
19
+
20
+ def __post_init__(self) -> None:
21
+ check.non_empty_str(self.escape)
22
+ check.arg(len(self.escape) == 1)
23
+ check.not_in(self.escape, self.escaped)
24
+ check.arg(len(set(self.escaped)) == len(self.escaped))
25
+
26
+ @cached_nullary
27
+ def replacements(self) -> ta.Sequence[ta.Tuple[str, str]]:
28
+ return [(l, self.escape + str(i)) for i, l in enumerate([self.escape, *self.escaped])]
29
+
30
+ def mangle(self, s: str) -> str:
31
+ for l, r in self.replacements():
32
+ s = s.replace(l, r)
33
+ return s
34
+
35
+ def unmangle(self, s: str) -> str:
36
+ for l, r in reversed(self.replacements()):
37
+ s = s.replace(r, l)
38
+ return s
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: omlish
3
- Version: 0.0.0.dev240
3
+ Version: 0.0.0.dev242
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=yWkALlaBMUZQGBFqzomouYR0oDIe1-AI6oxA2oWnPx4,3380
2
+ omlish/__about__.py,sha256=v3KofXu74aLHjSY2AMiIiv61EfsLsoedesWKEUsNxWA,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
@@ -8,7 +8,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
10
  omlish/libc.py,sha256=8K4c66YV1ziJerl5poAAYCmsV-VSsHkT3EHhPW04ufg,15639
11
- omlish/metadata.py,sha256=IJFczp-bkFk_lCYTUt5UmM_MvCbKICjJunEi2MqnC1w,3495
11
+ omlish/metadata.py,sha256=q8UG-fpcXhEF7BZnhVikIE_IHyud9-8YT8iv646zU2s,3589
12
12
  omlish/outcome.py,sha256=ABIE0zjjTyTNtn-ZqQ_9_mUzLiBQ3sDAyqc9JVD8N2k,7852
13
13
  omlish/runmodule.py,sha256=PWvuAaJ9wQQn6bx9ftEL3_d04DyotNn8dR_twm2pgw0,700
14
14
  omlish/shlex.py,sha256=bsW2XUD8GiMTUTDefJejZ5AyqT1pTgWMPD0BMoF02jE,248
@@ -98,8 +98,8 @@ omlish/asyncs/flavors.py,sha256=1mNxGNRVmjUHzA13K5ht8vdJv4CLEmzYTQ6BZXr1520,4866
98
98
  omlish/asyncs/trio.py,sha256=fmZ5b_lKdVV8NQ3euCUutWgnkqTFzSnOjvJSA_jvmrE,367
99
99
  omlish/asyncs/trio_asyncio.py,sha256=oqdOHy0slj9PjVxaDf3gJkq9AAgg7wYZbB469jOftVw,1327
100
100
  omlish/asyncs/asyncio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
101
- omlish/asyncs/asyncio/all.py,sha256=KpTanEtpTlc3rqv5SyiJaJGb_DXBzp_WKLhlRq6lrhY,212
102
- omlish/asyncs/asyncio/asyncio.py,sha256=3BMhVIF-QTjsFRGDtNYlRbBqKPCA3_AwJsjJoIWdM8k,1783
101
+ omlish/asyncs/asyncio/all.py,sha256=EksCHjRQKobiGrxuDW72IaH53WJMs7rdj_ZDBI3iKcg,315
102
+ omlish/asyncs/asyncio/asyncio.py,sha256=mDjYNm1cylUhQ8slWXwdPoXasuWfafjzu78GHt2Mdig,2437
103
103
  omlish/asyncs/asyncio/channels.py,sha256=ZbmsEmdK1fV96liHdcVpRqA2dAMkXJt4Q3rFAg3YOIw,1074
104
104
  omlish/asyncs/asyncio/streams.py,sha256=Uc9PCWSfBqrK2kdVtfjjQU1eaYTWYmZm8QISDj2xiuw,1004
105
105
  omlish/asyncs/asyncio/subprocesses.py,sha256=f30-wi-3n9R5dftm4CMrzp23EEa4GX283bORixm1_UU,6931
@@ -238,15 +238,16 @@ omlish/dispatch/dispatch.py,sha256=p3-RqBf9RKZaNub1FMGHZkETewF43mU_rv4fYD_ERqU,4
238
238
  omlish/dispatch/functions.py,sha256=S8ElsLi6DKxTdtFGigWaF0vAquwy2sK-3f4iRLaYq70,1522
239
239
  omlish/dispatch/methods.py,sha256=Sg134xzG41-__czfnWdzDlXdsxVt7ELOq90N2E6NSzI,5501
240
240
  omlish/docker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
241
- omlish/docker/all.py,sha256=2BqXMehpqj4j6-rVq_iOMcjoWgw4oyC3jWeHd_jPOZo,487
241
+ omlish/docker/all.py,sha256=xXRgJgLGPwAtr7bDMJ_Dp9jTfOwfGvohNhc6LsoELJc,514
242
242
  omlish/docker/cli.py,sha256=gtb9kitVfGnd4cr587NsVVk8D5Ok5y5SAsqD_SwGrSA,2565
243
243
  omlish/docker/compose.py,sha256=4drmnGQzbkOFJ9B6XSg9rnXkJeZz1ETmdcMe1PE790U,1237
244
244
  omlish/docker/consts.py,sha256=wvwfUtEFrEWZKfREWqSMrx8xjjl8P5MNUSF6qzzgJHY,70
245
245
  omlish/docker/detect.py,sha256=Qrdbosm2wJkxKDuy8gaGmbQoxk4Wnp1HJjAEz58NA8Y,614
246
246
  omlish/docker/hub.py,sha256=7LIuJGdA-N1Y1dmo50ynKM1KUTcnQM_5XbtPbdT_QLU,3940
247
247
  omlish/docker/manifests.py,sha256=LR4FpOGNUT3bZQ-gTjB6r_-1C3YiG30QvevZjrsVUQM,7068
248
- omlish/docker/portrelay.py,sha256=QlRoTnQXs5INguR7XOj1xH0gNdL9SUeZm5z45DUctXo,1222
249
- omlish/docker/timebomb.py,sha256=A_pgIDaXKsQwPiikrCTgIJl91gwYqkPGFY6j-Naq07Q,342
248
+ omlish/docker/ns1.py,sha256=75L_9zjXK5qZbvi20Kd0cus-bm3n4oKFYcmEtk1c5-o,2470
249
+ omlish/docker/ports.py,sha256=ov4Lq5JweAThD3qwnjaJbONcHAkRhEx96-NU7ftMcK0,2083
250
+ omlish/docker/timebomb.py,sha256=EnFt8pJeXkowF_F5NXnN0ogDmxUmVymV4h1CYPwyJr4,356
250
251
  omlish/formats/__init__.py,sha256=T0AG1gFnqQ5JiHN0UPQjQ-7g5tnxMIG-mgOvMYExYAM,21
251
252
  omlish/formats/cbor.py,sha256=o_Hbe4kthO9CeXK-FySrw0dHVlrdyTo2Y8PpGRDfZ3c,514
252
253
  omlish/formats/cloudpickle.py,sha256=16si4yUp_pAdDWGECAWf6HLA2PwSANGGgDoMLGZUem8,579
@@ -331,7 +332,7 @@ omlish/http/wsgi.py,sha256=czZsVUX-l2YTlMrUjKN49wRoP4rVpS0qpeBn4O5BoMY,948
331
332
  omlish/http/coro/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
332
333
  omlish/http/coro/fdio.py,sha256=bd9K4EYVWbXV3e3npDPXI9DuDAruJiyDmrgFpgNcjzY,4035
333
334
  omlish/http/coro/server.py,sha256=30FTcJG8kuFeThf0HJYpTzMZN-giLTBP7wr5Wl3b9X0,18285
334
- omlish/http/coro/simple.py,sha256=ys4l0F0UofvfZaioYgsEIMW2mG4txf45cXOSoHxfxsk,2873
335
+ omlish/http/coro/simple.py,sha256=_ZKFlfLda9Gatd3bNBNGJpITNQl4tuTAbL3P-Mr5j5w,3152
335
336
  omlish/http/coro/sockets.py,sha256=rtpZZ-XCOfC5tXr4Fmo1HSn-8f5nxfIOlJaPUkQeDyU,1654
336
337
  omlish/inject/__init__.py,sha256=n0RC9UDGsBQQ39cST39-XJqJPq2M0tnnh9yJubW9azo,1891
337
338
  omlish/inject/binder.py,sha256=DAbc8TZi5w8Mna0TUtq0mT4jeDVA7i7SlBtOFrh2swc,4185
@@ -436,7 +437,7 @@ omlish/lite/configs.py,sha256=Ev_19sbII67pTWzInYjYqa9VyTiZBvyjhZqyG8TtufE,908
436
437
  omlish/lite/contextmanagers.py,sha256=ciaMl0D3QDHToM7M28-kwZ-Q48LtwgCxiud3nekgutA,2863
437
438
  omlish/lite/dataclasses.py,sha256=t1G5-xOuvE6o6w9RyqHzLT9wHD0HkqBh5P8HUZWxGzs,1912
438
439
  omlish/lite/imports.py,sha256=o9WWrNrWg0hKeMvaj91giaovED_9VFanN2MyEHBGekY,1346
439
- omlish/lite/inject.py,sha256=qBUftFeXMiRgANYbNS2e7TePMYyFAcuLgsJiLyMTW5o,28769
440
+ omlish/lite/inject.py,sha256=-tTsOqqef-Ix5Tgl2DP_JAsNWJQDFUptERl3lk14Uzs,29007
440
441
  omlish/lite/json.py,sha256=7-02Ny4fq-6YAu5ynvqoijhuYXWpLmfCI19GUeZnb1c,740
441
442
  omlish/lite/logs.py,sha256=CWFG0NKGhqNeEgryF5atN2gkPYbUdTINEw_s1phbINM,51
442
443
  omlish/lite/marshal.py,sha256=UMwSLEM-QvkvnSHmcChJVkIXkGp9WAyMYyZMB-NZefw,18463
@@ -472,7 +473,7 @@ omlish/manifests/load.py,sha256=9mdsS3egmSX9pymO-m-y2Fhs4p6ruOdbsYaKT1-1Hwg,6655
472
473
  omlish/manifests/static.py,sha256=7YwOVh_Ek9_aTrWsWNO8kWS10_j4K7yv3TpXZSHsvDY,501
473
474
  omlish/manifests/types.py,sha256=IOt9dOe0r8okCHSL82ryi3sn4VZ6AT80g_QQR6oZtCE,306
474
475
  omlish/marshal/__init__.py,sha256=00D3S6qwUld1TUWd67hVHuNcrj3c_FAFSkCVXgGWT-s,2607
475
- omlish/marshal/base.py,sha256=tJ4iNuD7cW2GpGMznOhkAf2hugqp2pF2em0FaQcekrk,6740
476
+ omlish/marshal/base.py,sha256=s1wQRPG2Y6kH0qQXoL3d60ldYaVTqLuFs0NdbYXwAGg,6842
476
477
  omlish/marshal/exceptions.py,sha256=jwQWn4LcPnadT2KRI_1JJCOSkwWh0yHnYK9BmSkNN4U,302
477
478
  omlish/marshal/factories.py,sha256=Q926jSVjaQLEmStnHLhm_c_vqEysN1LnDCwAsFLIzXw,2970
478
479
  omlish/marshal/global_.py,sha256=K76wB1-pdg4VWgiqR7wyxRNYr-voJApexYW2nV-R4DM,1127
@@ -527,7 +528,7 @@ omlish/os/files.py,sha256=WJ_42vsZIZukQURN3TTccp-n74ZNhbux_ps3TLbHj18,1106
527
528
  omlish/os/forkhooks.py,sha256=yjodOvs90ClXskv5oBIJbHn0Y7dzajLmZmOpRMKbyxM,5656
528
529
  omlish/os/journald.py,sha256=2nI8Res1poXkbLc31--MPUlzYMESnCcPUkIxDOCjZW0,3903
529
530
  omlish/os/linux.py,sha256=whJ6scwMKSFBdXiVhJW0BCpJV4jOGMr-a_a3Bhwz6Ls,18938
530
- omlish/os/mangle.py,sha256=dRp5uBxPuM6g52GnxuBoJUdUKW3CVVRr2ZwvQPHHxnA,489
531
+ omlish/os/mangle.py,sha256=M0v-SDt4TMnL68I45GekQrUaXkTIILXIlPdqRxKBTKM,524
531
532
  omlish/os/paths.py,sha256=hqPiyg_eYaRoIVPdAeX4oeLEV4Kpln_XsH0tHvbOf8Q,844
532
533
  omlish/os/signals.py,sha256=FtzkovLb58N3vNdfxflUeXWFCqqKzseCjk5kBdWT-ds,267
533
534
  omlish/os/sizes.py,sha256=ohkALLvqSqBX4iR-7DMKJ4pfOCRdZXV8htH4QywUNM0,152
@@ -560,12 +561,14 @@ omlish/secrets/subprocesses.py,sha256=ffjfbgPbEE_Pwb_87vG4yYR2CGZy3I31mHNCo_0JtH
560
561
  omlish/secrets/tempssl.py,sha256=tlwRrbHHvgKJtNAC31I5sDKryya4fagqN6kGt-tV4Qg,1874
561
562
  omlish/sockets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
562
563
  omlish/sockets/addresses.py,sha256=vbVeQBkzI513H4vRv-JS89QtRbr9U8v5zqkm3oODl_s,1869
563
- omlish/sockets/bind.py,sha256=uRHZJvBxWXadd3Qt7deDEJ1rWzjC-lVodRgPA6VMwNU,8025
564
+ omlish/sockets/bind.py,sha256=J1SfFFFnVf3H5nqESDX2NGEY8DmjyIMUXZciZM33zQY,8003
564
565
  omlish/sockets/handlers.py,sha256=Gj6xZoo4vommge8XvkehYw3B7O4aql2P4qzZIIa0p24,462
565
566
  omlish/sockets/io.py,sha256=lfhTkB7NnAIx9kuQhAkwgsEUXY78Mp1_WtYrIQNS_k8,1408
567
+ omlish/sockets/ports.py,sha256=Wm4mRFFz5MdD8KbdaEfT1c4PbJnsuK_iyJlZJE_-8jo,1402
566
568
  omlish/sockets/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
567
- omlish/sockets/server/handlers.py,sha256=y41oftA22gpOoR6MCaOp0_A6T4HgI144Wg9kj0XFWes,3865
569
+ omlish/sockets/server/handlers.py,sha256=PPsb1X5oU9dN8jfztaMGsRiqWTyEANT-1aSLbS6bUVg,3867
568
570
  omlish/sockets/server/server.py,sha256=mZmHPkCRPitous56_7FJdAsDLZag2wDqjj-LaYM8_Fg,4943
571
+ omlish/sockets/server/ssl.py,sha256=VE0GpdA-gYsN2m9_uvfDwWmXtIbRQqJomVdpGJO8o2M,1061
569
572
  omlish/sockets/server/threading.py,sha256=YmW3Ym_p5j_F4SIH9BgRHIObywjq1HS39j9CGWIcMAY,2856
570
573
  omlish/specs/__init__.py,sha256=zZwF8yXTEkSstYtORkDhVLK-_hWU8WOJCuBpognb_NY,118
571
574
  omlish/specs/irc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -629,8 +632,8 @@ omlish/specs/openapi/__init__.py,sha256=zilQhafjvteRDF_TUIRgF293dBC6g-TJChmUb6T9
629
632
  omlish/specs/openapi/marshal.py,sha256=Z-E2Knm04C81N8AA8cibCVSl2ImhSpHZVc7yAhmPx88,2135
630
633
  omlish/specs/openapi/openapi.py,sha256=y4h04jeB7ORJSVrcy7apaBdpwLjIyscv1Ub5SderH2c,12682
631
634
  omlish/sql/__init__.py,sha256=TpZLsEJKJzvJ0eMzuV8hwOJJbkxBCV1RZPUMLAVB6io,173
632
- omlish/sql/abc.py,sha256=K3AmEPVxzvQrrc1AXdlbM9-9LERGq6lko9slx88kB90,2074
633
- omlish/sql/dbapi.py,sha256=5ghJH-HexsmDlYdWlhf00nCGQX2IC98_gxIxMkucOas,3195
635
+ omlish/sql/abc.py,sha256=yrUSO2OSY6rtMyA0_MlrEFEyIKXLJymSPpmJwEjEJuc,4038
636
+ omlish/sql/dbapi.py,sha256=o6umqE7zVFI_ax0TnCKAikhAIPXChYOwVULb_OdcL1c,2974
634
637
  omlish/sql/dbs.py,sha256=65e388987upJpsFX8bNL7uhiYv2sCsmk9Y04V0MXdsI,1873
635
638
  omlish/sql/params.py,sha256=Z4VPet6GhNqD1T_MXSWSHkdy3cpUEhST-OplC4B_fYI,4433
636
639
  omlish/sql/qualifiedname.py,sha256=rlW3gVmyucJbqwcxj_7BfK4X2HoXrMroZT2H45zPgJQ,2264
@@ -641,7 +644,7 @@ omlish/sql/alchemy/exprs.py,sha256=gO4Fj4xEY-PuDgV-N8hBMy55glZz7O-4H7v1LWabfZY,3
641
644
  omlish/sql/alchemy/secrets.py,sha256=WEeaec1ejQcE3Yaa7p5BSP9AMGEzy1lwr7QMSRL0VBw,180
642
645
  omlish/sql/alchemy/sqlean.py,sha256=RbkuOuFIfM4fowwKk8-sQ6Dxk-tTUwxS94nY5Kxt52s,403
643
646
  omlish/sql/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
644
- omlish/sql/api/base.py,sha256=iPvLwI_noMzazgPlYOZb9xzpXfyb_OfX2WKD9nzWpQc,1506
647
+ omlish/sql/api/base.py,sha256=fvCg-bGVPEZOwrob_Kn4bpk_rYdiEKutyPXamCbLrWw,1515
645
648
  omlish/sql/api/columns.py,sha256=UBol4bfwZ1nhcjv2gE1JhUMzRFeqtiCDo2T9CUGYb64,1943
646
649
  omlish/sql/api/dbapi.py,sha256=H3bFoWI-ox81APX3xBbjylNWyMUXh78ICMQjRrlDNxw,2426
647
650
  omlish/sql/api/errors.py,sha256=YtC2gz5DqRTT3uCJniUOufVH1GEnFIc5ElkYLK3BHwM,230
@@ -691,7 +694,7 @@ omlish/testing/__init__.py,sha256=M_BQrcCHkoL-ZvE-UpQ8XxXNYRRawhjUz4rCJnAqM2A,15
691
694
  omlish/testing/testing.py,sha256=TT2wwSzPZ_KhIvKxpM1qc1yHKD-LHDNgGrcr_h8vs7c,2895
692
695
  omlish/testing/pytest/__init__.py,sha256=rOpQYgp7jYjEIMjInzl-a_uIMqmOtVwGzDgJyCDpvxg,206
693
696
  omlish/testing/pytest/helpers.py,sha256=TJpD60mBtLi9FtxX4TThfuXvg5FIRPSiZk1aeRwe-D4,197
694
- omlish/testing/pytest/skip.py,sha256=NxTkAQiS3HKZR3sfFdxOR2LCFwtCveY6Ap-qtexiZbw,839
697
+ omlish/testing/pytest/skip.py,sha256=Mk7iIfXxp6JWl8vJRP9BK9LoirSOLTJ3mLeLL6Ipi9M,984
695
698
  omlish/testing/pytest/inject/__init__.py,sha256=pdRKv1HcDmJ_yArKJbYITPXXZthRSGgBJWqITr0Er38,117
696
699
  omlish/testing/pytest/inject/harness.py,sha256=_Qf7lLcYc_dpauYOE68u_a65jPCFWmQUYv9m_OOdNqs,5724
697
700
  omlish/testing/pytest/plugins/__init__.py,sha256=ys1zXrYrNm7Uo6YOIVJ6Bd3dQo6kv387k7MbTYlqZSI,467
@@ -720,12 +723,13 @@ omlish/text/asdl.py,sha256=AS3irh-sag5pqyH3beJif78PjCbOaFso1NeKq-HXuTs,16867
720
723
  omlish/text/delimit.py,sha256=ubPXcXQmtbOVrUsNh5gH1mDq5H-n1y2R4cPL5_DQf68,4928
721
724
  omlish/text/glyphsplit.py,sha256=kqqjglRdxGo0czYZxOz9Vi8aBmVsCOq8h6lPwRA5xe0,3803
722
725
  omlish/text/indent.py,sha256=YjtJEBYWuk8--b9JU_T6q4yxV85_TR7VEVr5ViRCFwk,1336
726
+ omlish/text/mangle.py,sha256=kfzFLfvepH-chl1P89_mdc5vC4FSqyPA2aVtgzuB8IY,1133
723
727
  omlish/text/minja.py,sha256=jZC-fp3Xuhx48ppqsf2Sf1pHbC0t8XBB7UpUUoOk2Qw,5751
724
728
  omlish/text/parts.py,sha256=JkNZpyR2tv2CNcTaWJJhpQ9E4F0yPR8P_YfDbZfMtwQ,6182
725
729
  omlish/text/random.py,sha256=jNWpqiaKjKyTdMXC-pWAsSC10AAP-cmRRPVhm59ZWLk,194
726
- omlish-0.0.0.dev240.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
727
- omlish-0.0.0.dev240.dist-info/METADATA,sha256=Ok-6z9aF1wMJzIfXr6vec_L1ztsSkFtHnwfMZPOFi54,4176
728
- omlish-0.0.0.dev240.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
729
- omlish-0.0.0.dev240.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
730
- omlish-0.0.0.dev240.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
731
- omlish-0.0.0.dev240.dist-info/RECORD,,
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,,