async-task-kit 0.1.1__tar.gz → 0.1.13__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 (23) hide show
  1. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/PKG-INFO +50 -1
  2. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/README.md +48 -0
  3. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit/core/rabbitmq.py +67 -24
  4. async_task_kit-0.1.13/async_task_kit/utils/env_loader.py +55 -0
  5. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit.egg-info/PKG-INFO +50 -1
  6. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit.egg-info/requires.txt +1 -0
  7. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/pyproject.toml +7 -3
  8. async_task_kit-0.1.1/async_task_kit/utils/env_loader.py +0 -16
  9. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/LICENSE +0 -0
  10. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit/__init__.py +0 -0
  11. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit/consumer/__init__.py +0 -0
  12. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit/consumer/base.py +0 -0
  13. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit/consumer/coroutine.py +0 -0
  14. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit/consumer/process.py +0 -0
  15. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit/consumer/thread.py +0 -0
  16. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit/core/__init__.py +0 -0
  17. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit/core/processor.py +0 -0
  18. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit/utils/__init__.py +0 -0
  19. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit/utils/logger.py +0 -0
  20. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit.egg-info/SOURCES.txt +0 -0
  21. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit.egg-info/dependency_links.txt +0 -0
  22. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/async_task_kit.egg-info/top_level.txt +0 -0
  23. {async_task_kit-0.1.1 → async_task_kit-0.1.13}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async-task-kit
3
- Version: 0.1.1
3
+ Version: 0.1.13
4
4
  Summary: A powerful async task processing kit based on RabbitMQ with Coroutine, Thread, and Process support.
5
5
  Author-email: realwrtoff <realwrtoff@gmail.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -10,6 +10,7 @@ Requires-Python: >=3.12
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  Requires-Dist: aio-pika>=9.4.0
13
+ Requires-Dist: asyncpg>=0.31.0
13
14
  Requires-Dist: python-dotenv>=1.0.0
14
15
  Dynamic: license-file
15
16
 
@@ -174,6 +175,54 @@ if __name__ == "__main__":
174
175
  asyncio.run(publish())
175
176
  ```
176
177
 
178
+ ### 5. RabbitMQ Client API
179
+
180
+ `RabbitMQ` 提供连接池、`push` / `pop`、延迟重试队列,以及 `queue_length` 查询队列深度。
181
+
182
+ #### `queue_length`
183
+
184
+ ```python
185
+ from async_task_kit import RabbitMQ
186
+
187
+ rmq = RabbitMQ("amqp://guest:guest@localhost/")
188
+ await rmq.init()
189
+
190
+ length = await rmq.queue_length("my_demo_queue")
191
+ if length == RabbitMQ.QUEUE_LENGTH_UNAVAILABLE:
192
+ # -1:连不上或队列不存在,稍后重试
193
+ ...
194
+ elif length == 0:
195
+ # 队列存在且为空
196
+ ...
197
+ else:
198
+ # 当前消息数
199
+ ...
200
+
201
+ await rmq.close()
202
+ ```
203
+
204
+ | 返回值 | 含义 |
205
+ |--------|------|
206
+ | `>= 0` | 队列真实消息数(`0` = 空队列) |
207
+ | `-1` (`QUEUE_LENGTH_UNAVAILABLE`) | 连接失败、队列不存在或其它异常,应重试 |
208
+
209
+ 实现要点:
210
+
211
+ - 使用 **`passive=True`** 声明:只查询已存在的队列,**不会**自动创建空队列。
212
+ - 断连时会关闭旧连接池并自动重建(与 `push` / `pop` 一致)。
213
+
214
+ #### `pop` 的 `durable` 参数
215
+
216
+ `pop` 在取消息前会 `declare_queue`。`push` 默认以 **`durable=True`** 创建队列(重启后队列仍在),因此 `pop` 默认同样传 `durable=True`,保证声明参数与已有队列一致,避免 RabbitMQ 返回 `PRECONDITION_FAILED`。
217
+
218
+ 若你的队列是非持久化的,调用时需显式传入:
219
+
220
+ ```python
221
+ await rmq.pop("my_queue", durable=False)
222
+ ```
223
+
224
+ `queue_length` 使用 passive 模式,无需传 `durable`。
225
+
177
226
  ## License
178
227
 
179
228
  MIT
@@ -159,6 +159,54 @@ if __name__ == "__main__":
159
159
  asyncio.run(publish())
160
160
  ```
161
161
 
162
+ ### 5. RabbitMQ Client API
163
+
164
+ `RabbitMQ` 提供连接池、`push` / `pop`、延迟重试队列,以及 `queue_length` 查询队列深度。
165
+
166
+ #### `queue_length`
167
+
168
+ ```python
169
+ from async_task_kit import RabbitMQ
170
+
171
+ rmq = RabbitMQ("amqp://guest:guest@localhost/")
172
+ await rmq.init()
173
+
174
+ length = await rmq.queue_length("my_demo_queue")
175
+ if length == RabbitMQ.QUEUE_LENGTH_UNAVAILABLE:
176
+ # -1:连不上或队列不存在,稍后重试
177
+ ...
178
+ elif length == 0:
179
+ # 队列存在且为空
180
+ ...
181
+ else:
182
+ # 当前消息数
183
+ ...
184
+
185
+ await rmq.close()
186
+ ```
187
+
188
+ | 返回值 | 含义 |
189
+ |--------|------|
190
+ | `>= 0` | 队列真实消息数(`0` = 空队列) |
191
+ | `-1` (`QUEUE_LENGTH_UNAVAILABLE`) | 连接失败、队列不存在或其它异常,应重试 |
192
+
193
+ 实现要点:
194
+
195
+ - 使用 **`passive=True`** 声明:只查询已存在的队列,**不会**自动创建空队列。
196
+ - 断连时会关闭旧连接池并自动重建(与 `push` / `pop` 一致)。
197
+
198
+ #### `pop` 的 `durable` 参数
199
+
200
+ `pop` 在取消息前会 `declare_queue`。`push` 默认以 **`durable=True`** 创建队列(重启后队列仍在),因此 `pop` 默认同样传 `durable=True`,保证声明参数与已有队列一致,避免 RabbitMQ 返回 `PRECONDITION_FAILED`。
201
+
202
+ 若你的队列是非持久化的,调用时需显式传入:
203
+
204
+ ```python
205
+ await rmq.pop("my_queue", durable=False)
206
+ ```
207
+
208
+ `queue_length` 使用 passive 模式,无需传 `durable`。
209
+
162
210
  ## License
163
211
 
164
212
  MIT
@@ -29,7 +29,11 @@ class BasicQueue(ABC):
29
29
  @abstractmethod
30
30
  async def pop(self, queue_name: str,** kwargs: Any) -> Any:
31
31
  raise NotImplementedError
32
-
32
+
33
+ @abstractmethod
34
+ async def queue_length(self, queue_name: str) -> int:
35
+ raise NotImplementedError
36
+
33
37
  @abstractmethod
34
38
  async def ack(self, message: Any) -> None:
35
39
  raise NotImplementedError
@@ -50,6 +54,7 @@ class RabbitMQ(BasicQueue):
50
54
  DEFAULT_DEAD_LETTER_DAYS = 7
51
55
  DEFAULT_DELAY_SECONDS = 30
52
56
  DEFAULT_POP_TIMEOUT = 1.0
57
+ QUEUE_LENGTH_UNAVAILABLE = -1
53
58
 
54
59
  def __init__(
55
60
  self,
@@ -92,6 +97,23 @@ class RabbitMQ(BasicQueue):
92
97
  logger.error(f"[RabbitMQ] 初始化失败: {e}", exc_info=True)
93
98
  raise
94
99
 
100
+ async def _invalidate_pools(self) -> None:
101
+ """断连或 close 时关闭 pool,下次 init() 会重建。"""
102
+ async with self._init_lock:
103
+ self._ready.clear()
104
+ channel_pool = self._channel_pool
105
+ connection_pool = self._connection_pool
106
+ self._channel_pool = None
107
+ self._connection_pool = None
108
+
109
+ for pool, label in ((channel_pool, "channel"), (connection_pool, "connection")):
110
+ if pool is None:
111
+ continue
112
+ try:
113
+ await pool.close()
114
+ except Exception as e:
115
+ logger.warning(f"[RabbitMQ] 关闭旧 {label} pool 失败: {e}")
116
+
95
117
  async def _get_connection(self) -> aio_pika.abc.AbstractRobustConnection:
96
118
  try:
97
119
  return await aio_pika.connect_robust(
@@ -118,24 +140,33 @@ class RabbitMQ(BasicQueue):
118
140
  count = int(kwargs.get("count", 0))
119
141
  durable = kwargs.get("durable", True)
120
142
 
121
- async with self._channel_pool.acquire() as channel:
122
- if 0 < count < self.max_retry_count:
123
- rk = await self._setup_delay_queue(channel, queue_name,** kwargs)
124
- elif count >= self.max_retry_count:
125
- rk = await self._setup_dead_letter_queue(channel, queue_name, **kwargs)
126
- else:
127
- rk = await self._setup_normal_queue(channel, queue_name, durable)
128
-
129
- msg = self._create_message(data, count)
130
- await channel.default_exchange.publish(msg, routing_key=rk)
131
- logger.debug(f"[PUSH] queue={queue_name} count={count}")
132
- return msg
143
+ try:
144
+ async with self._channel_pool.acquire() as channel:
145
+ if 0 < count < self.max_retry_count:
146
+ rk = await self._setup_delay_queue(channel, queue_name,** kwargs)
147
+ elif count >= self.max_retry_count:
148
+ rk = await self._setup_dead_letter_queue(channel, queue_name, **kwargs)
149
+ else:
150
+ rk = await self._setup_normal_queue(channel, queue_name, durable)
151
+
152
+ msg = self._create_message(data, count)
153
+ await channel.default_exchange.publish(msg, routing_key=rk)
154
+ logger.debug(f"[PUSH] queue={queue_name} count={count}")
155
+ return msg
156
+ except (ConnectionClosed, ChannelClosed):
157
+ logger.warning("[PUSH] 连接断开,准备重连")
158
+ await self._invalidate_pools()
159
+ return None
160
+ except Exception as e:
161
+ logger.error(f"[PUSH] 异常 queue={queue_name}: {e}", exc_info=True)
162
+ return None
133
163
 
134
164
  async def pop(self, queue_name: str,** kwargs: Any) -> Optional[aio_pika.abc.AbstractIncomingMessage]:
135
165
  if self._closed:
136
166
  return None
137
167
 
138
168
  await self.init()
169
+ # durable 须与 push 声明队列时一致(默认 True),否则 re-declare 可能 PRECONDITION_FAILED
139
170
  durable = kwargs.get("durable", True)
140
171
 
141
172
  try:
@@ -148,11 +179,31 @@ class RabbitMQ(BasicQueue):
148
179
  return None
149
180
  except (ConnectionClosed, ChannelClosed):
150
181
  logger.warning("[POP] 连接断开,准备重连")
151
- self._ready.clear()
182
+ await self._invalidate_pools()
152
183
  return None
153
184
  except Exception as e:
154
185
  logger.error(f"[POP] 异常 queue={queue_name}: {e}", exc_info=True)
155
186
  return None
187
+
188
+ async def queue_length(self, queue_name: str) -> int:
189
+ """Return queue message count, or QUEUE_LENGTH_UNAVAILABLE (-1) on failure."""
190
+ if self._closed:
191
+ return self.QUEUE_LENGTH_UNAVAILABLE
192
+
193
+ try:
194
+ await self.init()
195
+ async with self._channel_pool.acquire() as channel:
196
+ # passive:只查询已存在队列,不创建;队列不存在则 ChannelClosed → -1
197
+ queue = await channel.declare_queue(queue_name, passive=True)
198
+ count = queue.declaration_result.message_count
199
+ return count if count is not None else 0
200
+ except (ConnectionClosed, ChannelClosed):
201
+ logger.warning("[QUEUE_LENGTH] 连接断开,准备重连")
202
+ await self._invalidate_pools()
203
+ return self.QUEUE_LENGTH_UNAVAILABLE
204
+ except Exception as e:
205
+ logger.error(f"[QUEUE_LENGTH] 异常 queue={queue_name}: {e}", exc_info=True)
206
+ return self.QUEUE_LENGTH_UNAVAILABLE
156
207
 
157
208
  async def ack(self, message: aio_pika.abc.AbstractIncomingMessage) -> None:
158
209
  try:
@@ -217,13 +268,5 @@ class RabbitMQ(BasicQueue):
217
268
  if self._closed:
218
269
  return
219
270
  self._closed = True
220
- self._ready.clear()
221
-
222
- try:
223
- if self._channel_pool:
224
- await self._channel_pool.close()
225
- if self._connection_pool:
226
- await self._connection_pool.close()
227
- logger.info("[RabbitMQ] 已关闭")
228
- except Exception as e:
229
- logger.error(f"[关闭失败] {e}")
271
+ await self._invalidate_pools()
272
+ logger.info("[RabbitMQ] 已关闭")
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+
8
+ class EnvLoader:
9
+ def __init__(self, task_id: str | None = None):
10
+ self.prefix = f"{task_id.upper()}_" if task_id else ""
11
+
12
+ def _candidate_keys(self, key: str) -> list[str]:
13
+ uk = key.upper()
14
+ if self.prefix:
15
+ # 有任务前缀:优先前缀变量,其次全局公共变量
16
+ return [self.prefix + uk, uk]
17
+ # 无task_id,只查全局公共变量
18
+ return [uk]
19
+
20
+ def get(self, key: str, default: str | None = None) -> str | None:
21
+ for k in self._candidate_keys(key):
22
+ val = os.getenv(k)
23
+ if val is not None:
24
+ return val.strip()
25
+ return default
26
+
27
+ def get_int(self, key: str, default: int | None = None) -> int | None:
28
+ raw_val = None
29
+ for k in self._candidate_keys(key):
30
+ raw_val = os.getenv(k)
31
+ if raw_val is not None:
32
+ break
33
+ if raw_val is None:
34
+ return default
35
+ try:
36
+ return int(raw_val.strip())
37
+ except (ValueError, TypeError):
38
+ return default
39
+
40
+ def get_bool(self, key: str, default: bool = False) -> bool:
41
+ raw_val = None
42
+ for k in self._candidate_keys(key):
43
+ raw_val = os.getenv(k)
44
+ if raw_val is not None:
45
+ break
46
+ if raw_val is None:
47
+ return default
48
+ v = raw_val.strip().lower()
49
+ true_set = {"1", "true", "yes", "on", "y"}
50
+ false_set = {"0", "false", "no", "off", "n"}
51
+ if v in true_set:
52
+ return True
53
+ if v in false_set:
54
+ return False
55
+ return default
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async-task-kit
3
- Version: 0.1.1
3
+ Version: 0.1.13
4
4
  Summary: A powerful async task processing kit based on RabbitMQ with Coroutine, Thread, and Process support.
5
5
  Author-email: realwrtoff <realwrtoff@gmail.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -10,6 +10,7 @@ Requires-Python: >=3.12
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  Requires-Dist: aio-pika>=9.4.0
13
+ Requires-Dist: asyncpg>=0.31.0
13
14
  Requires-Dist: python-dotenv>=1.0.0
14
15
  Dynamic: license-file
15
16
 
@@ -174,6 +175,54 @@ if __name__ == "__main__":
174
175
  asyncio.run(publish())
175
176
  ```
176
177
 
178
+ ### 5. RabbitMQ Client API
179
+
180
+ `RabbitMQ` 提供连接池、`push` / `pop`、延迟重试队列,以及 `queue_length` 查询队列深度。
181
+
182
+ #### `queue_length`
183
+
184
+ ```python
185
+ from async_task_kit import RabbitMQ
186
+
187
+ rmq = RabbitMQ("amqp://guest:guest@localhost/")
188
+ await rmq.init()
189
+
190
+ length = await rmq.queue_length("my_demo_queue")
191
+ if length == RabbitMQ.QUEUE_LENGTH_UNAVAILABLE:
192
+ # -1:连不上或队列不存在,稍后重试
193
+ ...
194
+ elif length == 0:
195
+ # 队列存在且为空
196
+ ...
197
+ else:
198
+ # 当前消息数
199
+ ...
200
+
201
+ await rmq.close()
202
+ ```
203
+
204
+ | 返回值 | 含义 |
205
+ |--------|------|
206
+ | `>= 0` | 队列真实消息数(`0` = 空队列) |
207
+ | `-1` (`QUEUE_LENGTH_UNAVAILABLE`) | 连接失败、队列不存在或其它异常,应重试 |
208
+
209
+ 实现要点:
210
+
211
+ - 使用 **`passive=True`** 声明:只查询已存在的队列,**不会**自动创建空队列。
212
+ - 断连时会关闭旧连接池并自动重建(与 `push` / `pop` 一致)。
213
+
214
+ #### `pop` 的 `durable` 参数
215
+
216
+ `pop` 在取消息前会 `declare_queue`。`push` 默认以 **`durable=True`** 创建队列(重启后队列仍在),因此 `pop` 默认同样传 `durable=True`,保证声明参数与已有队列一致,避免 RabbitMQ 返回 `PRECONDITION_FAILED`。
217
+
218
+ 若你的队列是非持久化的,调用时需显式传入:
219
+
220
+ ```python
221
+ await rmq.pop("my_queue", durable=False)
222
+ ```
223
+
224
+ `queue_length` 使用 passive 模式,无需传 `durable`。
225
+
177
226
  ## License
178
227
 
179
228
  MIT
@@ -1,2 +1,3 @@
1
1
  aio-pika>=9.4.0
2
+ asyncpg>=0.31.0
2
3
  python-dotenv>=1.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "async-task-kit"
7
- version = "0.1.1"
7
+ version = "0.1.13"
8
8
  description = "A powerful async task processing kit based on RabbitMQ with Coroutine, Thread, and Process support."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -13,7 +13,8 @@ authors = [
13
13
  ]
14
14
  dependencies = [
15
15
  "aio-pika>=9.4.0",
16
- "python-dotenv>=1.0.0"
16
+ "asyncpg>=0.31.0",
17
+ "python-dotenv>=1.0.0",
17
18
  ]
18
19
  classifiers = [
19
20
  "Programming Language :: Python :: 3",
@@ -21,6 +22,9 @@ classifiers = [
21
22
  "Operating System :: OS Independent",
22
23
  ]
23
24
 
25
+ [dependency-groups]
26
+ dev = ["build", "twine"]
27
+
24
28
  [tool.setuptools.packages.find]
25
29
  where = ["."]
26
- include = ["async_task_kit*"]
30
+ include = ["async_task_kit*"]
@@ -1,16 +0,0 @@
1
- import os
2
- from dotenv import load_dotenv
3
- load_dotenv()
4
-
5
- class EnvLoader:
6
- def __init__(self, task_id=None):
7
- self.prefix = f"{task_id}_".upper() if task_id else ""
8
-
9
- def get(self, key, default=None):
10
- return os.getenv(f"{self.prefix}{key}", default)
11
-
12
- def get_int(self, key, default=1):
13
- try:
14
- return int(os.getenv(f"{self.prefix}{key}", default))
15
- except:
16
- return default
File without changes