async-task-kit 0.1.2__tar.gz → 0.1.14__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.
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/PKG-INFO +49 -1
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/README.md +48 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/consumer/coroutine.py +6 -3
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/consumer/process.py +1 -1
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/consumer/thread.py +1 -1
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/core/rabbitmq.py +83 -28
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit.egg-info/PKG-INFO +49 -1
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/pyproject.toml +1 -1
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/LICENSE +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/__init__.py +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/consumer/__init__.py +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/consumer/base.py +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/core/__init__.py +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/core/processor.py +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/utils/__init__.py +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/utils/env_loader.py +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/utils/logger.py +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit.egg-info/SOURCES.txt +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit.egg-info/dependency_links.txt +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit.egg-info/requires.txt +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit.egg-info/top_level.txt +0 -0
- {async_task_kit-0.1.2 → async_task_kit-0.1.14}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: async-task-kit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.14
|
|
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
|
|
@@ -175,6 +175,54 @@ if __name__ == "__main__":
|
|
|
175
175
|
asyncio.run(publish())
|
|
176
176
|
```
|
|
177
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
|
+
|
|
178
226
|
## License
|
|
179
227
|
|
|
180
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
|
|
@@ -18,6 +18,7 @@ class CoroutineConsumer(BaseConsumer):
|
|
|
18
18
|
concurrency: int = 1,
|
|
19
19
|
max_retry: int = 3,
|
|
20
20
|
retry_delay: int = 30,
|
|
21
|
+
rmq: RabbitMQ | None = None,
|
|
21
22
|
):
|
|
22
23
|
self.amqp_url = amqp_url
|
|
23
24
|
self.queue_name = queue_name
|
|
@@ -26,7 +27,8 @@ class CoroutineConsumer(BaseConsumer):
|
|
|
26
27
|
self.max_retry = max_retry
|
|
27
28
|
self.retry_delay = retry_delay
|
|
28
29
|
|
|
29
|
-
self.
|
|
30
|
+
self._owns_rmq = rmq is None
|
|
31
|
+
self._rmq = rmq or RabbitMQ(amqp_url)
|
|
30
32
|
self._stop_event = asyncio.Event()
|
|
31
33
|
|
|
32
34
|
async def start(self):
|
|
@@ -93,5 +95,6 @@ class CoroutineConsumer(BaseConsumer):
|
|
|
93
95
|
|
|
94
96
|
async def stop(self):
|
|
95
97
|
self._stop_event.set()
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
if self._owns_rmq:
|
|
99
|
+
await self._rmq.close()
|
|
100
|
+
logger.info(f"[CoroutineConsumer] stopped | queue={self.queue_name}")
|
|
@@ -12,6 +12,7 @@ from aio_pika.exceptions import (
|
|
|
12
12
|
ChannelClosed,
|
|
13
13
|
AMQPError
|
|
14
14
|
)
|
|
15
|
+
from aiormq.exceptions import ChannelInvalidStateError
|
|
15
16
|
from aio_pika.pool import Pool
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
@@ -29,7 +30,11 @@ class BasicQueue(ABC):
|
|
|
29
30
|
@abstractmethod
|
|
30
31
|
async def pop(self, queue_name: str,** kwargs: Any) -> Any:
|
|
31
32
|
raise NotImplementedError
|
|
32
|
-
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
async def queue_length(self, queue_name: str) -> int:
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
33
38
|
@abstractmethod
|
|
34
39
|
async def ack(self, message: Any) -> None:
|
|
35
40
|
raise NotImplementedError
|
|
@@ -50,6 +55,7 @@ class RabbitMQ(BasicQueue):
|
|
|
50
55
|
DEFAULT_DEAD_LETTER_DAYS = 7
|
|
51
56
|
DEFAULT_DELAY_SECONDS = 30
|
|
52
57
|
DEFAULT_POP_TIMEOUT = 1.0
|
|
58
|
+
QUEUE_LENGTH_UNAVAILABLE = -1
|
|
53
59
|
|
|
54
60
|
def __init__(
|
|
55
61
|
self,
|
|
@@ -58,6 +64,7 @@ class RabbitMQ(BasicQueue):
|
|
|
58
64
|
channel_size: int = DEFAULT_CHANNEL_SIZE,
|
|
59
65
|
heartbeat: int = 60,
|
|
60
66
|
max_retry_count: int = DEFAULT_MAX_RETRY_COUNT,
|
|
67
|
+
|
|
61
68
|
):
|
|
62
69
|
self.rabbit_url = rabbit_url
|
|
63
70
|
self.pool_size = pool_size
|
|
@@ -92,6 +99,23 @@ class RabbitMQ(BasicQueue):
|
|
|
92
99
|
logger.error(f"[RabbitMQ] 初始化失败: {e}", exc_info=True)
|
|
93
100
|
raise
|
|
94
101
|
|
|
102
|
+
async def _invalidate_pools(self) -> None:
|
|
103
|
+
"""断连或 close 时关闭 pool,下次 init() 会重建。"""
|
|
104
|
+
async with self._init_lock:
|
|
105
|
+
self._ready.clear()
|
|
106
|
+
channel_pool = self._channel_pool
|
|
107
|
+
connection_pool = self._connection_pool
|
|
108
|
+
self._channel_pool = None
|
|
109
|
+
self._connection_pool = None
|
|
110
|
+
|
|
111
|
+
for pool, label in ((channel_pool, "channel"), (connection_pool, "connection")):
|
|
112
|
+
if pool is None:
|
|
113
|
+
continue
|
|
114
|
+
try:
|
|
115
|
+
await pool.close()
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.warning(f"[RabbitMQ] 关闭旧 {label} pool 失败: {e}")
|
|
118
|
+
|
|
95
119
|
async def _get_connection(self) -> aio_pika.abc.AbstractRobustConnection:
|
|
96
120
|
try:
|
|
97
121
|
return await aio_pika.connect_robust(
|
|
@@ -118,41 +142,80 @@ class RabbitMQ(BasicQueue):
|
|
|
118
142
|
count = int(kwargs.get("count", 0))
|
|
119
143
|
durable = kwargs.get("durable", True)
|
|
120
144
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
try:
|
|
146
|
+
async with self._channel_pool.acquire() as channel:
|
|
147
|
+
if 0 < count < self.max_retry_count:
|
|
148
|
+
rk = await self._setup_delay_queue(channel, queue_name,** kwargs)
|
|
149
|
+
elif count >= self.max_retry_count:
|
|
150
|
+
rk = await self._setup_dead_letter_queue(channel, queue_name, **kwargs)
|
|
151
|
+
else:
|
|
152
|
+
rk = await self._setup_normal_queue(channel, queue_name, durable)
|
|
153
|
+
|
|
154
|
+
msg = self._create_message(data, count)
|
|
155
|
+
await channel.default_exchange.publish(msg, routing_key=rk)
|
|
156
|
+
logger.debug(f"[PUSH] queue={queue_name} count={count}")
|
|
157
|
+
return msg
|
|
158
|
+
except (ConnectionClosed, ChannelClosed):
|
|
159
|
+
logger.warning("[PUSH] 连接断开,准备重连")
|
|
160
|
+
await self._invalidate_pools()
|
|
161
|
+
return None
|
|
162
|
+
except ChannelInvalidStateError:
|
|
163
|
+
logger.warning("[PUSH] 信道 RPC 异常,准备重建 pool")
|
|
164
|
+
await self._invalidate_pools()
|
|
165
|
+
return None
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(f"[PUSH] 异常 queue={queue_name}: {e}", exc_info=True)
|
|
168
|
+
return None
|
|
133
169
|
|
|
134
170
|
async def pop(self, queue_name: str,** kwargs: Any) -> Optional[aio_pika.abc.AbstractIncomingMessage]:
|
|
135
171
|
if self._closed:
|
|
136
172
|
return None
|
|
137
173
|
|
|
138
174
|
await self.init()
|
|
175
|
+
# durable 须与 push 声明队列时一致(默认 True),否则 re-declare 可能 PRECONDITION_FAILED
|
|
139
176
|
durable = kwargs.get("durable", True)
|
|
140
177
|
|
|
141
178
|
try:
|
|
142
179
|
async with self._channel_pool.acquire() as channel:
|
|
143
180
|
queue = await channel.declare_queue(queue_name, durable=durable)
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
# 使用 aio_pika 自带 timeout,勿用 asyncio.wait_for 取消 get(会关闭 channel)
|
|
182
|
+
return await queue.get(
|
|
183
|
+
timeout=self.DEFAULT_POP_TIMEOUT,
|
|
184
|
+
fail=False,
|
|
146
185
|
)
|
|
147
186
|
except (QueueEmpty, asyncio.TimeoutError):
|
|
148
187
|
return None
|
|
149
|
-
except (ConnectionClosed, ChannelClosed):
|
|
150
|
-
logger.warning("[POP]
|
|
151
|
-
self.
|
|
188
|
+
except (ConnectionClosed, ChannelClosed, ChannelInvalidStateError):
|
|
189
|
+
logger.warning("[POP] 连接/信道异常,准备重建 pool")
|
|
190
|
+
await self._invalidate_pools()
|
|
152
191
|
return None
|
|
153
192
|
except Exception as e:
|
|
154
193
|
logger.error(f"[POP] 异常 queue={queue_name}: {e}", exc_info=True)
|
|
155
194
|
return None
|
|
195
|
+
|
|
196
|
+
async def queue_length(self, queue_name: str) -> int:
|
|
197
|
+
"""Return queue message count, or QUEUE_LENGTH_UNAVAILABLE (-1) on failure."""
|
|
198
|
+
if self._closed:
|
|
199
|
+
return self.QUEUE_LENGTH_UNAVAILABLE
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
await self.init()
|
|
203
|
+
async with self._channel_pool.acquire() as channel:
|
|
204
|
+
# passive:只查询已存在队列,不创建;队列不存在则 ChannelClosed → -1
|
|
205
|
+
queue = await channel.declare_queue(queue_name, passive=True)
|
|
206
|
+
count = queue.declaration_result.message_count
|
|
207
|
+
return count if count is not None else 0
|
|
208
|
+
except (ConnectionClosed, ChannelClosed):
|
|
209
|
+
logger.warning("[QUEUE_LENGTH] 连接断开,准备重连")
|
|
210
|
+
await self._invalidate_pools()
|
|
211
|
+
return self.QUEUE_LENGTH_UNAVAILABLE
|
|
212
|
+
except ChannelInvalidStateError:
|
|
213
|
+
logger.warning("[QUEUE_LENGTH] 信道 RPC 异常,准备重建 pool")
|
|
214
|
+
await self._invalidate_pools()
|
|
215
|
+
return self.QUEUE_LENGTH_UNAVAILABLE
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error(f"[QUEUE_LENGTH] 异常 queue={queue_name}: {e}", exc_info=True)
|
|
218
|
+
return self.QUEUE_LENGTH_UNAVAILABLE
|
|
156
219
|
|
|
157
220
|
async def ack(self, message: aio_pika.abc.AbstractIncomingMessage) -> None:
|
|
158
221
|
try:
|
|
@@ -217,13 +280,5 @@ class RabbitMQ(BasicQueue):
|
|
|
217
280
|
if self._closed:
|
|
218
281
|
return
|
|
219
282
|
self._closed = True
|
|
220
|
-
self.
|
|
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}")
|
|
283
|
+
await self._invalidate_pools()
|
|
284
|
+
logger.info("[RabbitMQ] 已关闭")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: async-task-kit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.14
|
|
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
|
|
@@ -175,6 +175,54 @@ if __name__ == "__main__":
|
|
|
175
175
|
asyncio.run(publish())
|
|
176
176
|
```
|
|
177
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
|
+
|
|
178
226
|
## License
|
|
179
227
|
|
|
180
228
|
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "async-task-kit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.14"
|
|
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"
|
|
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
|