omlish 0.0.0.dev216__py3-none-any.whl → 0.0.0.dev218__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 +2 -2
- omlish/asyncs/asyncio/all.py +4 -3
- omlish/dataclasses/__init__.py +6 -2
- omlish/dataclasses/utils.py +0 -12
- omlish/docker/oci/__init__.py +0 -0
- omlish/docker/oci/data.py +71 -0
- omlish/docker/oci/media.py +124 -0
- omlish/docker/portrelay.py +49 -0
- omlish/formats/json5/Json5.g4 +0 -3
- omlish/http/coro/server.py +45 -25
- omlish/http/handlers.py +11 -1
- omlish/iterators/tools.py +1 -0
- omlish/lang/imports.py +16 -8
- omlish/lite/dataclasses.py +3 -1
- omlish/logs/all.py +13 -0
- omlish/logs/callers.py +45 -0
- omlish/logs/protocol.py +176 -0
- omlish/marshal/dataclasses.py +26 -0
- omlish/sockets/addresses.py +13 -4
- omlish/sockets/bind.py +332 -0
- omlish/sockets/handlers.py +2 -20
- omlish/sockets/io.py +69 -0
- omlish/sockets/server/__init__.py +0 -0
- omlish/sockets/server/handlers.py +99 -0
- omlish/sockets/server/server.py +144 -0
- omlish/sockets/server/threading.py +123 -0
- omlish/subprocesses.py +65 -3
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/RECORD +33 -22
- omlish/sockets/server.py +0 -66
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/top_level.txt +0 -0
    
        omlish/logs/protocol.py
    ADDED
    
    | @@ -0,0 +1,176 @@ | |
| 1 | 
            +
            # ruff: noqa: UP006 UP007
         | 
| 2 | 
            +
            # @omlish-lite
         | 
| 3 | 
            +
            import abc
         | 
| 4 | 
            +
            import logging
         | 
| 5 | 
            +
            import sys
         | 
| 6 | 
            +
            import typing as ta
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            from .callers import LoggingCaller
         | 
| 9 | 
            +
             | 
| 10 | 
            +
             | 
| 11 | 
            +
            LogLevel = int  # ta.TypeAlias
         | 
| 12 | 
            +
             | 
| 13 | 
            +
             | 
| 14 | 
            +
            ##
         | 
| 15 | 
            +
             | 
| 16 | 
            +
             | 
| 17 | 
            +
            class Logging(ta.Protocol):
         | 
| 18 | 
            +
                def isEnabledFor(self, level: LogLevel) -> bool:  # noqa
         | 
| 19 | 
            +
                    ...
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def getEffectiveLevel(self) -> LogLevel:  # noqa
         | 
| 22 | 
            +
                    ...
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                #
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def debug(self, msg: str, *args, **kwargs) -> None:
         | 
| 27 | 
            +
                    ...
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def info(self, msg: str, *args, **kwargs) -> None:
         | 
| 30 | 
            +
                    ...
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def warning(self, msg: str, *args, **kwargs) -> None:
         | 
| 33 | 
            +
                    ...
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def error(self, msg: str, *args, **kwargs) -> None:
         | 
| 36 | 
            +
                    ...
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def exception(self, msg: str, *args, exc_info=True, **kwargs) -> None:
         | 
| 39 | 
            +
                    ...
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def critical(self, msg: str, *args, **kwargs) -> None:
         | 
| 42 | 
            +
                    ...
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def log(self, level: LogLevel, msg: str, *args, **kwargs) -> None:
         | 
| 45 | 
            +
                    ...
         | 
| 46 | 
            +
             | 
| 47 | 
            +
             | 
| 48 | 
            +
            ##
         | 
| 49 | 
            +
             | 
| 50 | 
            +
             | 
| 51 | 
            +
            class AbstractLogging(abc.ABC):
         | 
| 52 | 
            +
                @ta.final
         | 
| 53 | 
            +
                def isEnabledFor(self, level: LogLevel) -> bool:  # noqa
         | 
| 54 | 
            +
                    return self.is_enabled_for(level)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def is_enabled_for(self, level: LogLevel) -> bool:  # noqa
         | 
| 57 | 
            +
                    return level >= self.getEffectiveLevel()
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                @ta.final
         | 
| 60 | 
            +
                def getEffectiveLevel(self) -> LogLevel:  # noqa
         | 
| 61 | 
            +
                    return self.get_effective_level()
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                @abc.abstractmethod
         | 
| 64 | 
            +
                def get_effective_level(self) -> LogLevel:  # noqa
         | 
