redis-message-queue 0.8.0__tar.gz → 0.9.0__tar.gz
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.
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/PKG-INFO +3 -3
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/pyproject.toml +5 -5
- redis_message_queue-0.9.0/redis_message_queue/__init__.py +13 -0
- redis_message_queue-0.9.0/redis_message_queue/_config.py +66 -0
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/_redis_gateway.py +9 -2
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/asyncio/_redis_gateway.py +9 -2
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/asyncio/redis_message_queue.py +3 -1
- redis_message_queue-0.9.0/redis_message_queue/interrupt_handler/__init__.py +4 -0
- redis_message_queue-0.9.0/redis_message_queue/interrupt_handler/_implementation.py +29 -0
- redis_message_queue-0.9.0/redis_message_queue/interrupt_handler/_interface.py +7 -0
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/redis_message_queue.py +3 -1
- redis_message_queue-0.8.0/redis_message_queue/__init__.py +0 -4
- redis_message_queue-0.8.0/redis_message_queue/_config.py +0 -36
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/LICENSE +0 -0
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/README.md +0 -0
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/_queue_key_manager.py +0 -0
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/asyncio/__init__.py +0 -0
- {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/asyncio/_abstract_redis_gateway.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: redis-message-queue
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Python message queuing with Redis and message deduplication
|
|
5
5
|
Author: Elijas
|
|
6
6
|
Author-email: 4084885+Elijas@users.noreply.github.com
|
|
@@ -10,8 +10,8 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.9
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Requires-Dist: redis
|
|
14
|
-
Requires-Dist: tenacity
|
|
13
|
+
Requires-Dist: redis
|
|
14
|
+
Requires-Dist: tenacity
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
17
17
|
# redis-message-queue
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "redis-message-queue"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
description = "Python message queuing with Redis and message deduplication"
|
|
5
5
|
authors = ["Elijas <4084885+Elijas@users.noreply.github.com>"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
|
|
8
8
|
[tool.poetry.dependencies]
|
|
9
9
|
python = "^3.8"
|
|
10
|
-
redis = "
|
|
11
|
-
tenacity = "
|
|
10
|
+
redis = "*"
|
|
11
|
+
tenacity = "*"
|
|
12
12
|
|
|
13
13
|
[tool.poetry.group.test.dependencies]
|
|
14
|
-
pytest = "
|
|
15
|
-
fakeredis = "
|
|
14
|
+
pytest = "*"
|
|
15
|
+
fakeredis = "*"
|
|
16
16
|
|
|
17
17
|
[build-system]
|
|
18
18
|
requires = ["poetry-core"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from redis_message_queue._abstract_redis_gateway import AbstractRedisGateway
|
|
2
|
+
from redis_message_queue.interrupt_handler import (
|
|
3
|
+
BaseGracefulInterruptHandler,
|
|
4
|
+
GracefulInterruptHandler,
|
|
5
|
+
)
|
|
6
|
+
from redis_message_queue.redis_message_queue import RedisMessageQueue
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"RedisMessageQueue",
|
|
10
|
+
"AbstractRedisGateway",
|
|
11
|
+
"GracefulInterruptHandler",
|
|
12
|
+
"BaseGracefulInterruptHandler",
|
|
13
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import redis
|
|
5
|
+
import redis.exceptions
|
|
6
|
+
from tenacity import (
|
|
7
|
+
RetryCallState,
|
|
8
|
+
after_log,
|
|
9
|
+
retry,
|
|
10
|
+
retry_base,
|
|
11
|
+
retry_if_exception,
|
|
12
|
+
stop_after_delay,
|
|
13
|
+
wait_exponential_jitter,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from redis_message_queue.interrupt_handler._interface import (
|
|
17
|
+
BaseGracefulInterruptHandler,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_redis_retryable_exception(exception):
|
|
24
|
+
return isinstance(
|
|
25
|
+
exception,
|
|
26
|
+
(
|
|
27
|
+
redis.exceptions.ConnectionError,
|
|
28
|
+
redis.exceptions.TimeoutError,
|
|
29
|
+
redis.exceptions.BusyLoadingError,
|
|
30
|
+
redis.exceptions.ClusterDownError,
|
|
31
|
+
redis.exceptions.TryAgainError,
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class interruptable_retry(retry_base):
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
interrupt: BaseGracefulInterruptHandler | None,
|
|
40
|
+
get_parent_retry: typing.Callable[[], retry_base],
|
|
41
|
+
) -> None:
|
|
42
|
+
self._parent_instance = get_parent_retry()
|
|
43
|
+
self.interrupt = interrupt
|
|
44
|
+
|
|
45
|
+
def __call__(self, retry_state: RetryCallState) -> bool:
|
|
46
|
+
if self.interrupt and self.interrupt.is_interrupted():
|
|
47
|
+
return False
|
|
48
|
+
return self._parent_instance.__call__(retry_state)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_default_redis_connection_retry_strategy(
|
|
52
|
+
*, interrupt: BaseGracefulInterruptHandler | None = None
|
|
53
|
+
):
|
|
54
|
+
return retry(
|
|
55
|
+
stop=stop_after_delay(120),
|
|
56
|
+
wait=wait_exponential_jitter(initial=0.01, exp_base=2, max=5, jitter=0.1),
|
|
57
|
+
retry=interruptable_retry(
|
|
58
|
+
interrupt=interrupt,
|
|
59
|
+
get_parent_retry=lambda: retry_if_exception(is_redis_retryable_exception),
|
|
60
|
+
),
|
|
61
|
+
after=after_log(logger, logging.ERROR),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
DEFAULT_MESSAGE_WAIT_INTERVAL_SECONDS = 5
|
|
66
|
+
DEFAULT_MESSAGE_DEDUPLICATION_LOG_TTL = 60 * 60 # 1 hour = 60 seconds * 60 minutes
|
{redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/_redis_gateway.py
RENAMED
|
@@ -8,7 +8,10 @@ from redis_message_queue._abstract_redis_gateway import AbstractRedisGateway
|
|
|
8
8
|
from redis_message_queue._config import (
|
|
9
9
|
DEFAULT_MESSAGE_DEDUPLICATION_LOG_TTL,
|
|
10
10
|
DEFAULT_MESSAGE_WAIT_INTERVAL_SECONDS,
|
|
11
|
-
|
|
11
|
+
get_default_redis_connection_retry_strategy,
|
|
12
|
+
)
|
|
13
|
+
from redis_message_queue.interrupt_handler._interface import (
|
|
14
|
+
BaseGracefulInterruptHandler,
|
|
12
15
|
)
|
|
13
16
|
|
|
14
17
|
|
|
@@ -20,9 +23,13 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
20
23
|
retry_strategy: Optional[Callable] = None,
|
|
21
24
|
message_deduplication_log_ttl_seconds: Optional[int] = None,
|
|
22
25
|
message_wait_interval_seconds: Optional[int] = None,
|
|
26
|
+
interrupt: BaseGracefulInterruptHandler | None = None,
|
|
23
27
|
):
|
|
24
28
|
self._redis_client = redis_client
|
|
25
|
-
self._retry_strategy =
|
|
29
|
+
self._retry_strategy = (
|
|
30
|
+
retry_strategy
|
|
31
|
+
or get_default_redis_connection_retry_strategy(interrupt=interrupt)
|
|
32
|
+
)
|
|
26
33
|
self._message_deduplication_log_ttl_seconds = (
|
|
27
34
|
message_deduplication_log_ttl_seconds
|
|
28
35
|
or DEFAULT_MESSAGE_DEDUPLICATION_LOG_TTL
|
|
@@ -7,7 +7,10 @@ from redis_message_queue._abstract_redis_gateway import AbstractRedisGateway
|
|
|
7
7
|
from redis_message_queue._config import (
|
|
8
8
|
DEFAULT_MESSAGE_DEDUPLICATION_LOG_TTL,
|
|
9
9
|
DEFAULT_MESSAGE_WAIT_INTERVAL_SECONDS,
|
|
10
|
-
|
|
10
|
+
get_default_redis_connection_retry_strategy,
|
|
11
|
+
)
|
|
12
|
+
from redis_message_queue.interrupt_handler._interface import (
|
|
13
|
+
BaseGracefulInterruptHandler,
|
|
11
14
|
)
|
|
12
15
|
|
|
13
16
|
|
|
@@ -19,9 +22,13 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
19
22
|
retry_strategy: Optional[Callable] = None,
|
|
20
23
|
message_deduplication_log_ttl_seconds: Optional[int] = None,
|
|
21
24
|
message_wait_interval_seconds: Optional[int] = None,
|
|
25
|
+
interrupt: BaseGracefulInterruptHandler | None = None,
|
|
22
26
|
):
|
|
23
27
|
self._redis_client = redis_client
|
|
24
|
-
self._retry_strategy =
|
|
28
|
+
self._retry_strategy = (
|
|
29
|
+
retry_strategy
|
|
30
|
+
or get_default_redis_connection_retry_strategy(interrupt=interrupt)
|
|
31
|
+
)
|
|
25
32
|
self._message_deduplication_log_ttl_seconds = (
|
|
26
33
|
message_deduplication_log_ttl_seconds
|
|
27
34
|
or DEFAULT_MESSAGE_DEDUPLICATION_LOG_TTL
|
|
@@ -8,6 +8,7 @@ import redis.exceptions
|
|
|
8
8
|
from redis_message_queue._queue_key_manager import QueueKeyManager
|
|
9
9
|
from redis_message_queue.asyncio._abstract_redis_gateway import AbstractRedisGateway
|
|
10
10
|
from redis_message_queue.asyncio._redis_gateway import RedisGateway
|
|
11
|
+
from redis_message_queue.interrupt_handler import BaseGracefulInterruptHandler
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class RedisMessageQueue:
|
|
@@ -22,6 +23,7 @@ class RedisMessageQueue:
|
|
|
22
23
|
enable_failed_queue: bool = False,
|
|
23
24
|
key_separator: str = "::",
|
|
24
25
|
get_deduplication_key: Optional[Callable] = None,
|
|
26
|
+
interrupt: BaseGracefulInterruptHandler | None = None,
|
|
25
27
|
):
|
|
26
28
|
self._redis_client = client
|
|
27
29
|
self.key = QueueKeyManager(name, key_separator=key_separator)
|
|
@@ -35,7 +37,7 @@ class RedisMessageQueue:
|
|
|
35
37
|
elif not client:
|
|
36
38
|
raise ValueError("Either 'client' or 'gateway' must be provided.")
|
|
37
39
|
else:
|
|
38
|
-
self._redis = RedisGateway(redis_client=client)
|
|
40
|
+
self._redis = RedisGateway(redis_client=client, interrupt=interrupt)
|
|
39
41
|
|
|
40
42
|
async def publish(self, message: str | dict) -> bool:
|
|
41
43
|
if isinstance(message, dict):
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import signal
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
|
|
4
|
+
from redis_message_queue.interrupt_handler._interface import (
|
|
5
|
+
BaseGracefulInterruptHandler,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GracefulInterruptHandler(BaseGracefulInterruptHandler):
|
|
10
|
+
_DEFAULT_SIGNALS = (signal.SIGINT, signal.SIGTERM, signal.SIGHUP)
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
verbose: bool = True,
|
|
15
|
+
signals: Iterable[signal.Signals] = _DEFAULT_SIGNALS,
|
|
16
|
+
):
|
|
17
|
+
self._interrupted = False
|
|
18
|
+
self._verbose = verbose
|
|
19
|
+
self._signals = signals
|
|
20
|
+
for sig in self._signals:
|
|
21
|
+
signal.signal(sig, self._signal_handler)
|
|
22
|
+
|
|
23
|
+
def is_interrupted(self) -> bool:
|
|
24
|
+
return self._interrupted
|
|
25
|
+
|
|
26
|
+
def _signal_handler(self, signum, frame):
|
|
27
|
+
if self._verbose:
|
|
28
|
+
print(f"Received signal: {signal.strsignal(signum)}")
|
|
29
|
+
self._interrupted = True
|
{redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/redis_message_queue.py
RENAMED
|
@@ -8,6 +8,7 @@ import redis.exceptions
|
|
|
8
8
|
from redis_message_queue._abstract_redis_gateway import AbstractRedisGateway
|
|
9
9
|
from redis_message_queue._queue_key_manager import QueueKeyManager
|
|
10
10
|
from redis_message_queue._redis_gateway import RedisGateway
|
|
11
|
+
from redis_message_queue.interrupt_handler import BaseGracefulInterruptHandler
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class RedisMessageQueue:
|
|
@@ -22,6 +23,7 @@ class RedisMessageQueue:
|
|
|
22
23
|
enable_failed_queue: bool = False,
|
|
23
24
|
key_separator: str = "::",
|
|
24
25
|
get_deduplication_key: Optional[Callable] = None,
|
|
26
|
+
interrupt: BaseGracefulInterruptHandler | None = None,
|
|
25
27
|
):
|
|
26
28
|
self._redis_client = client
|
|
27
29
|
self.key = QueueKeyManager(name, key_separator=key_separator)
|
|
@@ -35,7 +37,7 @@ class RedisMessageQueue:
|
|
|
35
37
|
elif not client:
|
|
36
38
|
raise ValueError("Either 'client' or 'gateway' must be provided.")
|
|
37
39
|
else:
|
|
38
|
-
self._redis = RedisGateway(redis_client=client)
|
|
40
|
+
self._redis = RedisGateway(redis_client=client, interrupt=interrupt)
|
|
39
41
|
|
|
40
42
|
def publish(self, message: str | dict) -> bool:
|
|
41
43
|
if isinstance(message, dict):
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
import redis
|
|
4
|
-
import redis.exceptions
|
|
5
|
-
from tenacity import (
|
|
6
|
-
after_log,
|
|
7
|
-
retry,
|
|
8
|
-
retry_if_exception,
|
|
9
|
-
stop_after_delay,
|
|
10
|
-
wait_exponential_jitter,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def is_redis_retryable_exception(exception):
|
|
17
|
-
return isinstance(
|
|
18
|
-
exception,
|
|
19
|
-
(
|
|
20
|
-
redis.exceptions.ConnectionError,
|
|
21
|
-
redis.exceptions.TimeoutError,
|
|
22
|
-
redis.exceptions.BusyLoadingError,
|
|
23
|
-
redis.exceptions.ClusterDownError,
|
|
24
|
-
redis.exceptions.TryAgainError,
|
|
25
|
-
),
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
DEFAULT_REDIS_CONNECTION_RETRY_STRATEGY = retry(
|
|
30
|
-
stop=stop_after_delay(120),
|
|
31
|
-
wait=wait_exponential_jitter(initial=0.01, exp_base=2, max=5, jitter=0.1),
|
|
32
|
-
retry=retry_if_exception(is_redis_retryable_exception),
|
|
33
|
-
after=after_log(logger, logging.ERROR),
|
|
34
|
-
)
|
|
35
|
-
DEFAULT_MESSAGE_WAIT_INTERVAL_SECONDS = 5
|
|
36
|
-
DEFAULT_MESSAGE_DEDUPLICATION_LOG_TTL = 60 * 60 # 1 hour = 60 seconds * 60 minutes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/_queue_key_manager.py
RENAMED
|
File without changes
|
{redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/asyncio/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|