redqueue 0.11.0__tar.gz → 0.11.1__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.
- {redqueue-0.11.0 → redqueue-0.11.1}/CHANGELOG.md +21 -4
- {redqueue-0.11.0 → redqueue-0.11.1}/PKG-INFO +1 -1
- {redqueue-0.11.0 → redqueue-0.11.1}/docs/API.md +3 -3
- {redqueue-0.11.0 → redqueue-0.11.1}/pyproject.toml +1 -1
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/_version.py +1 -1
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/async_client.py +44 -22
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/client.py +35 -19
- {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_project_skeleton.py +116 -7
- {redqueue-0.11.0 → redqueue-0.11.1}/.github/workflows/ci.yml +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/.gitignore +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/CODE_OF_CONDUCT.md +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/CONTRIBUTING.md +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/LICENSE +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/NOTICE +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/README-zh-CN.md +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/README.md +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/docs/RELEASE.md +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/examples/README.md +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/examples/__init__.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/examples/async_list_queue.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/examples/common.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/examples/compatibility_check.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/examples/custom_serializer.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/examples/delayed_tasks.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/examples/monitoring_hooks.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/examples/stream_queue.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/examples/sync_list_queue.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/requirements.txt +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/scripts/check.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/__init__.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/__init__.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/async_delay.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/async_list.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/async_stream.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/base.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/delay.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/list.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/stream.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/compat.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/config.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/connection.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/exceptions.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/message.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/monitoring.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/serialization.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/tests/README.md +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/tests/__init__.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/tests/fakes.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_availability.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_backend_contracts.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_integration_redis.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_performance.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_real_redis_availability.py +0 -0
- {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_real_redis_performance.py +0 -0
|
@@ -4,10 +4,27 @@ All notable public release changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
所有公开发布版本的重要变更都会记录在此文件中。
|
|
6
6
|
|
|
7
|
-
Development versions are tracked separately from formal release versions.
|
|
8
|
-
开发版本与正式版本分开管理。
|
|
9
|
-
|
|
10
|
-
## [0.11.
|
|
7
|
+
Development versions are tracked separately from formal release versions.
|
|
8
|
+
开发版本与正式版本分开管理。
|
|
9
|
+
|
|
10
|
+
## [0.11.1] - 2026-06-21
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fixed resource cleanup in sync and async `from_url()` when Redis capability
|
|
15
|
+
detection, configuration validation, or backend initialization fails after the
|
|
16
|
+
client created an owned Redis connection.
|
|
17
|
+
- Added explicit `owns_redis` override support to sync and async `from_url()`
|
|
18
|
+
for advanced ownership control.
|
|
19
|
+
|
|
20
|
+
### 修复
|
|
21
|
+
|
|
22
|
+
- 修复同步和异步 `from_url()` 在自动创建 Redis 连接后,如果 Redis 能力探测、
|
|
23
|
+
配置校验或后端初始化失败,已创建连接未释放的问题。
|
|
24
|
+
- 同步和异步 `from_url()` 新增显式 `owns_redis` 覆盖支持,用于高级资源所有权
|
|
25
|
+
控制。
|
|
26
|
+
|
|
27
|
+
## [0.11.0] - 2026-06-21
|
|
11
28
|
|
|
12
29
|
### Added
|
|
13
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redqueue
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.1
|
|
4
4
|
Summary: Redis-backed Python message queue library with List, Streams, delayed tasks, and monitoring.
|
|
5
5
|
Project-URL: Homepage, https://github.com/SpringMirror-pear/redqueue
|
|
6
6
|
Project-URL: Repository, https://github.com/SpringMirror-pear/redqueue.git
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# RedQueue API / RedQueue API 文档
|
|
2
2
|
|
|
3
|
-
This document describes the public API available in RedQueue `0.11.
|
|
4
|
-
|
|
5
|
-
本文档描述 RedQueue `0.11.
|
|
3
|
+
This document describes the public API available in RedQueue `0.11.1`.
|
|
4
|
+
|
|
5
|
+
本文档描述 RedQueue `0.11.1` 的公开 API。
|
|
6
6
|
|
|
7
7
|
## Clients / 客户端
|
|
8
8
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "redqueue"
|
|
7
|
-
version = "0.11.
|
|
7
|
+
version = "0.11.1"
|
|
8
8
|
description = "Redis-backed Python message queue library with List, Streams, delayed tasks, and monitoring."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -99,28 +99,50 @@ class AsyncQueueClient:
|
|
|
99
99
|
QueueConfigError: If configuration values are invalid.
|
|
100
100
|
"""
|
|
101
101
|
|
|
102
|
-
redis = options.pop("redis", None)
|
|
103
|
-
pool_options = options.pop("pool_options", None) or {}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
else
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
102
|
+
redis = options.pop("redis", None)
|
|
103
|
+
pool_options = options.pop("pool_options", None) or {}
|
|
104
|
+
explicit_owns_redis = options.pop("owns_redis", None)
|
|
105
|
+
owns_redis = (
|
|
106
|
+
bool(explicit_owns_redis)
|
|
107
|
+
if explicit_owns_redis is not None
|
|
108
|
+
else False
|
|
109
|
+
)
|
|
110
|
+
if redis is None:
|
|
111
|
+
if connection_manager is not None:
|
|
112
|
+
redis = connection_manager.redis()
|
|
113
|
+
else:
|
|
114
|
+
redis = Redis.from_url(url, **pool_options)
|
|
115
|
+
owns_redis = (
|
|
116
|
+
True
|
|
117
|
+
if explicit_owns_redis is None
|
|
118
|
+
else bool(explicit_owns_redis)
|
|
119
|
+
)
|
|
120
|
+
try:
|
|
121
|
+
capabilities = options.pop(
|
|
122
|
+
"capabilities",
|
|
123
|
+
None,
|
|
124
|
+
) or await detect_capabilities_async(cast(AsyncRedisInfoClient, redis))
|
|
125
|
+
config = QueueConfig(queue=queue, backend=backend, **options)
|
|
126
|
+
client = cls(
|
|
127
|
+
config=config,
|
|
128
|
+
redis=redis,
|
|
129
|
+
capabilities=capabilities,
|
|
130
|
+
owns_redis=owns_redis,
|
|
131
|
+
)
|
|
132
|
+
await client._ensure_backend()
|
|
133
|
+
return client
|
|
134
|
+
except Exception:
|
|
135
|
+
if owns_redis:
|
|
136
|
+
close = getattr(redis, "aclose", None) or getattr(
|
|
137
|
+
redis,
|
|
138
|
+
"close",
|
|
139
|
+
None,
|
|
140
|
+
)
|
|
141
|
+
if close is not None:
|
|
142
|
+
result = close()
|
|
143
|
+
if hasattr(result, "__await__"):
|
|
144
|
+
await result
|
|
145
|
+
raise
|
|
124
146
|
|
|
125
147
|
async def publish(
|
|
126
148
|
self,
|
|
@@ -107,25 +107,41 @@ class QueueClient:
|
|
|
107
107
|
QueueConfigError: If configuration values are invalid.
|
|
108
108
|
"""
|
|
109
109
|
|
|
110
|
-
redis = options.pop("redis", None)
|
|
111
|
-
pool_options = options.pop("pool_options", None) or {}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
else
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
110
|
+
redis = options.pop("redis", None)
|
|
111
|
+
pool_options = options.pop("pool_options", None) or {}
|
|
112
|
+
explicit_owns_redis = options.pop("owns_redis", None)
|
|
113
|
+
owns_redis = (
|
|
114
|
+
bool(explicit_owns_redis)
|
|
115
|
+
if explicit_owns_redis is not None
|
|
116
|
+
else False
|
|
117
|
+
)
|
|
118
|
+
if redis is None:
|
|
119
|
+
if connection_manager is not None:
|
|
120
|
+
redis = connection_manager.redis()
|
|
121
|
+
else:
|
|
122
|
+
redis = Redis.from_url(url, **pool_options)
|
|
123
|
+
owns_redis = (
|
|
124
|
+
True
|
|
125
|
+
if explicit_owns_redis is None
|
|
126
|
+
else bool(explicit_owns_redis)
|
|
127
|
+
)
|
|
128
|
+
try:
|
|
129
|
+
capabilities = options.pop("capabilities", None) or detect_capabilities(
|
|
130
|
+
cast(RedisInfoClient, redis)
|
|
131
|
+
)
|
|
132
|
+
config = QueueConfig(queue=queue, backend=backend, **options)
|
|
133
|
+
return cls(
|
|
134
|
+
config=config,
|
|
135
|
+
redis=redis,
|
|
136
|
+
capabilities=capabilities,
|
|
137
|
+
owns_redis=owns_redis,
|
|
138
|
+
)
|
|
139
|
+
except Exception:
|
|
140
|
+
if owns_redis:
|
|
141
|
+
close = getattr(redis, "close", None)
|
|
142
|
+
if close is not None:
|
|
143
|
+
close()
|
|
144
|
+
raise
|
|
129
145
|
|
|
130
146
|
def publish(
|
|
131
147
|
self,
|
|
@@ -50,7 +50,7 @@ from tests.fakes import (
|
|
|
50
50
|
|
|
51
51
|
class ProjectSkeletonTests(unittest.TestCase):
|
|
52
52
|
def test_version_is_current_dev_version(self) -> None:
|
|
53
|
-
self.assertEqual(__version__, "0.11.
|
|
53
|
+
self.assertEqual(__version__, "0.11.1")
|
|
54
54
|
|
|
55
55
|
def test_queue_config_accepts_and_normalizes_backend(self) -> None:
|
|
56
56
|
config = QueueConfig(queue=" emails ", backend="stream")
|
|
@@ -653,7 +653,7 @@ class ProjectSkeletonTests(unittest.TestCase):
|
|
|
653
653
|
with self.assertRaises(RuntimeError):
|
|
654
654
|
manager.redis()
|
|
655
655
|
|
|
656
|
-
def test_sync_from_url_accepts_connection_manager_and_pool_options(self) -> None:
|
|
656
|
+
def test_sync_from_url_accepts_connection_manager_and_pool_options(self) -> None:
|
|
657
657
|
redis = FakeListRedis()
|
|
658
658
|
client = QueueClient.from_url(
|
|
659
659
|
"redis://127.0.0.1:6379/0",
|
|
@@ -682,8 +682,58 @@ class ProjectSkeletonTests(unittest.TestCase):
|
|
|
682
682
|
self.assertIs(managed_client.redis.connection_pool, manager.pool)
|
|
683
683
|
managed_client.close()
|
|
684
684
|
self.assertIs(manager.redis().connection_pool, manager.pool)
|
|
685
|
-
finally:
|
|
686
|
-
manager.close()
|
|
685
|
+
finally:
|
|
686
|
+
manager.close()
|
|
687
|
+
|
|
688
|
+
def test_sync_from_url_closes_owned_redis_when_initialization_fails(self) -> None:
|
|
689
|
+
class OwnedRedis(FakeListRedis):
|
|
690
|
+
def info(self, section: str | None = None) -> dict[str, str]:
|
|
691
|
+
return {"redis_version": "7.0.0"}
|
|
692
|
+
|
|
693
|
+
redis = OwnedRedis()
|
|
694
|
+
|
|
695
|
+
with self.assertRaises(QueueConfigError):
|
|
696
|
+
QueueClient.from_url(
|
|
697
|
+
"redis://127.0.0.1:6379/0",
|
|
698
|
+
queue="bad queue",
|
|
699
|
+
redis=redis,
|
|
700
|
+
owns_redis=True,
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
self.assertIn("close", redis.commands)
|
|
704
|
+
|
|
705
|
+
def test_sync_from_url_leaves_injected_redis_open_when_init_fails(self) -> None:
|
|
706
|
+
class InjectedRedis(FakeListRedis):
|
|
707
|
+
def info(self, section: str | None = None) -> dict[str, str]:
|
|
708
|
+
return {"redis_version": "7.0.0"}
|
|
709
|
+
|
|
710
|
+
redis = InjectedRedis()
|
|
711
|
+
|
|
712
|
+
with self.assertRaises(QueueConfigError):
|
|
713
|
+
QueueClient.from_url(
|
|
714
|
+
"redis://127.0.0.1:6379/0",
|
|
715
|
+
queue="bad queue",
|
|
716
|
+
redis=redis,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
self.assertNotIn("close", redis.commands)
|
|
720
|
+
|
|
721
|
+
def test_sync_from_url_closes_owned_redis_when_capability_probe_fails(self) -> None:
|
|
722
|
+
class BrokenInfoRedis(FakeListRedis):
|
|
723
|
+
def info(self, section: str | None = None) -> dict[str, str]:
|
|
724
|
+
raise TimeoutError("redis unavailable")
|
|
725
|
+
|
|
726
|
+
redis = BrokenInfoRedis()
|
|
727
|
+
|
|
728
|
+
with self.assertRaises(BackendUnavailableError):
|
|
729
|
+
QueueClient.from_url(
|
|
730
|
+
"redis://127.0.0.1:6379/0",
|
|
731
|
+
queue="emails",
|
|
732
|
+
redis=redis,
|
|
733
|
+
owns_redis=True,
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
self.assertIn("close", redis.commands)
|
|
687
737
|
|
|
688
738
|
def test_async_connection_manager_creates_pooled_clients(self) -> None:
|
|
689
739
|
async def run() -> None:
|
|
@@ -705,7 +755,7 @@ class ProjectSkeletonTests(unittest.TestCase):
|
|
|
705
755
|
|
|
706
756
|
asyncio.run(run())
|
|
707
757
|
|
|
708
|
-
def test_async_from_url_accepts_connection_manager_and_pool_options(self) -> None:
|
|
758
|
+
def test_async_from_url_accepts_connection_manager_and_pool_options(self) -> None:
|
|
709
759
|
async def run() -> None:
|
|
710
760
|
redis = FakeAsyncListRedis()
|
|
711
761
|
client = await AsyncQueueClient.from_url(
|
|
@@ -737,8 +787,67 @@ class ProjectSkeletonTests(unittest.TestCase):
|
|
|
737
787
|
self.assertIs(manager.redis().connection_pool, manager.pool)
|
|
738
788
|
finally:
|
|
739
789
|
await manager.close()
|
|
740
|
-
|
|
741
|
-
asyncio.run(run())
|
|
790
|
+
|
|
791
|
+
asyncio.run(run())
|
|
792
|
+
|
|
793
|
+
def test_async_from_url_closes_owned_redis_when_initialization_fails(self) -> None:
|
|
794
|
+
class OwnedAsyncRedis(FakeAsyncListRedis):
|
|
795
|
+
async def info(self, section: str | None = None) -> dict[str, str]:
|
|
796
|
+
return {"redis_version": "7.0.0"}
|
|
797
|
+
|
|
798
|
+
async def run() -> FakeAsyncListRedis:
|
|
799
|
+
redis = OwnedAsyncRedis()
|
|
800
|
+
with self.assertRaises(QueueConfigError):
|
|
801
|
+
await AsyncQueueClient.from_url(
|
|
802
|
+
"redis://127.0.0.1:6379/0",
|
|
803
|
+
queue="bad queue",
|
|
804
|
+
redis=redis,
|
|
805
|
+
owns_redis=True,
|
|
806
|
+
)
|
|
807
|
+
return redis
|
|
808
|
+
|
|
809
|
+
redis = asyncio.run(run())
|
|
810
|
+
|
|
811
|
+
self.assertIn("aclose", redis.commands)
|
|
812
|
+
|
|
813
|
+
def test_async_from_url_leaves_injected_redis_open_when_init_fails(self) -> None:
|
|
814
|
+
class InjectedAsyncRedis(FakeAsyncListRedis):
|
|
815
|
+
async def info(self, section: str | None = None) -> dict[str, str]:
|
|
816
|
+
return {"redis_version": "7.0.0"}
|
|
817
|
+
|
|
818
|
+
async def run() -> FakeAsyncListRedis:
|
|
819
|
+
redis = InjectedAsyncRedis()
|
|
820
|
+
with self.assertRaises(QueueConfigError):
|
|
821
|
+
await AsyncQueueClient.from_url(
|
|
822
|
+
"redis://127.0.0.1:6379/0",
|
|
823
|
+
queue="bad queue",
|
|
824
|
+
redis=redis,
|
|
825
|
+
)
|
|
826
|
+
return redis
|
|
827
|
+
|
|
828
|
+
redis = asyncio.run(run())
|
|
829
|
+
|
|
830
|
+
self.assertNotIn("aclose", redis.commands)
|
|
831
|
+
|
|
832
|
+
def test_async_from_url_closes_owned_redis_on_probe_failure(self) -> None:
|
|
833
|
+
class BrokenInfoAsyncRedis(FakeAsyncListRedis):
|
|
834
|
+
async def info(self, section: str | None = None) -> dict[str, str]:
|
|
835
|
+
raise TimeoutError("redis unavailable")
|
|
836
|
+
|
|
837
|
+
async def run() -> FakeAsyncListRedis:
|
|
838
|
+
redis = BrokenInfoAsyncRedis()
|
|
839
|
+
with self.assertRaises(BackendUnavailableError):
|
|
840
|
+
await AsyncQueueClient.from_url(
|
|
841
|
+
"redis://127.0.0.1:6379/0",
|
|
842
|
+
queue="jobs",
|
|
843
|
+
redis=redis,
|
|
844
|
+
owns_redis=True,
|
|
845
|
+
)
|
|
846
|
+
return redis
|
|
847
|
+
|
|
848
|
+
redis = asyncio.run(run())
|
|
849
|
+
|
|
850
|
+
self.assertIn("aclose", redis.commands)
|
|
742
851
|
|
|
743
852
|
def test_async_list_backend_ack_uses_original_serialized_payload(self) -> None:
|
|
744
853
|
class NonDeterministicSerializer:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|