| 65 | 
            +
                    raise NotImplementedError
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                #
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def debug(self, msg: str, *args, **kwargs) -> None:
         | 
| 70 | 
            +
                    if self.is_enabled_for(logging.DEBUG):
         | 
| 71 | 
            +
                        self.log(logging.DEBUG, msg, args, **kwargs)
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def info(self, msg: str, *args, **kwargs) -> None:
         | 
| 74 | 
            +
                    if self.is_enabled_for(logging.INFO):
         | 
| 75 | 
            +
                        self.log(logging.INFO, msg, args, **kwargs)
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def warning(self, msg: str, *args, **kwargs) -> None:
         | 
| 78 | 
            +
                    if self.is_enabled_for(logging.WARNING):
         | 
| 79 | 
            +
                        self.log(logging.WARNING, msg, args, **kwargs)
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                def error(self, msg: str, *args, **kwargs) -> None:
         | 
| 82 | 
            +
                    if self.is_enabled_for(logging.ERROR):
         | 
| 83 | 
            +
                        self.log(logging.ERROR, msg, args, **kwargs)
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def exception(self, msg: str, *args, exc_info=True, **kwargs) -> None:
         | 
| 86 | 
            +
                    self.error(msg, *args, exc_info=exc_info, **kwargs)
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def critical(self, msg: str, *args, **kwargs) -> None:
         | 
| 89 | 
            +
                    if self.is_enabled_for(logging.CRITICAL):
         | 
| 90 | 
            +
                        self.log(logging.CRITICAL, msg, args, **kwargs)
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def log(self, level: LogLevel, msg: str, *args, **kwargs) -> None:
         | 
| 93 | 
            +
                    if not isinstance(level, int):
         | 
| 94 | 
            +
                        raise TypeError('Level must be an integer.')
         | 
| 95 | 
            +
                    if self.is_enabled_for(level):
         | 
| 96 | 
            +
                        self._log(level, msg, args, **kwargs)
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                @abc.abstractmethod
         | 
| 99 | 
            +
                def _log(
         | 
| 100 | 
            +
                        self,
         | 
| 101 | 
            +
                        level: int,
         | 
| 102 | 
            +
                        msg: str,
         | 
| 103 | 
            +
                        args: ta.Any,
         | 
| 104 | 
            +
                        *,
         | 
| 105 | 
            +
                        exc_info: ta.Any = None,
         | 
| 106 | 
            +
                        extra: ta.Any = None,
         | 
| 107 | 
            +
                        stack_info: bool = False,
         | 
| 108 | 
            +
                ) -> None:
         | 
| 109 | 
            +
                    raise NotImplementedError
         | 
| 110 | 
            +
             | 
| 111 | 
            +
             | 
| 112 | 
            +
            ##
         | 
| 113 | 
            +
             | 
| 114 | 
            +
             | 
| 115 | 
            +
            class NopLogging(AbstractLogging):
         | 
| 116 | 
            +
                def get_effective_level(self) -> LogLevel:
         | 
| 117 | 
            +
                    return logging.CRITICAL + 1
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def _log(self, *args: ta.Any, **kwargs: ta.Any) -> None:
         | 
| 120 | 
            +
                    pass
         | 
| 121 | 
            +
             | 
| 122 | 
            +
             | 
| 123 | 
            +
            ##
         | 
| 124 | 
            +
             | 
| 125 | 
            +
             | 
| 126 | 
            +
            class StdlibLogging(AbstractLogging):
         | 
| 127 | 
            +
                def __init__(self, underlying: logging.Logger) -> None:
         | 
| 128 | 
            +
                    super().__init__()
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                    if not isinstance(underlying, logging.Logger):
         | 
| 131 | 
            +
                        raise TypeError(underlying)
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                    self._underlying = underlying
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                #
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                def is_enabled_for(self, level: int) -> bool:  # noqa
         | 
| 138 | 
            +
                    return self._underlying.isEnabledFor(level)
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                def get_effective_level(self) -> int:  # noqa
         | 
| 141 | 
            +
                    return self._underlying.getEffectiveLevel()
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                #
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                def _log(
         | 
| 146 | 
            +
                        self,
         | 
| 147 | 
            +
                        level: int,
         | 
| 148 | 
            +
                        msg: str,
         | 
| 149 | 
            +
                        args: ta.Any,
         | 
| 150 | 
            +
                        *,
         | 
| 151 | 
            +
                        exc_info: ta.Any = None,
         | 
| 152 | 
            +
                        extra: ta.Any = None,
         | 
| 153 | 
            +
                        stack_info: bool = False,
         | 
| 154 | 
            +
                ) -> None:
         | 
