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 +67 -0
- onestep/_utils.py +21 -0
- onestep/broker/__init__.py +22 -0
- onestep/broker/base.py +144 -0
- onestep/broker/cron.py +51 -0
- onestep/broker/memory.py +75 -0
- onestep/broker/mysql.py +63 -0
- onestep/broker/rabbitmq.py +153 -0
- onestep/broker/redis/__init__.py +9 -0
- onestep/broker/redis/pubsub.py +93 -0
- onestep/broker/redis/stream.py +114 -0
- onestep/broker/sqs/__init__.py +4 -0
- onestep/broker/sqs/sns.py +53 -0
- onestep/broker/sqs/sqs.py +181 -0
- onestep/broker/webhook.py +84 -0
- onestep/cli.py +80 -0
- onestep/cron.py +211 -0
- onestep/exception.py +35 -0
- onestep/message.py +169 -0
- onestep/middleware/__init__.py +7 -0
- onestep/middleware/base.py +32 -0
- onestep/middleware/config.py +77 -0
- onestep/middleware/unique.py +48 -0
- onestep/onestep.py +281 -0
- onestep/retry.py +117 -0
- onestep/signal.py +11 -0
- onestep/state.py +23 -0
- onestep/worker.py +205 -0
- onestep-0.5.0.dist-info/METADATA +116 -0
- onestep-0.5.0.dist-info/RECORD +32 -0
- onestep-0.5.0.dist-info/WHEEL +4 -0
- onestep-0.5.0.dist-info/entry_points.txt +2 -0
onestep/cron.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Cron DSL/Builder 与宏解析
|
|
4
|
+
|
|
5
|
+
提供人性化的 API 来生成 cron 表达式,并支持常用别名/宏:
|
|
6
|
+
- Cron.every(minutes=5) / Cron.every(seconds=10) / Cron.every(hours=2) / Cron.every(days=3) / Cron.every(months=1)
|
|
7
|
+
- Cron.daily(at="09:00")
|
|
8
|
+
- Cron.weekly(on="mon" or ["mon","fri"], at="10:30")
|
|
9
|
+
- Cron.monthly(on_day=1, at="00:00")
|
|
10
|
+
- Cron.yearly(on="01-01", at="00:00")
|
|
11
|
+
- 宏别名:@hourly/@daily/@weekly/@monthly/@yearly
|
|
12
|
+
- 扩展宏:@workdays(1-5)/@weekends(0,6)/@every <n><unit>(unit: s/m/h/d/mo)
|
|
13
|
+
|
|
14
|
+
注意:
|
|
15
|
+
- 标准 5 字段:minute hour day month day_of_week
|
|
16
|
+
- 当包含 seconds(如 `at` 使用 HH:MM:SS 或 `every(seconds=...)`)时输出 6 字段:second minute hour day month day_of_week
|
|
17
|
+
- `@every <n>d` 等按“日/月字段取模”的语义生成表达式,并非滚动间隔。
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import List, Optional, Tuple, Union
|
|
22
|
+
|
|
23
|
+
DOW_MAP = {
|
|
24
|
+
"sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ALIAS_MAP = {
|
|
28
|
+
"@hourly": "0 * * * *",
|
|
29
|
+
"@daily": "0 0 * * *",
|
|
30
|
+
"@weekly": "0 0 * * 0",
|
|
31
|
+
"@monthly": "0 0 1 * *",
|
|
32
|
+
"@yearly": "0 0 1 1 *",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_at(at: Optional[str]) -> Tuple[Optional[int], int, int]:
|
|
37
|
+
"""解析 at 参数,支持 HH:MM 或 HH:MM:SS。
|
|
38
|
+
返回 (second, minute, hour)。如果不传 at,默认 minute=0, hour=0。
|
|
39
|
+
"""
|
|
40
|
+
if not at:
|
|
41
|
+
return None, 0, 0
|
|
42
|
+
parts = at.strip().split(":")
|
|
43
|
+
if len(parts) == 2:
|
|
44
|
+
h, m = parts
|
|
45
|
+
return None, int(m), int(h)
|
|
46
|
+
elif len(parts) == 3:
|
|
47
|
+
h, m, s = parts
|
|
48
|
+
return int(s), int(m), int(h)
|
|
49
|
+
raise ValueError(f"Invalid time format for at='{at}', expected HH:MM or HH:MM:SS")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _to_dow_expr(on: Union[str, int, List[Union[str, int]]]) -> str:
|
|
53
|
+
"""将星期输入转换为 day_of_week 字段表达式。
|
|
54
|
+
支持:字符串缩写(mon...sun)、数字 0-6、列表。
|
|
55
|
+
"""
|
|
56
|
+
def _to_num(v: Union[str, int]) -> int:
|
|
57
|
+
if isinstance(v, int):
|
|
58
|
+
if v < 0 or v > 6:
|
|
59
|
+
raise ValueError("day_of_week must be in 0-6")
|
|
60
|
+
return v
|
|
61
|
+
v = str(v).lower()
|
|
62
|
+
if v.isdigit():
|
|
63
|
+
num = int(v)
|
|
64
|
+
if num < 0 or num > 6:
|
|
65
|
+
raise ValueError("day_of_week must be in 0-6")
|
|
66
|
+
return num
|
|
67
|
+
if v not in DOW_MAP:
|
|
68
|
+
raise ValueError(f"Unknown day_of_week '{v}'")
|
|
69
|
+
return DOW_MAP[v]
|
|
70
|
+
|
|
71
|
+
if isinstance(on, (list, tuple, set)):
|
|
72
|
+
vals = sorted({_to_num(v) for v in on})
|
|
73
|
+
return ",".join(str(x) for x in vals)
|
|
74
|
+
else:
|
|
75
|
+
return str(_to_num(on))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _join_fields(second: Optional[int], minute: Union[int, str], hour: Union[int, str], day: Union[int, str], month: Union[int, str], dow: Union[int, str]) -> str:
|
|
79
|
+
"""组合 5/6 字段。若 second 为 None,输出 5 字段,否则输出 6 字段(秒在最后)。"""
|
|
80
|
+
if second is None:
|
|
81
|
+
return f"{minute} {hour} {day} {month} {dow}"
|
|
82
|
+
return f"{minute} {hour} {day} {month} {dow} {second}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Cron:
|
|
86
|
+
@staticmethod
|
|
87
|
+
def every(*, seconds: Optional[int] = None, minutes: Optional[int] = None, hours: Optional[int] = None, days: Optional[int] = None, months: Optional[int] = None) -> str:
|
|
88
|
+
"""每隔指定单位执行。
|
|
89
|
+
只能指定一个非 None 的单位。
|
|
90
|
+
- seconds: 6 字段:*/s * * * * *
|
|
91
|
+
- minutes: 5 字段:*/m * * * *
|
|
92
|
+
- hours: 5 字段:0 */h * * *
|
|
93
|
+
- days: 5 字段:0 0 */d * *
|
|
94
|
+
- months: 5 字段:0 0 1 */mo *
|
|
95
|
+
"""
|
|
96
|
+
units = [("seconds", seconds), ("minutes", minutes), ("hours", hours), ("days", days), ("months", months)]
|
|
97
|
+
chosen = [(k, v) for k, v in units if v is not None]
|
|
98
|
+
if len(chosen) != 1:
|
|
99
|
+
raise ValueError("Cron.every() must specify exactly one unit")
|
|
100
|
+
k, v = chosen[0]
|
|
101
|
+
if not isinstance(v, int) or v <= 0:
|
|
102
|
+
raise ValueError("Interval must be a positive integer")
|
|
103
|
+
if k == "seconds":
|
|
104
|
+
return f"* * * * * */{v}"
|
|
105
|
+
if k == "minutes":
|
|
106
|
+
return "*/%d * * * *" % v
|
|
107
|
+
if k == "hours":
|
|
108
|
+
return "0 */%d * * *" % v
|
|
109
|
+
if k == "days":
|
|
110
|
+
return "0 0 */%d * *" % v
|
|
111
|
+
if k == "months":
|
|
112
|
+
return "0 0 1 */%d *" % v
|
|
113
|
+
raise RuntimeError("unreachable")
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def daily(*, at: Optional[str] = None) -> str:
|
|
117
|
+
s, m, h = _parse_at(at)
|
|
118
|
+
day, month, dow = "*", "*", "*"
|
|
119
|
+
return _join_fields(s, m, h, day, month, dow)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def weekly(*, on: Union[str, int, List[Union[str, int]]] = "mon", at: Optional[str] = None) -> str:
|
|
123
|
+
s, m, h = _parse_at(at)
|
|
124
|
+
dow = _to_dow_expr(on)
|
|
125
|
+
day, month = "*", "*"
|
|
126
|
+
return _join_fields(s, m, h, day, month, dow)
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def monthly(*, on_day: Union[int, List[int]] = 1, at: Optional[str] = None) -> str:
|
|
130
|
+
s, m, h = _parse_at(at)
|
|
131
|
+
month, dow = "*", "*"
|
|
132
|
+
def _to_day(v: Union[int, str]) -> int:
|
|
133
|
+
iv = int(v)
|
|
134
|
+
if iv < 1 or iv > 31:
|
|
135
|
+
raise ValueError("day_of_month must be in 1-31")
|
|
136
|
+
return iv
|
|
137
|
+
if isinstance(on_day, (list, tuple, set)):
|
|
138
|
+
day = ",".join(str(_to_day(v)) for v in sorted({int(x) for x in on_day}))
|
|
139
|
+
else:
|
|
140
|
+
day = str(_to_day(on_day))
|
|
141
|
+
return _join_fields(s, m, h, day, month, dow)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def yearly(*, on: str = "01-01", at: Optional[str] = None) -> str:
|
|
145
|
+
s, m, h = _parse_at(at)
|
|
146
|
+
dow = "*"
|
|
147
|
+
try:
|
|
148
|
+
mon_str, day_str = on.split("-")
|
|
149
|
+
month = str(int(mon_str))
|
|
150
|
+
day = str(int(day_str))
|
|
151
|
+
except Exception:
|
|
152
|
+
raise ValueError("yearly(on=...) expects 'MM-DD', e.g., '01-01'")
|
|
153
|
+
return _join_fields(s, m, h, day, month, dow)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def alias(name: str) -> str:
|
|
157
|
+
"""返回内置宏别名对应的表达式(如果已知)。未知别名原样返回,以便 croniter 自行处理。"""
|
|
158
|
+
return ALIAS_MAP.get(name.strip().lower(), name)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def resolve_cron(cron_like: str) -> str:
|
|
162
|
+
"""将类似 cron 的输入(字符串/宏)解析为标准 cron 表达式。
|
|
163
|
+
- 内置关键字直接映射或保留(croniter 也支持);
|
|
164
|
+
- 扩展宏:@workdays/@weekends/@every <n><unit>(unit: s/m/h/d/mo)。
|
|
165
|
+
"""
|
|
166
|
+
s = str(cron_like).strip()
|
|
167
|
+
if not s.startswith("@"):
|
|
168
|
+
return s
|
|
169
|
+
lower = s.lower()
|
|
170
|
+
# 内置别名
|
|
171
|
+
if lower in ALIAS_MAP:
|
|
172
|
+
return ALIAS_MAP[lower]
|
|
173
|
+
# 扩展:工作日/周末(默认 00:00 执行)
|
|
174
|
+
if lower == "@workdays":
|
|
175
|
+
return "0 0 * * 1-5"
|
|
176
|
+
if lower == "@weekends":
|
|
177
|
+
return "0 0 * * 0,6"
|
|
178
|
+
# 扩展:@every <n><unit>
|
|
179
|
+
if lower.startswith("@every"):
|
|
180
|
+
tail = lower.replace("@every", "", 1).strip()
|
|
181
|
+
if not tail:
|
|
182
|
+
raise ValueError("@every requires an interval, e.g., '@every 5m'")
|
|
183
|
+
# 支持类似 "5m" / "10 h" / "2d" / "3mo" / "30s"
|
|
184
|
+
tail = tail.replace(" ", "")
|
|
185
|
+
# 处理 'mo' 与单位优先级
|
|
186
|
+
if tail.endswith("mo"):
|
|
187
|
+
num = int(tail[:-2])
|
|
188
|
+
if num <= 0:
|
|
189
|
+
raise ValueError("@every months must be > 0")
|
|
190
|
+
return "0 0 1 */%d *" % num
|
|
191
|
+
unit = tail[-1]
|
|
192
|
+
try:
|
|
193
|
+
num = int(tail[:-1])
|
|
194
|
+
except Exception:
|
|
195
|
+
raise ValueError("@every expects '<n><unit>', units: s/m/h/d/mo")
|
|
196
|
+
if num <= 0:
|
|
197
|
+
raise ValueError("@every interval must be > 0")
|
|
198
|
+
if unit == "s":
|
|
199
|
+
return "* * * * * */%d" % num
|
|
200
|
+
if unit == "m":
|
|
201
|
+
return "*/%d * * * *" % num
|
|
202
|
+
if unit == "h":
|
|
203
|
+
return "0 */%d * * *" % num
|
|
204
|
+
if unit == "d":
|
|
205
|
+
return "0 0 */%d * *" % num
|
|
206
|
+
if unit == "w":
|
|
207
|
+
# Cron 不支持“每 N 周”,仅建议直接使用 weekly(on=..., at=...)
|
|
208
|
+
raise ValueError("@every <n>w is not supported; use weekly(on=..., at=...) instead")
|
|
209
|
+
raise ValueError("Unknown unit for @every; use s/m/h/d/mo")
|
|
210
|
+
# 未知宏直接返回,交由 croniter 处理(可能抛错)
|
|
211
|
+
return s
|
onestep/exception.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class StopMiddleware(Exception):
|
|
2
|
+
...
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RetryException(Exception):
|
|
6
|
+
def __init__(self, message=None, times=None, **kwargs):
|
|
7
|
+
self.message = message
|
|
8
|
+
self.times = times
|
|
9
|
+
self.kwargs = kwargs
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RetryInQueue(RetryException):
|
|
13
|
+
"""消息重试-通过重试队列
|
|
14
|
+
|
|
15
|
+
抛出此异常,消息将被重新放入队列,等待下次消费。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RetryInLocal(RetryException):
|
|
20
|
+
"""消息重试-本地
|
|
21
|
+
|
|
22
|
+
不经过队列,直接在本地重试,直到达到重试次数。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DropMessage(Exception):
|
|
27
|
+
"""从 Brokers 中 丢弃该消息"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RejectMessage(DropMessage):
|
|
31
|
+
"""从 Brokers 中 丢弃该消息"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RequeueMessage(Exception):
|
|
35
|
+
"""重新入队该消息"""
|
onestep/message.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from traceback import TracebackException
|
|
7
|
+
from typing import Optional, Any, Union, Type
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
|
|
10
|
+
from onestep._utils import catch_error
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MessageTracebackException(TracebackException):
|
|
14
|
+
def __init__(self, exc_type: Type[BaseException], exc_value: BaseException, exc_traceback: Optional[TracebackType], **kwargs):
|
|
15
|
+
super().__init__(exc_type, exc_value, exc_traceback, **kwargs)
|
|
16
|
+
self.exc_value = exc_value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Extra:
|
|
21
|
+
task_id: Optional[str] = None
|
|
22
|
+
publish_time: Optional[float] = None
|
|
23
|
+
failure_count: int = 0
|
|
24
|
+
|
|
25
|
+
def __post_init__(self):
|
|
26
|
+
self.task_id = self.task_id or str(uuid.uuid4())
|
|
27
|
+
self.publish_time = self.publish_time or round(time.time(), 3)
|
|
28
|
+
|
|
29
|
+
def to_dict(self):
|
|
30
|
+
return {
|
|
31
|
+
'task_id': self.task_id,
|
|
32
|
+
'publish_time': self.publish_time,
|
|
33
|
+
'failure_count': self.failure_count,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def __str__(self):
|
|
37
|
+
return str(self.to_dict())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Message:
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
body: Optional[Union[dict, Any]] = None,
|
|
45
|
+
extra: Optional[Union[dict, Extra]] = None,
|
|
46
|
+
message: Optional[Any] = None,
|
|
47
|
+
broker=None
|
|
48
|
+
):
|
|
49
|
+
""" Message
|
|
50
|
+
|
|
51
|
+
:param body: 解析后的消息体
|
|
52
|
+
:param extra: 额外信息
|
|
53
|
+
:param message: 原始消息体
|
|
54
|
+
:param broker: 当前消息所属的 broker
|
|
55
|
+
"""
|
|
56
|
+
self.body = body
|
|
57
|
+
self.extra = self._set_extra(extra)
|
|
58
|
+
self.message = message
|
|
59
|
+
|
|
60
|
+
self.broker = broker
|
|
61
|
+
self._exception = None
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _set_extra(extra):
|
|
65
|
+
if isinstance(extra, Extra):
|
|
66
|
+
return extra
|
|
67
|
+
elif isinstance(extra, dict):
|
|
68
|
+
return Extra(**extra)
|
|
69
|
+
else:
|
|
70
|
+
return Extra()
|
|
71
|
+
|
|
72
|
+
def set_exception(self):
|
|
73
|
+
"""设置异常信息,会自动获取"""
|
|
74
|
+
exc_type, exc_value, exc_tb = sys.exc_info()
|
|
75
|
+
if exc_type is None or exc_value is None:
|
|
76
|
+
exc_type = Exception
|
|
77
|
+
exc_value = Exception("No exception info")
|
|
78
|
+
self._exception = MessageTracebackException(exc_type, exc_value, exc_tb)
|
|
79
|
+
self.failure_count = self.failure_count + 1
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def exception(self) -> Optional[MessageTracebackException]:
|
|
83
|
+
return self._exception
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def fail(self):
|
|
87
|
+
return self.exception is not None
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def failure_count(self):
|
|
91
|
+
return self.extra.failure_count
|
|
92
|
+
|
|
93
|
+
@failure_count.setter
|
|
94
|
+
def failure_count(self, value):
|
|
95
|
+
self.extra.failure_count = value
|
|
96
|
+
|
|
97
|
+
def replace(self, **kwargs):
|
|
98
|
+
"""替换当前message的属性"""
|
|
99
|
+
for key, value in kwargs.items():
|
|
100
|
+
if not hasattr(self, key):
|
|
101
|
+
continue
|
|
102
|
+
if key == 'extra':
|
|
103
|
+
value = self._set_extra(value)
|
|
104
|
+
setattr(self, key, value)
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def to_dict(self, include_exception=False) -> dict:
|
|
108
|
+
data = {'body': self.body, 'extra': self.extra.to_dict()}
|
|
109
|
+
if include_exception and self.exception:
|
|
110
|
+
data['exception'] = "".join(self.exception.format(chain=True)) # noqa
|
|
111
|
+
|
|
112
|
+
return data
|
|
113
|
+
|
|
114
|
+
def to_json(self, include_exception=False) -> str:
|
|
115
|
+
return json.dumps(self.to_dict(include_exception))
|
|
116
|
+
|
|
117
|
+
@catch_error()
|
|
118
|
+
def confirm(self, **kwargs):
|
|
119
|
+
"""确认消息"""
|
|
120
|
+
broker = getattr(self, 'broker', None)
|
|
121
|
+
if broker and hasattr(broker, 'confirm'):
|
|
122
|
+
if hasattr(broker, 'before_emit'):
|
|
123
|
+
broker.before_emit('confirm', message=self, step=kwargs.get('step'))
|
|
124
|
+
broker.confirm(self)
|
|
125
|
+
if hasattr(broker, 'after_emit'):
|
|
126
|
+
broker.after_emit('confirm', message=self, step=kwargs.get('step'))
|
|
127
|
+
|
|
128
|
+
@catch_error()
|
|
129
|
+
def reject(self, **kwargs):
|
|
130
|
+
"""拒绝消息"""
|
|
131
|
+
broker = getattr(self, 'broker', None)
|
|
132
|
+
if broker and hasattr(broker, 'reject'):
|
|
133
|
+
if hasattr(broker, 'before_emit'):
|
|
134
|
+
broker.before_emit('reject', message=self, step=kwargs.get('step'))
|
|
135
|
+
broker.reject(self)
|
|
136
|
+
if hasattr(broker, 'after_emit'):
|
|
137
|
+
broker.after_emit('reject', message=self, step=kwargs.get('step'))
|
|
138
|
+
|
|
139
|
+
@catch_error()
|
|
140
|
+
def requeue(self, is_source=False, **kwargs):
|
|
141
|
+
"""
|
|
142
|
+
重发消息:先拒绝 再 重入
|
|
143
|
+
|
|
144
|
+
:param is_source: 是否是源消息,True: 使用消息的最新数据重入当前队列,False: 使用消息的最新数据重入当前队列
|
|
145
|
+
"""
|
|
146
|
+
broker = getattr(self, 'broker', None)
|
|
147
|
+
if broker and hasattr(broker, 'requeue'):
|
|
148
|
+
if hasattr(broker, 'before_emit'):
|
|
149
|
+
broker.before_emit('requeue', message=self, step=kwargs.get('step'))
|
|
150
|
+
broker.requeue(self, is_source=is_source)
|
|
151
|
+
if hasattr(broker, 'after_emit'):
|
|
152
|
+
broker.after_emit('requeue', message=self, step=kwargs.get('step'))
|
|
153
|
+
|
|
154
|
+
def __getattr__(self, item):
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def __delattr__(self, item):
|
|
158
|
+
if hasattr(self, item):
|
|
159
|
+
setattr(self, item, None)
|
|
160
|
+
|
|
161
|
+
def __str__(self):
|
|
162
|
+
return str(self.to_dict())
|
|
163
|
+
|
|
164
|
+
def __repr__(self):
|
|
165
|
+
return f"<{self.__class__.__name__} {self.body}>"
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def from_broker(cls, broker_message: Any):
|
|
169
|
+
return cls(body=broker_message, extra=None, message=broker_message, broker=None)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .base import BaseMiddleware
|
|
2
|
+
from .config import (
|
|
3
|
+
BaseConfigMiddleware, PublishConfigMixin, ConsumeConfigMixin,
|
|
4
|
+
NacosConfigMixin, NacosPublishConfigMiddleware, NacosConsumeConfigMiddleware,
|
|
5
|
+
RedisConfigMixin, RedisPublishConfigMiddleware, RedisConsumeConfigMiddleware
|
|
6
|
+
)
|
|
7
|
+
from .unique import UniqueMiddleware, MemoryUniqueMiddleware
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class DeprecationMeta(type):
|
|
2
|
+
|
|
3
|
+
def __new__(cls, name, bases, attrs):
|
|
4
|
+
if "before_receive" in attrs or "after_receive" in attrs:
|
|
5
|
+
raise DeprecationWarning(
|
|
6
|
+
f"`{name}` "
|
|
7
|
+
"The before_receive and after_receive methods are deprecated, "
|
|
8
|
+
"please use before_consume and after_consume instead"
|
|
9
|
+
)
|
|
10
|
+
return super().__new__(cls, name, bases, attrs)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseMiddleware(metaclass=DeprecationMeta):
|
|
14
|
+
|
|
15
|
+
def before_send(self, step, message, *args, **kwargs):
|
|
16
|
+
"""消息发送之前"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def after_send(self, step, message, *args, **kwargs):
|
|
20
|
+
"""消息发送之后"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def before_consume(self, step, message, *args, **kwargs):
|
|
24
|
+
"""消费消息之前"""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def after_consume(self, step, message, *args, **kwargs):
|
|
28
|
+
"""消费消息之后"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
def __repr__(self):
|
|
32
|
+
return f"<{self.__class__.__name__}>"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from .base import BaseMiddleware
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseConfigMiddleware(BaseMiddleware):
|
|
5
|
+
|
|
6
|
+
def __init__(self, client, *args, **kwargs):
|
|
7
|
+
self.config_key = kwargs.pop('config_key', None)
|
|
8
|
+
self.client = client
|
|
9
|
+
|
|
10
|
+
def process(self, step, message, *args, **kwargs):
|
|
11
|
+
"""实际获取配置的逻辑"""
|
|
12
|
+
config = self.get(self.config_key)
|
|
13
|
+
return config
|
|
14
|
+
|
|
15
|
+
def get(self, key):
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PublishConfigMixin:
|
|
20
|
+
def before_send(self, step, message, *args, **kwargs):
|
|
21
|
+
"""消息发送之前,给消息添加配置"""
|
|
22
|
+
config = self.process(step, message, *args, **kwargs) # noqa
|
|
23
|
+
# 持久性,会跟随消息传递到其他 broker
|
|
24
|
+
message.extra['config'] = config
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConsumeConfigMixin:
|
|
28
|
+
def before_consume(self, step, message, *args, **kwargs):
|
|
29
|
+
"""消息消费之前,给消息附加配置"""
|
|
30
|
+
config = self.process(step, message, *args, **kwargs) # noqa
|
|
31
|
+
# 一次性,不跟随消息传递到其他 broker
|
|
32
|
+
message.config = config
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NacosConfigMixin:
|
|
36
|
+
|
|
37
|
+
def __init__(self, client, *args, **kwargs):
|
|
38
|
+
super().__init__(client, *args, **kwargs)
|
|
39
|
+
self.config_group = kwargs.pop("config_group", "DEFAULT_GROUP")
|
|
40
|
+
|
|
41
|
+
def get(self, key):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def set(self, key, value):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class NacosPublishConfigMiddleware(NacosConfigMixin, PublishConfigMixin, BaseConfigMiddleware):
|
|
49
|
+
"""发布前附加来自 Nacos 的配置"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NacosConsumeConfigMiddleware(NacosConfigMixin, ConsumeConfigMixin, BaseConfigMiddleware):
|
|
53
|
+
"""消费前附加来自 Nacos 的配置"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RedisConfigMixin:
|
|
57
|
+
|
|
58
|
+
def __init__(self, client, *args, **kwargs):
|
|
59
|
+
super().__init__(client, *args, **kwargs)
|
|
60
|
+
self.config_group = kwargs.pop('config_group', 'onestep:config')
|
|
61
|
+
|
|
62
|
+
def get(self, key):
|
|
63
|
+
value = self.client.hget(name=self.config_group, key=key) # noqa
|
|
64
|
+
if isinstance(value, bytes):
|
|
65
|
+
value = value.decode()
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
def set(self, key, value):
|
|
69
|
+
return self.client.hset(name=self.config_group, key=key, value=value) # noqa
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class RedisPublishConfigMiddleware(RedisConfigMixin, PublishConfigMixin, BaseConfigMiddleware):
|
|
73
|
+
"""发布前附加来自 Redis 的配置"""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RedisConsumeConfigMiddleware(RedisConfigMixin, ConsumeConfigMixin, BaseConfigMiddleware):
|
|
77
|
+
"""消费前附加来自 Redis 的配置"""
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from .base import BaseMiddleware
|
|
5
|
+
from ..exception import DropMessage
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class hash_func:
|
|
9
|
+
""" Default message to hash function """
|
|
10
|
+
|
|
11
|
+
def __call__(self, body):
|
|
12
|
+
data = json.dumps(body) if isinstance(body, dict) else str(body)
|
|
13
|
+
return hashlib.sha1(data.encode("utf-8")).hexdigest()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UniqueMiddleware(BaseMiddleware):
|
|
17
|
+
default_hash_func = hash_func()
|
|
18
|
+
|
|
19
|
+
def before_consume(self, step, message, *args, **kwargs):
|
|
20
|
+
message_hash = self.default_hash_func(message.body)
|
|
21
|
+
if self.has_seen(message_hash):
|
|
22
|
+
raise DropMessage(f"Message<{message}> has been seen before")
|
|
23
|
+
|
|
24
|
+
def after_consume(self, step, message, *args, **kwargs):
|
|
25
|
+
if message.fail or message.body is None:
|
|
26
|
+
return
|
|
27
|
+
message_hash = self.default_hash_func(message.body)
|
|
28
|
+
self.mark_seen(message_hash)
|
|
29
|
+
|
|
30
|
+
def has_seen(self, message):
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
def mark_seen(self, message):
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MemoryUniqueMiddleware(UniqueMiddleware):
|
|
38
|
+
"""基于内存的去重中间件"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, *args, **kwargs):
|
|
41
|
+
super().__init__(*args, **kwargs)
|
|
42
|
+
self.seen = set()
|
|
43
|
+
|
|
44
|
+
def has_seen(self, hash_value):
|
|
45
|
+
return hash_value in self.seen
|
|
46
|
+
|
|
47
|
+
def mark_seen(self, hash_value):
|
|
48
|
+
self.seen.add(hash_value)
|