qena-shared-lib 0.1.12__py3-none-any.whl → 0.1.14__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,15 @@
1
+ from . import logstash
2
+ from ._base import (
3
+ BaseRemoteLogSender,
4
+ LogLevel,
5
+ RemoteLogRecord,
6
+ SenderResponse,
7
+ )
8
+
9
+ __all__ = [
10
+ "BaseRemoteLogSender",
11
+ "LogLevel",
12
+ "logstash",
13
+ "RemoteLogRecord",
14
+ "SenderResponse",
15
+ ]
@@ -17,9 +17,9 @@ from ..logging import LoggerProvider
17
17
  from ..utils import AsyncEventLoopMixin
18
18
 
19
19
  __all__ = [
20
- "BaseLogstashSender",
20
+ "BaseRemoteLogSender",
21
21
  "LogLevel",
22
- "LogstashLogRecord",
22
+ "RemoteLogRecord",
23
23
  ]
24
24
 
25
25
 
@@ -30,7 +30,7 @@ class LogLevel(Enum):
30
30
  ERROR = 3
31
31
 
32
32
 
33
- class LogstashLogRecord:
33
+ class RemoteLogRecord:
34
34
  def __init__(
35
35
  self,
36
36
  message: str,
@@ -139,8 +139,9 @@ class LogstashLogRecord:
139
139
 
140
140
  def __repr__(self) -> str:
141
141
  return (
142
- "LogstashLogRecord (\n\tlevel : `%s`,\n\tmessage : `%s`,\n\ttags : %s,\n\tlabel : %s,\n\terror_type : `%s`,\n\terror_message: `%s`\n)%s"
142
+ "%s (\n\tlevel : `%s`,\n\tmessage : `%s`,\n\ttags : %s,\n\tlabel : %s,\n\terror_type : `%s`,\n\terror_message: `%s`\n)%s"
143
143
  % (
144
+ self.__class__.__name__,
144
145
  self._log_level.name,
145
146
  self._message,
146
147
  self._tags or [],
@@ -161,31 +162,6 @@ class LogstashLogRecord:
161
162
  def log_retries(self, log_retries: int) -> None:
162
163
  self._log_retries = log_retries
163
164
 
164
- def to_dict(self) -> dict:
165
- log: dict[str, str | list | dict[str, str]] = {
166
- "message": self._message,
167
- "service.name": self._service_name,
168
- "log.level": self._log_level.name.lower(),
169
- "log.logger": self._log_logger,
170
- }
171
-
172
- if self._tags is not None:
173
- log["tags"] = self._tags
174
-
175
- if self._labels is not None:
176
- log["labels"] = self._labels
177
-
178
- if self._error_type is not None:
179
- log["error.type"] = self._error_type
180
-
181
- if self._error_message is not None:
182
- log["error.message"] = self._error_message
183
-
184
- if self._error_stack_trace is not None:
185
- log["error.stack_trace"] = self._error_stack_trace
186
-
187
- return log
188
-
189
165
 
190
166
  @dataclass
191
167
  class SenderResponse:
@@ -198,20 +174,20 @@ class EndOfLogMarker:
198
174
  pass
199
175
 
200
176
 
201
- class BaseLogstashSender(AsyncEventLoopMixin):
202
- LOGSTASH_LOGS = Counter(
203
- name="successful_logstash_logs",
204
- documentation="Successfully sent logstash log count",
177
+ class BaseRemoteLogSender(AsyncEventLoopMixin):
178
+ REMOTE_LOGS = Counter(
179
+ name="successful_remote_logs",
180
+ documentation="Successfully sent remote log count",
205
181
  labelnames=["log_level"],
206
182
  )
207
- FAILED_LOGSTASH_LOGS = Counter(
208
- name="failed_logstash_logs",
209
- documentation="Failed logstash log count",
183
+ FAILED_REMOTE_LOGS = Counter(
184
+ name="failed_remote_logs",
185
+ documentation="Failed remote log count",
210
186
  labelnames=["log_level", "exception"],
211
187
  )
212
- LOGSTASH_SENDER_STATE = PrometheusEnum(
213
- name="logstash_sender_state",
214
- documentation="Logstash sender state",
188
+ REMOTE_SENDER_STATE = PrometheusEnum(
189
+ name="remote_sender_state",
190
+ documentation="Remote sender state",
215
191
  states=["running", "stopped"],
216
192
  )
217
193
 
@@ -222,25 +198,27 @@ class BaseLogstashSender(AsyncEventLoopMixin):
222
198
  log_queue_size: int = 1000,
223
199
  failed_log_queue_size: int = 1000,
224
200
  ) -> None:
225
- self._sender = f"qena_shared_lib.logstash.{self.__class__.__name__}"
201
+ self._sender = (
202
+ f"qena_shared_lib.remotelogging.{self.__class__.__name__}"
203
+ )
226
204
  self._service_name = service_name
227
205
  self._max_log_retry = max_log_retry
228
206
  self._started = False
229
207
  self._closed = False
230
- self._log_queue: Queue[LogstashLogRecord | EndOfLogMarker] = Queue(
208
+ self._log_queue: Queue[RemoteLogRecord | EndOfLogMarker] = Queue(
231
209
  log_queue_size
232
210
  )
233
- self._dead_letter_log_queue: Queue[
234
- LogstashLogRecord | EndOfLogMarker
235
- ] = Queue(failed_log_queue_size)
211
+ self._dead_letter_log_queue: Queue[RemoteLogRecord | EndOfLogMarker] = (
212
+ Queue(failed_log_queue_size)
213
+ )
236
214
  self._level = LogLevel.INFO
237
215
  self._logger = LoggerProvider.default().get_logger(
238
- f"logstash.{self.__class__.__name__.lower()}"
216
+ f"remotelogging.{self.__class__.__name__.lower()}"
239
217
  )
240
218
 
241
219
  async def start(self) -> None:
242
220
  if self._started:
243
- raise RuntimeError("logstash sender already started")
221
+ raise RuntimeError("remote sender already started")
244
222
 
245
223
  self._started = True
246
224
  self._closed = False
@@ -254,10 +232,10 @@ class BaseLogstashSender(AsyncEventLoopMixin):
254
232
  self._on_log_flusher_closed
255
233
  )
256
234
  self._logger.info(
257
- "logstash logger `%s` started accepting logs",
235
+ "remote logger `%s` started accepting logs",
258
236
  self.__class__.__name__,
259
237
  )
260
- self.LOGSTASH_SENDER_STATE.state("running")
238
+ self.REMOTE_SENDER_STATE.state("running")
261
239
 
262
240
  def _hook_on_start(self) -> None:
263
241
  pass
@@ -265,7 +243,7 @@ class BaseLogstashSender(AsyncEventLoopMixin):
265
243
  async def _hook_on_start_async(self) -> None:
266
244
  pass
267
245
 
268
- def _on_log_flusher_closed(self, task: Task) -> None:
246
+ def _on_log_flusher_closed(self, task: Task[None]) -> None:
269
247
  if task.cancelled():
270
248
  self._close_future.set_result(None)
271
249
 
@@ -283,13 +261,13 @@ class BaseLogstashSender(AsyncEventLoopMixin):
283
261
  self._hook_on_stop_async(),
284
262
  ).add_done_callback(self._on_close_hook_done)
285
263
 
286
- def _on_close_hook_done(self, task_or_future: Task | Future) -> None:
287
- if task_or_future.cancelled():
264
+ def _on_close_hook_done(self, future: Future[tuple[None, None]]) -> None:
265
+ if future.cancelled():
288
266
  self._close_future.set_result(None)
289
267
 
290
268
  return
291
269
 
292
- exception = task_or_future.exception()
270
+ exception = future.exception()
293
271
 
294
272
  if exception is not None:
295
273
  self._close_future.set_exception(exception)
@@ -298,12 +276,12 @@ class BaseLogstashSender(AsyncEventLoopMixin):
298
276
 
299
277
  self._close_future.set_result(None)
300
278
  self._logger.debug(
301
- "logstash http logger closed, will no longer accept logs"
279
+ "remote http logger closed, will no longer accept logs"
302
280
  )
303
281
 
304
- def stop(self) -> Future:
282
+ def stop(self) -> Future[None]:
305
283
  if self._closed:
306
- raise RuntimeError("logstash sender already closed")
284
+ raise RuntimeError("remote sender already closed")
307
285
 
308
286
  self._closed = True
309
287
  self._started = False
@@ -318,10 +296,10 @@ class BaseLogstashSender(AsyncEventLoopMixin):
318
296
 
319
297
  return self._close_future
320
298
 
321
- def _on_close_future_done(self, future: Future) -> None:
299
+ def _on_close_future_done(self, future: Future[None]) -> None:
322
300
  del future
323
301
 
324
- self.LOGSTASH_SENDER_STATE.state("stopped")
302
+ self.REMOTE_SENDER_STATE.state("stopped")
325
303
 
326
304
  def _hook_on_stop(self) -> None:
327
305
  pass
@@ -340,7 +318,7 @@ class BaseLogstashSender(AsyncEventLoopMixin):
340
318
  if not self._dead_letter_log_queue.empty():
341
319
  log = await self._dead_letter_log_queue.get()
342
320
 
343
- if isinstance(log, LogstashLogRecord):
321
+ if isinstance(log, RemoteLogRecord):
344
322
  if log.log_retries >= self._max_log_retry:
345
323
  self._logger.exception(
346
324
  "failed to send log too many times, falling back to stdout or stderr. \n%r",
@@ -368,9 +346,9 @@ class BaseLogstashSender(AsyncEventLoopMixin):
368
346
  except Exception as e:
369
347
  self._put_to_dead_letter_log_queue(log)
370
348
  self._logger.exception(
371
- "error occurred while sending log to logstash"
349
+ "error occurred while sending log to remote logging facility"
372
350
  )
373
- self.FAILED_LOGSTASH_LOGS.labels(
351
+ self.FAILED_REMOTE_LOGS.labels(
374
352
  log_level=log.log_level.name, exception=e.__class__.__name__
375
353
  ).inc()
376
354
 
@@ -393,15 +371,17 @@ class BaseLogstashSender(AsyncEventLoopMixin):
393
371
  sender_response.reason or "No reason",
394
372
  )
395
373
  else:
396
- self.LOGSTASH_LOGS.labels(log_level=log.log_level.name).inc()
397
- self._logger.debug("log sent to logstash.\n%r", log)
374
+ self.REMOTE_LOGS.labels(log_level=log.log_level.name).inc()
375
+ self._logger.debug(
376
+ "log sent to remote logging facility.\n%r", log
377
+ )
398
378
 
399
- async def _send(self, log: LogstashLogRecord) -> SenderResponse:
379
+ async def _send(self, log: RemoteLogRecord) -> SenderResponse:
400
380
  del log
401
381
 
402
382
  raise NotImplementedError()
403
383
 
404
- def _put_to_dead_letter_log_queue(self, log: LogstashLogRecord) -> None:
384
+ def _put_to_dead_letter_log_queue(self, log: RemoteLogRecord) -> None:
405
385
  if self._closed:
406
386
  self._logger.error(
407
387
  "%s logger closed, falling back to stdout or stderr.\n%r",
@@ -524,7 +504,7 @@ class BaseLogstashSender(AsyncEventLoopMixin):
524
504
  exception: BaseException | None = None,
525
505
  ) -> None:
526
506
  if self._closed:
527
- self._logger.warning("Logstash http logger is already close")
507
+ self._logger.warning("Remote logger is already close")
528
508
 
529
509
  return
530
510
 
@@ -551,8 +531,8 @@ class BaseLogstashSender(AsyncEventLoopMixin):
551
531
  tags: list[str] | None = None,
552
532
  extra: dict[str, str] | None = None,
553
533
  exception: BaseException | None = None,
554
- ) -> LogstashLogRecord:
555
- log = LogstashLogRecord(
534
+ ) -> RemoteLogRecord:
535
+ log = RemoteLogRecord(
556
536
  message=message,
557
537
  service_name=self._service_name,
558
538
  log_level=level,
@@ -0,0 +1,9 @@
1
+ from ._base import BaseLogstashSender
2
+ from ._http_sender import HTTPSender
3
+ from ._tcp_sender import TCPSender
4
+
5
+ __all__ = [
6
+ "BaseLogstashSender",
7
+ "HTTPSender",
8
+ "TCPSender",
9
+ ]
@@ -0,0 +1,32 @@
1
+ from typing import Any
2
+
3
+ from .._base import BaseRemoteLogSender, RemoteLogRecord
4
+
5
+
6
+ class BaseLogstashSender(BaseRemoteLogSender):
7
+ def remote_log_record_to_ecs(self, log: RemoteLogRecord) -> dict[str, Any]:
8
+ log_dict: dict[str, Any] = {
9
+ "message": log.message,
10
+ "service.name": log.service_name,
11
+ "log.level": log.log_level.name.lower(),
12
+ "log.logger": log.log_logger,
13
+ }
14
+
15
+ if log.tags is not None:
16
+ log_dict["tags"] = log.tags
17
+
18
+ if log.labels is not None:
19
+ log_dict["labels"] = log.labels
20
+
21
+ error_type, error_message, error_stack_trace = log.error
22
+
23
+ if error_type is not None:
24
+ log_dict["error.type"] = error_type
25
+
26
+ if error_message is not None:
27
+ log_dict["error.message"] = error_message
28
+
29
+ if error_stack_trace is not None:
30
+ log_dict["error.stack_trace"] = error_stack_trace
31
+
32
+ return log_dict
@@ -1,7 +1,8 @@
1
1
  from httpx import AsyncClient, Timeout
2
2
 
3
- from ..logging import LoggerProvider
4
- from ._base import BaseLogstashSender, LogstashLogRecord, SenderResponse
3
+ from ...logging import LoggerProvider
4
+ from .._base import RemoteLogRecord, SenderResponse
5
+ from ._base import BaseLogstashSender
5
6
 
6
7
  __all__ = ["HTTPSender"]
7
8
 
@@ -42,10 +43,10 @@ class HTTPSender(BaseLogstashSender):
42
43
  "logstash.httpsender"
43
44
  )
44
45
 
45
- async def _send(self, log: LogstashLogRecord) -> SenderResponse:
46
+ async def _send(self, log: RemoteLogRecord) -> SenderResponse:
46
47
  send_log_response = await self._client.post(
47
48
  url=self._url,
48
- json=log.to_dict(),
49
+ json=self.remote_log_record_to_ecs(log),
49
50
  )
50
51
 
51
52
  if not send_log_response.is_success:
@@ -1,9 +1,11 @@
1
1
  from asyncio import StreamWriter, open_connection
2
+ from typing import Any
2
3
 
3
4
  from pydantic_core import to_json
4
5
 
5
- from ..logging import LoggerProvider
6
- from ._base import BaseLogstashSender, LogstashLogRecord, SenderResponse
6
+ from ...logging import LoggerProvider
7
+ from .._base import RemoteLogRecord, SenderResponse
8
+ from ._base import BaseLogstashSender
7
9
 
8
10
  __all__ = ["TCPSender"]
9
11
 
@@ -28,8 +30,8 @@ class TCPSender(BaseLogstashSender):
28
30
  self._client = AsyncTcpClient(host=host, port=port)
29
31
  self._logger = LoggerProvider.default().get_logger("logstash.tcpsender")
30
32
 
31
- async def _send(self, log: LogstashLogRecord) -> SenderResponse:
32
- await self._client.write(log.to_dict())
33
+ async def _send(self, log: RemoteLogRecord) -> SenderResponse:
34
+ await self._client.write(self.remote_log_record_to_ecs(log))
33
35
 
34
36
  return SenderResponse(sent=True)
35
37
 
@@ -47,7 +49,7 @@ class AsyncTcpClient:
47
49
  self._writer: StreamWriter | None = None
48
50
  self._client_closed = False
49
51
 
50
- async def write(self, json: dict) -> None:
52
+ async def write(self, json: dict[str, Any]) -> None:
51
53
  if self._client_closed:
52
54
  raise RuntimeError("async tcp client already closed")
53
55
 
@@ -1,11 +1,11 @@
1
- from asyncio import Future, Task, iscoroutinefunction, sleep
1
+ from asyncio import Future, Task, gather, iscoroutinefunction, sleep
2
2
  from dataclasses import dataclass
3
3
  from datetime import datetime
4
4
  from functools import partial
5
5
  from importlib import import_module
6
6
  from inspect import signature
7
7
  from os import name as osname
8
- from typing import Any, Callable, TypeVar
8
+ from typing import Any, Callable, TypeVar, cast
9
9
  from zoneinfo import ZoneInfo
10
10
 
11
11
  from cronsim import CronSim
@@ -14,7 +14,7 @@ from punq import Container, Scope
14
14
 
15
15
  from .dependencies.miscellaneous import validate_annotation
16
16
  from .logging import LoggerProvider
17
- from .logstash import BaseLogstashSender
17
+ from .remotelogging import BaseRemoteLogSender
18
18
  from .utils import AsyncEventLoopMixin
19
19
 
20
20
  __all__ = [
@@ -32,7 +32,7 @@ SCHEDULED_TASK_ATTRIBUTE = "__scheduled_task__"
32
32
 
33
33
  @dataclass
34
34
  class ScheduledTask:
35
- task: Callable
35
+ task: Callable[..., None]
36
36
  cron_expression: str
37
37
  zone_info: ZoneInfo | None
38
38
 
@@ -78,8 +78,8 @@ class Scheduler:
78
78
 
79
79
  def schedule(
80
80
  self, cron_expression: str, timezone: str | None = None
81
- ) -> Callable[[Callable], Callable]:
82
- def wrapper(task: Callable) -> Callable:
81
+ ) -> Callable[[Callable[..., None]], Callable[..., None]]:
82
+ def wrapper(task: Callable[..., Any]) -> Callable[..., None]:
83
83
  self.add_task(
84
84
  task=task, cron_expression=cron_expression, timezone=timezone
85
85
  )
@@ -89,7 +89,10 @@ class Scheduler:
89
89
  return wrapper
90
90
 
91
91
  def add_task(
92
- self, task: Callable, cron_expression: str, timezone: str | None
92
+ self,
93
+ task: Callable[..., Any],
94
+ cron_expression: str,
95
+ timezone: str | None,
93
96
  ) -> None:
94
97
  self._scheduled_tasks.append(
95
98
  ScheduledTask(
@@ -121,8 +124,8 @@ class ScheduledTaskMeta:
121
124
 
122
125
  def schedule(
123
126
  cron_expression: str, *, timezone: str | None = None
124
- ) -> Callable[[Callable], Callable]:
125
- def wrapper(task: Callable) -> Callable:
127
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
128
+ def wrapper(task: Callable[..., Any]) -> Callable[..., Any]:
126
129
  setattr(
127
130
  task,
128
131
  SCHEDULED_TASK_ATTRIBUTE,
@@ -145,7 +148,7 @@ class SchedulerBase:
145
148
  f"{self.__class__.__name__} not a scheduler, possibly no annotated with either `Scheduler`"
146
149
  )
147
150
 
148
- return scheduler
151
+ return cast(Scheduler, scheduler)
149
152
 
150
153
  def register_scheduled_tasks(self) -> Scheduler:
151
154
  scheduler = self.get_scheduler()
@@ -186,14 +189,16 @@ class ScheduleManager(AsyncEventLoopMixin):
186
189
 
187
190
  def __init__(
188
191
  self,
189
- logstash: BaseLogstashSender,
192
+ remote_logger: BaseRemoteLogSender,
190
193
  container: Container | None = None,
191
194
  ) -> None:
192
195
  self._container = container or Container()
193
- self._logstash = logstash
196
+ self._remote_logger = remote_logger
194
197
  self._scheduled_tasks: list[ScheduledTask] = []
195
198
  self._next_run_in: int | None = None
196
- self._scheduler_task: Task | None = None
199
+ self._scheduler_task: Task[None] | None = None
200
+ self._scheduled_tasks_or_futures: list[Task[Any] | Future[Any]] = []
201
+ self._stopped = False
197
202
  self._logger = LoggerProvider.default().get_logger("schedule_manager")
198
203
 
199
204
  def include_scheduler(
@@ -260,20 +265,26 @@ class ScheduleManager(AsyncEventLoopMixin):
260
265
  ]:
261
266
  self._scheduled_tasks.extend(scheduler.scheduled_tasks)
262
267
 
263
- def stop(self) -> None:
268
+ async def stop(self) -> None:
269
+ self._stopped = True
270
+
271
+ _ = await gather(
272
+ *self._scheduled_tasks_or_futures, return_exceptions=True
273
+ )
274
+
264
275
  if self._scheduler_task is not None and not self._scheduler_task.done():
265
276
  self._scheduler_task.cancel()
266
277
 
267
278
  self.SCHEDULE_MANAGER_STATE.state("stopped")
268
279
 
269
- def _on_scheduler_done(self, task: Task) -> None:
280
+ def _on_scheduler_done(self, task: Task[None]) -> None:
270
281
  if task.cancelled():
271
282
  return
272
283
 
273
284
  exception = task.exception()
274
285
 
275
286
  if exception is not None:
276
- self._logstash.error(
287
+ self._remote_logger.error(
277
288
  message="error occured in schedule manager",
278
289
  exception=exception,
279
290
  )
@@ -309,7 +320,7 @@ class ScheduleManager(AsyncEventLoopMixin):
309
320
  return True
310
321
 
311
322
  async def _run_scheduler(self) -> None:
312
- while True:
323
+ while not self._stopped:
313
324
  self._calculate_next_schedule()
314
325
  self._logger.debug(
315
326
  "next tasks will be executed after `%d` seconds",
@@ -331,15 +342,23 @@ class ScheduleManager(AsyncEventLoopMixin):
331
342
  continue
332
343
 
333
344
  args = self._resolve_dependencies(scheduled_task)
345
+ scheduled_task_or_future: Task[Any] | Future[Any] | None = None
334
346
 
335
347
  if iscoroutinefunction(scheduled_task.task):
336
- self.loop.create_task(
348
+ scheduled_task_or_future = self.loop.create_task(
337
349
  scheduled_task.task(**args)
338
- ).add_done_callback(self._on_task_done)
350
+ )
339
351
  else:
340
- self.loop.run_in_executor(
341
- executor=None, func=scheduled_task.task
342
- ).add_done_callback(partial(self._on_task_done, **args))
352
+ scheduled_task_or_future = self.loop.run_in_executor(
353
+ executor=None, func=partial(scheduled_task.task, **args)
354
+ )
355
+
356
+ assert scheduled_task_or_future is not None
357
+
358
+ scheduled_task_or_future.add_done_callback(self._on_task_done)
359
+ self._scheduled_tasks_or_futures.append(
360
+ scheduled_task_or_future
361
+ )
343
362
 
344
363
  scheduled_task.ran = True
345
364
 
@@ -356,14 +375,20 @@ class ScheduleManager(AsyncEventLoopMixin):
356
375
 
357
376
  return args
358
377
 
359
- def _on_task_done(self, task_or_future: Task | Future) -> None:
378
+ def _on_task_done(self, task_or_future: Task[Any] | Future[Any]) -> None:
379
+ if (
380
+ not self._stopped
381
+ and task_or_future in self._scheduled_tasks_or_futures
382
+ ):
383
+ self._scheduled_tasks_or_futures.remove(task_or_future)
384
+
360
385
  if task_or_future.cancelled():
361
386
  return
362
387
 
363
388
  exception = task_or_future.exception()
364
389
 
365
390
  if exception is not None:
366
- self._logstash.error(
391
+ self._remote_logger.error(
367
392
  message="error occured while executing task",
368
393
  exception=exception,
369
394
  )
@@ -1,7 +1,7 @@
1
1
  from asyncio import Future
2
2
  from enum import Enum
3
3
  from os import environ
4
- from typing import AbstractSet, Annotated, Any
4
+ from typing import AbstractSet, Annotated, Any, cast
5
5
 
6
6
  from fastapi import Depends, Header
7
7
  from jwt import JWT, AbstractJWKBase, jwk_from_dict
@@ -129,7 +129,7 @@ async def extract_user_info(
129
129
  extract_exc_info=True,
130
130
  ) from e
131
131
 
132
- return user_info
132
+ return cast(UserInfo, user_info)
133
133
 
134
134
 
135
135
  class PermissionMatch(Enum):
qena_shared_lib/utils.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from asyncio import AbstractEventLoop, get_running_loop
2
- from functools import lru_cache
3
2
 
4
3
  from pydantic import TypeAdapter
5
4
 
@@ -7,10 +6,17 @@ __all__ = ["AsyncEventLoopMixin", "TypeAdapterCache"]
7
6
 
8
7
 
9
8
  class AsyncEventLoopMixin:
9
+ _LOOP: AbstractEventLoop | None = None
10
+
10
11
  @property
11
- @lru_cache
12
12
  def loop(self) -> AbstractEventLoop:
13
- return get_running_loop()
13
+ if self._LOOP is None:
14
+ self._LOOP = get_running_loop()
15
+
16
+ return self._LOOP
17
+
18
+ def init(self) -> None:
19
+ self._LOOP = get_running_loop()
14
20
 
15
21
 
16
22
  class TypeAdapterCache: