omlish 0.0.0.dev217__py3-none-any.whl → 0.0.0.dev218__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev217'
2
- __revision__ = 'd43ca6092cff650a9fc533e97d87ce9112f6f71a'
1
+ __version__ = '0.0.0.dev218'
2
+ __revision__ = '0d15b9d5ec9832621506a90856baf7d140dd63ad'
3
3
 
4
4
 
5
5
  #
File without changes
@@ -0,0 +1,71 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import abc
4
+ import dataclasses as dc
5
+ import typing as ta
6
+
7
+ from ...lite.marshal import OBJ_MARSHALER_FIELD_KEY
8
+ from ...lite.marshal import OBJ_MARSHALER_OMIT_IF_NONE
9
+
10
+
11
+ ##
12
+
13
+
14
+ @dc.dataclass(frozen=True)
15
+ class OciDataclass(abc.ABC): # noqa
16
+ pass
17
+
18
+
19
+ ##
20
+
21
+
22
+ @dc.dataclass(frozen=True)
23
+ class OciImageConfig(OciDataclass):
24
+ """https://github.com/opencontainers/image-spec/blob/92353b0bee778725c617e7d57317b568a7796bd0/config.md"""
25
+
26
+ architecture: str
27
+ os: str
28
+
29
+ @dc.dataclass(frozen=True)
30
+ class RootFs:
31
+ type: str
32
+ diff_ids: ta.Sequence[str]
33
+
34
+ rootfs: RootFs
35
+
36
+ #
37
+
38
+ created: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
39
+ author: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
40
+ os_version: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_FIELD_KEY: 'os.version', OBJ_MARSHALER_OMIT_IF_NONE: True}) # noqa
41
+ os_features: ta.Optional[ta.Sequence[str]] = dc.field(default=None, metadata={OBJ_MARSHALER_FIELD_KEY: 'os.features', OBJ_MARSHALER_OMIT_IF_NONE: True}) # noqa
42
+ variant: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
43
+
44
+ """
45
+ config object, OPTIONAL
46
+ User string, OPTIONAL
47
+ ExposedPorts object, OPTIONAL
48
+ Env array of strings, OPTIONAL
49
+ Entrypoint array of strings, OPTIONAL
50
+ Cmd array of strings, OPTIONAL
51
+ Volumes object, OPTIONAL
52
+ WorkingDir string, OPTIONAL
53
+ Labels object, OPTIONAL
54
+ StopSignal string, OPTIONAL
55
+ ArgsEscaped boolean, OPTIONAL
56
+ Memory integer, OPTIONAL
57
+ MemorySwap integer, OPTIONAL
58
+ CpuShares integer, OPTIONAL
59
+ Healthcheck object, OPTIONAL
60
+ """
61
+ config: ta.Optional[ta.Mapping[str, ta.Any]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
62
+
63
+ @dc.dataclass(frozen=True)
64
+ class History:
65
+ created: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
66
+ author: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
67
+ created_by: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
68
+ comment: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
69
+ empty_layer: ta.Optional[bool] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
70
+
71
+ history: ta.Optional[ta.Sequence[History]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
@@ -0,0 +1,124 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import abc
4
+ import dataclasses as dc
5
+ import typing as ta
6
+
7
+ from ...lite.check import check
8
+ from ...lite.marshal import OBJ_MARSHALER_FIELD_KEY
9
+ from ...lite.marshal import OBJ_MARSHALER_OMIT_IF_NONE
10
+ from ...lite.marshal import unmarshal_obj
11
+ from .data import OciDataclass
12
+ from .data import OciImageConfig
13
+
14
+
15
+ ##
16
+
17
+
18
+ @dc.dataclass(frozen=True)
19
+ class OciMediaDataclass(OciDataclass, abc.ABC): # noqa
20
+ SCHEMA_VERSION: ta.ClassVar[int]
21
+ MEDIA_TYPE: ta.ClassVar[str]
22
+
23
+
24
+ _REGISTERED_OCI_MEDIA_DATACLASSES: ta.Dict[str, ta.Type[OciMediaDataclass]] = {}
25
+
26
+
27
+ def _register_oci_media_dataclass(cls):
28
+ check.issubclass(cls, OciMediaDataclass)
29
+ check.arg(dc.is_dataclass(cls))
30
+ mt = check.non_empty_str(cls.__dict__['MEDIA_TYPE'])
31
+ check.not_in(mt, _REGISTERED_OCI_MEDIA_DATACLASSES)
32
+ _REGISTERED_OCI_MEDIA_DATACLASSES[mt] = cls
33
+ return cls
34
+
35
+
36
+ def get_registered_oci_media_dataclass(media_type: str) -> ta.Optional[ta.Type[OciMediaDataclass]]:
37
+ return _REGISTERED_OCI_MEDIA_DATACLASSES.get(media_type)
38
+
39
+
40
+ def unmarshal_oci_media_dataclass(
41
+ dct: ta.Mapping[str, ta.Any],
42
+ *,
43
+ media_type: ta.Optional[str] = None,
44
+ ) -> ta.Any:
45
+ if media_type is None:
46
+ media_type = check.non_empty_str(dct['mediaType'])
47
+ cls = _REGISTERED_OCI_MEDIA_DATACLASSES[media_type]
48
+ return unmarshal_obj(dct, cls)
49
+
50
+
51
+ #
52
+
53
+
54
+ @dc.dataclass(frozen=True)
55
+ class OciMediaDescriptor(OciDataclass):
56
+ """https://github.com/opencontainers/image-spec/blob/92353b0bee778725c617e7d57317b568a7796bd0/descriptor.md#properties""" # noqa
57
+
58
+ media_type: str = dc.field(metadata={OBJ_MARSHALER_FIELD_KEY: 'mediaType'})
59
+ digest: str
60
+ size: int
61
+
62
+ #
63
+
64
+ urls: ta.Optional[ta.Sequence[str]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
65
+ annotations: ta.Optional[ta.Mapping[str, str]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True}) # noqa
66
+ data: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
67
+ artifact_type: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_FIELD_KEY: 'artifactType', OBJ_MARSHALER_OMIT_IF_NONE: True}) # noqa
68
+
69
+ #
70
+
71
+ platform: ta.Optional[ta.Mapping[str, ta.Any]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True}) # noqa
72
+
73
+
74
+ @_register_oci_media_dataclass
75
+ @dc.dataclass(frozen=True)
76
+ class OciMediaImageIndex(OciMediaDataclass):
77
+ """https://github.com/opencontainers/image-spec/blob/92353b0bee778725c617e7d57317b568a7796bd0/image-index.md"""
78
+
79
+ manifests: ta.Sequence[OciMediaDescriptor] # -> OciMediaImageIndex | OciMediaImageManifest
80
+
81
+ #
82
+
83
+ annotations: ta.Optional[ta.Mapping[str, str]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True}) # noqa
84
+
85
+ #
86
+
87
+ SCHEMA_VERSION: ta.ClassVar[int] = 2
88
+ schema_version: int = dc.field(default=SCHEMA_VERSION, metadata={OBJ_MARSHALER_FIELD_KEY: 'schemaVersion'})
89
+
90
+ MEDIA_TYPE: ta.ClassVar[str] = 'application/vnd.oci.image.index.v1+json'
91
+ media_type: str = dc.field(default=MEDIA_TYPE, metadata={OBJ_MARSHALER_FIELD_KEY: 'mediaType'})
92
+
93
+
94
+ @_register_oci_media_dataclass
95
+ @dc.dataclass(frozen=True)
96
+ class OciMediaImageManifest(OciMediaDataclass):
97
+ """https://github.com/opencontainers/image-spec/blob/92353b0bee778725c617e7d57317b568a7796bd0/manifest.md"""
98
+
99
+ config: OciMediaDescriptor # -> OciMediaImageConfig
100
+
101
+ # MEDIA_TYPES: ta.ClassVar[ta.Mapping[str, str]] = {
102
+ # 'TAR': 'application/vnd.oci.image.layer.v1.tar',
103
+ # 'TAR_GZIP': 'application/vnd.oci.image.layer.v1.tar+gzip',
104
+ # 'TAR_ZSTD': 'application/vnd.oci.image.layer.v1.tar+zstd',
105
+ # }
106
+ layers: ta.Sequence[OciMediaDescriptor]
107
+
108
+ #
109
+
110
+ SCHEMA_VERSION: ta.ClassVar[int] = 2
111
+ schema_version: int = dc.field(default=SCHEMA_VERSION, metadata={OBJ_MARSHALER_FIELD_KEY: 'schemaVersion'})
112
+
113
+ MEDIA_TYPE: ta.ClassVar[str] = 'application/vnd.oci.image.manifest.v1+json'
114
+ media_type: str = dc.field(default=MEDIA_TYPE, metadata={OBJ_MARSHALER_FIELD_KEY: 'mediaType'})
115
+
116
+
117
+ @_register_oci_media_dataclass
118
+ @dc.dataclass(frozen=True)
119
+ class OciMediaImageConfig(OciImageConfig, OciMediaDataclass):
120
+ SCHEMA_VERSION: ta.ClassVar[int] = 2
121
+ schema_version: int = dc.field(default=SCHEMA_VERSION, metadata={OBJ_MARSHALER_FIELD_KEY: 'schemaVersion'})
122
+
123
+ MEDIA_TYPE: ta.ClassVar[str] = 'application/vnd.oci.image.config.v1+json'
124
+ media_type: str = dc.field(default=MEDIA_TYPE, metadata={OBJ_MARSHALER_FIELD_KEY: 'mediaType'})
@@ -6,9 +6,6 @@
6
6
  // See https://json5.org/
