qena-shared-lib 0.1.17__py3-none-any.whl → 0.1.19__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.
Files changed (46) hide show
  1. qena_shared_lib/__init__.py +20 -2
  2. qena_shared_lib/alias.py +27 -0
  3. qena_shared_lib/application.py +4 -4
  4. qena_shared_lib/background.py +9 -7
  5. qena_shared_lib/cache.py +61 -0
  6. qena_shared_lib/enums.py +8 -0
  7. qena_shared_lib/eventbus.py +373 -0
  8. qena_shared_lib/exception_handling.py +409 -0
  9. qena_shared_lib/exceptions.py +167 -57
  10. qena_shared_lib/http/__init__.py +110 -0
  11. qena_shared_lib/{http.py → http/_base.py} +36 -36
  12. qena_shared_lib/http/_exception_handlers.py +202 -0
  13. qena_shared_lib/http/_request.py +24 -0
  14. qena_shared_lib/http/_response.py +24 -0
  15. qena_shared_lib/kafka/__init__.py +21 -0
  16. qena_shared_lib/kafka/_base.py +233 -0
  17. qena_shared_lib/kafka/_consumer.py +597 -0
  18. qena_shared_lib/kafka/_exception_handlers.py +124 -0
  19. qena_shared_lib/kafka/_producer.py +133 -0
  20. qena_shared_lib/logging.py +17 -13
  21. qena_shared_lib/mongodb.py +575 -0
  22. qena_shared_lib/rabbitmq/__init__.py +6 -6
  23. qena_shared_lib/rabbitmq/_base.py +68 -132
  24. qena_shared_lib/rabbitmq/_channel.py +2 -4
  25. qena_shared_lib/rabbitmq/_exception_handlers.py +69 -142
  26. qena_shared_lib/rabbitmq/_listener.py +245 -180
  27. qena_shared_lib/rabbitmq/_publisher.py +5 -5
  28. qena_shared_lib/rabbitmq/_rpc_client.py +21 -22
  29. qena_shared_lib/rabbitmq/message/__init__.py +19 -0
  30. qena_shared_lib/rabbitmq/message/_inbound.py +13 -0
  31. qena_shared_lib/rabbitmq/message/_outbound.py +13 -0
  32. qena_shared_lib/redis.py +47 -0
  33. qena_shared_lib/remotelogging/_base.py +34 -28
  34. qena_shared_lib/remotelogging/logstash/_base.py +3 -2
  35. qena_shared_lib/remotelogging/logstash/_http_sender.py +2 -4
  36. qena_shared_lib/remotelogging/logstash/_tcp_sender.py +2 -2
  37. qena_shared_lib/scheduler.py +24 -15
  38. qena_shared_lib/security.py +39 -32
  39. qena_shared_lib/sync.py +91 -0
  40. qena_shared_lib/utils.py +13 -11
  41. {qena_shared_lib-0.1.17.dist-info → qena_shared_lib-0.1.19.dist-info}/METADATA +395 -32
  42. qena_shared_lib-0.1.19.dist-info/RECORD +50 -0
  43. qena_shared_lib-0.1.19.dist-info/WHEEL +4 -0
  44. qena_shared_lib/exception_handlers.py +0 -235
  45. qena_shared_lib-0.1.17.dist-info/RECORD +0 -31
  46. qena_shared_lib-0.1.17.dist-info/WHEEL +0 -4
