qena-shared-lib 0.1.10__py3-none-any.whl → 0.1.12__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.
@@ -1,5 +1,5 @@
1
1
  from enum import Enum
2
- from typing import TypeVar
2
+ from typing import Any, TypeVar
3
3
 
4
4
  from fastapi import APIRouter, FastAPI
5
5
  from fastapi.exceptions import RequestValidationError
@@ -18,6 +18,7 @@ from .http import ControllerBase
18
18
  __all__ = [
19
19
  "Builder",
20
20
  "Environment",
21
+ "FastAPI",
21
22
  ]
22
23
 
23
24
  D = TypeVar("D")
@@ -29,19 +30,19 @@ class Environment(Enum):
29
30
 
30
31
 
31
32
  class Builder:
32
- def __init__(self):
33
+ def __init__(self) -> None:
33
34
  self._environment = Environment.DEVELOPMENT
34
35
  self._debug = False
35
36
  self._title = "Qena shared lib"
36
37
  self._description = "Qena shared tools for microservice"
37
38
  self._version = "0.1.0"
38
39
  self._lifespan = None
39
- self._openapi_url = "/openapi.json"
40
- self._docs_url = "/docs"
41
- self._redoc_url = "/redoc"
42
- self._metrics_endpoint = None
43
- self._instrumentator = None
44
- self._routers = []
40
+ self._openapi_url: str | None = "/openapi.json"
41
+ self._docs_url: str | None = "/docs"
42
+ self._redoc_url: str | None = "/redoc"
43
+ self._metrics_endpoint: str | None = None
44
+ self._instrumentator: Instrumentator | None = None
45
+ self._routers: list[APIRouter] = []
45
46
  self._container = Container()
46
47
  self._built = False
47
48
 
@@ -107,7 +108,11 @@ class Builder:
107
108
  return self
108
109
 
109
110
  def with_singleton(
110
- self, service: type[D], factory=empty, instance=empty, **kwargs
111
+ self,
112
+ service: type[D],
113
+ factory: Any = empty,
114
+ instance: Any = empty,
115
+ **kwargs: Any,
111
116
  ) -> "Builder":