| 155 | 
            +
                    caller = LoggingCaller.find(stack_info)
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                    if exc_info:
         | 
| 158 | 
            +
                        if isinstance(exc_info, BaseException):
         | 
| 159 | 
            +
                            exc_info = (type(exc_info), exc_info, exc_info.__traceback__)
         | 
| 160 | 
            +
                        elif not isinstance(exc_info, tuple):
         | 
| 161 | 
            +
                            exc_info = sys.exc_info()
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                    record = self._underlying.makeRecord(
         | 
| 164 | 
            +
                        name=self._underlying.name,
         | 
| 165 | 
            +
                        level=level,
         | 
| 166 | 
            +
                        fn=caller.filename,
         | 
| 167 | 
            +
                        lno=caller.lineno,
         | 
| 168 | 
            +
                        msg=msg,
         | 
| 169 | 
            +
                        args=args,
         | 
| 170 | 
            +
                        exc_info=exc_info,
         | 
| 171 | 
            +
                        func=caller.func,
         | 
| 172 | 
            +
                        extra=extra,
         | 
| 173 | 
            +
                        sinfo=caller.sinfo,
         | 
| 174 | 
            +
                    )
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                    self._underlying.handle(record)
         | 
    
        omlish/marshal/dataclasses.py
    CHANGED
    
    | @@ -9,6 +9,7 @@ from .. import collections as col | |
| 9 9 | 
             
            from .. import dataclasses as dc
         | 
| 10 10 | 
             
            from .. import lang
         | 
| 11 11 | 
             
            from .. import reflect as rfl
         | 
| 12 | 
            +
            from ..lite import marshal as lm
         | 
| 12 13 | 
             
            from .base import MarshalContext
         | 
| 13 14 | 
             
            from .base import Marshaler
         | 
| 14 15 | 
             
            from .base import MarshalerFactory
         | 
| @@ -96,6 +97,31 @@ def get_field_infos( | |
| 96 97 | 
             
                                unmarshal_names=col.unique([fmd.name, *(fmd.alts or ())]),
         | 
| 97 98 | 
             
                            )
         | 
| 98 99 |  | 
| 100 | 
            +
                    else:
         | 
| 101 | 
            +
                        try:
         | 
| 102 | 
            +
                            lfk = field.metadata[lm.OBJ_MARSHALER_FIELD_KEY]
         | 
| 103 | 
            +
                        except KeyError:
         | 
| 104 | 
            +
                            pass
         | 
| 105 | 
            +
                        else:
         | 
| 106 | 
            +
                            if lfk is not None:
         | 
| 107 | 
            +
                                check.non_empty_str(lfk)
         | 
| 108 | 
            +
                                has_set_name = True
         | 
| 109 | 
            +
                                fi_kw.update(
         | 
| 110 | 
            +
                                    marshal_name=lfk,
         | 
| 111 | 
            +
                                    unmarshal_names=[lfk],
         | 
| 112 | 
            +
                                )
         | 
| 113 | 
            +
                            else:
         | 
| 114 | 
            +
                                fo_kw.update(
         | 
| 115 | 
            +
                                    no_marshal=True,
         | 
| 116 | 
            +
                                    no_unmarshal=True,
         | 
| 117 | 
            +
                                )
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                        if (lon := field.metadata.get(lm.OBJ_MARSHALER_OMIT_IF_NONE)) is not None:
         | 
| 120 | 
            +
                            if check.isinstance(lon, bool):
         | 
| 121 | 
            +
                                fo_kw.update(
         | 
| 122 | 
            +
                                    omit_if=lang.is_none,
         | 
| 123 | 
            +
                                )
         | 
| 124 | 
            +
             | 
| 99 125 | 
             
                    if fo_kw.get('embed') and not has_set_name:
         | 
