qena-shared-lib 0.1.0__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.
- qena_shared_lib/__init__.py +27 -0
- qena_shared_lib/application.py +190 -0
- qena_shared_lib/background.py +109 -0
- qena_shared_lib/dependencies/__init__.py +19 -0
- qena_shared_lib/dependencies/http.py +62 -0
- qena_shared_lib/dependencies/miscellaneous.py +35 -0
- qena_shared_lib/exception_handlers.py +165 -0
- qena_shared_lib/exceptions.py +319 -0
- qena_shared_lib/http.py +631 -0
- qena_shared_lib/logging.py +63 -0
- qena_shared_lib/logstash/__init__.py +17 -0
- qena_shared_lib/logstash/_base.py +573 -0
- qena_shared_lib/logstash/_http_sender.py +61 -0
- qena_shared_lib/logstash/_tcp_sender.py +84 -0
- qena_shared_lib/py.typed +0 -0
- qena_shared_lib/rabbitmq/__init__.py +52 -0
- qena_shared_lib/rabbitmq/_base.py +741 -0
- qena_shared_lib/rabbitmq/_channel.py +196 -0
- qena_shared_lib/rabbitmq/_exception_handlers.py +159 -0
- qena_shared_lib/rabbitmq/_exceptions.py +46 -0
- qena_shared_lib/rabbitmq/_listener.py +1292 -0
- qena_shared_lib/rabbitmq/_pool.py +74 -0
- qena_shared_lib/rabbitmq/_publisher.py +73 -0
- qena_shared_lib/rabbitmq/_rpc_client.py +286 -0
- qena_shared_lib/rabbitmq/_utils.py +18 -0
- qena_shared_lib/scheduler.py +402 -0
- qena_shared_lib/security.py +205 -0
- qena_shared_lib/utils.py +28 -0
- qena_shared_lib-0.1.0.dist-info/METADATA +473 -0
- qena_shared_lib-0.1.0.dist-info/RECORD +31 -0
- qena_shared_lib-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,196 @@
|
|
1
|
+
from asyncio import Future
|
2
|
+
from random import uniform
|
3
|
+
from uuid import UUID, uuid4
|
4
|
+
|
5
|
+
from pika.adapters.asyncio_connection import AsyncioConnection
|
6
|
+
from pika.channel import Channel
|
7
|
+
from pika.exceptions import ChannelClosedByClient
|
8
|
+
from pika.spec import Basic
|
9
|
+
|
10
|
+
from ..logging import LoggerProvider
|
11
|
+
from ..utils import AsyncEventLoopMixin
|
12
|
+
|
13
|
+
__all__ = ["BaseChannel"]
|
14
|
+
|
15
|
+
|
16
|
+
class BaseChannel(AsyncEventLoopMixin):
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
connection: AsyncioConnection,
|
20
|
+
reopen_delay: int = 1,
|
21
|
+
reopen_delay_jitter: tuple[float, float] = (0.0, 2.0),
|
22
|
+
failed_reopen_threshold: int | None = 5,
|
23
|
+
):
|
24
|
+
self._connection = connection
|
25
|
+
self._reopen_delay = reopen_delay
|
26
|
+
self._reopen_delay_jitter = reopen_delay_jitter
|
27
|
+
self._failed_reopen_threshold = failed_reopen_threshold
|
28
|
+
self._completion_future = self.loop.create_future()
|
29
|
+
self._channel = None
|
30
|
+
self._channel_id = uuid4()
|
31
|
+
self._reopen_failures = 0
|
32
|
+
self._reserved = False
|
33
|
+
self._can_be_disposed = False
|
34
|
+
self._logger = LoggerProvider.default().get_logger(
|
35
|
+
"rabbitmq.base_channel"
|
36
|
+
)
|
37
|
+
|
38
|
+
def open(self) -> Future[UUID]:
|
39
|
+
if self._channel is not None:
|
40
|
+
raise RuntimeError("channel already opened")
|
41
|
+
|
42
|
+
try:
|
43
|
+
_ = self._connection.channel(
|
44
|
+
channel_number=None, on_open_callback=self._on_channel_opened
|
45
|
+
)
|
46
|
+
except Exception as e:
|
47
|
+
self._can_be_disposed = True
|
48
|
+
|
49
|
+
if not self._completion_future.done():
|
50
|
+
self._completion_future.set_exception(e)
|
51
|
+
|
52
|
+
return self._completion_future
|
53
|
+
|
54
|
+
def reserve(self):
|
55
|
+
self._reserved = True
|
56
|
+
|
57
|
+
def release(self):
|
58
|
+
self._reserved = False
|
59
|
+
|
60
|
+
def _on_channel_opened(self, channel: Channel):
|
61
|
+
self._channel = channel
|
62
|
+
|
63
|
+
try:
|
64
|
+
self._hook_on_channel_opened()
|
65
|
+
except Exception as e:
|
66
|
+
if not self._completion_future.done():
|
67
|
+
self._completion_future.set_exception(e)
|
68
|
+
else:
|
69
|
+
if not self.channel_closed():
|
70
|
+
self._channel.close()
|
71
|
+
|
72
|
+
self._reopen()
|
73
|
+
|
74
|
+
return
|
75
|
+
|
76
|
+
self._channel.add_on_cancel_callback(self._on_cancelled)
|
77
|
+
self._channel.add_on_close_callback(self._on_channel_closed)
|
78
|
+
|
79
|
+
self._completion_future.set_result(self._channel_id)
|
80
|
+
|
81
|
+
def _hook_on_channel_opened(self):
|
82
|
+
pass
|
83
|
+
|
84
|
+
def channel_closed(self) -> bool:
|
85
|
+
if self._channel is None:
|
86
|
+
raise RuntimeError("underlying channel not set")
|
87
|
+
|
88
|
+
return self._channel.is_closing or self._channel.is_closed
|
89
|
+
|
90
|
+
def _on_cancelled(self, method: Basic.Cancel):
|
91
|
+
del method
|
92
|
+
|
93
|
+
if not self._completion_future.done():
|
94
|
+
self._completion_future.set_exception(
|
95
|
+
RuntimeError("cancellation recieved from rabbitmq server")
|
96
|
+
)
|
97
|
+
|
98
|
+
return
|
99
|
+
|
100
|
+
try:
|
101
|
+
self._hook_on_cancelled()
|
102
|
+
except:
|
103
|
+
self._logger.exception("error occured on invoking cancel hook")
|
104
|
+
|
105
|
+
def _hook_on_cancelled(self):
|
106
|
+
pass
|
107
|
+
|
108
|
+
def _on_channel_closed(self, channel: Channel, error: BaseException):
|
109
|
+
del channel
|
110
|
+
|
111
|
+
try:
|
112
|
+
self._hook_on_channel_closed(error)
|
113
|
+
except:
|
114
|
+
self._reopen_failures += 1
|
115
|
+
|
116
|
+
return
|
117
|
+
|
118
|
+
if not isinstance(error, ChannelClosedByClient):
|
119
|
+
self._reopen()
|
120
|
+
|
121
|
+
def _hook_on_channel_closed(self, error: BaseException):
|
122
|
+
del error
|
123
|
+
|
124
|
+
def _reopen(self):
|
125
|
+
if (
|
126
|
+
self._failed_reopen_threshold is not None
|
127
|
+
and self._reopen_failures >= self._failed_reopen_threshold
|
128
|
+
):
|
129
|
+
self._can_be_disposed = True
|
130
|
+
|
131
|
+
return
|
132
|
+
|
133
|
+
self.loop.call_later(
|
134
|
+
delay=self._reopen_delay + uniform(*self._reopen_delay_jitter),
|
135
|
+
callback=self._on_time_to_reopen,
|
136
|
+
)
|
137
|
+
|
138
|
+
def _on_time_to_reopen(self):
|
139
|
+
if self._connection.is_closing or self._connection.is_closed:
|
140
|
+
self._can_be_disposed = True
|
141
|
+
|
142
|
+
return
|
143
|
+
|
144
|
+
try:
|
145
|
+
_ = self._connection.channel(
|
146
|
+
channel_number=None, on_open_callback=self._on_channel_opened
|
147
|
+
)
|
148
|
+
except:
|
149
|
+
self._logger.exception(
|
150
|
+
"coudn't reopen channel %s", self._channel_id
|
151
|
+
)
|
152
|
+
self._reopen()
|
153
|
+
|
154
|
+
def __enter__(self):
|
155
|
+
if not self._reserved:
|
156
|
+
self.reserve()
|
157
|
+
|
158
|
+
if self._channel is None:
|
159
|
+
raise RuntimeError("underlying channel not opened yet")
|
160
|
+
|
161
|
+
return self._channel
|
162
|
+
|
163
|
+
def __exit__(self, *_):
|
164
|
+
self.release()
|
165
|
+
|
166
|
+
@property
|
167
|
+
def channel_id(self) -> UUID:
|
168
|
+
return self._channel_id
|
169
|
+
|
170
|
+
@property
|
171
|
+
def healthy(self) -> bool:
|
172
|
+
return (
|
173
|
+
not self._can_be_disposed
|
174
|
+
and self._channel is not None
|
175
|
+
and not self._channel.is_closing
|
176
|
+
and not self._channel.is_closed
|
177
|
+
)
|
178
|
+
|
179
|
+
@property
|
180
|
+
def reserved(self) -> bool:
|
181
|
+
return self._reserved
|
182
|
+
|
183
|
+
@property
|
184
|
+
def can_be_disposed(self) -> bool:
|
185
|
+
return self._can_be_disposed
|
186
|
+
|
187
|
+
@property
|
188
|
+
def connection(self) -> AsyncioConnection:
|
189
|
+
return self._connection
|
190
|
+
|
191
|
+
@property
|
192
|
+
def channel(self) -> Channel:
|
193
|
+
if self._channel is None:
|
194
|
+
raise RuntimeError("underlying channel not opened yet")
|
195
|
+
|
196
|
+
return self._channel
|
@@ -0,0 +1,159 @@
|
|
1
|
+
from typing import Annotated
|
2
|
+
|
3
|
+
from pydantic import ValidationError
|
4
|
+
|
5
|
+
from ..dependencies.miscellaneous import DependsOn
|
6
|
+
from ..exceptions import ServiceException, Severity
|
7
|
+
from ..logging import LoggerProvider
|
8
|
+
from ..logstash._base import BaseLogstashSender
|
9
|
+
from ._exceptions import RabbitMQException
|
10
|
+
from ._listener import ListenerContext
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
"handle_general_mq_exception",
|
14
|
+
"handle_microservice_exception",
|
15
|
+
"handle_rabbitmq_exception",
|
16
|
+
"handle_validation_error",
|
17
|
+
]
|
18
|
+
|
19
|
+
RABBITMQ_EXCEPTION_HANDLER_LOGGER_NAME = "rabbitmq.exception_handler"
|
20
|
+
|
21
|
+
|
22
|
+
def handle_rabbitmq_exception(
|
23
|
+
context: ListenerContext,
|
24
|
+
exception: RabbitMQException,
|
25
|
+
logstash: Annotated[BaseLogstashSender, DependsOn(BaseLogstashSender)],
|
26
|
+
logger_provider: Annotated[LoggerProvider, DependsOn(LoggerProvider)],
|
27
|
+
):
|
28
|
+
logger = logger_provider.get_logger(RABBITMQ_EXCEPTION_HANDLER_LOGGER_NAME)
|
29
|
+
|
30
|
+
if not exception.logstash_logging:
|
31
|
+
logger.warning("%r", exception)
|
32
|
+
|
33
|
+
return
|
34
|
+
|
35
|
+
logstash.warning(
|
36
|
+
message=exception.message,
|
37
|
+
tags=exception.tags
|
38
|
+
or [
|
39
|
+
"RabbitMQ",
|
40
|
+
"RabbitMQException",
|
41
|
+
str(exception.code),
|
42
|
+
context.queue,
|
43
|
+
context.listener_name or "__default__",
|
44
|
+
],
|
45
|
+
extra=exception.extra
|
46
|
+
or {
|
47
|
+
"serviceType": "RabbitMQ",
|
48
|
+
"queue": context.queue,
|
49
|
+
"listenerName": context.listener_name,
|
50
|
+
"exception": "RabbitMQException",
|
51
|
+
},
|
52
|
+
)
|
53
|
+
|
54
|
+
|
55
|
+
def handle_validation_error(
|
56
|
+
context: ListenerContext,
|
57
|
+
exception: ValidationError,
|
58
|
+
logstash: Annotated[BaseLogstashSender, DependsOn(BaseLogstashSender)],
|
59
|
+
):
|
60
|
+
logstash.error(
|
61
|
+
message=f"invalid rabbitmq request data at queue `{context.queue}` and listener `{context.listener_name}`",
|
62
|
+
tags=[
|
63
|
+
"RabbitMQ",
|
64
|
+
"ValidationError",
|
65
|
+
context.queue,
|
66
|
+
context.listener_name or "__default__",
|
67
|
+
],
|
68
|
+
extra={
|
69
|
+
"serviceType": "RabbitMQ",
|
70
|
+
"queue": context.queue,
|
71
|
+
"listenerName": context.listener_name,
|
72
|
+
"exception": "ValidationError",
|
73
|
+
},
|
74
|
+
exception=exception,
|
75
|
+
)
|
76
|
+
|
77
|
+
|
78
|
+
def handle_microservice_exception(
|
79
|
+
context: ListenerContext,
|
80
|
+
exception: ServiceException,
|
81
|
+
logstash: Annotated[BaseLogstashSender, DependsOn(BaseLogstashSender)],
|
82
|
+
logger_provider: Annotated[LoggerProvider, DependsOn(LoggerProvider)],
|
83
|
+
):
|
84
|
+
logger = logger_provider.get_logger(RABBITMQ_EXCEPTION_HANDLER_LOGGER_NAME)
|
85
|
+
tags = [
|
86
|
+
"RabbitMQ",
|
87
|
+
type(exception).__name__,
|
88
|
+
context.queue,
|
89
|
+
context.listener_name or "__default__",
|
90
|
+
]
|
91
|
+
|
92
|
+
if exception.tags:
|
93
|
+
exception.tags.extend(tags)
|
94
|
+
|
95
|
+
extra = {
|
96
|
+
"serviceType": "RabbitMQ",
|
97
|
+
"queue": context.queue,
|
98
|
+
"listenerName": context.listener_name,
|
99
|
+
"exception": exception.__class__.__name__,
|
100
|
+
}
|
101
|
+
|
102
|
+
if exception.extra:
|
103
|
+
exception.extra.update(extra)
|
104
|
+
|
105
|
+
exc_info = (
|
106
|
+
(type(exception), exception, exception.__traceback__)
|
107
|
+
if exception.extract_exc_info
|
108
|
+
else None
|
109
|
+
)
|
110
|
+
|
111
|
+
match exception.severity:
|
112
|
+
case Severity.HIGH:
|
113
|
+
logstash_logger_method = logstash.error
|
114
|
+
logger_method = logger.error
|
115
|
+
case Severity.MEDIUM:
|
116
|
+
logstash_logger_method = logstash.warning
|
117
|
+
logger_method = logger.warning
|
118
|
+
case _:
|
119
|
+
logstash_logger_method = logstash.info
|
120
|
+
logger_method = logger.info
|
121
|
+
|
122
|
+
if exception.logstash_logging:
|
123
|
+
logstash_logger_method(
|
124
|
+
message=exception.message,
|
125
|
+
tags=exception.tags or tags,
|
126
|
+
extra=exception.extra or extra,
|
127
|
+
exception=exception if exception.extract_exc_info else None,
|
128
|
+
)
|
129
|
+
else:
|
130
|
+
logger_method(
|
131
|
+
"\nRabbitMQ `%s` -> `%s`\n%s",
|
132
|
+
context.queue,
|
133
|
+
context.listener_name,
|
134
|
+
exception.message,
|
135
|
+
exc_info=exc_info,
|
136
|
+
)
|
137
|
+
|
138
|
+
|
139
|
+
def handle_general_mq_exception(
|
140
|
+
context: ListenerContext,
|
141
|
+
exception: Exception,
|
142
|
+
logstash: Annotated[BaseLogstashSender, DependsOn(BaseLogstashSender)],
|
143
|
+
):
|
144
|
+
logstash.error(
|
145
|
+
message=f"something went wrong while consuming message on queue `{context.queue}` and listener `{context.listener_name}`",
|
146
|
+
tags=[
|
147
|
+
"RabbitMQ",
|
148
|
+
exception.__class__.__name__,
|
149
|
+
context.queue,
|
150
|
+
context.listener_name or "__default__",
|
151
|
+
],
|
152
|
+
extra={
|
153
|
+
"serviceType": "RabbitMQ",
|
154
|
+
"queue": context.queue,
|
155
|
+
"listenerName": context.listener_name,
|
156
|
+
"exception": exception.__class__.__name__,
|
157
|
+
},
|
158
|
+
exception=exception,
|
159
|
+
)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class RabbitMQException(Exception):
|
2
|
+
def __init__(
|
3
|
+
self,
|
4
|
+
code: int,
|
5
|
+
message: str,
|
6
|
+
tags: list[str] | None = None,
|
7
|
+
extra: dict[str, str] | None = None,
|
8
|
+
logstash_logging: bool = True,
|
9
|
+
extract_exc_info: bool = True,
|
10
|
+
):
|
11
|
+
self._code = code
|
12
|
+
self._message = message
|
13
|
+
self._tags = tags
|
14
|
+
self._extra = extra
|
15
|
+
self._logstash_logging = logstash_logging
|
16
|
+
self._extract_exc_info = extract_exc_info
|
17
|
+
|
18
|
+
@property
|
19
|
+
def code(self) -> int:
|
20
|
+
return self._code
|
21
|
+
|
22
|
+
@property
|
23
|
+
def message(self) -> str:
|
24
|
+
return self._message
|
25
|
+
|
26
|
+
@property
|
27
|
+
def tags(self) -> list[str] | None:
|
28
|
+
return self._tags
|
29
|
+
|
30
|
+
@property
|
31
|
+
def extra(self) -> dict[str, str] | None:
|
32
|
+
return self._extra
|
33
|
+
|
34
|
+
@property
|
35
|
+
def logstash_logging(self) -> bool:
|
36
|
+
return self._logstash_logging
|
37
|
+
|
38
|
+
@property
|
39
|
+
def extract_exc_info(self) -> bool:
|
40
|
+
return self._extract_exc_info
|
41
|
+
|
42
|
+
def __str__(self) -> str:
|
43
|
+
return f"message `{self.message}`, code `{self.code}`"
|
44
|
+
|
45
|
+
def __repr__(self) -> str:
|
46
|
+
return f"{self.__class__.__name__} ( message: `{self._message}`, code: {self._code} )"
|