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.
@@ -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)
@@ -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'] + '_',
@@ -2,8 +2,7 @@
2
2
  # @omlish-lite
3
3
  """
4
4
  TODO:
5
- - SocketClientAddress family / tuple pairs
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
- ) -> ta.Tuple[socket.AddressFamily, SocketAddress]:
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)
@@ -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
- SocketHandlerFactory = ta.Callable[[SocketAddress, ta.BinaryIO, ta.BinaryIO], 'SocketHandler']
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