| 100 126 | 
             
                        fi_kw.update(
         | 
| 101 127 | 
             
                            marshal_name=fi_kw['marshal_name'] + '_',
         | 
    
        omlish/sockets/addresses.py
    CHANGED
    
    | @@ -2,8 +2,7 @@ | |
| 2 2 | 
             
            # @omlish-lite
         | 
| 3 3 | 
             
            """
         | 
| 4 4 | 
             
            TODO:
         | 
| 5 | 
            -
             -  | 
| 6 | 
            -
              + codification of https://docs.python.org/3/library/socket.html#socket-families
         | 
| 5 | 
            +
             - codification of https://docs.python.org/3/library/socket.html#socket-families
         | 
| 7 6 | 
             
            """
         | 
| 8 7 | 
             
            import dataclasses as dc
         | 
| 9 8 | 
             
            import socket
         | 
| @@ -35,11 +34,16 @@ class SocketAddressInfo: | |
| 35 34 | 
             
                sockaddr: SocketAddress
         | 
| 36 35 |  | 
| 37 36 |  | 
| 37 | 
            +
            class SocketFamilyAndAddress(ta.NamedTuple):
         | 
| 38 | 
            +
                family: socket.AddressFamily
         | 
| 39 | 
            +
                address: SocketAddress
         | 
| 40 | 
            +
             | 
| 41 | 
            +
             | 
| 38 42 | 
             
            def get_best_socket_family(
         | 
| 39 43 | 
             
                    host: ta.Optional[str],
         | 
| 40 44 | 
             
                    port: ta.Union[str, int, None],
         | 
| 41 45 | 
             
                    family: ta.Union[int, socket.AddressFamily] = socket.AddressFamily.AF_UNSPEC,
         | 
| 42 | 
            -
            ) ->  | 
| 46 | 
            +
            ) -> SocketFamilyAndAddress:
         | 
| 43 47 | 
             
                """https://github.com/python/cpython/commit/f289084c83190cc72db4a70c58f007ec62e75247"""
         | 