@@ -0,0 +1,409 @@
1
+ from asyncio import Future, Task, iscoroutinefunction
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ from functools import partial
5
+ from typing import Any, Callable, TypeVar, cast
6
+
7
+ from prometheus_client import Counter
8
+ from punq import Container, Scope
9
+ from pydantic import ValidationError
10
+ from pydantic.alias_generators import to_snake
11
+ from typing_extensions import Self
12
+
13
+ from .exceptions import (
14
+ HTTPServiceError,
15
+ RabbitMQServiceException,
16
+ ServiceException,
17
+ Severity,
18
+ )
19
+ from .logging import LoggerFactory
20
+ from .remotelogging import BaseRemoteLogSender
21
+ from .utils import AsyncEventLoopMixin
22
+
23
+ __all__ = [
24
+ "AbstractServiceExceptionHandler",
25
+ "ExceptionHandlerServiceType",
26
+ "ExceptionHandlingManager",
27
+ "GeneralExceptionHandler",
28
+ "ServiceContext",
29
+ "ServiceInformation",
30
+ "ServiceExceptionHandler",
31
+ "ValidationErrorHandler",
32
+ ]
33
+
34
+
35
+ ServiceContextDataType = TypeVar("ServiceContextDataType")
36
+
37
+
38
+ class ExceptionHandlerServiceType(str, Enum):
39
+ RABBIT_MQ = "RABBITMQ"
40
+ HTTP = "HTTP"
41
+ KAFKA = "KAFKA"
42
+
43
+
44
+ class ServiceContext:
45
+ def add_data(
46
+ self,
47
+ data_type: type[ServiceContextDataType],
48
+ value: ServiceContextDataType,
49
+ ) -> Self:
50
+ if getattr(self, "_data", None):
51
+ self._data = {}
52
+
53
+ self._data[data_type] = value
54
+
55
+ return self
56
+
57
+ def get_data(
58
+ self, data_type: type[ServiceContextDataType]
59
+ ) -> ServiceContextDataType | None:
60
+ if getattr(self, "_data", None) is None:
61
+ return None
62
+
63
+ return cast(
64
+ dict[type[ServiceContextDataType], ServiceContextDataType],
65
+ self._data,
66
+ )[data_type]
67
+
68
+ def set_labels(self, labels: dict[str, Any]) -> Self:
69
+ self._labels = labels
70
+
71
+ return self
72
+
73
+ def get_labels(self) -> dict[str, Any]:
74
+ if getattr(self, "_labels", None) is None:
75
+ raise ValueError("service context labels not set")
76
+
77
+ return self._labels
78
+
79
+
80
+ @dataclass
81
+ class ServiceInformation:
82
+ service_type: ExceptionHandlerServiceType
83
+ tags: list[str]
84
+ extra: dict[str, str]
85
+ message: str | None = None
86
+
87
+
88
+ class AbstractServiceExceptionHandler:
89
+ @property
90
+ def exception(self) -> type[Exception]:
91
+ raise NotImplementedError()
92
+
93
+ def handle(
94
+ self, service_information: ServiceInformation, exception: BaseException
95
+ ) -> None:
96
+ del service_information, exception
97
+
98
+ raise NotImplementedError()
99
+
100
+
101
+ @dataclass
102
+ class ExceptionHandlerMetadata:
103
+ exception_handler: AbstractServiceExceptionHandler
104
+
105
+ def __post_init__(self) -> None:
106
+ self._is_async_exception_handler = self._check_async_exception_handler(
107
+ self.exception_handler
108
+ )
109
+
110
+ def _check_async_exception_handler(
111
+ self, exception_handler: AbstractServiceExceptionHandler
112
+ ) -> bool:
113
+ exception_handler_callable = getattr(
114
+ exception_handler, "__call__", None
115
+ )
116
+
117
+ if exception_handler_callable is None:
118
+ raise RuntimeError(
119
+ "exception handler has no `__call__(ServiceContext, BaseException)` method"
120
+ )
121
+
122
+ return iscoroutinefunction(exception_handler_callable)
123
+
124
+ @property
125
+ def is_async_listener(self) -> bool:
126
+ return self._is_async_exception_handler
127
+
128
+
129
+ class ExceptionHandlingManager(AsyncEventLoopMixin):
130
+ _HANDLER_EXCEPTIONS_COUNTER_METRICS: dict[
131
+ ExceptionHandlerServiceType, Counter
132
+ ] = {}
133
+
134
+ def __init__(
135
+ self,
136
+ service_type: ExceptionHandlerServiceType,
137
+ container: Container,
138
+ remote_logger: BaseRemoteLogSender,
139
+ label_name: list[str],
140
+ ):
141
+ self._service_type = service_type
142
+ self._container = container
143
+ self._exception_handlers: dict[
144
+ type[Exception], ExceptionHandlerMetadata
145
+ ] = {}
146
+ self._remote_logger = remote_logger
147
+ self._exception_handling_done_hook: (
148
+ Callable[[ServiceContext], None] | None
149
+ ) = None
150
+
151
+ if service_type not in self._HANDLER_EXCEPTIONS_COUNTER_METRICS:
152
+ self._HANDLER_EXCEPTIONS_COUNTER_METRICS[service_type] = Counter(
153
+ name=f"{to_snake(service_type.name)}_handled_exceptions",
154
+ documentation=f"{service_type.name.capitalize()} handled exceptions",
155
+ labelnames=label_name,
156
+ )
157
+
158
+ def set_exception_handlers(
159
+ self, *exception_handlers: type[AbstractServiceExceptionHandler]
160
+ ) -> None:
161
+ for index, exception_handler in enumerate(exception_handlers):
162
+ if not isinstance(exception_handler, type) or not issubclass(
163
+ exception_handler, AbstractServiceExceptionHandler
164
+ ):
165
+ raise TypeError(
166
+ f"exception handler {index} is {type(exception_handler)}, expected instance of type or subclass of `AbstractServiceExceptionHandler`"
167
+ )
168
+
169
+ self._container.register(
170
+ service=AbstractServiceExceptionHandler,
171
+ factory=exception_handler,
172
+ scope=Scope.singleton,
173
+ )
174
+
175
+ def set_exception_handling_done_hook(
176
+ self, exception_handling_done_hook: Callable[[ServiceContext], None]
177
+ ) -> None:
178
+ if not callable(exception_handling_done_hook):
179
+ raise ValueError("`exception_handler_done_hook` is not a callable")
180
+
181
+ self._exception_handling_done_hook = exception_handling_done_hook
182
+
183
+ def resolve_exception_handlers(self) -> None:
184
+ for exception_handler in self._container.resolve_all(
185
+ AbstractServiceExceptionHandler
186
+ ):
187
+ exception_handler = cast(
188
+ AbstractServiceExceptionHandler, exception_handler
189
+ )
190
+
191
+ if not callable(exception_handler):
192
+ raise ValueError(
193
+ f"exception handler {exception_handler.__class__.__name__} is not callable"
194
+ )
195
+
196
+ self._exception_handlers[exception_handler.exception] = (
197
+ ExceptionHandlerMetadata(exception_handler)
198
+ )
199
+
200
+ def submit_exception(
201
+ self,
202
+ context: ServiceContext,
203
+ exception: BaseException,
204
+ ) -> bool:
205
+ exception_handler_metadata = None
206
+
207
+ for exception_type in type(exception).mro():
208
+ exception_handler_metadata = self._exception_handlers.get(
209
+ exception_type
210
+ )
211
+
212
+ if exception_handler_metadata is not None:
213
+ break
214
+
215
+ if exception_handler_metadata is None:
216
+ return False
217
+
218
+ assert callable(exception_handler_metadata.exception_handler)
219
+
220
+ if exception_handler_metadata.is_async_listener:
221
+ self.loop.create_task(
222
+ exception_handler_metadata.exception_handler(context, exception)
223
+ ).add_done_callback(
224
+ partial(self._on_exception_handler_done, context)
225
+ )
226
+ else:
227
+ self.loop.run_in_executor(
228
+ executor=None,
229
+ func=partial(
230
+ exception_handler_metadata.exception_handler,
231
+ context,
232
+ exception,
233
+ ),
234
+ ).add_done_callback(
235
+ partial(self._on_exception_handler_done, context)
236
+ )
237
+
238
+ self._HANDLER_EXCEPTIONS_COUNTER_METRICS[self._service_type].labels(
239
+ *context.get_labels()
240
+ ).inc()
241
+
242
+ return True
243
+
244
+ def _on_exception_handler_done(
245
+ self, context: ServiceContext, task_or_future: Task[Any] | Future[Any]
246
+ ) -> None:
247
+ if task_or_future.cancelled():
248
+ return
249
+
250
+ exception = task_or_future.exception()
251
+ service_information = context.get_data(ServiceInformation)
252
+
253
+ if service_information is not None:
254
+ service_type = service_information.service_type.name.lower()
255
+ tags = service_information.tags
256
+ extra = service_information.extra
257
+ else:
258
+ service_type = "unknown"
259
+ tags = ["exception_handling"]
260
+ extra = {"serviceType": "exception_handling"}
261
+
262
+ if exception is not None:
263
+ self._remote_logger.error(
264
+ message=f"error occured in {service_type} service exception handler",
265
+ tags=tags,
266
+ extra=extra,
267
+ exception=exception,
268
+ )
269
+
270
+ if self._exception_handling_done_hook is None:
271
+ return
272
+
273
+ try:
274
+ self._exception_handling_done_hook(context)
275
+ except:
276
+ tags.append("exception_handler_done_hook")
277
+ self._remote_logger.exception(
278
+ message="error occured while executing `exception_handler_done_hook`",
279
+ tags=tags,
280
+ extra=extra,
281
+ )
282
+
283
+
284
+ EXCEPTION_HANDLING_LOGGER_NAME = "exception_handling"
285
+
286
+
287
+ class ServiceExceptionHandler(AbstractServiceExceptionHandler):
288
+ @property
289
+ def exception(self) -> type[Exception]:
290
+ return cast(type[Exception], ServiceException)
291
+
292
+ def __init__(self, remote_logger: BaseRemoteLogSender):
293
+ self._logger = LoggerFactory.get_logger(EXCEPTION_HANDLING_LOGGER_NAME)
294
+ self._remote_logger = remote_logger
295
+
296
+ def handle(
297
+ self,
298
+ service_information: ServiceInformation,
299
+ exception: BaseException,
300
+ ) -> None:
301
+ if not isinstance(exception, ServiceException):
302
+ self._logger.warning(
303
+ "%s cannot be handled by handler", exception.__class__.__name__
304
+ )
305
+
306
+ return
307
+
308
+ match exception:
309
+ case HTTPServiceError() as http_service_error:
310
+ if http_service_error.status_code is not None:
311
+ str_status_code = str(http_service_error.status_code)
312
+ service_information.extra["statusCode"] = str_status_code
313
+
314
+ service_information.tags.append(str_status_code)
315
+
316
+ if http_service_error.response_code is not None:
317
+ str_response_code = str(http_service_error.response_code)
318
+ service_information.extra["responseCode"] = (
319
+ str_response_code
320
+ )
321
+
322
+ service_information.tags.append(str_response_code)
323
+ case RabbitMQServiceException() as rabbitmq_service_exception:
324
+ str_error_code = str(rabbitmq_service_exception.code)
325
+ service_information.extra["code"] = str_error_code
326
+
327
+ service_information.tags.append(str_error_code)
328
+
329
+ if exception.tags:
330
+ service_information.tags.extend(exception.tags)
331
+
332
+ if exception.extra:
333
+ service_information.extra.update(exception.extra)
334
+
335
+ exc_info = (
336
+ (type(exception), exception, exception.__traceback__)
337
+ if exception.extract_exc_info
338
+ else None
339
+ )
340
+
341
+ match exception.severity:
342
+ case Severity.HIGH:
343
+ remote_logger_method = self._remote_logger.error
344
+ logger_method = self._logger.error
345
+ case Severity.MEDIUM:
346
+ remote_logger_method = self._remote_logger.warning
347
+ logger_method = self._logger.warning
348
+ case _:
349
+ remote_logger_method = self._remote_logger.info
350
+ logger_method = self._logger.info
351
+
352
+ if exception.remote_logging:
353
+ remote_logger_method(
354
+ message=service_information.message or exception.message,
355
+ tags=service_information.tags,
356
+ extra=service_information.extra,
357
+ exception=exception if exception.extract_exc_info else None,
358
+ )
359
+ else:
360
+ logger_method(
361
+ "[service_type = `%s`] `%s`",
362
+ service_information.service_type.name.lower(),
363
+ service_information.message or exception.message,
364
+ exc_info=exc_info,
365
+ )
366
+
367
+
368
+ class ValidationErrorHandler(AbstractServiceExceptionHandler):
369
+ @property
370
+ def exception(self) -> type[Exception]:
371
+ return cast(type[Exception], ValidationError)
372
+
373
+ def __init__(self, remote_logger: BaseRemoteLogSender):
374
+ self._remote_logger = remote_logger
375
+
376
+ def handle(
377
+ self,
378
+ service_information: ServiceInformation,
379
+ exception: ValidationError,
380
+ ) -> None:
381
+ self._remote_logger.error(
382
+ message=service_information.message
383
+ or f"invalid request data for {service_information.service_type.name.lower()} service",
384
+ tags=service_information.tags,
385
+ extra=service_information.extra,
386
+ exception=exception,
387
+ )
388
+
389
+
390
+ class GeneralExceptionHandler(AbstractServiceExceptionHandler):
391
+ @property
392
+ def exception(self) -> type[Exception]:
393
+ return Exception
394
+
395
+ def __init__(self, remote_logger: BaseRemoteLogSender):
396
+ self._remote_logger = remote_logger
397
+
398
+ def handle(
399
+ self,
400
+ service_information: ServiceInformation,
401
+ exception: BaseException,
402
+ ) -> None:
403
+ self._remote_logger.error(
404
+ message=service_information.message
405
+ or f"something went wrong while processing data for {service_information.service_type.name.lower()} service",
406
+ tags=service_information.tags,
407
+ extra=service_information.extra,
408
+ exception=exception,
409
+ )