aiomisc 17.5.31__py3-none-any.whl → 17.6.3__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.
aiomisc/aggregate.py CHANGED
@@ -6,14 +6,7 @@ from asyncio import CancelledError, Event, Future, Lock, wait_for
6
6
  from dataclasses import dataclass
7
7
  from inspect import Parameter
8
8
  from typing import (
9
- Any,
10
- Callable,
11
- Coroutine,
12
- Generic,
13
- Iterable,
14
- List,
15
- Optional,
16
- Protocol,
9
+ Any, Callable, Coroutine, Generic, Iterable, List, Optional, Protocol,
17
10
  TypeVar,
18
11
  )
19
12
 
@@ -245,7 +238,7 @@ class Aggregator(AggregatorAsync[V, R], Generic[V, R]):
245
238
 
246
239
 
247
240
  def aggregate(
248
- leeway_ms: float, max_count: Optional[int] = None
241
+ leeway_ms: float, max_count: Optional[int] = None,
249
242
  ) -> Callable[[AggregateFunc[V, R]], Callable[[V], Coroutine[Any, Any, R]]]:
250
243
  """
251
244
  Parametric decorator that aggregates multiple
@@ -276,7 +269,7 @@ def aggregate(
276
269
  :return:
277
270
  """
278
271
  def decorator(
279
- func: AggregateFunc[V, R]
272
+ func: AggregateFunc[V, R],
280
273
  ) -> Callable[[V], Coroutine[Any, Any, R]]:
281
274
  aggregator = Aggregator(
282
275
  func, max_count=max_count, leeway_ms=leeway_ms,
@@ -289,7 +282,7 @@ def aggregate_async(
289
282
  leeway_ms: float, max_count: Optional[int] = None,
290
283
  ) -> Callable[
291
284
  [AggregateAsyncFunc[V, R]],
292
- Callable[[V], Coroutine[Any, Any, R]]
285
+ Callable[[V], Coroutine[Any, Any, R]],
293
286
  ]:
294
287
  """
295
288
  Same as ``aggregate``, but with ``func`` arguments of type ``Arg``
@@ -302,7 +295,7 @@ def aggregate_async(
302
295
  :return:
303
296
  """
304
297
  def decorator(
305
- func: AggregateAsyncFunc[V, R]
298
+ func: AggregateAsyncFunc[V, R],
306
299
  ) -> Callable[[V], Coroutine[Any, Any, R]]:
307
300
  aggregator = AggregatorAsync(
308
301
  func, max_count=max_count, leeway_ms=leeway_ms,
aiomisc/backoff.py CHANGED
@@ -89,7 +89,7 @@ def asyncbackoff(
89
89
  exceptions += asyncio.TimeoutError,
90
90
 
91
91
  def decorator(
92
- func: Callable[P, Coroutine[Any, Any, T]]
92
+ func: Callable[P, Coroutine[Any, Any, T]],
93
93
  ) -> Callable[P, Coroutine[Any, Any, T]]:
94
94
  if attempt_timeout is not None:
95
95
  func = timeout(attempt_timeout)(func)
aiomisc/entrypoint.py CHANGED
@@ -6,8 +6,8 @@ import sys
6
6
  import threading
7
7
  from concurrent.futures import Executor
8
8
  from typing import (
9
- Any, Callable, Coroutine, FrozenSet, MutableSet, Optional, Set, Tuple,
10
- TypeVar, Union,
9
+ Any, Callable, Coroutine, FrozenSet, Iterable, MutableSet, Optional, Set,
10
+ Tuple, TypeVar, Union,
11
11
  )
12
12
  from weakref import WeakSet
13
13
 
@@ -115,6 +115,7 @@ class Entrypoint:
115
115
  date_format=self.log_date_format,
116
116
  buffered=self.log_buffering,
117
117
  loop=self.loop,
118
+ handlers=self.log_handlers,
118
119
  buffer_size=self.log_buffer_size,
119
120
  flush_interval=self.log_flush_interval,
120
121
  )
@@ -146,6 +147,7 @@ class Entrypoint:
146
147
  log_date_format: Optional[str] = DEFAULT_LOG_DATE_FORMAT,
147
148
  log_flush_interval: float = DEFAULT_AIOMISC_LOG_FLUSH,
148
149
  log_config: bool = DEFAULT_AIOMISC_LOG_CONFIG,
150
+ log_handlers: Iterable[logging.Handler] = (),
149
151
  policy: asyncio.AbstractEventLoopPolicy = event_loop_policy,
150
152
  debug: bool = DEFAULT_AIOMISC_DEBUG,
151
153
  catch_signals: Optional[Tuple[int, ...]] = None,
@@ -196,6 +198,7 @@ class Entrypoint:
196
198
  self.log_date_format = log_date_format
197
199
  self.log_flush_interval = log_flush_interval
198
200
  self.log_format = log_format
201
+ self.log_handlers = tuple(log_handlers)
199
202
  self.log_level = log_level
200
203
  self.policy = policy
201
204
 
aiomisc/log.py CHANGED
@@ -1,45 +1,82 @@
1
1
  import asyncio
2
- import atexit
3
- import logging
4
2
  import logging.handlers
5
- import time
3
+ import threading
6
4
  import traceback
5
+ import warnings
7
6
  from contextlib import suppress
8
- from functools import partial
7
+ from queue import Empty, Queue
9
8
  from socket import socket
10
9
  from typing import (
11
10
  Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, Union,
12
11
  )
13
- from weakref import finalize
14
12
 
15
13
  import aiomisc_log
16
14
  from aiomisc_log.enum import LogFormat, LogLevel
17
15
 
18
- from .thread_pool import run_in_new_thread
19
-
20
-
21
- def _thread_flusher(
22
- handler: logging.handlers.MemoryHandler,
23
- flush_interval: Union[float, int],
24
- loop: asyncio.AbstractEventLoop,
25
- ) -> None:
26
- def has_no_target() -> bool:
27
- return True
28
-
29
- def has_target() -> bool:
30
- return bool(handler.target)
31
-
32
- is_target = has_no_target
33
-
34
- if isinstance(handler, logging.handlers.MemoryHandler):
35
- is_target = has_target
36
-
37
- while not loop.is_closed() and is_target():
38
- with suppress(Exception):
39
- if handler.buffer:
40
- handler.flush()
41
-
42
- time.sleep(flush_interval)
16
+ from .counters import Statistic
17
+
18
+
19
+ class ThreadedHandlerStatistic(Statistic):
20
+ threads: int
21
+ records: int
22
+ errors: int
23
+ flushes: int
24
+
25
+
26
+ class ThreadedHandler(logging.Handler):
27
+ def __init__(
28
+ self, target: logging.Handler, flush_interval: float = 0.1,
29
+ buffered: bool = True, queue_size: int = 0,
30
+ ):
31
+ super().__init__()
32
+ self._buffered = buffered
33
+ self._target = target
34
+ self._flush_interval = flush_interval
35
+ self._flush_event = threading.Event()
36
+ self._queue: Queue[Optional[logging.LogRecord]] = Queue(queue_size)
37
+ self._close_event = threading.Event()
38
+ self._thread = threading.Thread(target=self._in_thread, daemon=True)
39
+ self._statistic = ThreadedHandlerStatistic()
40
+
41
+ def start(self) -> None:
42
+ self._statistic.threads += 1
43
+ self._thread.start()
44
+
45
+ def close(self) -> None:
46
+ self._queue.put(None)
47
+ del self._queue
48
+ self.flush()
49
+ self._close_event.set()
50
+ super().close()
51
+
52
+ def flush(self) -> None:
53
+ self._statistic.flushes += 1
54
+ self._flush_event.set()
55
+
56
+ def emit(self, record: logging.LogRecord) -> None:
57
+ if self._buffered:
58
+ self._queue.put_nowait(record)
59
+ else:
60
+ self._queue.put(record)
61
+ self._statistic.records += 1
62
+
63
+ def _in_thread(self) -> None:
64
+ queue = self._queue
65
+ while not self._close_event.is_set():
66
+ self._flush_event.wait(self._flush_interval)
67
+ try:
68
+ self.acquire()
69
+ while True:
70
+ record = queue.get(timeout=self._flush_interval)
71
+ if record is None:
72
+ return
73
+ with suppress(Exception):
74
+ self._target.handle(record)
75
+ except Empty:
76
+ pass
77
+ finally:
78
+ self.release()
79
+ self._statistic.threads -= 1
43
80
 
44
81
 
45
82
  def suppressor(
@@ -54,27 +91,18 @@ def suppressor(
54
91
 
55
92
  def wrap_logging_handler(
56
93
  handler: logging.Handler,
57
- loop: Optional[asyncio.AbstractEventLoop] = None,
58
94
  buffer_size: int = 1024,
59
95
  flush_interval: Union[float, int] = 0.1,
96
+ loop: Optional[asyncio.AbstractEventLoop] = None,
60
97
  ) -> logging.Handler:
61
- buffered_handler = logging.handlers.MemoryHandler(
62
- buffer_size,
98
+ warnings.warn("wrap_logging_handler is deprecated", DeprecationWarning)
99
+ handler = ThreadedHandler(
63
100
  target=handler,
64
- flushLevel=logging.CRITICAL,
65
- )
66
-
67
- run_in_new_thread(
68
- _thread_flusher, args=(
69
- buffered_handler, flush_interval, loop,
70
- ), no_return=True, statistic_name="logger",
101
+ queue_size=buffer_size,
102
+ flush_interval=flush_interval,
71
103
  )
72
-
73
- at_exit_flusher = suppressor(handler.flush)
74
- atexit.register(at_exit_flusher)
75
- finalize(buffered_handler, partial(atexit.unregister, at_exit_flusher))
76
-
77
- return buffered_handler
104
+ handler.start()
105
+ return handler
78
106
 
79
107
 
80
108
  class UnhandledLoopHook(aiomisc_log.UnhandledHook):
@@ -109,7 +137,7 @@ class UnhandledLoopHook(aiomisc_log.UnhandledHook):
109
137
  protocol: Optional[asyncio.Protocol] = context.pop("protocol", None)
110
138
  transport: Optional[asyncio.Transport] = context.pop("transport", None)
111
139
  sock: Optional[socket] = context.pop("socket", None)
112
- source_traceback: List[traceback.FrameSummary] = (
140
+ source_tb: List[traceback.FrameSummary] = (
113
141
  context.pop("source_traceback", None) or []
114
142
  )
115
143
 
@@ -129,51 +157,52 @@ class UnhandledLoopHook(aiomisc_log.UnhandledHook):
129
157
 
130
158
  self._fill_transport_extra(transport, extra)
131
159
  self.logger.exception(message, exc_info=exception, extra=extra)
132
- if source_traceback:
133
- self.logger.error(
134
- "".join(traceback.format_list(source_traceback)),
135
- )
160
+ if source_tb:
161
+ self.logger.error("".join(traceback.format_list(source_tb)))
136
162
 
137
163
 
138
164
  def basic_config(
139
165
  level: Union[int, str] = LogLevel.default(),
140
166
  log_format: Union[str, LogFormat] = LogFormat.default(),
141
- buffered: bool = True, buffer_size: int = 1024,
167
+ buffered: bool = True,
168
+ buffer_size: int = 0,
142
169
  flush_interval: Union[int, float] = 0.2,
143
170
  loop: Optional[asyncio.AbstractEventLoop] = None,
144
171
  handlers: Iterable[logging.Handler] = (),
145
172
  **kwargs: Any,
146
173
  ) -> None:
147
- loop = loop or asyncio.get_event_loop()
148
174
  unhandled_hook = UnhandledLoopHook(logger_name="asyncio.unhandled")
149
175
 
150
- def wrap_handler(handler: logging.Handler) -> logging.Handler:
151
- nonlocal buffer_size, buffered, loop, unhandled_hook
176
+ if loop is None:
177
+ loop = asyncio.get_event_loop()
152
178
 
153
- unhandled_hook.add_handler(handler)
179
+ forever_task = asyncio.gather(
180
+ loop.create_future(), return_exceptions=True,
181
+ )
182
+ loop.set_exception_handler(unhandled_hook)
154
183
 
155
- if buffered:
156
- return wrap_logging_handler(
157
- handler=handler,
158
- buffer_size=buffer_size,
159
- flush_interval=flush_interval,
160
- loop=loop,
161
- )
162
- return handler
184
+ log_handlers = []
185
+
186
+ for user_handler in handlers:
187
+ handler = ThreadedHandler(
188
+ buffered=buffered,
189
+ flush_interval=flush_interval,
190
+ queue_size=buffer_size,
191
+ target=user_handler,
192
+ )
193
+ unhandled_hook.add_handler(handler)
194
+ forever_task.add_done_callback(lambda _: handler.close())
195
+ log_handlers.append(handler)
196
+ handler.start()
163
197
 
164
198
  aiomisc_log.basic_config(
165
- level=level,
166
- log_format=log_format,
167
- handler_wrapper=wrap_handler,
168
- handlers=handlers,
169
- **kwargs,
199
+ level=level, log_format=log_format, handlers=log_handlers, **kwargs,
170
200
  )
171
201
 
172
- loop.set_exception_handler(unhandled_hook)
173
-
174
202
 
175
203
  __all__ = (
176
204
  "LogFormat",
177
205
  "LogLevel",
178
206
  "basic_config",
207
+ "ThreadedHandler",
179
208
  )
@@ -117,7 +117,7 @@ class UvicornService(Service, abc.ABC):
117
117
  self.sock = config.bind_socket()
118
118
  self.server = Server(config)
119
119
  self.serve_task = asyncio.create_task(
120
- self.server.serve(sockets=[self.sock])
120
+ self.server.serve(sockets=[self.sock]),
121
121
  )
122
122
 
123
123
  async def stop(self, exception: Optional[Exception] = None) -> None:
aiomisc/timeout.py CHANGED
@@ -16,7 +16,7 @@ Number = Union[int, float]
16
16
 
17
17
 
18
18
  def timeout(
19
- value: Number
19
+ value: Number,
20
20
  ) -> Callable[
21
21
  [Callable[P, Coroutine[Any, Any, T]]],
22
22
  Callable[P, Coroutine[Any, Any, T]],
aiomisc/version.py CHANGED
@@ -2,5 +2,5 @@
2
2
  # BY: poem-plugins "git" plugin
3
3
  # NEVER EDIT THIS FILE MANUALLY
4
4
 
5
- version_info = (17, 5, 31)
6
- __version__ = "17.5.31"
5
+ version_info = (17, 6, 3)
6
+ __version__ = "17.6.3"
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: aiomisc
3
- Version: 17.5.31
3
+ Version: 17.6.3
4
4
  Summary: aiomisc - miscellaneous utils for asyncio
5
- Home-page: https://github.com/aiokitchen/aiomisc
6
5
  License: MIT
7
6
  Author: Dmitry Orlov
8
7
  Author-email: me@mosquito.su
@@ -70,6 +69,7 @@ Requires-Dist: uvicorn (>=0.27,<0.28) ; extra == "uvicorn"
70
69
  Requires-Dist: uvloop (>=0.19,<1) ; extra == "uvloop"
71
70
  Project-URL: Changelog, https://github.com/aiokitchen/aiomisc/blob/master/CHANGELOG.md
72
71
  Project-URL: Documentation, https://docs.aiomisc.com/
72
+ Project-URL: Homepage, https://github.com/aiokitchen/aiomisc
73
73
  Project-URL: Source, https://github.com/aiokitchen/aiomisc
74
74
  Project-URL: Tracker, https://github.com/aiokitchen/aiomisc/issues
75
75
  Description-Content-Type: text/x-rst
@@ -1,16 +1,16 @@
1
1
  aiomisc/__init__.py,sha256=mgLaoGB3-WTAnrUfEN9nM_0Z_XU6xojkx1ISmW204UA,2253
2
2
  aiomisc/_context_vars.py,sha256=28A7j_NABitKMtpxuFvxQ2wCr-Fq79dAC3YjqVLLeTQ,689
3
- aiomisc/aggregate.py,sha256=4yAsJhtRc-ikm9hXer05DgYhJFGf94IXeWJRTCSSDpw,8898
4
- aiomisc/backoff.py,sha256=v8G4A-z9o6ou4oa-hSv3UtLPX2Km8PLsEGqHHAFtu24,5701
3
+ aiomisc/aggregate.py,sha256=fqe_9-oXnJlzl094RpeeUd03Ud2lV6qUk7FUxjdpMi4,8874
4
+ aiomisc/backoff.py,sha256=5ct-7LLln3MhpLs6AAO2ADbFXL_Y5nZwPEkwTeXBwWE,5702
5
5
  aiomisc/circuit_breaker.py,sha256=nZVLuGg4rKxn84CHaIvRfBYumgdwx2VZloF8OprQKuk,12581
6
6
  aiomisc/compat.py,sha256=aYVe0J-2CvAzUHPAULNhrfMLFYNKN-z3Sg7nlBkzfxw,3291
7
7
  aiomisc/context.py,sha256=j2YRNGDAJbrg94OkFyIMyYKHKzHRRL8tSAWma1yxNKE,1549
8
8
  aiomisc/counters.py,sha256=ZU7K3FBY7V1jIKohWXXyXqby98FuOmq6H5Lv7Cf-c70,3686
9
9
  aiomisc/cron.py,sha256=zAxdrLn8r7ERhkVaoUOv8xrQVZn7Hmm8HBS-MmsIu2s,2165
10
- aiomisc/entrypoint.py,sha256=0KLgUnLexL70N9PNXv368qRQ99t7ll4AzuwjQCe0Pdk,14100
10
+ aiomisc/entrypoint.py,sha256=x85478I-nmza4niFc4Yb4QsnMdRrdyUQ1Ud5dACRduI,14256
11
11
  aiomisc/io.py,sha256=PWBy_D6CVlfhHokQoVfzmHIvGb37VtUo1P-yedB9aL8,10004
12
12
  aiomisc/iterator_wrapper.py,sha256=d1aZ5jKTrK3kVCsfRPJPUq8X9ijVNi4o5Zs2pV4RS70,7243
13
- aiomisc/log.py,sha256=HyUy4RjT6snYyQxj6_gcPidS5nKQ1sXj4wOBQeU-ZQ0,5263
13
+ aiomisc/log.py,sha256=UTu5fMwIK5OU9QrDoJqxyVwdQHVDkzPl1laav0SIb4o,6323
14
14
  aiomisc/periodic.py,sha256=OFYZbPkcGgeMjBk8zwsLr2TqPRTS6MNewaL3q9qK5js,2252
15
15
  aiomisc/plugins/__init__.py,sha256=eHKGec_217rBjXGf8u-Joyf7dzpO1O0QAuFan1IEyYE,1057
16
16
  aiomisc/plugins/__main__.py,sha256=y3mykRQmimDRHb_PvY4n08vegjWTjd9CONFbCecAxxw,1365
@@ -40,14 +40,14 @@ aiomisc/service/tcp.py,sha256=gIdm4wXT191TMpL3Yzm_I7y--1kVxBLioN5lBP8mlhs,5507
40
40
  aiomisc/service/tls.py,sha256=oFTYVe2zf1Ow_Gniem9WyNj_SkD3q95bcsz8LWAYqdI,7325
41
41
  aiomisc/service/tracer.py,sha256=_dxk5y2JEteki9J1OXnOkI-EowD9vakSfsLaRDB4uMQ,2826
42
42
  aiomisc/service/udp.py,sha256=_uHzMAkpd7y-yt3tE9jN2llEETG7r47g2DF1SO7xyig,3616
43
- aiomisc/service/uvicorn.py,sha256=dR-d5ge3KsCIEJfJ6SiFQ7rIyX8rKWNuGV2ZrONToj4,4323
43
+ aiomisc/service/uvicorn.py,sha256=I2KYQL9ysFFqQHPYK00G4EoceA7J9p9Or6v1ATpLGw8,4324
44
44
  aiomisc/signal.py,sha256=_iiC2jukXg7-LLirIl1YATlKIIsKLbmTNFr1Ezheu7g,1728
45
45
  aiomisc/thread_pool.py,sha256=591u5HV1aBmBRcaVm4kAtMLPZOb6veqhT9wkts_knqE,14017
46
- aiomisc/timeout.py,sha256=OhXCvQGbPZA9IQ7cRT7ZlZnsbmDgH5ioErQyjeqIEzY,919
46
+ aiomisc/timeout.py,sha256=5jNDooLW4MAe6ZUIKhkiX29CoyI0zlXd-dZoZwQPxak,920
47
47
  aiomisc/utils.py,sha256=6yTfTpeRCVzfZp-MJCmB1oayOHUBVwQwzC3U5PBAjn8,11919
48
- aiomisc/version.py,sha256=inQlcadD8kWvAqeup4DUVRxcUEOCEEwskUqD2Cu1nXM,156
48
+ aiomisc/version.py,sha256=JY19fjXJJZWLMfFzMzs0Ghip8ShxPsVTYU-QmqfsVjI,154
49
49
  aiomisc/worker_pool.py,sha256=GA91KdOrBlqHthbVSTxu_d6BsBIbl-uKqW2NxqSafG0,11107
50
- aiomisc_log/__init__.py,sha256=ZD-Q-YTWoZdxJpMqMNMIraA_gaN0QawgnVpXz6mxd2o,4855
50
+ aiomisc_log/__init__.py,sha256=N3g8Ea1YE2dWPDd-MwohuAn3Y0pBxSyL4l4Jsxqkv0Q,4854
51
51
  aiomisc_log/enum.py,sha256=_zfCZPYCGyI9KL6TqHiYVlOfA5U5MCbsuCuDKxDHdxg,1549
52
52
  aiomisc_log/formatter/__init__.py,sha256=1dBtF7yRvyvAhQRAofhBN682r7COoAB8blzibJxlvS8,235
53
53
  aiomisc_log/formatter/color.py,sha256=6S-lwwceDGRN4svIrWmzq48XjWjDYuRWKN_gOMwY8dE,1276
@@ -63,8 +63,8 @@ aiomisc_worker/process_inner.py,sha256=8ZtjCSLrgySW57OIbuGrpEWxfysRLYKx1or1YaAqx
63
63
  aiomisc_worker/protocol.py,sha256=1smmlBbdreSmnrxuhHaUMUC10FO9xMIEcedhweQJX_A,2705
64
64
  aiomisc_worker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
65
  aiomisc_worker/worker.py,sha256=f8nCFhlKh84UUBUaEgCllwMRvVZiD8_UUXaeit6g3T8,3236
66
- aiomisc-17.5.31.dist-info/COPYING,sha256=Ky_8CQMaIixfyOreUBsl0hKN6A5fLnPF8KPQ9molMYA,1125
67
- aiomisc-17.5.31.dist-info/METADATA,sha256=4Mvo60Wnj7oJLOqGnrPrbiIGcSEiKY1HHCTj5dVHqa0,15724
68
- aiomisc-17.5.31.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
69
- aiomisc-17.5.31.dist-info/entry_points.txt,sha256=KRsSPCwKJyGTWrvzpwbS0yIDwzsgDA2X6f0CBWYmNao,55
70
- aiomisc-17.5.31.dist-info/RECORD,,
66
+ aiomisc-17.6.3.dist-info/COPYING,sha256=Ky_8CQMaIixfyOreUBsl0hKN6A5fLnPF8KPQ9molMYA,1125
67
+ aiomisc-17.6.3.dist-info/METADATA,sha256=Vm4QmFfrIFH-DhzfzKJ8JO0eKdfYfZJGG1JX9SqP9Qc,15735
68
+ aiomisc-17.6.3.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
69
+ aiomisc-17.6.3.dist-info/entry_points.txt,sha256=KRsSPCwKJyGTWrvzpwbS0yIDwzsgDA2X6f0CBWYmNao,55
70
+ aiomisc-17.6.3.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.0.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
aiomisc_log/__init__.py CHANGED
@@ -125,7 +125,6 @@ def basic_config(
125
125
  handlers: Iterable[logging.Handler] = (),
126
126
  **kwargs: Any,
127
127
  ) -> None:
128
-
129
128
  if isinstance(level, str):
130
129
  level = LogLevel[level]
131
130