7
7
  // Derived from ../json/JSON.g4 which original derived from http://json.org
8
8
 
9
- // $antlr-format alignTrailingComments true, columnLimit 150, minEmptyLines 1, maxEmptyLinesToKeep 1, reflowComments false, useTab false
10
- // $antlr-format allowShortRulesOnASingleLine false, allowShortBlocksOnASingleLine true, alignSemicolons hanging, alignColons hanging
11
-
12
9
  grammar Json5;
13
10
 
14
11
  json5
@@ -65,9 +65,12 @@ import typing as ta
65
65
 
66
66
  from ...lite.check import check
67
67
  from ...sockets.addresses import SocketAddress
68
- from ...sockets.handlers import SocketHandler
68
+ from ...sockets.handlers import SocketHandler # noqa
69
+ from ...sockets.io import SocketIoPair
69
70
  from ..handlers import HttpHandler
70
71
  from ..handlers import HttpHandlerRequest
72
+ from ..handlers import HttpHandlerResponseData
73
+ from ..handlers import HttpHandlerResponseStreamedData
71
74
  from ..handlers import UnsupportedMethodHttpHandlerError
72
75
  from ..parsing import EmptyParsedHttpResult
73
76
  from ..parsing import HttpRequestParser
@@ -193,7 +196,7 @@ class CoroHttpServer:
193
196
 
