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.
Files changed (19) hide show
  1. {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/PKG-INFO +3 -3
  2. {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/pyproject.toml +5 -5
  3. redis_message_queue-0.9.0/redis_message_queue/__init__.py +13 -0
  4. redis_message_queue-0.9.0/redis_message_queue/_config.py +66 -0
  5. {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/_redis_gateway.py +9 -2
  6. {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/asyncio/_redis_gateway.py +9 -2
  7. {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/asyncio/redis_message_queue.py +3 -1
  8. redis_message_queue-0.9.0/redis_message_queue/interrupt_handler/__init__.py +4 -0
  9. redis_message_queue-0.9.0/redis_message_queue/interrupt_handler/_implementation.py +29 -0
  10. redis_message_queue-0.9.0/redis_message_queue/interrupt_handler/_interface.py +7 -0
  11. {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/redis_message_queue.py +3 -1
  12. redis_message_queue-0.8.0/redis_message_queue/__init__.py +0 -4
  13. redis_message_queue-0.8.0/redis_message_queue/_config.py +0 -36
  14. {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/LICENSE +0 -0
  15. {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/README.md +0 -0
  16. {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/_abstract_redis_gateway.py +0 -0
  17. {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/_queue_key_manager.py +0 -0
  18. {redis_message_queue-0.8.0 → redis_message_queue-0.9.0}/redis_message_queue/asyncio/__init__.py +0 -0
  19. {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.8.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 (>=5.0.1,<6.0.0)
14
- Requires-Dist: tenacity (>=8.2.3,<9.0.0)
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.8.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 = "^5.0.1"
11
- tenacity = "^8.2.3"
10
+ redis = "*"
11
+ tenacity = "*"
12
12
 
13
13
  [tool.poetry.group.test.dependencies]
14
- pytest = "^7.4.4"
15
- fakeredis = "^2.20.1"
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
@@ -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
- DEFAULT_REDIS_CONNECTION_RETRY_STRATEGY,
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 = retry_strategy or DEFAULT_REDIS_CONNECTION_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
- DEFAULT_REDIS_CONNECTION_RETRY_STRATEGY,
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 = retry_strategy or DEFAULT_REDIS_CONNECTION_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,4 @@
1
+ from ._implementation import GracefulInterruptHandler
2
+ from ._interface import BaseGracefulInterruptHandler
3
+
4
+ __all__ = ["GracefulInterruptHandler", "BaseGracefulInterruptHandler"]
@@ -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
@@ -0,0 +1,7 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class BaseGracefulInterruptHandler(ABC):
5
+ @abstractmethod
6
+ def is_interrupted(self) -> bool:
7
+ pass
@@ -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,4 +0,0 @@
1
- from redis_message_queue._abstract_redis_gateway import AbstractRedisGateway
2
- from redis_message_queue.redis_message_queue import RedisMessageQueue
3
-
4
- __all__ = ["RedisMessageQueue", "AbstractRedisGateway"]
@@ -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