onestep 0.5.0__py3-none-any.whl

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.
onestep/__init__.py ADDED
@@ -0,0 +1,67 @@
1
+ from .onestep import step
2
+ from .cron import Cron
3
+ from .retry import (
4
+ BaseRetry, BaseErrorCallback, NackErrorCallBack,
5
+ NeverRetry, AlwaysRetry, TimesRetry, RetryIfException, AdvancedRetry
6
+ )
7
+ from .broker import (
8
+ BaseBroker, BaseConsumer,
9
+ MemoryBroker, RabbitMQBroker, WebHookBroker, CronBroker, RedisStreamBroker, RedisPubSubBroker, SQSBroker
10
+ )
11
+ from .middleware import (
12
+ BaseMiddleware, BaseConfigMiddleware,
13
+ NacosPublishConfigMiddleware, NacosConsumeConfigMiddleware,
14
+ RedisPublishConfigMiddleware, RedisConsumeConfigMiddleware,
15
+ UniqueMiddleware, MemoryUniqueMiddleware,
16
+ )
17
+ from .exception import (
18
+ StopMiddleware, DropMessage,
19
+ RetryException, RetryInQueue, RetryInLocal
20
+ )
21
+
22
+ __all__ = [
23
+ 'step', 'Cron',
24
+
25
+ # broker
26
+ 'BaseBroker',
27
+ 'BaseConsumer',
28
+ 'MemoryBroker',
29
+ 'RabbitMQBroker',
30
+ 'WebHookBroker',
31
+ 'CronBroker',
32
+ 'RedisStreamBroker',
33
+ 'RedisPubSubBroker',
34
+ 'SQSBroker',
35
+
36
+ # retry
37
+ 'BaseRetry',
38
+ 'NeverRetry',
39
+ 'AlwaysRetry',
40
+ 'TimesRetry',
41
+ 'RetryIfException',
42
+ 'AdvancedRetry',
43
+ # error callback
44
+ 'BaseErrorCallback',
45
+ 'NackErrorCallBack',
46
+
47
+ # middleware
48
+ 'BaseMiddleware',
49
+ 'BaseConfigMiddleware',
50
+ 'NacosPublishConfigMiddleware',
51
+ 'NacosConsumeConfigMiddleware',
52
+ 'RedisPublishConfigMiddleware',
53
+ 'RedisConsumeConfigMiddleware',
54
+ 'UniqueMiddleware',
55
+ 'MemoryUniqueMiddleware',
56
+
57
+ # exception
58
+ 'StopMiddleware',
59
+ 'DropMessage',
60
+ 'RetryException',
61
+ 'RetryInQueue',
62
+ 'RetryInLocal',
63
+
64
+ '__version__'
65
+ ]
66
+
67
+ __version__ = '0.4.4'
onestep/_utils.py ADDED
@@ -0,0 +1,21 @@
1
+ import functools
2
+ import logging
3
+
4
+
5
+ def catch_error(return_val=None):
6
+ """
7
+ 捕获异常装饰器
8
+ """
9
+
10
+ def decorator(func):
11
+ @functools.wraps(func)
12
+ def wrapper(*args, **kwargs):
13
+ try:
14
+ return func(*args, **kwargs)
15
+ except Exception as e:
16
+ logging.debug(e)
17
+ return return_val
18
+
19
+ return wrapper
20
+
21
+ return decorator
@@ -0,0 +1,22 @@
1
+ from .base import (
2
+ BaseBroker, BaseConsumer
3
+ )
4
+ from .memory import MemoryBroker, MemoryConsumer
5
+ from .webhook import WebHookBroker
6
+ from .rabbitmq import RabbitMQBroker, RabbitMQConsumer
7
+ from .mysql import MysqlBroker
8
+ from .sqs import SQSBroker, SQSConsumer, SNSBroker
9
+ from .redis import RedisStreamBroker, RedisPubSubBroker
10
+ from .cron import CronBroker
11
+
12
+
13
+ __all__ = [
14
+ "BaseBroker", "BaseConsumer",
15
+ "MemoryBroker", "MemoryConsumer",
16
+ "WebHookBroker",
17
+ "RabbitMQBroker", "RabbitMQConsumer",
18
+ "MysqlBroker",
19
+ "SQSBroker", "SQSConsumer", "SNSBroker",
20
+ "RedisStreamBroker", "RedisPubSubBroker",
21
+ "CronBroker"
22
+ ]
onestep/broker/base.py ADDED
@@ -0,0 +1,144 @@
1
+ # -*- coding: utf-8 -*-
2
+ import abc
3
+ import logging
4
+ from queue import Queue, Empty
5
+ from typing import Any, Optional, List, Callable
6
+
7
+ from onestep.middleware import BaseMiddleware
8
+ from onestep.exception import StopMiddleware
9
+ from onestep.message import Message
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class BaseBroker:
15
+ message_cls = Message
16
+
17
+ def __init__(self,
18
+ name: Optional[str] = None,
19
+ queue: Optional[Queue] = None,
20
+ middlewares: Optional[List[BaseMiddleware]] = None,
21
+ once: bool = False,
22
+ cancel_consume: Optional[Callable] = None):
23
+ """
24
+ @param name: broker name
25
+ @param queue: broker queue name
26
+ @param middlewares: broker middlewares
27
+ @param once: just run once
28
+ if once is True, when broker receive a message, it will shutdown
29
+ @param cancel_consume: cancel consume
30
+ cancel_consume(message) -> bool
31
+ if cancel_consume return True, broker will shutdown
32
+ """
33
+ self.queue = queue
34
+ self.name = name or "broker"
35
+ self.middlewares = []
36
+ self.once = once
37
+ self.cancel_consume = cancel_consume
38
+
39
+ if middlewares:
40
+ for middleware in middlewares:
41
+ self.add_middleware(middleware)
42
+
43
+ def add_middleware(self, middleware: BaseMiddleware):
44
+ if not isinstance(middleware, BaseMiddleware):
45
+ raise TypeError(f"middleware must be BaseMiddleware instance, not {type(middleware)}")
46
+ self.middlewares.append(middleware)
47
+
48
+ def send(self, message, *args, **kwargs):
49
+ """对消息进行预处理,然后再发送"""
50
+ if not isinstance(message, Message):
51
+ message = self.message_cls(body=message)
52
+ self.before_emit("send", message=message, step=kwargs.get("step"))
53
+ # TODO: 对消息发送进行N次重试,确保消息发送成功。
54
+ result = self.publish(message.to_json(), *args, **kwargs)
55
+ self.after_emit("send", message=message, step=kwargs.get("step"))
56
+ return result
57
+
58
+ @abc.abstractmethod
59
+ def publish(self, message: Any, *args, **kwargs):
60
+ """
61
+ 将消息原样发布到 broker 中。如果当前Broker是Job的to_broker, 则必须实现此方法
62
+ """
63
+ raise NotImplementedError('Please implement in subclasses.')
64
+
65
+ @abc.abstractmethod
66
+ def consume(self, *args, **kwargs):
67
+ """
68
+ 消费消息。如果当前Broker是Job的from_broker, 则必须实现此方法
69
+ """
70
+ raise NotImplementedError('Please implement in subclasses.')
71
+
72
+ @abc.abstractmethod
73
+ def confirm(self, message: Message):
74
+ """确认消息"""
75
+ raise NotImplementedError('Please implement in subclasses.')
76
+
77
+ @abc.abstractmethod
78
+ def reject(self, message: Message):
79
+ """拒绝消息"""
80
+ raise NotImplementedError('Please implement in subclasses.')
81
+
82
+ @abc.abstractmethod
83
+ def requeue(self, message: Message, is_source=False):
84
+ """
85
+ 重发消息:先拒绝 再 重入
86
+ is_source = False 重入使用消息的当前状态
87
+ is_source = True 重入使用消息的初始状态
88
+ """
89
+ raise NotImplementedError('Please implement in subclasses.')
90
+
91
+ def before_emit(self, signal, **kwargs):
92
+ signal = "before_" + signal
93
+ self._emit(signal, **kwargs)
94
+
95
+ def after_emit(self, signal, **kwargs):
96
+ signal = "after_" + signal
97
+ self._emit(signal, **kwargs)
98
+
99
+ def _emit(self, signal, **kwargs):
100
+ for middleware in self.middlewares:
101
+ if not hasattr(middleware, signal):
102
+ continue
103
+ try:
104
+ params = dict(kwargs)
105
+ params.setdefault("broker", self)
106
+ params.setdefault("step", None)
107
+ getattr(middleware, signal)(**params)
108
+ except StopMiddleware:
109
+ break
110
+
111
+ def shutdown(self):
112
+ """关闭Broker"""
113
+
114
+ def __repr__(self):
115
+ return f"<{self.__class__.__name__} {self.name}>"
116
+
117
+ def __str__(self):
118
+ return self.name
119
+
120
+
121
+ class BaseConsumer:
122
+
123
+ def __init__(self, broker: BaseBroker, *args, **kwargs):
124
+ self.queue = broker.queue
125
+ self.message_cls = broker.message_cls or Message
126
+ self.timeout = kwargs.pop("timeout", 1000)
127
+
128
+ def __next__(self):
129
+ try:
130
+ q = self.queue
131
+ if q is None:
132
+ return None
133
+ timeout_ms = self.timeout
134
+ if isinstance(timeout_ms, (int, float)) and timeout_ms > 0:
135
+ data = q.get(timeout=timeout_ms / 1000.0)
136
+ else:
137
+ # 当超时为0、负数或非数字时,使用非阻塞获取以避免ValueError
138
+ data = q.get_nowait()
139
+ return self.message_cls.from_broker(broker_message=data)
140
+ except (Empty, ValueError):
141
+ return None
142
+
143
+ def __iter__(self):
144
+ return self
onestep/broker/cron.py ADDED
@@ -0,0 +1,51 @@
1
+ """
2
+ 使用 CRON 表达式触发任务执行
3
+
4
+ 支持人性化 DSL 与宏:
5
+ - DSL:Cron.every(minutes=5)、Cron.daily(at="09:00")、Cron.weekly(on=["mon","fri"], at="10:30") 等
6
+ - 宏:@hourly/@daily/@weekly/@monthly/@yearly 以及扩展 @workdays/@weekends/@every 5m/2h/3d/1mo
7
+ """
8
+ import logging
9
+ import threading
10
+ from datetime import datetime
11
+ from typing import Any
12
+
13
+ from croniter import croniter
14
+ from ..cron import resolve_cron
15
+
16
+ from .memory import MemoryBroker, MemoryConsumer
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class CronBroker(MemoryBroker):
22
+
23
+ def __init__(self, cron, name=None, middlewares=None, body: Any = None, start_time=None, *args, **kwargs):
24
+ super().__init__(name=name, middlewares=middlewares, *args, **kwargs)
25
+ # 支持 DSL/宏/原始字符串:统一解析为标准表达式
26
+ self.cron = resolve_cron(cron)
27
+ self.start_time = start_time or datetime.now()
28
+ self.itr = croniter(self.cron, self.start_time)
29
+ self.next_fire_time = self.itr.get_next(datetime)
30
+ self.body = body
31
+ self._thread = None
32
+
33
+ def _scheduler(self):
34
+ if self.next_fire_time <= datetime.now():
35
+ self.next_fire_time = self.itr.get_next(datetime)
36
+ self.publish(self.body)
37
+
38
+ self._thread = threading.Timer(interval=1, function=self._scheduler)
39
+ self._thread.start()
40
+
41
+ def consume(self, *args, **kwargs):
42
+ self._scheduler()
43
+ return CronConsumer(self, *args, **kwargs)
44
+
45
+ def shutdown(self):
46
+ if self._thread:
47
+ self._thread.cancel()
48
+
49
+
50
+ class CronConsumer(MemoryConsumer):
51
+ ...
@@ -0,0 +1,75 @@
1
+ import logging
2
+ import json
3
+ from queue import Queue, Full as FullException
4
+ from typing import Any
5
+
6
+ from .base import BaseBroker, BaseConsumer
7
+
8
+ from ..message import Message
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class MemoryMessage(Message):
14
+
15
+ @classmethod
16
+ def from_broker(cls, broker_message: Any):
17
+ if isinstance(broker_message, (str, bytes, bytearray)):
18
+ try:
19
+ message = json.loads(broker_message)
20
+ except json.JSONDecodeError:
21
+ message = {"body": broker_message}
22
+ else:
23
+ message = broker_message
24
+ if not isinstance(message, dict):
25
+ message = {"body": message}
26
+ if "body" not in message:
27
+ # 来自 外部的消息 可能没有 body, 故直接认为都是 message.body
28
+ message = {"body": message}
29
+ extra_val = message.get("extra")
30
+ if not isinstance(extra_val, dict):
31
+ extra_val = None
32
+ return cls(body=message.get("body"), extra=extra_val, message=broker_message)
33
+
34
+
35
+ class MemoryBroker(BaseBroker):
36
+ message_cls = MemoryMessage
37
+
38
+ def __init__(self, maxsize=0, *args, **kwargs):
39
+ super().__init__(*args, **kwargs)
40
+ self.queue = Queue(maxsize)
41
+
42
+ def publish(self, message: Any, *args, **kwargs):
43
+ try:
44
+ self.queue.put_nowait(message)
45
+ except FullException:
46
+ logger.warning("CronBroker queue is full, skip this task, "
47
+ "you can increase maxsize with `maxsize` argument")
48
+
49
+ def consume(self, *args, **kwargs):
50
+ return MemoryConsumer(self, *args, **kwargs)
51
+
52
+ def confirm(self, message: Message):
53
+ """确认消息"""
54
+ pass
55
+
56
+ def reject(self, message: Message):
57
+ """拒绝消息"""
58
+ pass
59
+
60
+ def requeue(self, message: Message, is_source=False):
61
+ """重发消息:先拒绝 再 重入"""
62
+ if is_source:
63
+ self.publish(message.message)
64
+ else:
65
+ self.send(message)
66
+
67
+ def __repr__(self):
68
+ return f"<{self.__class__.__name__} {self.name}>"
69
+
70
+ def __str__(self):
71
+ return self.name
72
+
73
+
74
+ class MemoryConsumer(BaseConsumer):
75
+ ...
@@ -0,0 +1,63 @@
1
+ from queue import Queue
2
+ from typing import Optional, Dict, Any, cast
3
+
4
+ try:
5
+ from use_mysql import MysqlStore, Model, SQLModel
6
+ except ImportError:
7
+ MysqlStore = None
8
+
9
+ from .base import BaseBroker, BaseConsumer
10
+ from ..message import Message
11
+
12
+
13
+ class MysqlBroker(BaseBroker):
14
+
15
+ def __init__(
16
+ self,
17
+ params: Optional[Dict] = None,
18
+ auto_create: Optional[bool] = True,
19
+ *args,
20
+ **kwargs
21
+ ):
22
+ if MysqlStore is None or Model is None:
23
+ raise ImportError("未安装 use-mysql 依赖")
24
+
25
+ super().__init__(*args, **kwargs)
26
+ self.queue = Queue()
27
+ params = params or {}
28
+ auto_create = auto_create or False
29
+ self.client = MysqlStore(generate_schemas=auto_create, **params)
30
+ self.client.init()
31
+ self._shutdown = False
32
+
33
+ def send(self, message, *args, **kwargs):
34
+ if not isinstance(message, Message):
35
+ message = self.message_cls(body=message)
36
+ return self.publish(message, *args, **kwargs)
37
+
38
+
39
+ def publish(self, message: Any, **kwargs):
40
+ body_raw = message.body if isinstance(message, Message) else message
41
+ if not isinstance(body_raw, SQLModel):
42
+ raise TypeError("MySQL Broker 仅支持 Model 类型的消息体")
43
+
44
+ body = cast(SQLModel, body_raw)
45
+ return self.client.create(body.__class__, **body.model_dump())
46
+
47
+
48
+ def confirm(self, message: Message):
49
+ return None
50
+
51
+ def reject(self, message: Message):
52
+ return None
53
+
54
+ def requeue(self, message: Message, is_source=False):
55
+ return None
56
+
57
+ def shutdown(self):
58
+ self._shutdown = True
59
+ self.client.shutdown()
60
+
61
+
62
+ class MysqlConsumer(BaseConsumer):
63
+ ...
@@ -0,0 +1,153 @@
1
+ import json
2
+ import threading
3
+ from queue import Queue
4
+ from typing import Optional, Dict, Any
5
+
6
+ try:
7
+ import amqpstorm
8
+ from use_rabbitmq import useRabbitMQ as RabbitMQStore
9
+ except ImportError:
10
+ amqpstorm = None
11
+ RabbitMQStore = None
12
+
13
+ from .base import BaseBroker, BaseConsumer
14
+ from ..message import Message
15
+
16
+
17
+ class _RabbitMQMessage(Message):
18
+
19
+ @classmethod
20
+ def from_broker(cls, broker_message):
21
+ try:
22
+ message = json.loads(broker_message.body)
23
+ except json.JSONDecodeError:
24
+ message = {"body": broker_message.body}
25
+ if not isinstance(message, dict):
26
+ message = {"body": message}
27
+ if "body" not in message:
28
+ # 来自 外部的消息 可能没有 body, 故直接认为都是 message.body
29
+ message = {"body": message}
30
+
31
+ return cls(body=message.get("body"), extra=message.get("extra"), message=broker_message)
32
+
33
+
34
+ class RabbitMQBroker(BaseBroker):
35
+ message_cls = _RabbitMQMessage
36
+
37
+ def __init__(
38
+ self,
39
+ queue_name,
40
+ params: Optional[Dict] = None,
41
+ prefetch: Optional[int] = 1,
42
+ auto_create: Optional[bool]=True,
43
+ queue_params: Optional[Dict]=None,
44
+ *args,
45
+ **kwargs
46
+ ):
47
+ """
48
+ Initializes the RabbitMQ broker.
49
+
50
+ Args:
51
+ queue_name (str): The name of the queue.
52
+ params (Optional[Dict], optional): Parameters for RabbitMQStore. Defaults to None.
53
+ prefetch (Optional[int], optional): Number of messages to prefetch. Defaults to 1.
54
+ auto_create (Optional[bool], optional): Whether to automatically create the queue. Defaults to True.
55
+ queue_params (Optional[Dict], optional): Parameters for queue declaration. Defaults to None.
56
+ *args: Variable length argument list.
57
+ **kwargs: Arbitrary keyword arguments.
58
+
59
+ Attributes:
60
+ queue_name (str): The name of the queue.
61
+ queue (Queue): The queue instance.
62
+ client (RabbitMQStore): The RabbitMQ client instance.
63
+ prefetch (int): Number of messages to prefetch.
64
+ threads (list): List of threads.
65
+ """
66
+
67
+ if RabbitMQStore is None:
68
+ raise ImportError("RabbitMQ dependencies not installed. Please install 'use-rabbitmq' package.")
69
+
70
+ super().__init__(*args, **kwargs)
71
+ self.queue_name = queue_name
72
+ self.queue = Queue()
73
+ params = params or {}
74
+ self.client = RabbitMQStore(**params)
75
+ if auto_create:
76
+ self.client.declare_queue(self.queue_name, **(queue_params or {}))
77
+ self.prefetch = prefetch
78
+ self.threads = []
79
+ self._consuming_started = False
80
+ self._consume_lock = threading.Lock()
81
+
82
+ def _consume(self, *args, **kwargs):
83
+ def callback(message):
84
+ self.queue.put(message)
85
+
86
+ prefetch = kwargs.pop("prefetch", self.prefetch)
87
+ self.client.start_consuming(queue_name=self.queue_name, callback=callback, prefetch=prefetch, **kwargs)
88
+
89
+ def consume(self, *args, **kwargs):
90
+ daemon = kwargs.pop('daemon', True)
91
+ with self._consume_lock:
92
+ if not self._consuming_started:
93
+ thread = threading.Thread(target=self._consume, args=args, kwargs=kwargs)
94
+ thread.daemon = daemon
95
+ thread.start()
96
+ self.threads.append(thread)
97
+ self._consuming_started = True
98
+ return RabbitMQConsumer(self)
99
+
100
+ def publish(self, message: Any, properties: Optional[dict] = None, **kwargs):
101
+ """发布消息"""
102
+ self.client.send(self.queue_name, message, properties=properties, **kwargs)
103
+
104
+ def confirm(self, message: Message):
105
+ """确认消息"""
106
+ broker_msg = getattr(message, "message", None)
107
+ if broker_msg is not None and hasattr(broker_msg, "ack"):
108
+ broker_msg.ack()
109
+ else:
110
+ # 无可确认的原始消息,忽略确认
111
+ return
112
+
113
+ def reject(self, message: Message):
114
+ """拒绝消息"""
115
+ broker_msg = getattr(message, "message", None)
116
+ if broker_msg is not None and hasattr(broker_msg, "reject"):
117
+ broker_msg.reject(requeue=False)
118
+ else:
119
+ # 无法拒绝(无原始消息对象),忽略
120
+ return
121
+
122
+ def requeue(self, message: Message, is_source=False):
123
+ """
124
+ 重发消息:先拒绝 再 重入
125
+
126
+ :param message: 消息
127
+ :param is_source: 是否是原始消息,True: 使用原始消息重入当前队列,False: 使用消息的最新数据重入当前队列
128
+ """
129
+ broker_msg = getattr(message, "message", None)
130
+ if broker_msg is not None and hasattr(broker_msg, "reject"):
131
+ if is_source:
132
+ broker_msg.reject(requeue=True)
133
+ else:
134
+ broker_msg.reject(requeue=False)
135
+ self.send(message)
136
+ else:
137
+ # 没有原始消息控制能力,回退为直接发送当前消息状态
138
+ if is_source and broker_msg is not None and hasattr(broker_msg, "body"):
139
+ # 尝试使用原始消息体重入队列
140
+ self.client.send(self.queue_name, broker_msg.body)
141
+ else:
142
+ self.send(message)
143
+
144
+ def shutdown(self):
145
+ self.client.shutdown()
146
+ for thread in self.threads:
147
+ thread.join()
148
+ self._consuming_started = False
149
+ self.threads = []
150
+
151
+
152
+ class RabbitMQConsumer(BaseConsumer):
153
+ ...
@@ -0,0 +1,9 @@
1
+ from .stream import RedisStreamBroker, RedisStreamConsumer
2
+ from .pubsub import RedisPubSubBroker, RedisPubSubConsumer
3
+
4
+ __all__ = [
5
+ "RedisStreamBroker",
6
+ "RedisStreamConsumer",
7
+ "RedisPubSubBroker",
8
+ "RedisPubSubConsumer"
9
+ ]