194
197
  message: ta.Optional[str] = None
195
198
  headers: ta.Optional[ta.Sequence['CoroHttpServer._Header']] = None
196
- data: ta.Optional[bytes] = None
199
+ data: ta.Optional[HttpHandlerResponseData] = None
197
200
  close_connection: ta.Optional[bool] = False
198
201
 
199
202
  def get_header(self, key: str) -> ta.Optional['CoroHttpServer._Header']:
@@ -204,7 +207,7 @@ class CoroHttpServer:
204
207
 
205
208
  #
206
209
 
207
- def _build_response_bytes(self, a: _Response) -> bytes:
210
+ def _build_response_head_bytes(self, a: _Response) -> bytes:
208
211
  out = io.BytesIO()
209
212
 
210
213
  if a.version >= HttpProtocolVersions.HTTP_1_0:
@@ -219,11 +222,22 @@ class CoroHttpServer:
219
222
 
220
223
  out.write(b'\r\n')
221
224
 
222
- if a.data is not None:
223
- out.write(a.data)
224
-
225
225
  return out.getvalue()
226
226
 
227
+ def _yield_response_data(self, a: _Response) -> ta.Iterator[bytes]:
228
+ if a.data is None:
229
+ return
230
+
231
+ elif isinstance(a.data, bytes):
232
+ yield a.data
233
+ return
234
+
235
+ elif isinstance(a.data, HttpHandlerResponseStreamedData):
236
+ yield from a.data.iter
237
+
238
+ else:
239
+ raise TypeError(a.data)
240
+
227
241
  #
