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.
Files changed (54) hide show
  1. {redqueue-0.11.0 → redqueue-0.11.1}/CHANGELOG.md +21 -4
  2. {redqueue-0.11.0 → redqueue-0.11.1}/PKG-INFO +1 -1
  3. {redqueue-0.11.0 → redqueue-0.11.1}/docs/API.md +3 -3
  4. {redqueue-0.11.0 → redqueue-0.11.1}/pyproject.toml +1 -1
  5. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/_version.py +1 -1
  6. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/async_client.py +44 -22
  7. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/client.py +35 -19
  8. {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_project_skeleton.py +116 -7
  9. {redqueue-0.11.0 → redqueue-0.11.1}/.github/workflows/ci.yml +0 -0
  10. {redqueue-0.11.0 → redqueue-0.11.1}/.gitignore +0 -0
  11. {redqueue-0.11.0 → redqueue-0.11.1}/CODE_OF_CONDUCT.md +0 -0
  12. {redqueue-0.11.0 → redqueue-0.11.1}/CONTRIBUTING.md +0 -0
  13. {redqueue-0.11.0 → redqueue-0.11.1}/LICENSE +0 -0
  14. {redqueue-0.11.0 → redqueue-0.11.1}/NOTICE +0 -0
  15. {redqueue-0.11.0 → redqueue-0.11.1}/README-zh-CN.md +0 -0
  16. {redqueue-0.11.0 → redqueue-0.11.1}/README.md +0 -0
  17. {redqueue-0.11.0 → redqueue-0.11.1}/docs/RELEASE.md +0 -0
  18. {redqueue-0.11.0 → redqueue-0.11.1}/examples/README.md +0 -0
  19. {redqueue-0.11.0 → redqueue-0.11.1}/examples/__init__.py +0 -0
  20. {redqueue-0.11.0 → redqueue-0.11.1}/examples/async_list_queue.py +0 -0
  21. {redqueue-0.11.0 → redqueue-0.11.1}/examples/common.py +0 -0
  22. {redqueue-0.11.0 → redqueue-0.11.1}/examples/compatibility_check.py +0 -0
  23. {redqueue-0.11.0 → redqueue-0.11.1}/examples/custom_serializer.py +0 -0
  24. {redqueue-0.11.0 → redqueue-0.11.1}/examples/delayed_tasks.py +0 -0
  25. {redqueue-0.11.0 → redqueue-0.11.1}/examples/monitoring_hooks.py +0 -0
  26. {redqueue-0.11.0 → redqueue-0.11.1}/examples/stream_queue.py +0 -0
  27. {redqueue-0.11.0 → redqueue-0.11.1}/examples/sync_list_queue.py +0 -0
  28. {redqueue-0.11.0 → redqueue-0.11.1}/requirements.txt +0 -0
  29. {redqueue-0.11.0 → redqueue-0.11.1}/scripts/check.py +0 -0
  30. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/__init__.py +0 -0
  31. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/__init__.py +0 -0
  32. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/async_delay.py +0 -0
  33. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/async_list.py +0 -0
  34. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/async_stream.py +0 -0
  35. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/base.py +0 -0
  36. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/delay.py +0 -0
  37. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/list.py +0 -0
  38. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/backends/stream.py +0 -0
  39. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/compat.py +0 -0
  40. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/config.py +0 -0
  41. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/connection.py +0 -0
  42. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/exceptions.py +0 -0
  43. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/message.py +0 -0
  44. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/monitoring.py +0 -0
  45. {redqueue-0.11.0 → redqueue-0.11.1}/src/redqueue/serialization.py +0 -0
  46. {redqueue-0.11.0 → redqueue-0.11.1}/tests/README.md +0 -0
  47. {redqueue-0.11.0 → redqueue-0.11.1}/tests/__init__.py +0 -0
  48. {redqueue-0.11.0 → redqueue-0.11.1}/tests/fakes.py +0 -0
  49. {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_availability.py +0 -0
  50. {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_backend_contracts.py +0 -0
  51. {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_integration_redis.py +0 -0
  52. {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_performance.py +0 -0
  53. {redqueue-0.11.0 → redqueue-0.11.1}/tests/test_real_redis_availability.py +0 -0
  54. {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.0] - 2026-06-21
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.0
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.0`.
4
-
5
- 本文档描述 RedQueue `0.11.0` 的公开 API。
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.0"
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"
@@ -3,4 +3,4 @@
3
3
 
4
4
  """Package version."""
5
5
 
6
- __version__ = "0.11.0"
6
+ __version__ = "0.11.1"
@@ -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
- owns_redis = False
105
- if redis is None:
106
- if connection_manager is not None:
107
- redis = connection_manager.redis()
108
- else:
109
- redis = Redis.from_url(url, **pool_options)
110
- owns_redis = True
111
- capabilities = options.pop(
112
- "capabilities",
113
- None,
114
- ) or await detect_capabilities_async(cast(AsyncRedisInfoClient, redis))
115
- config = QueueConfig(queue=queue, backend=backend, **options)
116
- client = cls(
117
- config=config,
118
- redis=redis,
119
- capabilities=capabilities,
120
- owns_redis=owns_redis,
121
- )
122
- await client._ensure_backend()
123
- return client
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
- owns_redis = False
113
- if redis is None:
114
- if connection_manager is not None:
115
- redis = connection_manager.redis()
116
- else:
117
- redis = Redis.from_url(url, **pool_options)
118
- owns_redis = True
119
- capabilities = options.pop("capabilities", None) or detect_capabilities(
120
- cast(RedisInfoClient, redis)
121
- )
122
- config = QueueConfig(queue=queue, backend=backend, **options)
123
- return cls(
124
- config=config,
125
- redis=redis,
126
- capabilities=capabilities,
127
- owns_redis=owns_redis,
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.0")
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