| 44 48 |  | 
| 45 49 | 
             
                infos = socket.getaddrinfo(
         | 
| @@ -50,4 +54,9 @@ def get_best_socket_family( | |
| 50 54 | 
             
                    flags=socket.AI_PASSIVE,
         | 
| 51 55 | 
             
                )
         | 
| 52 56 | 
             
                ai = SocketAddressInfo(*next(iter(infos)))
         | 
| 53 | 
            -
                return ai.family, ai.sockaddr
         | 
| 57 | 
            +
                return SocketFamilyAndAddress(ai.family, ai.sockaddr)
         | 
| 58 | 
            +
             | 
| 59 | 
            +
             | 
| 60 | 
            +
            class SocketAndAddress(ta.NamedTuple):
         | 
| 61 | 
            +
                socket: socket.socket
         | 
| 62 | 
            +
                address: SocketAddress
         | 
    
        omlish/sockets/bind.py
    ADDED
    
    | @@ -0,0 +1,332 @@ | |
| 1 | 
            +
            # ruff: noqa: UP006 UP007
         | 
| 2 | 
            +
            # @omlish-lite
         | 
| 3 | 
            +
            """
         | 
| 4 | 
            +
            TODO:
         | 
| 5 | 
            +
             - DupSocketBinder
         | 
| 6 | 
            +
            """
         | 
| 7 | 
            +
            import abc
         | 
| 8 | 
            +
            import dataclasses as dc
         | 
| 9 | 
            +
            import errno
         | 
| 10 | 
            +
            import os
         | 
| 11 | 
            +
            import socket as socket_
         | 
| 12 | 
            +
            import stat
         | 
| 13 | 
            +
            import typing as ta
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            from omlish.lite.check import check
         | 
| 16 | 
            +
            from omlish.lite.dataclasses import dataclass_maybe_post_init
         | 
| 17 | 
            +
            from omlish.sockets.addresses import SocketAddress
         | 
| 18 | 
            +
            from omlish.sockets.addresses import SocketAndAddress
         | 
| 19 | 
            +
             | 
| 20 | 
            +
             | 
| 21 | 
            +
            SocketBinderT = ta.TypeVar('SocketBinderT', bound='SocketBinder')
         | 
| 22 | 
            +
            SocketBinderConfigT = ta.TypeVar('SocketBinderConfigT', bound='SocketBinder.Config')
         | 
| 23 | 
            +
             | 
| 24 | 
            +
             | 
| 25 | 
            +
            ##
         | 
| 26 | 
            +
             | 
| 27 | 
            +
             | 
| 28 | 
            +
            class SocketBinder(abc.ABC, ta.Generic[SocketBinderConfigT]):
         | 
| 29 | 
            +
                @dc.dataclass(frozen=True)
         | 
| 30 | 
            +
                class Config:
         | 
| 31 | 
            +
                    listen_backlog: int = 5
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    allow_reuse_address: bool = True
         | 
| 34 | 
            +
                    allow_reuse_port: bool = True
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    set_inheritable: bool = False
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    #
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    @classmethod
         | 
| 41 | 
            +
                    def new(
         | 
| 42 | 
            +
                            cls,
         | 
| 43 | 
            +
                            target: ta.Union[
         | 
| 44 | 
            +
                                int,
         | 
| 45 | 
            +
                                ta.Tuple[str, int],
         | 
| 46 | 
            +
                                str,
         | 
| 47 | 
            +
                            ],
         | 
| 48 | 
            +
                    ) -> 'SocketBinder.Config':
         | 
| 49 | 
            +
                        if isinstance(target, int):
         | 
| 50 | 
            +
                            return TcpSocketBinder.Config(
         | 
| 51 | 
            +
                                port=target,
         | 
| 52 | 
            +
                            )
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                        elif isinstance(target, tuple):
         | 
| 55 | 
            +
                            host, port = target
         | 
| 56 | 
            +
                            return TcpSocketBinder.Config(
         | 
| 57 | 
            +
                                host=host,
         | 
| 58 | 
            +
                                port=port,
         | 
| 59 | 
            +
                            )
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                        elif isinstance(target, str):
         | 
| 62 | 
            +
                            return UnixSocketBinder.Config(
         | 
| 63 | 
            +
                                file=target,
         | 
| 64 | 
            +
                            )
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                        else:
         | 
| 67 | 
            +
                            raise TypeError(target)
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                #
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def __init__(self, config: SocketBinderConfigT) -> None:
         | 
| 72 | 
            +
                    super().__init__()
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    self._config = config
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                #
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                @classmethod
         | 
| 79 | 
            +
                def new(cls, target: ta.Any) -> 'SocketBinder':
         | 
| 80 | 
            +
                    config: SocketBinder.Config
         | 
| 81 | 
            +
                    if isinstance(target, SocketBinder.Config):
         | 
| 82 | 
            +
                        config = target
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    else:
         | 
| 85 | 
            +
                        config = SocketBinder.Config.new(target)
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                    if isinstance(config, TcpSocketBinder.Config):
         | 
| 88 | 
            +
                        return TcpSocketBinder(config)
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                    elif isinstance(config, UnixSocketBinder.Config):
         | 
| 91 | 
            +
                        return UnixSocketBinder(config)
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                    else:
         | 
| 94 | 
            +
                        raise TypeError(config)
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                #
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                class Error(RuntimeError):
         | 
| 99 | 
            +
                    pass
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                class NotBoundError(Error):
         | 
| 102 | 
            +
                    pass
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                class AlreadyBoundError(Error):
         | 
| 105 | 
            +
                    pass
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                #
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                @property
         | 
| 110 | 
            +
                @abc.abstractmethod
         | 
| 111 | 
            +
                def address_family(self) -> int:
         | 
| 112 | 
            +
                    raise NotImplementedError
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                @property
         | 
| 115 | 
            +
                @abc.abstractmethod
         | 
| 116 | 
            +
                def address(self) -> SocketAddress:
         | 
| 117 | 
            +
                    raise NotImplementedError
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                #
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                _socket: socket_.socket
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                @property
         | 
| 124 | 
            +
                def is_bound(self) -> bool:
         | 
| 125 | 
            +
                    return hasattr(self, '_socket')
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                @property
         | 
| 128 | 
            +
                def socket(self) -> socket_.socket:
         | 
| 129 | 
            +
                    try:
         | 
| 130 | 
            +
                        return self._socket
         | 
| 131 | 
            +
                    except AttributeError:
         | 
| 132 | 
            +
                        raise self.NotBoundError from None
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                _name: str
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                @property
         | 
| 137 | 
            +
                def name(self) -> str:
         | 
| 138 | 
            +
                    try:
         | 
| 139 | 
            +
                        return self._name
         | 
| 140 | 
            +
                    except AttributeError:
         | 
| 141 | 
            +
                        raise self.NotBoundError from None
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                _port: ta.Optional[int]
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                @property
         | 
| 146 | 
            +
                def port(self) -> ta.Optional[int]:
         | 
| 147 | 
            +
                    try:
         | 
| 148 | 
            +
                        return self._port
         | 
| 149 | 
            +
                    except AttributeError:
         | 
| 150 | 
            +
                        raise self.NotBoundError from None
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                #
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                def fileno(self) -> int:
         | 
| 155 | 
            +
                    return self.socket.fileno()
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                #
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                def __enter__(self: SocketBinderT) -> SocketBinderT:
         | 
| 160 | 
            +
                    self.bind()
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                    return self
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                def __exit__(self, exc_type, exc_val, exc_tb) -> None:
         | 
| 165 | 
            +
                    self.close()
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                #
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                def _init_socket(self) -> None:
         | 
| 170 | 
            +
                    if hasattr(self, '_socket'):
         | 
| 171 | 
            +
                        raise self.AlreadyBoundError
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                    socket = socket_.socket(self.address_family, socket_.SOCK_STREAM)
         | 
| 174 | 
            +
                    self._socket = socket
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                    if self._config.allow_reuse_address and hasattr(socket_, 'SO_REUSEADDR'):
         | 
| 177 | 
            +
                        socket.setsockopt(socket_.SOL_SOCKET, socket_.SO_REUSEADDR, 1)
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                    # Since Linux 6.12.9, SO_REUSEPORT is not allowed on other address families than AF_INET/AF_INET6.
         | 
| 180 | 
            +
                    if (
         | 
| 181 | 
            +
                            self._config.allow_reuse_port and hasattr(socket_, 'SO_REUSEPORT') and
         | 
| 182 | 
            +
                            self.address_family in (socket_.AF_INET, socket_.AF_INET6)
         | 
| 183 | 
            +
                    ):
         | 
| 184 | 
            +
                        try:
         | 
| 185 | 
            +
                            socket.setsockopt(socket_.SOL_SOCKET, socket_.SO_REUSEPORT, 1)
         | 
| 186 | 
            +
                        except OSError as err:
         | 
| 187 | 
            +
                            if err.errno not in (errno.ENOPROTOOPT, errno.EINVAL):
         | 
| 188 | 
            +
                                raise
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                    if self._config.set_inheritable and hasattr(socket, 'set_inheritable'):
         | 
| 191 | 
            +
                        socket.set_inheritable(True)
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                def _pre_bind(self) -> None:
         | 
| 194 | 
            +
                    pass
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                def _post_bind(self) -> None:
         | 
| 197 | 
            +
                    pass
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                def bind(self) -> None:
         | 
| 200 | 
            +
                    self._init_socket()
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                    self._pre_bind()
         | 
| 203 | 
            +
             | 
| 204 | 
            +
                    self.socket.bind(self.address)
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                    self._post_bind()
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                    check.state(all(hasattr(self, a) for a in ('_socket', '_name', '_port')))
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                #
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                def close(self) -> None:
         | 
| 213 | 
            +
                    if hasattr(self, '_socket'):
         | 
| 214 | 
            +
                        self._socket.close()
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                #
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                def listen(self) -> None:
         | 
| 219 | 
            +
                    self.socket.listen(self._config.listen_backlog)
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                @abc.abstractmethod
         | 
| 222 | 
            +
                def accept(self, socket: ta.Optional[socket_.socket] = None) -> SocketAndAddress:
         | 
| 223 | 
            +
                    raise NotImplementedError
         | 
| 224 | 
            +
             | 
| 225 | 
            +
             | 
| 226 | 
            +
            ##
         | 
| 227 | 
            +
             | 
| 228 | 
            +
             | 
| 229 | 
            +
            class TcpSocketBinder(SocketBinder):
         | 
| 230 | 
            +
                @dc.dataclass(frozen=True)
         | 
| 231 | 
            +
                class Config(SocketBinder.Config):
         | 
| 232 | 
            +
                    DEFAULT_HOST: ta.ClassVar[str] = 'localhost'
         | 
| 233 | 
            +
                    host: str = DEFAULT_HOST
         | 
| 234 | 
            +
             | 
| 235 | 
            +
                    port: int = 0
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                    def __post_init__(self) -> None:
         | 
| 238 | 
            +
                        dataclass_maybe_post_init(super())
         | 
| 239 | 
            +
                        check.non_empty_str(self.host)
         | 
| 240 | 
            +
                        check.isinstance(self.port, int)
         | 
| 241 | 
            +
                        check.arg(self.port > 0)
         | 
| 242 | 
            +
             | 
| 243 | 
            +
                def __init__(self, config: Config) -> None:
         | 
| 244 | 
            +
                    super().__init__(check.isinstance(config, self.Config))
         | 
| 245 | 
            +
             | 
| 246 | 
            +
                    self._address = (config.host, config.port)
         | 
| 247 | 
            +
             | 
| 248 | 
            +
                #
         | 
| 249 | 
            +
             | 
| 250 | 
            +
                address_family = socket_.AF_INET
         | 
| 251 | 
            +
             | 
| 252 | 
            +
                @property
         | 
| 253 | 
            +
                def address(self) -> SocketAddress:
         | 
| 254 | 
            +
                    return self._address
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                #
         | 
| 257 | 
            +
             | 
| 258 | 
            +
                def _post_bind(self) -> None:
         | 
| 259 | 
            +
                    super()._post_bind()
         | 
| 260 | 
            +
             | 
| 261 | 
            +
                    host, port, *_ = self.socket.getsockname()
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                    self._name = socket_.getfqdn(host)
         | 
| 264 | 
            +
                    self._port = port
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                #
         | 
| 267 | 
            +
             | 
| 268 | 
            +
                def accept(self, socket: ta.Optional[socket_.socket] = None) -> SocketAndAddress:
         | 
| 269 | 
            +
                    if socket is None:
         | 
| 270 | 
            +
                        socket = self.socket
         | 
| 271 | 
            +
             | 
| 272 | 
            +
                    conn, client_address = socket.accept()
         | 
| 273 | 
            +
                    return SocketAndAddress(conn, client_address)
         | 
| 274 | 
            +
             | 
| 275 | 
            +
             | 
| 276 | 
            +
            ##
         | 
| 277 | 
            +
             | 
| 278 | 
            +
             | 
| 279 | 
            +
            class UnixSocketBinder(SocketBinder):
         | 
| 280 | 
            +
                @dc.dataclass(frozen=True)
         | 
| 281 | 
            +
                class Config(SocketBinder.Config):
         | 
| 282 | 
            +
                    file: str = ''
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                    unlink: bool = False
         | 
| 285 | 
            +
             | 
| 286 | 
            +
                    def __post_init__(self) -> None:
         | 
| 287 | 
            +
                        dataclass_maybe_post_init(super())
         | 
| 288 | 
            +
                        check.non_empty_str(self.file)
         | 
| 289 | 
            +
             | 
| 290 | 
            +
                def __init__(self, config: Config) -> None:
         | 
| 291 | 
            +
                    super().__init__(check.isinstance(config, self.Config))
         | 
| 292 | 
            +
             | 
| 293 | 
            +
                    self._address = config.file
         | 
| 294 | 
            +
             | 
| 295 | 
            +
                #
         | 
| 296 | 
            +
             | 
| 297 | 
            +
                address_family = socket_.AF_UNIX
         | 
| 298 | 
            +
             | 
| 299 | 
            +
                @property
         | 
| 300 | 
            +
                def address(self) -> SocketAddress:
         | 
| 301 | 
            +
                    return self._address
         | 
| 302 | 
            +
             | 
| 303 | 
            +
                #
         | 
| 304 | 
            +
             | 
| 305 | 
            +
                def _pre_bind(self) -> None:
         | 
| 306 | 
            +
                    super()._pre_bind()
         | 
| 307 | 
            +
             | 
| 308 | 
            +
                    if self._config.unlink:
         | 
| 309 | 
            +
                        try:
         | 
| 310 | 
            +
                            os.unlink(self._config.file)
         | 
| 311 | 
            +
                        except FileNotFoundError:
         | 
| 312 | 
            +
                            pass
         | 
| 313 | 
            +
             | 
| 314 | 
            +
                def _post_bind(self) -> None:
         | 
| 315 | 
            +
                    super()._post_bind()
         | 
| 316 | 
            +
             | 
| 317 | 
            +
                    name = self.socket.getsockname()
         | 
| 318 | 
            +
             | 
| 319 | 
            +
                    os.chmod(name, stat.S_IRWXU | stat.S_IRWXG)  # noqa
         | 
| 320 | 
            +
             | 
| 321 | 
            +
                    self._name = name
         | 
| 322 | 
            +
                    self._port = None
         | 
| 323 | 
            +
             | 
| 324 | 
            +
                #
         | 
| 325 | 
            +
             | 
| 326 | 
            +
                def accept(self, sock: ta.Optional[socket_.socket] = None) -> SocketAndAddress:
         | 
| 327 | 
            +
                    if sock is None:
         | 
| 328 | 
            +
                        sock = self.socket
         | 
| 329 | 
            +
             | 
| 330 | 
            +
                    conn, _ = sock.accept()
         | 
| 331 | 
            +
                    client_address = ('', 0)
         | 
| 332 | 
            +
                    return SocketAndAddress(conn, client_address)
         | 
    
        omlish/sockets/handlers.py
    CHANGED
    
    | @@ -1,30 +1,12 @@ | |
| 1 1 | 
             
            # ruff: noqa: UP006 UP007
         | 
| 2 2 | 
             
            # @omlish-lite
         | 
| 3 | 
            -
            import abc
         | 
| 4 3 | 
             
            import typing as ta
         | 
| 5 4 |  | 
| 6 5 | 
             
            from .addresses import SocketAddress
         | 
| 6 | 
            +
            from .io import SocketIoPair  # noqa
         | 
| 7 7 |  | 
| 8 8 |  | 
| 9 | 
            -
             | 
| 9 | 
            +
            SocketHandler = ta.Callable[[SocketAddress, 'SocketIoPair'], None]  # ta.TypeAlias
         | 
| 10 10 |  | 
| 11 11 |  | 
| 12 12 | 
             
            ##
         | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
            class SocketHandler(abc.ABC):
         | 
| 16 | 
            -
                def __init__(
         | 
| 17 | 
            -
                        self,
         | 
| 18 | 
            -
                        client_address: SocketAddress,
         | 
| 19 | 
            -
                        rfile: ta.BinaryIO,
         | 
| 20 | 
            -
                        wfile: ta.BinaryIO,
         | 
| 21 | 
            -
                ) -> None:
         | 
| 22 | 
            -
                    super().__init__()
         | 
| 23 | 
            -
             | 
| 24 | 
            -
                    self._client_address = client_address
         | 
| 25 | 
            -
                    self._rfile = rfile
         | 
| 26 | 
            -
                    self._wfile = wfile
         | 
| 27 | 
            -
             | 
| 28 | 
            -
                @abc.abstractmethod
         | 
| 29 | 
            -
                def handle(self) -> None:
         | 
| 30 | 
            -
                    raise NotImplementedError
         | 
    
        omlish/sockets/io.py
    ADDED
    
    | @@ -0,0 +1,69 @@ | |
| 1 | 
            +
            # ruff: noqa: UP006 UP007
         | 
| 2 | 
            +
            # @omlish-lite
         | 
| 3 | 
            +
            import io
         | 
| 4 | 
            +
            import socket
         | 
| 5 | 
            +
            import typing as ta
         | 
| 6 | 
            +
             | 
| 7 | 
            +
             | 
| 8 | 
            +
            ##
         | 
| 9 | 
            +
             | 
| 10 | 
            +
             | 
| 11 | 
            +
            class SocketWriter(io.BufferedIOBase):
         | 
| 12 | 
            +
                """
         | 
| 13 | 
            +
                Simple writable BufferedIOBase implementation for a socket
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                Does not hold data in a buffer, avoiding any need to call flush().
         | 
| 16 | 
            +
                """
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def __init__(self, sock):
         | 
| 19 | 
            +
                    super().__init__()
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    self._sock = sock
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def writable(self):
         | 
| 24 | 
            +
                    return True
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def write(self, b):
         | 
| 27 | 
            +
                    self._sock.sendall(b)
         | 
| 28 | 
            +
                    with memoryview(b) as view:
         | 
| 29 | 
            +
                        return view.nbytes
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def fileno(self):
         | 
| 32 | 
            +
                    return self._sock.fileno()
         | 
| 33 | 
            +
             | 
| 34 | 
            +
             | 
| 35 | 
            +
            class SocketIoPair(ta.NamedTuple):
         | 
| 36 | 
            +
                r: ta.BinaryIO
         | 
| 37 | 
            +
                w: ta.BinaryIO
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                @classmethod
         | 
| 40 | 
            +
                def from_socket(
         | 
| 41 | 
            +
                        cls,
         | 
| 42 | 
            +
                        sock: socket.socket,
         | 
| 43 | 
            +
                        *,
         | 
| 44 | 
            +
                        r_buf_size: int = -1,
         | 
| 45 | 
            +
                        w_buf_size: int = 0,
         | 
| 46 | 
            +
                ) -> 'SocketIoPair':
         | 
| 47 | 
            +
                    rf: ta.Any = sock.makefile('rb', r_buf_size)
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    if w_buf_size:
         | 
| 50 | 
            +
                        wf: ta.Any = SocketWriter(sock)
         | 
| 51 | 
            +
                    else:
         | 
| 52 | 
            +
                        wf = sock.makefile('wb', w_buf_size)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    return cls(rf, wf)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
             | 
| 57 | 
            +
            ##
         | 
| 58 | 
            +
             | 
| 59 | 
            +
             | 
| 60 | 
            +
            def close_socket_immediately(sock: socket.socket) -> None:
         | 
| 61 | 
            +
                try:
         | 
| 62 | 
            +
                    # Explicitly shutdown. socket.close() merely releases the socket and waits for GC to perform the actual close.
         | 
| 63 | 
            +
                    sock.shutdown(socket.SHUT_WR)
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                except OSError:
         | 
| 66 | 
            +
                    # Some platforms may raise ENOTCONN here
         | 
| 67 | 
            +
                    pass
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                sock.close()
         | 
| 
            File without changes
         |