228
242
 
229
243
  DEFAULT_CONTENT_TYPE = 'text/plain'
@@ -234,8 +248,17 @@ class CoroHttpServer:
234
248
 
235
249
  if resp.get_header('Content-Type') is None:
236
250
  nh.append(self._Header('Content-Type', self._default_content_type))
251
+
237
252
  if resp.data is not None and resp.get_header('Content-Length') is None:
238
- nh.append(self._Header('Content-Length', str(len(resp.data))))
253
+ cl: ta.Optional[int]
254
+ if isinstance(resp.data, bytes):
255
+ cl = len(resp.data)
256
+ elif isinstance(resp.data, HttpHandlerResponseStreamedData):
257
+ cl = resp.data.length
258
+ else:
259
+ raise TypeError(resp.data)
260
+ if cl is not None:
261
+ nh.append(self._Header('Content-Length', str(cl)))
239
262
 
240
263
  if nh:
241
264
  kw.update(headers=[*(resp.headers or []), *nh])
@@ -409,9 +432,13 @@ class CoroHttpServer:
409
432
 
410
433
  elif isinstance(o, self._Response):
411
434
  i = None
435
+
412
436
  r = self._preprocess_response(o)
413
- b = self._build_response_bytes(r)
414
- check.none((yield self.WriteIo(b)))
437
+ hb = self._build_response_head_bytes(r)
438
+ check.none((yield self.WriteIo(hb)))
439
+
440
+ for b in self._yield_response_data(r):
441
+ yield self.WriteIo(b)
415
442
 
416
443
  else:
417
444
  raise TypeError(o)
@@ -530,27 +557,20 @@ class CoroHttpServer:
530
557
  ##
531
558
 
532
559
 
533
- class CoroHttpServerSocketHandler(SocketHandler):
560
+ class CoroHttpServerSocketHandler: # SocketHandler
534
561
  def __init__(
535
562
  self,
536
- client_address: SocketAddress,
537
- rfile: ta.BinaryIO,
538
- wfile: ta.BinaryIO,
539
- *,
540
563
  server_factory: CoroHttpServerFactory,
564
+ *,
541
565
  log_handler: ta.Optional[ta.Callable[[CoroHttpServer, CoroHttpServer.AnyLogIo], None]] = None,
542
566
  ) -> None:
543
- super().__init__(
544
- client_address,
545
- rfile,
546
- wfile,
547
- )
567
+ super().__init__()
548
568
 
549
569
  self._server_factory = server_factory
550
570
  self._log_handler = log_handler
551
571
 
552
- def handle(self) -> None:
553
- server = self._server_factory(self._client_address)
572
+ def __call__(self, client_address: SocketAddress, fp: SocketIoPair) -> None:
573
+ server = self._server_factory(client_address)
554
574
 
555
575
  gen = server.coro_handle()
556
576
 
@@ -562,15 +582,15 @@ class CoroHttpServerSocketHandler(SocketHandler):
562
582
  self._log_handler(server, o)
563
583
 
564
584
  elif isinstance(o, CoroHttpServer.ReadIo):
565
- i = self._rfile.read(o.sz)
585
+ i = fp.r.read(o.sz)
566
586
 
567
587
  elif isinstance(o, CoroHttpServer.ReadLineIo):
568
- i = self._rfile.readline(o.sz)
588
+ i = fp.r.readline(o.sz)
569
589
 
570
590
  elif isinstance(o, CoroHttpServer.WriteIo):
571
591
  i = None
572
- self._wfile.write(o.data)
573
- self._wfile.flush()
592
+ fp.w.write(o.data)
593
+ fp.w.flush()
574
594
 
575
595
  else:
576
596
  raise TypeError(o)
omlish/http/handlers.py CHANGED
@@ -9,6 +9,10 @@ from .parsing import HttpHeaders
9
9
 