112
117
  self._container.register(
113
118
  service=service,
@@ -120,7 +125,7 @@ class Builder:
120
125
  return self
121
126
 
122
127
  def with_transient(
123
- self, service: type[D], factory=empty, **kwargs
128
+ self, service: type[D], factory: Any = empty, **kwargs: Any
124
129
  ) -> "Builder":
125
130
  self._container.register(
126
131
  service=service,
@@ -172,7 +177,7 @@ class Builder:
172
177
 
173
178
  return app
174
179
 
175
- def _resolve_api_controllers(self, app: FastAPI):
180
+ def _resolve_api_controllers(self, app: FastAPI) -> None:
176
181
  api_controller_routers = [
177
182
  api_controller.register_route_handlers()
178
183
  for api_controller in self._container.resolve_all(ControllerBase)
@@ -28,8 +28,8 @@ class Background(AsyncEventLoopMixin):
28
28
  def __init__(
29
29
  self,
30
30
  logstash: BaseLogstashSender,
31
- ):
32
- self._queue = Queue()
31
+ ) -> None:
32
+ self._queue: Queue[tuple[BackgroundTask | None, str | None]] = Queue()
33
33
  self._started = False
34
34
  self._stopped = False
35
35
  self._logstash = logstash
@@ -38,7 +38,7 @@ class Background(AsyncEventLoopMixin):
38
38
 
39
39
  async def _task_manager(
40
40
  self, task: BackgroundTask, task_id: str | None = None
41
- ):
41
+ ) -> None:
42
42
  self._logger.info(
43
43
  "running %s: %s with %s", task_id, task.func.__name__, task.args
44
44
  )
@@ -60,29 +60,32 @@ class Background(AsyncEventLoopMixin):
60
60
  self._logger.info("finished running %s", task.func.__name__)
61
61
  self._tasks.pop(task_id, None)
62
62
 
63
- def _run(self, task: BackgroundTask, task_id: str | None = None):
63
+ def _run(self, task: BackgroundTask, task_id: str | None = None) -> None:
64
64
  if not self._stopped and (
65
65
  task_id is None or task_id not in self._tasks
66
66
  ):
67
67
  self.loop.create_task(self._task_manager(task, task_id))
68
68
 
69
- async def _run_tasks(self):
69
+ async def _run_tasks(self) -> None:
70
70
  while not self._stopped or not self._queue.empty():
71
71
  task, task_id = await self._queue.get()
72
72
 
73
73
  if task is None and task_id is None:
74
74
  break
75
75
 
76
- self._run(task=task, task_id=task_id)
76
+ if task is not None:
77
+ self._run(task=task, task_id=task_id)
77
78
 
78
79
  tasks = [t for _, t in self._tasks.items() if not t.done()]
79
80
 
80
81
  await gather(*tasks)
81
82
 
82
- def add_task(self, task: BackgroundTask, task_id: str | None = None):
83
+ def add_task(
84
+ self, task: BackgroundTask, task_id: str | None = None
85
+ ) -> None:
83
86
  self._queue.put_nowait((task, task_id))
84
87
 
85
- def start(self):
88
+ def start(self) -> None:
86
89
  if self._started:
87
90
  raise RuntimeError("background runner already running")
88
91
 
@@ -91,7 +94,7 @@ class Background(AsyncEventLoopMixin):
91
94
 
92
95
  self._started = True
93
96
 
94
- def stop(self):
97
+ def stop(self) -> None:
95
98
  if self._stopped:
96
99
  raise RuntimeError("background runner already stopped")
97
100
 
@@ -99,11 +102,11 @@ class Background(AsyncEventLoopMixin):
99
102
  self._queue.put_nowait((None, None))
100
103
  self.BACKGROUND_RUNNER_STATE.state("stopped")
101
104
 
102
- def is_alive(self, task_id: str):
105
+ def is_alive(self, task_id: str) -> bool:
103
106
  if task_id in self._tasks and not self._tasks[task_id].done():
104
107
  return True
105
108
 
106
109
  return False
107
110
 
108
- def count(self):
111
+ def count(self) -> int:
109
112
  return len(self._tasks)
@@ -27,11 +27,11 @@ def get_container(app: FastAPI) -> Container:
27
27
  def add_service(
28
28
  app: FastAPI,
29
29
  service: type[D],
30
- factory=empty,
30
+ factory: object = empty,
31
31
  instance: D = empty,
32
32
  scope: Scope = Scope.transient,
33
- **kwargs,
34
- ):
33
+ **kwargs: Any,
34
+ ) -> None:
35
35
  get_container(app).register(
36
36
  service=service,
37
37
  factory=factory,
@@ -30,6 +30,7 @@ def handle_http_service_error(
30
30
  logger_provider = get_service(app=request.app, service_key=LoggerProvider)
31
31
  logger = logger_provider.get_logger("http.exception_handler")
32
32
  exception_severity = exception.severity or Severity.LOW
33
+ user_agent = request.headers.get("user-agent", "__unknown__")
33
34
  message = exception.message
34
35
  tags = [
35
36
  "HTTP",
@@ -41,6 +42,7 @@ def handle_http_service_error(
41
42
  "serviceType": "HTTP",
42
43
  "method": request.method,
43
44
  "path": request.url.path,
45
+ "userAgent": user_agent,
44
46
  "exception": exception.__class__.__name__,
45
47
  }
46
48
  exc_info = (
@@ -165,6 +167,7 @@ def handle_general_http_exception(
165
167
  request: Request, exception: Exception
166
168
  ) -> Response:
167
169
  logstash = get_service(app=request.app, service_key=BaseLogstashSender)
170
+ user_agent = request.get("user-agent", "__unknown__")
168
171
 
169
172
  logstash.error(
170
173
  message=f"something went wrong on endpoint `{request.method} {request.url.path}`",
@@ -178,6 +181,7 @@ def handle_general_http_exception(
178
181
  "serviceType": "HTTP",
179
182
  "method": request.method,
180
183
  "path": request.url.path,
184
+ "userAgent": user_agent,
181
185
  "exception": exception.__class__.__name__,
182
186
  },
183
187
  exception=exception,
@@ -1,6 +1,8 @@
1
1
  from enum import Enum
2
2
  from typing import Any
3
3
 
4
+ from fastapi import status
5
+
4
6
  __all__ = [
5
7
  "BadGateway",
6
8
  "BadRequest",
@@ -62,8 +64,8 @@ class Severity(Enum):
62
64
 
63
65
 
64
66
  class ServiceException(Exception):
65
- _GENERAL_SEVERITY = None
66
- _GENERAL_EXTRACT_EXC_INFO = None
67
+ _GENERAL_SEVERITY: Severity | None = None
68
+ _GENERAL_EXTRACT_EXC_INFO: bool | None = None
67
69
 
68
70
  def __init__(
69
71
  self,
@@ -78,8 +80,10 @@ class ServiceException(Exception):
78
80
 
79
81
  if severity is not None:
80
82
  self._severity = severity
81
- else:
83
+ elif self._GENERAL_SEVERITY is not None:
82
84
  self._severity = self._GENERAL_SEVERITY
85
+ else:
86
+ self._severity = Severity.LOW
83
87
 
84
88
  self._tags = tags
85
89
  self._extra = extra
@@ -91,8 +95,10 @@ class ServiceException(Exception):
91
95
 
92
96
  if extract_exc_info is not None:
93
97
  self._extract_exc_info = extract_exc_info
94
- else:
98
+ elif self._GENERAL_EXTRACT_EXC_INFO is not None:
95
99
  self._extract_exc_info = self._GENERAL_EXTRACT_EXC_INFO
100
+ else:
101
+ self._extract_exc_info = False
96
102
 
97
103
  @property
98
104
  def message(self) -> str:
@@ -126,7 +132,7 @@ class ServiceException(Exception):
126
132
 
127
133
 
128
134
  class HTTPServiceError(ServiceException):
129
- _GENERAL_STATUS_CODE = None
135
+ _GENERAL_STATUS_CODE: int | None = None
130
136
 
131
137
  def __init__(
132
138
  self,
@@ -152,6 +158,7 @@ class HTTPServiceError(ServiceException):
152
158
  )
153
159
 
154
160
  self._body = body
161
+ self._status_code: int | None = None
155
162
 
156
163
  if status_code is not None:
157
164
  self._status_code = status_code
@@ -190,178 +197,179 @@ class HTTPServiceError(ServiceException):
190
197
 
191
198
 
192
199
  class ClientError(HTTPServiceError):
193
- _GENERAL_EXTRACT_EXC_INFO = False
194
200
  _GENERAL_SEVERITY = Severity.MEDIUM
201
+ _GENERAL_EXTRACT_EXC_INFO = False
195
202
 
196
203
 
197
204
  class BadRequest(ClientError):
198
- _GENERAL_STATUS_CODE = 400
205
+ _GENERAL_STATUS_CODE = status.HTTP_400_BAD_REQUEST
199
206
 
200
207
 
201
208
  class Unauthorized(ClientError):
202
- _GENERAL_STATUS_CODE = 401
209
+ _GENERAL_STATUS_CODE = status.HTTP_401_UNAUTHORIZED
203
210
 
204
211
 
205
212
  class PaymentRequired(ClientError):
206
- _GENERAL_STATUS_CODE = 402
213
+ _GENERAL_STATUS_CODE = status.HTTP_402_PAYMENT_REQUIRED
207
214
 
208
215
 
209
216
  class Forbidden(ClientError):
210
- _GENERAL_STATUS_CODE = 403
217
+ _GENERAL_STATUS_CODE = status.HTTP_403_FORBIDDEN
211
218
 
212
219
 
213
220
  class NotFound(ClientError):
214
- _GENERAL_STATUS_CODE = 404
221
+ _GENERAL_STATUS_CODE = status.HTTP_404_NOT_FOUND
215
222
 
216
223
 
217
224
  class MethodNotAllowed(ClientError):
218
- _GENERAL_STATUS_CODE = 405
225
+ _GENERAL_STATUS_CODE = status.HTTP_405_METHOD_NOT_ALLOWED
219
226
 
220
227
 
221
228
  class NotAcceptable(ClientError):
222
- _GENERAL_STATUS_CODE = 406
229
+ _GENERAL_STATUS_CODE = status.HTTP_406_NOT_ACCEPTABLE
223
230
 
224
231
 
225
232
  class ProxyAuthenticationRequired(ClientError):
226
- _GENERAL_STATUS_CODE = 407
233
+ _GENERAL_STATUS_CODE = status.HTTP_407_PROXY_AUTHENTICATION_REQUIRED
227
234
 
228
235
 
229
236
  class RequestTimeout(ClientError):
230
- _GENERAL_STATUS_CODE = 408
237
+ _GENERAL_STATUS_CODE = status.HTTP_408_REQUEST_TIMEOUT
231
238
 
232
239
 
233
240
  class Conflict(ClientError):
234
- _GENERAL_STATUS_CODE = 409
241
+ _GENERAL_STATUS_CODE = status.HTTP_409_CONFLICT
235
242
 
236
243
 
237
244
  class Gone(ClientError):
238
- _GENERAL_STATUS_CODE = 410
245
+ _GENERAL_STATUS_CODE = status.HTTP_410_GONE
239
246
 
240
247
 
241
248
  class LengthRequired(ClientError):
242
- _GENERAL_STATUS_CODE = 411
249
+ _GENERAL_STATUS_CODE = status.HTTP_411_LENGTH_REQUIRED
243
250
 
244
251
 
245
252
  class PreconditionFailed(ClientError):
246
- _GENERAL_STATUS_CODE = 412
253
+ _GENERAL_STATUS_CODE = status.HTTP_412_PRECONDITION_FAILED
247
254
 
248
255
 
249
256
  class PayloadTooLarge(ClientError):
250
- _GENERAL_STATUS_CODE = 413
257
+ _GENERAL_STATUS_CODE = status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
251
258
 
252
259
 
253
260
  class URITooLong(ClientError):
254
- _GENERAL_STATUS_CODE = 414
261
+ _GENERAL_STATUS_CODE = status.HTTP_414_REQUEST_URI_TOO_LONG
255
262
 
256
263
 
257
264
  class UnsupportedMediaType(ClientError):
258
- _GENERAL_STATUS_CODE = 415
265
+ _GENERAL_STATUS_CODE = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
259
266
 
260
267
 
261
268
  class RangeNotSatisfiable(ClientError):
262
- _GENERAL_STATUS_CODE = 416
269
+ _GENERAL_STATUS_CODE = status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE
263
270
 
264
271
 
265
272
  class ExpectationFailed(ClientError):
266
- _GENERAL_STATUS_CODE = 417
273
+ _GENERAL_STATUS_CODE = status.HTTP_417_EXPECTATION_FAILED
267
274
 
268
275
 
269
276
  class IAmATeapot(ClientError):
270
- _GENERAL_STATUS_CODE = 418
277
+ _GENERAL_STATUS_CODE = status.HTTP_418_IM_A_TEAPOT
271
278
 
272
279
 
273
280
  class MisdirectedRequest(ClientError):
274
- _GENERAL_STATUS_CODE = 421
281
+ _GENERAL_STATUS_CODE = status.HTTP_421_MISDIRECTED_REQUEST
275
282
 
276
283
 
277
284
  class UnprocessableEntity(ClientError):
278
- _GENERAL_STATUS_CODE = 422
285
+ _GENERAL_STATUS_CODE = status.HTTP_422_UNPROCESSABLE_ENTITY
279
286
 
280
287
 
281
288
  class Locked(ClientError):
282
- _GENERAL_STATUS_CODE = 423
289
+ _GENERAL_STATUS_CODE = status.HTTP_423_LOCKED
283
290
 
284
291
 
285
292
  class FailedDependency(ClientError):
286
- _GENERAL_STATUS_CODE = 424
293
+ _GENERAL_STATUS_CODE = status.HTTP_424_FAILED_DEPENDENCY
287
294
 
288
295
 
289
296
  class TooEarly(ClientError):
290
- _GENERAL_STATUS_CODE = 425
297
+ _GENERAL_STATUS_CODE = status.HTTP_425_TOO_EARLY
291
298
 
292
299
 
293
300
  class UpgradeRequired(ClientError):
294
- _GENERAL_STATUS_CODE = 426
301
+ _GENERAL_STATUS_CODE = status.HTTP_426_UPGRADE_REQUIRED
295
302
 
296
303
 
297
304
  class PreconditionRequired(ClientError):
298
- _GENERAL_STATUS_CODE = 428
305
+ _GENERAL_STATUS_CODE = status.HTTP_428_PRECONDITION_REQUIRED
299
306
 
300
307
 
301
308
  class TooManyRequests(ClientError):
302
- _GENERAL_STATUS_CODE = 429
309
+ _GENERAL_STATUS_CODE = status.HTTP_429_TOO_MANY_REQUESTS
303
310
 
304
311
 
305
312
  class RequestHeaderFieldsTooLarge(ClientError):
306
- _GENERAL_STATUS_CODE = 431
313
+ _GENERAL_STATUS_CODE = status.HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE
307
314
 
308
315
 
309
316
  class UnavailableForLegalReasons(ClientError):
310
- _GENERAL_STATUS_CODE = 451
317
+ _GENERAL_STATUS_CODE = status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS
311
318
 
312
319
 
313
320
  class ServerError(HTTPServiceError):
314
- _GENERAL_EXTRACT_EXC_INFO = True
315
321
  _GENERAL_SEVERITY = Severity.HIGH
322
+ _GENERAL_EXTRACT_EXC_INFO = True
316
323
 
317
324
 
318
325
  class InternalServerError(ServerError):
319
- _GENERAL_STATUS_CODE = 500
326
+ _GENERAL_STATUS_CODE = status.HTTP_500_INTERNAL_SERVER_ERROR
320
327
 
321
328
 
322
329
  class NotImplemented(ServerError):
323
- _GENERAL_STATUS_CODE = 501
330
+ _GENERAL_STATUS_CODE = status.HTTP_501_NOT_IMPLEMENTED
324
331
 
325
332
 
326
333
  class BadGateway(ServerError):
327
- _GENERAL_STATUS_CODE = 502
334
+ _GENERAL_STATUS_CODE = status.HTTP_502_BAD_GATEWAY
328
335
 
329
336
 
330
337
  class ServiceUnavailable(ServerError):
331
- _GENERAL_STATUS_CODE = 503
338
+ _GENERAL_STATUS_CODE = status.HTTP_503_SERVICE_UNAVAILABLE
332
339
 
333
340
 
334
341
  class GatewayTimeout(ServerError):
335
- _GENERAL_STATUS_CODE = 504
342
+ _GENERAL_STATUS_CODE = status.HTTP_504_GATEWAY_TIMEOUT
336
343
 
337
344
 
338
345
  class HTTPVersionNotSupported(ServerError):
339
- _GENERAL_STATUS_CODE = 505
346
+ _GENERAL_STATUS_CODE = status.HTTP_505_HTTP_VERSION_NOT_SUPPORTED
340
347
 
341
348
 
342
349
  class VariantAlsoNegotiates(ServerError):
343
- _GENERAL_STATUS_CODE = 506
350
+ _GENERAL_STATUS_CODE = status.HTTP_506_VARIANT_ALSO_NEGOTIATES
344
351
 
345
352
 
346
353
  class InsufficientStorage(ServerError):
347
- _GENERAL_STATUS_CODE = 507
354
+ _GENERAL_STATUS_CODE = status.HTTP_507_INSUFFICIENT_STORAGE
348
355
 
349
356
 
350
357
  class LoopDetected(ServerError):
351
- _GENERAL_STATUS_CODE = 508
358
+ _GENERAL_STATUS_CODE = status.HTTP_508_LOOP_DETECTED
352
359
 
353
360
 
354
361
  class NotExtended(ServerError):
355
- _GENERAL_STATUS_CODE = 510
362
+ _GENERAL_STATUS_CODE = status.HTTP_510_NOT_EXTENDED
356
363
 
357
364
 
358
365
  class NetworkAuthenticationRequired(ServerError):
359
- _GENERAL_STATUS_CODE = 511
366
+ _GENERAL_STATUS_CODE = status.HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
360
367
 
361
368
 
362
369
  class RabbitMQServiceException(ServiceException):
363
370
  _GENERAL_SEVERITY = Severity.HIGH
364
- _GENERAL_CODE = None
371
+ _GENERAL_EXTRACT_EXC_INFO = True
372
+ _GENERAL_CODE: int | None = None
365
373
 
366
374
  def __init__(
367
375
  self,
@@ -385,8 +393,10 @@ class RabbitMQServiceException(ServiceException):
385
393
 
386
394
  if code is not None:
387
395
  self._code = code
396
+ elif self._GENERAL_CODE is not None:
397
+ self._code = self._GENERAL_CODE
388
398
  else:
389
- self._code = self._GENERAL_CODE or 0
399
+ self._code = 0
390
400
 
391
401
  self._data = data
392
402
 
qena_shared_lib/http.py CHANGED
@@ -2,23 +2,75 @@ from dataclasses import dataclass
2
2
  from enum import Enum
3
3
  from typing import Any, Callable, Sequence, TypeVar
4
4
 
5
- from fastapi import APIRouter, Response
5
+ from fastapi import (
6
+ APIRouter,
7
+ BackgroundTasks,
8
+ Body,
9
+ Cookie,
10
+ File,
11
+ Form,
12
+ Header,
13
+ HTTPException,
14
+ Path,
15
+ Query,
16
+ Request,
17
+ Response,
18
+ Security,
19
+ UploadFile,
20
+ WebSocket,
21
+ WebSocketDisconnect,
22
+ WebSocketException,
23
+ status,
24
+ )
6
25
  from fastapi.datastructures import Default
7
26
  from fastapi.params import Depends
8
- from fastapi.responses import JSONResponse
27
+ from fastapi.responses import (
28
+ FileResponse,
29
+ HTMLResponse,
30
+ JSONResponse,
31
+ PlainTextResponse,
32
+ RedirectResponse,
33
+ StreamingResponse,
34
+ )
9
35
  from fastapi.types import IncEx
10
36
 
11
37
  __all__ = [
12
38
  "api_controller",
39
+ "APIRouter",
40
+ "BackgroundTasks",
41
+ "Body",
13
42
  "ControllerBase",
43
+ "Cookie",
14
44
  "delete",
45
+ "Depends",
46
+ "File",
47
+ "FileResponse",
48
+ "Form",
15
49
  "get",
16
50
  "head",
51
+ "Header",
52
+ "HTMLResponse",
53
+ "HTTPException",
54
+ "JSONResponse",
17
55
  "options",
18
56
  "patch",
57
+ "Path",
58
+ "PlainTextResponse",
19
59
  "post",
20
60
  "put",
61
+ "Query",
62
+ "RedirectResponse",
63
+ "Request",
64
+ "Response",
65
+ "Response",
66
+ "Security",
67
+ "status",
68
+ "StreamingResponse",
21
69
  "trace",
70
+ "UploadFile",
71
+ "WebSocket",
72
+ "WebSocketDisconnect",
73
+ "WebSocketException",
22
74
  ]
23
75
 
24
76
  AC = TypeVar("AC")
@@ -73,7 +125,9 @@ def api_controller(
73
125
  deprecated: bool | None = None,
74
126
  include_in_schema: bool = True,
75
127
  ) -> Callable[[type["ControllerBase"]], type["ControllerBase"]]:
76
- def annotate_class(api_controller_class: type[ControllerBase]):
128
+ def annotate_class(
129
+ api_controller_class: type[ControllerBase],
130
+ ) -> type[ControllerBase]:
77
131
  router = APIRouter(
78
132
  prefix=prefix or "",
79
133
  tags=tags,
@@ -18,7 +18,7 @@ ROOT_LOGGER_NAME = environ.get("LOGGER_NAME") or "qena_shared_lib"
18
18
 
19
19
 
20
20
  class LoggerProvider:
21
- def __init__(self):
21
+ def __init__(self) -> None:
22
22
  logger = self.get_logger()
23
23
 
24
24
  logger.setLevel(INFO)
@@ -55,9 +55,9 @@ class LoggerProvider:
55
55
 
56
56
  def _check_handler(
57
57
  self, handlers: list[type[Handler]], logger: Optional[Logger] = None
58
- ):
58
+ ) -> None:
59
59
  if logger is None:
60
- return handlers
60
+ return
61
61
 
62
62
  handlers.extend([handler.__class__ for handler in logger.handlers])
63
63
  self._check_handler(handlers=handlers, logger=logger.parent)