fast-mq-task 1.0.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.
File without changes
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: fast-mq-task
3
+ Version: 1.0.0
4
+ Summary: fast rabbitmq task for zsodata
5
+ Home-page: http://www.zsodata.com
6
+ Author: zsodata
7
+ Author-email: team@zso.io
8
+ License: BSD License
9
+ Platform: all
10
+ Requires-Python: >=3.7
11
+
12
+ pip uninstall fast-mq-task
13
+
14
+
15
+ pip install --upgrade fast-mq-task -i https://pypi.python.org/simple
16
+ pip show fast-mq-task
@@ -0,0 +1,5 @@
1
+ pip uninstall fast-mq-task
2
+
3
+
4
+ pip install --upgrade fast-mq-task -i https://pypi.python.org/simple
5
+ pip show fast-mq-task
File without changes
@@ -0,0 +1,88 @@
1
+ # -*- coding: utf-8 -*-
2
+ import logging
3
+ from typing import Dict, Union
4
+ from aio_pika import connect, Exchange, ExchangeType, Channel, Queue
5
+ from aio_pika.abc import AbstractExchange, AbstractQueue
6
+
7
+ from .keys import get_default_exchange_name, get_default_routing_key, get_default_queue_name
8
+
9
+
10
+ # --------------------------
11
+ # 客户端基础模块 (client.py)
12
+ # --------------------------
13
+ class RabbitMQClient:
14
+ def __init__(self, amqp_url: str):
15
+ self.amqp_url = amqp_url
16
+ self.connection = None
17
+ self.channels: Dict[str, Channel] = {}
18
+ self.exchanges: Dict[str, Union[Exchange, AbstractExchange]] = {}
19
+ self.queues: Dict[str, Union[Queue, AbstractQueue]] = {}
20
+
21
+ async def connect(self):
22
+ self.connection = await connect(self.amqp_url)
23
+ return self
24
+
25
+ async def close(self):
26
+ for channel in self.channels.values():
27
+ await channel.close()
28
+ await self.connection.close()
29
+
30
+ async def get_channel(self, task_type: str, prefetch: int = 4) -> Channel:
31
+ task_type = task_type or 'default'
32
+ if task_type not in self.channels:
33
+ channel = await self.connection.channel()
34
+ await channel.set_qos(
35
+ prefetch_count=prefetch
36
+ )
37
+ logging.info(f"信道定义: task_type={task_type}, prefetch={prefetch}")
38
+ self.channels[task_type] = channel
39
+
40
+ return self.channels[task_type]
41
+
42
+ async def get_exchange(self, channel: Channel, exchange_name: str = None, exchange_type: str = "topic") -> Exchange:
43
+ # 统一交换器类型为小写
44
+ exchange_type = exchange_type.lower()
45
+
46
+ # 添加交换器类型校验
47
+ if exchange_type not in ["direct", "topic", "fanout", "headers"]:
48
+ raise ValueError(f"Invalid exchange type: {exchange_type}")
49
+ if not exchange_name:
50
+ exchange_name = get_default_exchange_name()
51
+
52
+ if exchange_name not in self.exchanges:
53
+ self.exchanges[exchange_name] = await channel.declare_exchange(
54
+ name=exchange_name,
55
+ type=exchange_type,
56
+ durable=True, # 持久化交换器
57
+ auto_delete=False,
58
+ arguments={
59
+ "x-queue-type": "quorum" # 使用高可用队列类型
60
+ }
61
+ )
62
+ logging.info(f"交换机定义: exchange={exchange_name}, type={exchange_type}")
63
+
64
+ return self.exchanges[exchange_name]
65
+
66
+ async def ensure_declare(self, task_type: str, prefetch: int = 10):
67
+ # 创建专属channel避免相互影响
68
+ channel = await self.get_channel(task_type, prefetch)
69
+
70
+ exchange_key = get_default_exchange_name()
71
+ exchange_type = ExchangeType.TOPIC
72
+ routing_key = get_default_routing_key(task_type)
73
+ queue_key = get_default_queue_name(task_type)
74
+
75
+ # 声明交换器和队列
76
+ exchange = await self.get_exchange(channel=channel, exchange_name=exchange_key, exchange_type=exchange_type)
77
+ if queue_key not in self.queues:
78
+ queue = await channel.declare_queue(
79
+ name=queue_key,
80
+ durable=True,
81
+ arguments={
82
+ 'x-max-priority': 10 # 支持优先级队列
83
+ }
84
+ )
85
+ await queue.bind(exchange, routing_key)
86
+ self.queues[queue_key] = queue
87
+
88
+ return self.queues[queue_key], exchange
@@ -0,0 +1,98 @@
1
+ # -*- coding: utf-8 -*-
2
+ import asyncio
3
+ import inspect
4
+ import logging
5
+ import traceback
6
+ from functools import partial
7
+ from typing import Callable, Dict
8
+ from aio_pika import IncomingMessage
9
+ from .client import RabbitMQClient
10
+ from .decorators import TaskRegistry
11
+ from .models import TaskMessage, RabbitMeta
12
+ from concurrent.futures import ThreadPoolExecutor
13
+
14
+
15
+ # --------------------------
16
+ # 消费者模块 (consumer.py)
17
+ # --------------------------
18
+ class TaskConsumer(RabbitMQClient):
19
+ def __init__(
20
+ self,
21
+ amqp_url: str,
22
+ task_config: Dict[str, Dict] = None, # 新增任务专属配置
23
+ default_prefetch: int = 10
24
+ ):
25
+ super().__init__(amqp_url)
26
+ self.task_config = task_config or {}
27
+ self.default_prefetch = default_prefetch
28
+ self._running = False
29
+ self._executors: Dict[str, ThreadPoolExecutor] = {} # 任务专属线程池
30
+ self._semaphores: Dict[str, asyncio.Semaphore] = {} # 异步信号量控制
31
+
32
+ async def _init_resources(self):
33
+ """初始化每个任务的执行资源"""
34
+ for idx, task_type in enumerate(TaskRegistry._handlers):
35
+ # 创建独立线程池
36
+ config = self.task_config.get(task_type, {})
37
+ max_workers = config.get('max_workers', 4)
38
+ self._executors[task_type] = ThreadPoolExecutor(
39
+ max_workers=max_workers,
40
+ thread_name_prefix=f"Worker-{task_type}-"
41
+ )
42
+
43
+ # 创建异步信号量
44
+ prefetch = config.get('prefetch', self.default_prefetch)
45
+ self._semaphores[task_type] = asyncio.Semaphore(prefetch)
46
+ logger.info(f'消费者初始化[{idx}]: task_type={task_type}, prefetch={prefetch}, max_workers={max_workers}')
47
+
48
+ async def _process_message(self, rabbit_meta: RabbitMeta, msg: IncomingMessage):
49
+ """处理消息的优化版本"""
50
+ async with msg.process():
51
+ try:
52
+ handler: Callable = rabbit_meta.handler_func
53
+ # 获取任务配置
54
+ task_type = rabbit_meta.task_type
55
+ semaphore = self._semaphores[task_type]
56
+ executor = self._executors[task_type]
57
+
58
+ # 使用专属信号量控制并发
59
+ async with semaphore:
60
+ task = TaskMessage.parse_raw(msg.body)
61
+
62
+ if inspect.iscoroutinefunction(handler):
63
+ # 异步任务直接await
64
+ await handler(task)
65
+ else:
66
+ # 同步任务放入专属线程池
67
+ loop = asyncio.get_event_loop()
68
+ await loop.run_in_executor(
69
+ executor,
70
+ partial(handler, task)
71
+ )
72
+ except Exception as e:
73
+ logging.error(f"处理失败: {traceback.format_exc()}")
74
+
75
+ async def _setup_queues(self):
76
+ """队列初始化优化"""
77
+ for rabbit_meta in TaskRegistry._handlers.values():
78
+ queue, _ = await self.ensure_declare(task_type=rabbit_meta.task_type, prefetch=rabbit_meta.prefetch or self.default_prefetch)
79
+
80
+ # 为每个队列创建独立消费者
81
+ await queue.consume(
82
+ partial(self._process_message, rabbit_meta)
83
+ )
84
+
85
+ async def start(self):
86
+ """启动消费者"""
87
+ await self._init_resources()
88
+ await self._setup_queues()
89
+ self._running = True
90
+ while self._running:
91
+ await asyncio.sleep(1)
92
+
93
+ async def stop(self):
94
+ """优雅关闭"""
95
+ self._running = False
96
+ for executor in self._executors.values():
97
+ executor.shutdown(wait=True)
98
+ await super().close()
@@ -0,0 +1,32 @@
1
+ # -*- coding: utf-8 -*-
2
+ import logging
3
+ from typing import Callable, Dict
4
+ from .models import RabbitMeta
5
+
6
+
7
+ # --------------------------
8
+ # 装饰器模块 (decorators.py)
9
+ # --------------------------
10
+ class TaskRegistry:
11
+ _handlers: Dict[str, RabbitMeta] = {}
12
+
13
+ @classmethod
14
+ def register(cls, task_type: str) -> Callable:
15
+ # 新增参数校验
16
+ if not task_type:
17
+ raise ValueError("`task_type`不能全部为空")
18
+
19
+ def decorator(func: Callable) -> Callable:
20
+ meta = RabbitMeta(
21
+ task_type=task_type,
22
+ handler_func=func
23
+ )
24
+ cls._handlers[task_type] = meta
25
+ logging.info(f"处理器注册: task_type={task_type}, exchange={meta.queue_key}, func={func}")
26
+
27
+ return func
28
+
29
+ return decorator
30
+
31
+
32
+ task_handler = TaskRegistry.register
@@ -0,0 +1,19 @@
1
+ import os
2
+
3
+
4
+ def get_default_exchange_name():
5
+ return f"{os.getenv('RABBIT_EXCHANGE_PREFIX') or 'fast'}.{os.getenv('APP_CODE') or 'default'}"
6
+
7
+
8
+ def get_default_routing_key(task_type: str):
9
+ if not task_type:
10
+ raise ValueError(f'task_type must not none: task_type={task_type}')
11
+
12
+ return f"{os.getenv('RABBIT_ROUTING_KEY_PREFIX') or 'fast'}.{os.getenv('APP_CODE') or 'default'}.{task_type}"
13
+
14
+
15
+ def get_default_queue_name(task_type: str):
16
+ if not task_type:
17
+ raise ValueError(f'task_type must not none: task_type={task_type}')
18
+
19
+ return f"{os.getenv('RABBIT_QUEUE_PREFIX') or 'fast'}.{os.getenv('APP_CODE') or 'default'}.{task_type}"
@@ -0,0 +1,57 @@
1
+ # -*- coding: utf-8 -*-
2
+ import time
3
+ import uuid
4
+ from typing import Any, Dict, Callable
5
+ from aio_pika import ExchangeType
6
+ from pydantic import BaseModel
7
+
8
+ from . import keys
9
+
10
+
11
+ # --------------------------
12
+ # 模型类模块 (models.py)
13
+ # --------------------------
14
+ class RabbitMeta(BaseModel):
15
+ """
16
+ 消息发布元数据
17
+ """
18
+ # 交换机键
19
+ task_type: str = None
20
+ # 交换机键
21
+ exchange_key: str = None
22
+ # 交换机类型
23
+ exchange_type: str = None
24
+ # 绑定关系(路由)键
25
+ routing_key: str = None
26
+ # 队列名称
27
+ queue_key: str = None
28
+ # 队列名称
29
+ prefetch: int = None
30
+ # 队列名称
31
+ max_workers: int = None
32
+
33
+ # 处理函数
34
+ handler_func: Callable = None
35
+
36
+ def __init__(self, task_type: str = None, prefetch: int = None, max_workers: int = None, handler_func: Callable = None, **data: Any):
37
+ super().__init__(**data)
38
+ self.task_type = task_type
39
+
40
+ self.prefetch = prefetch
41
+ self.max_workers = max_workers
42
+ self.handler_func = handler_func
43
+
44
+ self.exchange_key = keys.get_default_exchange_name()
45
+ self.exchange_type = ExchangeType.TOPIC
46
+ self.routing_key = keys.get_default_routing_key(task_type)
47
+ self.queue_key = keys.get_default_queue_name(task_type)
48
+
49
+
50
+ class TaskMessage(BaseModel):
51
+ task_type: str
52
+ data: Dict[str, Any]
53
+ task_id: str = str(uuid.uuid4())
54
+ created_at: int = int(time.time())
55
+
56
+ def get_routing_key(self):
57
+ return keys.get_default_routing_key(self.task_type)
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf-8 -*-
2
+ import time
3
+ from aio_pika import Message, DeliveryMode
4
+ from .client import RabbitMQClient
5
+ from .models import TaskMessage
6
+
7
+
8
+ # --------------------------
9
+ # 生产者模块 (producer.py)
10
+ # --------------------------
11
+ class TaskProducer(RabbitMQClient):
12
+ async def publish_task(self, task: TaskMessage):
13
+ # 确保队列定义
14
+ _, exchange = await self.ensure_declare(task_type=task.task_type)
15
+
16
+ message = Message(
17
+ body=task.json().encode(),
18
+ content_type="application/json",
19
+ headers={
20
+ "x-task-type": task.task_type,
21
+ "x-created-at": time.time()
22
+ },
23
+ delivery_mode=DeliveryMode.PERSISTENT # 添加消息持久化
24
+ )
25
+ await exchange.publish(
26
+ message=message,
27
+ routing_key=task.get_routing_key()
28
+ )
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: fast-mq-task
3
+ Version: 1.0.0
4
+ Summary: fast rabbitmq task for zsodata
5
+ Home-page: http://www.zsodata.com
6
+ Author: zsodata
7
+ Author-email: team@zso.io
8
+ License: BSD License
9
+ Platform: all
10
+ Requires-Python: >=3.7
11
+
12
+ pip uninstall fast-mq-task
13
+
14
+
15
+ pip install --upgrade fast-mq-task -i https://pypi.python.org/simple
16
+ pip show fast-mq-task
@@ -0,0 +1,14 @@
1
+ MANIFEST.in
2
+ README.rst
3
+ setup.py
4
+ fast_mq_task/__init__.py
5
+ fast_mq_task/client.py
6
+ fast_mq_task/consumer.py
7
+ fast_mq_task/decorators.py
8
+ fast_mq_task/keys.py
9
+ fast_mq_task/models.py
10
+ fast_mq_task/producer.py
11
+ fast_mq_task.egg-info/PKG-INFO
12
+ fast_mq_task.egg-info/SOURCES.txt
13
+ fast_mq_task.egg-info/dependency_links.txt
14
+ fast_mq_task.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ fast_mq_task
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ from distutils.core import setup
2
+ from setuptools import find_packages
3
+
4
+ with open('README.rst', 'r', encoding="utf-8") as f:
5
+ long_description = f.read()
6
+
7
+ setup(name='fast-mq-task',
8
+ version='1.0.0',
9
+ description='fast rabbitmq task for zsodata',
10
+ long_description=long_description,
11
+ author='zsodata',
12
+ author_email='team@zso.io',
13
+ url='http://www.zsodata.com',
14
+ install_requires=[
15
+ ],
16
+ python_requires='>=3.7',
17
+ license='BSD License',
18
+ packages=find_packages(),
19
+ platforms=['all'],
20
+ include_package_data=True
21
+ )