10
10
 
11
11
  HttpHandler = ta.Callable[['HttpHandlerRequest'], 'HttpHandlerResponse'] # ta.TypeAlias
12
+ HttpHandlerResponseData = ta.Union[bytes, 'HttpHandlerResponseStreamedData'] # ta.TypeAlias # noqa
13
+
14
+
15
+ ##
12
16
 
13
17
 
14
18
  @dc.dataclass(frozen=True)
@@ -25,10 +29,16 @@ class HttpHandlerResponse:
25
29
  status: ta.Union[http.HTTPStatus, int]
26
30
 
27
31
  headers: ta.Optional[ta.Mapping[str, str]] = None
28
- data: ta.Optional[bytes] = None
32
+ data: ta.Optional[HttpHandlerResponseData] = None
29
33
  close_connection: ta.Optional[bool] = None
30
34
 
31
35
 
36
+ @dc.dataclass(frozen=True)
37
+ class HttpHandlerResponseStreamedData:
38
+ iter: ta.Iterable[bytes]
39
+ length: ta.Optional[int] = None
40
+
41
+
32
42
  class HttpHandlerError(Exception):
33
43
  pass
34
44
 
omlish/iterators/tools.py CHANGED
@@ -45,6 +45,7 @@ def take(n: int, iterable: ta.Iterable[T]) -> list[T]:
45
45
 
46
46
 
47
47
  def chunk(n: int, iterable: ta.Iterable[T], strict: bool = False) -> ta.Iterator[list[T]]:
48
+ # TODO: remove with 3.13 - 3.12 doesn't support strict
48
49
  iterator = iter(functools.partial(take, n, iter(iterable)), [])
49
50
  if strict:
50
51
  def ret():
omlish/logs/all.py CHANGED
@@ -1,3 +1,7 @@
1
+ from .callers import ( # noqa
2
+ LoggingCaller,
3
+ )
4
+
1
5
  from .color import ( # noqa
2
6
  ColorLogFormatter,
3
7
  )
@@ -18,6 +22,15 @@ from .noisy import ( # noqa
18
22
  silence_noisy_loggers,
19
23
  )
20
24
 
25
+ from .protocol import ( # noqa
26
+ LogLevel,
27
+
28
+ Logging,
29
+ NopLogging,
30
+ AbstractLogging,
31
+ StdlibLogging,
32
+ )
33
+
21
34
  from .proxy import ( # noqa
22
35
  ProxyLogFilterer,
23
36
  ProxyLogHandler,
omlish/logs/callers.py ADDED
@@ -0,0 +1,45 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import io
4
+ import sys
5
+ import traceback
6
+ import types
7
+ import typing as ta
8
+
9
+
10
+ class LoggingCaller(ta.NamedTuple):
11
+ filename: str
12
+ lineno: int
13
+ func: str
14
+ sinfo: ta.Optional[str]
15
+
16
+ @classmethod
17
+ def find_frame(cls, ofs: int = 0) -> types.FrameType:
18
+ f: ta.Any = sys._getframe(2 + ofs) # noqa
19
+ while hasattr(f, 'f_code'):
20
+ if f.f_code.co_filename != __file__:
21
+ return f
22
+ f = f.f_back
23
+ raise RuntimeError
24
+
25
+ @classmethod
26
+ def find(cls, stack_info: bool = False) -> 'LoggingCaller':
27
+ f = cls.find_frame(1)
28
+ # TODO: ('(unknown file)', 0, '(unknown function)', None) ?
29
+
30
+ sinfo = None
31
+ if stack_info:
32
+ sio = io.StringIO()
33
+ sio.write('Stack (most recent call last):\n')
34
+ traceback.print_stack(f, file=sio)
35
+ sinfo = sio.getvalue()
36
+ sio.close()
37
+ if sinfo[-1] == '\n':
38
+ sinfo = sinfo[:-1]
39
+
40
+ return cls(
41
+ f.f_code.co_filename,
42
+ f.f_lineno,
43
+ f.f_code.co_name,
44
+ sinfo,
45
+ )
@@ -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)
@@ -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