faster-cron 0.1.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.
- faster_cron-0.1.0/PKG-INFO +149 -0
- faster_cron-0.1.0/README.md +137 -0
- faster_cron-0.1.0/faster_cron/__init__.py +9 -0
- faster_cron-0.1.0/faster_cron/async_cron.py +59 -0
- faster_cron-0.1.0/faster_cron/base.py +42 -0
- faster_cron-0.1.0/faster_cron/sync_cron.py +104 -0
- faster_cron-0.1.0/faster_cron.egg-info/PKG-INFO +149 -0
- faster_cron-0.1.0/faster_cron.egg-info/SOURCES.txt +12 -0
- faster_cron-0.1.0/faster_cron.egg-info/dependency_links.txt +1 -0
- faster_cron-0.1.0/faster_cron.egg-info/top_level.txt +1 -0
- faster_cron-0.1.0/pyproject.toml +27 -0
- faster_cron-0.1.0/setup.cfg +4 -0
- faster_cron-0.1.0/test/test_async.py +50 -0
- faster_cron-0.1.0/test/test_sync.py +60 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: faster-cron
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 一个轻量、直观、支持异步与同步双模式的定时任务调度器
|
|
5
|
+
Author-email: Bernard Simon <bernardziyi@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/BernardSimon/faster-cron
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# FasterCron
|
|
14
|
+
|
|
15
|
+
**FasterCron** 是一个轻量级、直观且功能强大的 Python 定时任务调度工具库。它完美支持 **Asyncio (异步)** 和 **Threading (多线程)** 双模式,专为需要高可靠性、简单配置和任务并发控制的场景设计。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🌟 核心特性
|
|
20
|
+
|
|
21
|
+
* **双模式支持**:一套逻辑同时提供 `AsyncFasterCron`(异步)和 `FasterCron`(同步多线程)两种实现。
|
|
22
|
+
* **任务级并发控制**:通过 `allow_overlap` 参数精准控制同一个任务是否允许重叠执行(单例模式 vs 并发模式)。
|
|
23
|
+
* **智能参数注入**:自动检测任务函数签名,按需注入包含调度时间、任务名称的 `context` 上下文。
|
|
24
|
+
* **标准 Cron 支持**:兼容 5 位(分时日月周)和 6 位(秒分时日月周)Cron 表达式。
|
|
25
|
+
* **健壮性**:内置异常捕获机制,单个任务崩溃不影响调度器运行。
|
|
26
|
+
* **无外部依赖**:仅使用 Python 标准库实现,轻量无负担。
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 📦 安装
|
|
31
|
+
|
|
32
|
+
您可以直接通过 pip 安装:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install faster-cron
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
或者直接将源码放入您的项目中。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🚀 快速上手
|
|
44
|
+
|
|
45
|
+
### 1. 异步模式 (Async Mode)
|
|
46
|
+
|
|
47
|
+
适用于使用了 `aiohttp`, `httpx` 或 `tortoise-orm` 等异步库的项目。
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import asyncio
|
|
51
|
+
from faster_cron import AsyncFasterCron
|
|
52
|
+
|
|
53
|
+
cron = AsyncFasterCron()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# 示例:每 5 秒执行一次,禁止重叠(若上一个任务没跑完,则跳过本次)
|
|
57
|
+
@cron.schedule("*/5 * * * * *", allow_overlap=False)
|
|
58
|
+
async def my_async_job(context):
|
|
59
|
+
print(f"正在执行任务: {context['task_name']}, 计划时间: {context['scheduled_at']}")
|
|
60
|
+
await asyncio.sleep(6) # 模拟长耗时任务
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
await cron.start()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
asyncio.run(main())
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. 同步模式 (Sync Mode)
|
|
73
|
+
|
|
74
|
+
适用于传统的阻塞式脚本或爬虫。
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from faster_cron import FasterCron
|
|
78
|
+
import time
|
|
79
|
+
|
|
80
|
+
cron = FasterCron()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# 示例:每秒执行一次,允许并发执行
|
|
84
|
+
@cron.schedule("* * * * * *", allow_overlap=True)
|
|
85
|
+
def my_sync_job():
|
|
86
|
+
print("滴答,同步任务正在运行...")
|
|
87
|
+
time.sleep(2)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
cron.run()
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 🛠 核心 API 说明
|
|
98
|
+
|
|
99
|
+
### 调度装饰器 `schedule`
|
|
100
|
+
|
|
101
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
102
|
+
| --- | --- | --- | --- |
|
|
103
|
+
| `expression` | `str` | - | Cron 表达式。支持 `*`, `,`, `-`, `/`。 |
|
|
104
|
+
| `allow_overlap` | `bool` | `True` | **关键参数**。`True`: 时间点到达即执行;`False`: 若该任务的上一个实例未结束,则跳过本次执行循环。 |
|
|
105
|
+
|
|
106
|
+
### 上下文参数 `context`
|
|
107
|
+
|
|
108
|
+
如果您的任务函数接收名为 `context` 的参数,FasterCron 会自动注入以下字典:
|
|
109
|
+
|
|
110
|
+
* `task_name`: 函数名称。
|
|
111
|
+
* `scheduled_at`: 任务触发的精确 `datetime` 对象。
|
|
112
|
+
* `expression`: 该任务使用的 Cron 表达式。
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 📅 Cron 表达式参考
|
|
117
|
+
|
|
118
|
+
FasterCron 支持灵活的表达式定义:
|
|
119
|
+
|
|
120
|
+
* `* * * * * *` : 每秒执行。
|
|
121
|
+
* `*/5 * * * * *` : 每 5 秒执行。
|
|
122
|
+
* `0 0 * * * *` : 每整小时执行。
|
|
123
|
+
* `0 30 9-17 * * *` : 每天 9:00 到 17:00 之间的每半小时执行。
|
|
124
|
+
* `0 0 0 * * 0` : 每周日凌晨执行。
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 🧪 运行测试
|
|
129
|
+
|
|
130
|
+
本项目包含完善的单元测试。您可以使用 `pytest` 来验证功能:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# 安装测试依赖
|
|
134
|
+
pip install pytest pytest-asyncio
|
|
135
|
+
|
|
136
|
+
# 运行所有测试
|
|
137
|
+
pytest
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 📄 开源协议
|
|
144
|
+
|
|
145
|
+
MIT License.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
**如果您觉得好用,欢迎点一个 Star!🌟**
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# FasterCron
|
|
2
|
+
|
|
3
|
+
**FasterCron** 是一个轻量级、直观且功能强大的 Python 定时任务调度工具库。它完美支持 **Asyncio (异步)** 和 **Threading (多线程)** 双模式,专为需要高可靠性、简单配置和任务并发控制的场景设计。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🌟 核心特性
|
|
8
|
+
|
|
9
|
+
* **双模式支持**:一套逻辑同时提供 `AsyncFasterCron`(异步)和 `FasterCron`(同步多线程)两种实现。
|
|
10
|
+
* **任务级并发控制**:通过 `allow_overlap` 参数精准控制同一个任务是否允许重叠执行(单例模式 vs 并发模式)。
|
|
11
|
+
* **智能参数注入**:自动检测任务函数签名,按需注入包含调度时间、任务名称的 `context` 上下文。
|
|
12
|
+
* **标准 Cron 支持**:兼容 5 位(分时日月周)和 6 位(秒分时日月周)Cron 表达式。
|
|
13
|
+
* **健壮性**:内置异常捕获机制,单个任务崩溃不影响调度器运行。
|
|
14
|
+
* **无外部依赖**:仅使用 Python 标准库实现,轻量无负担。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 📦 安装
|
|
19
|
+
|
|
20
|
+
您可以直接通过 pip 安装:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install faster-cron
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
或者直接将源码放入您的项目中。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🚀 快速上手
|
|
32
|
+
|
|
33
|
+
### 1. 异步模式 (Async Mode)
|
|
34
|
+
|
|
35
|
+
适用于使用了 `aiohttp`, `httpx` 或 `tortoise-orm` 等异步库的项目。
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from faster_cron import AsyncFasterCron
|
|
40
|
+
|
|
41
|
+
cron = AsyncFasterCron()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# 示例:每 5 秒执行一次,禁止重叠(若上一个任务没跑完,则跳过本次)
|
|
45
|
+
@cron.schedule("*/5 * * * * *", allow_overlap=False)
|
|
46
|
+
async def my_async_job(context):
|
|
47
|
+
print(f"正在执行任务: {context['task_name']}, 计划时间: {context['scheduled_at']}")
|
|
48
|
+
await asyncio.sleep(6) # 模拟长耗时任务
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def main():
|
|
52
|
+
await cron.start()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. 同步模式 (Sync Mode)
|
|
61
|
+
|
|
62
|
+
适用于传统的阻塞式脚本或爬虫。
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from faster_cron import FasterCron
|
|
66
|
+
import time
|
|
67
|
+
|
|
68
|
+
cron = FasterCron()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# 示例:每秒执行一次,允许并发执行
|
|
72
|
+
@cron.schedule("* * * * * *", allow_overlap=True)
|
|
73
|
+
def my_sync_job():
|
|
74
|
+
print("滴答,同步任务正在运行...")
|
|
75
|
+
time.sleep(2)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
cron.run()
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 🛠 核心 API 说明
|
|
86
|
+
|
|
87
|
+
### 调度装饰器 `schedule`
|
|
88
|
+
|
|
89
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| `expression` | `str` | - | Cron 表达式。支持 `*`, `,`, `-`, `/`。 |
|
|
92
|
+
| `allow_overlap` | `bool` | `True` | **关键参数**。`True`: 时间点到达即执行;`False`: 若该任务的上一个实例未结束,则跳过本次执行循环。 |
|
|
93
|
+
|
|
94
|
+
### 上下文参数 `context`
|
|
95
|
+
|
|
96
|
+
如果您的任务函数接收名为 `context` 的参数,FasterCron 会自动注入以下字典:
|
|
97
|
+
|
|
98
|
+
* `task_name`: 函数名称。
|
|
99
|
+
* `scheduled_at`: 任务触发的精确 `datetime` 对象。
|
|
100
|
+
* `expression`: 该任务使用的 Cron 表达式。
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 📅 Cron 表达式参考
|
|
105
|
+
|
|
106
|
+
FasterCron 支持灵活的表达式定义:
|
|
107
|
+
|
|
108
|
+
* `* * * * * *` : 每秒执行。
|
|
109
|
+
* `*/5 * * * * *` : 每 5 秒执行。
|
|
110
|
+
* `0 0 * * * *` : 每整小时执行。
|
|
111
|
+
* `0 30 9-17 * * *` : 每天 9:00 到 17:00 之间的每半小时执行。
|
|
112
|
+
* `0 0 0 * * 0` : 每周日凌晨执行。
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 🧪 运行测试
|
|
117
|
+
|
|
118
|
+
本项目包含完善的单元测试。您可以使用 `pytest` 来验证功能:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# 安装测试依赖
|
|
122
|
+
pip install pytest pytest-asyncio
|
|
123
|
+
|
|
124
|
+
# 运行所有测试
|
|
125
|
+
pytest
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 📄 开源协议
|
|
132
|
+
|
|
133
|
+
MIT License.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
**如果您觉得好用,欢迎点一个 Star!🌟**
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Callable, Optional
|
|
6
|
+
from .base import CronBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AsyncFasterCron:
|
|
10
|
+
def __init__(self, log_level=logging.INFO):
|
|
11
|
+
self.tasks = []
|
|
12
|
+
self.logger = logging.getLogger("FasterCron.Async")
|
|
13
|
+
self.logger.setLevel(log_level)
|
|
14
|
+
self._running = False
|
|
15
|
+
|
|
16
|
+
def schedule(self, expression: str, allow_overlap: bool = True):
|
|
17
|
+
def decorator(func: Callable):
|
|
18
|
+
self.tasks.append({
|
|
19
|
+
"expression": expression,
|
|
20
|
+
"func": func,
|
|
21
|
+
"allow_overlap": allow_overlap,
|
|
22
|
+
"name": func.__name__
|
|
23
|
+
})
|
|
24
|
+
return func
|
|
25
|
+
|
|
26
|
+
return decorator
|
|
27
|
+
|
|
28
|
+
async def start(self):
|
|
29
|
+
self._running = True
|
|
30
|
+
listeners = [self._monitor(task) for task in self.tasks]
|
|
31
|
+
await asyncio.gather(*listeners)
|
|
32
|
+
|
|
33
|
+
async def _monitor(self, task):
|
|
34
|
+
last_ts = 0
|
|
35
|
+
current_task: Optional[asyncio.Task] = None
|
|
36
|
+
|
|
37
|
+
while self._running:
|
|
38
|
+
now = datetime.datetime.now()
|
|
39
|
+
ts = int(now.timestamp())
|
|
40
|
+
|
|
41
|
+
if ts != last_ts and CronBase.is_time_match(task["expression"], now):
|
|
42
|
+
last_ts = ts
|
|
43
|
+
if not task["allow_overlap"] and current_task and not current_task.done():
|
|
44
|
+
self.logger.warning(f"Skip {task['name']}: overlapping blocked.")
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
context = {"scheduled_at": now, "task_name": task["name"]}
|
|
48
|
+
current_task = asyncio.create_task(self._wrapper(task["func"], context))
|
|
49
|
+
|
|
50
|
+
await asyncio.sleep(1.0 - (now.microsecond / 1_000_000) + 0.01)
|
|
51
|
+
|
|
52
|
+
async def _wrapper(self, func, context):
|
|
53
|
+
try:
|
|
54
|
+
sig = inspect.signature(func)
|
|
55
|
+
kwargs = {"context": context} if "context" in sig.parameters or any(
|
|
56
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()) else {}
|
|
57
|
+
await func(**kwargs)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
self.logger.error(f"Task {func.__name__} failed: {e}", exc_info=True)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CronBase:
|
|
5
|
+
"""提供 Cron 表达式解析的底层逻辑"""
|
|
6
|
+
|
|
7
|
+
@staticmethod
|
|
8
|
+
def is_time_match(expression: str, now: datetime.datetime) -> bool:
|
|
9
|
+
parts = expression.split()
|
|
10
|
+
if len(parts) == 5:
|
|
11
|
+
# 分 时 日 月 周 -> 补齐秒为 0
|
|
12
|
+
sec_part, min_part, hour_part, day_part, month_part, weekday_part = "0", *parts
|
|
13
|
+
elif len(parts) == 6:
|
|
14
|
+
sec_part, min_part, hour_part, day_part, month_part, weekday_part = parts
|
|
15
|
+
else:
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
CronBase._match_field(sec_part, now.second) and
|
|
20
|
+
CronBase._match_field(min_part, now.minute) and
|
|
21
|
+
CronBase._match_field(hour_part, now.hour) and
|
|
22
|
+
CronBase._match_field(day_part, now.day) and
|
|
23
|
+
CronBase._match_field(month_part, now.month) and
|
|
24
|
+
CronBase._match_field(weekday_part, now.weekday())
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _match_field(pattern: str, value: int) -> bool:
|
|
29
|
+
if pattern == "*": return True
|
|
30
|
+
if "," in pattern: return any(CronBase._match_field(p, value) for p in pattern.split(","))
|
|
31
|
+
if "/" in pattern:
|
|
32
|
+
r, s = pattern.split("/")
|
|
33
|
+
step = int(s)
|
|
34
|
+
if r in ["*", ""]: return value % step == 0
|
|
35
|
+
if "-" in r:
|
|
36
|
+
start, end = map(int, r.split("-"))
|
|
37
|
+
return start <= value <= end and (value - start) % step == 0
|
|
38
|
+
return value >= int(r) and (value - int(r)) % step == 0
|
|
39
|
+
if "-" in pattern:
|
|
40
|
+
start, end = map(int, pattern.split("-"))
|
|
41
|
+
return start <= value <= end
|
|
42
|
+
return int(pattern) == value
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import List, Dict, Any, Callable
|
|
7
|
+
from .base import CronBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FasterCron:
|
|
11
|
+
def __init__(self, log_level=logging.INFO):
|
|
12
|
+
self.tasks: List[Dict[str, Any]] = []
|
|
13
|
+
self.logger = logging.getLogger("FasterCron.Sync")
|
|
14
|
+
self.logger.setLevel(log_level)
|
|
15
|
+
self._running = False
|
|
16
|
+
self._monitors: List[threading.Thread] = []
|
|
17
|
+
|
|
18
|
+
def schedule(self, expression: str, allow_overlap: bool = True):
|
|
19
|
+
"""
|
|
20
|
+
注册同步任务。
|
|
21
|
+
allow_overlap 为 True 时,会通过开启新线程来实现并发执行。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def decorator(func: Callable):
|
|
25
|
+
self.tasks.append({
|
|
26
|
+
"expression": expression,
|
|
27
|
+
"func": func,
|
|
28
|
+
"allow_overlap": allow_overlap,
|
|
29
|
+
"name": func.__name__,
|
|
30
|
+
"last_worker": None # 用于追踪此任务的上一个执行线程
|
|
31
|
+
})
|
|
32
|
+
return func
|
|
33
|
+
|
|
34
|
+
return decorator
|
|
35
|
+
|
|
36
|
+
def run(self):
|
|
37
|
+
"""阻塞启动所有任务监控器"""
|
|
38
|
+
self._running = True
|
|
39
|
+
self.logger.info(f"FasterCron (Sync Mode) started with {len(self.tasks)} tasks.")
|
|
40
|
+
|
|
41
|
+
for task in self.tasks:
|
|
42
|
+
t = threading.Thread(
|
|
43
|
+
target=self._monitor_loop,
|
|
44
|
+
args=(task,),
|
|
45
|
+
name=f"Monitor-{task['name']}",
|
|
46
|
+
daemon=True
|
|
47
|
+
)
|
|
48
|
+
t.start()
|
|
49
|
+
self._monitors.append(t)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
while self._running:
|
|
53
|
+
time.sleep(1)
|
|
54
|
+
except KeyboardInterrupt:
|
|
55
|
+
self.logger.info("FasterCron stopping...")
|
|
56
|
+
self._running = False
|
|
57
|
+
|
|
58
|
+
def _monitor_loop(self, task: Dict[str, Any]):
|
|
59
|
+
"""每个任务独立的监听循环"""
|
|
60
|
+
last_trigger_ts = 0
|
|
61
|
+
|
|
62
|
+
while self._running:
|
|
63
|
+
now = datetime.datetime.now()
|
|
64
|
+
current_ts = int(now.timestamp())
|
|
65
|
+
|
|
66
|
+
# 1. 时间匹配检查 (确保每秒只触发一次)
|
|
67
|
+
if current_ts != last_trigger_ts and CronBase.is_time_match(task["expression"], now):
|
|
68
|
+
last_trigger_ts = current_ts
|
|
69
|
+
|
|
70
|
+
# 2. 并发控制
|
|
71
|
+
if not task["allow_overlap"]:
|
|
72
|
+
# 单例模式:检查上一个工作线程是否还在跑
|
|
73
|
+
prev_worker = task.get("last_worker")
|
|
74
|
+
if prev_worker and prev_worker.is_alive():
|
|
75
|
+
self.logger.warning(f"Task '{task['name']}' is still running. Skipping this cycle.")
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# 3. 执行任务
|
|
79
|
+
# 无论是并发还是单例,都开启新线程执行工作函数,避免阻塞监控循环
|
|
80
|
+
context = {"scheduled_at": now, "task_name": task["name"]}
|
|
81
|
+
worker_thread = threading.Thread(
|
|
82
|
+
target=self._execute_task,
|
|
83
|
+
args=(task["func"], context),
|
|
84
|
+
name=f"Worker-{task['name']}-{current_ts}",
|
|
85
|
+
daemon=True
|
|
86
|
+
)
|
|
87
|
+
task["last_worker"] = worker_thread # 记录引用以便下次检查
|
|
88
|
+
worker_thread.start()
|
|
89
|
+
|
|
90
|
+
# 4. 精确对齐到下一秒
|
|
91
|
+
sleep_time = 1.0 - (now.microsecond / 1_000_000) + 0.01
|
|
92
|
+
time.sleep(sleep_time)
|
|
93
|
+
|
|
94
|
+
def _execute_task(self, func: Callable, context: Dict):
|
|
95
|
+
"""具体的任务执行包装器"""
|
|
96
|
+
try:
|
|
97
|
+
# 智能参数注入
|
|
98
|
+
sig = inspect.signature(func)
|
|
99
|
+
if 'context' in sig.parameters:
|
|
100
|
+
func(context=context)
|
|
101
|
+
else:
|
|
102
|
+
func()
|
|
103
|
+
except Exception as e:
|
|
104
|
+
self.logger.error(f"Error in task '{func.__name__}': {e}", exc_info=True)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: faster-cron
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 一个轻量、直观、支持异步与同步双模式的定时任务调度器
|
|
5
|
+
Author-email: Bernard Simon <bernardziyi@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/BernardSimon/faster-cron
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# FasterCron
|
|
14
|
+
|
|
15
|
+
**FasterCron** 是一个轻量级、直观且功能强大的 Python 定时任务调度工具库。它完美支持 **Asyncio (异步)** 和 **Threading (多线程)** 双模式,专为需要高可靠性、简单配置和任务并发控制的场景设计。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🌟 核心特性
|
|
20
|
+
|
|
21
|
+
* **双模式支持**:一套逻辑同时提供 `AsyncFasterCron`(异步)和 `FasterCron`(同步多线程)两种实现。
|
|
22
|
+
* **任务级并发控制**:通过 `allow_overlap` 参数精准控制同一个任务是否允许重叠执行(单例模式 vs 并发模式)。
|
|
23
|
+
* **智能参数注入**:自动检测任务函数签名,按需注入包含调度时间、任务名称的 `context` 上下文。
|
|
24
|
+
* **标准 Cron 支持**:兼容 5 位(分时日月周)和 6 位(秒分时日月周)Cron 表达式。
|
|
25
|
+
* **健壮性**:内置异常捕获机制,单个任务崩溃不影响调度器运行。
|
|
26
|
+
* **无外部依赖**:仅使用 Python 标准库实现,轻量无负担。
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 📦 安装
|
|
31
|
+
|
|
32
|
+
您可以直接通过 pip 安装:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install faster-cron
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
或者直接将源码放入您的项目中。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🚀 快速上手
|
|
44
|
+
|
|
45
|
+
### 1. 异步模式 (Async Mode)
|
|
46
|
+
|
|
47
|
+
适用于使用了 `aiohttp`, `httpx` 或 `tortoise-orm` 等异步库的项目。
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import asyncio
|
|
51
|
+
from faster_cron import AsyncFasterCron
|
|
52
|
+
|
|
53
|
+
cron = AsyncFasterCron()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# 示例:每 5 秒执行一次,禁止重叠(若上一个任务没跑完,则跳过本次)
|
|
57
|
+
@cron.schedule("*/5 * * * * *", allow_overlap=False)
|
|
58
|
+
async def my_async_job(context):
|
|
59
|
+
print(f"正在执行任务: {context['task_name']}, 计划时间: {context['scheduled_at']}")
|
|
60
|
+
await asyncio.sleep(6) # 模拟长耗时任务
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
await cron.start()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
asyncio.run(main())
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. 同步模式 (Sync Mode)
|
|
73
|
+
|
|
74
|
+
适用于传统的阻塞式脚本或爬虫。
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from faster_cron import FasterCron
|
|
78
|
+
import time
|
|
79
|
+
|
|
80
|
+
cron = FasterCron()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# 示例:每秒执行一次,允许并发执行
|
|
84
|
+
@cron.schedule("* * * * * *", allow_overlap=True)
|
|
85
|
+
def my_sync_job():
|
|
86
|
+
print("滴答,同步任务正在运行...")
|
|
87
|
+
time.sleep(2)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
cron.run()
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 🛠 核心 API 说明
|
|
98
|
+
|
|
99
|
+
### 调度装饰器 `schedule`
|
|
100
|
+
|
|
101
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
102
|
+
| --- | --- | --- | --- |
|
|
103
|
+
| `expression` | `str` | - | Cron 表达式。支持 `*`, `,`, `-`, `/`。 |
|
|
104
|
+
| `allow_overlap` | `bool` | `True` | **关键参数**。`True`: 时间点到达即执行;`False`: 若该任务的上一个实例未结束,则跳过本次执行循环。 |
|
|
105
|
+
|
|
106
|
+
### 上下文参数 `context`
|
|
107
|
+
|
|
108
|
+
如果您的任务函数接收名为 `context` 的参数,FasterCron 会自动注入以下字典:
|
|
109
|
+
|
|
110
|
+
* `task_name`: 函数名称。
|
|
111
|
+
* `scheduled_at`: 任务触发的精确 `datetime` 对象。
|
|
112
|
+
* `expression`: 该任务使用的 Cron 表达式。
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 📅 Cron 表达式参考
|
|
117
|
+
|
|
118
|
+
FasterCron 支持灵活的表达式定义:
|
|
119
|
+
|
|
120
|
+
* `* * * * * *` : 每秒执行。
|
|
121
|
+
* `*/5 * * * * *` : 每 5 秒执行。
|
|
122
|
+
* `0 0 * * * *` : 每整小时执行。
|
|
123
|
+
* `0 30 9-17 * * *` : 每天 9:00 到 17:00 之间的每半小时执行。
|
|
124
|
+
* `0 0 0 * * 0` : 每周日凌晨执行。
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 🧪 运行测试
|
|
129
|
+
|
|
130
|
+
本项目包含完善的单元测试。您可以使用 `pytest` 来验证功能:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# 安装测试依赖
|
|
134
|
+
pip install pytest pytest-asyncio
|
|
135
|
+
|
|
136
|
+
# 运行所有测试
|
|
137
|
+
pytest
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 📄 开源协议
|
|
144
|
+
|
|
145
|
+
MIT License.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
**如果您觉得好用,欢迎点一个 Star!🌟**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
faster_cron/__init__.py
|
|
4
|
+
faster_cron/async_cron.py
|
|
5
|
+
faster_cron/base.py
|
|
6
|
+
faster_cron/sync_cron.py
|
|
7
|
+
faster_cron.egg-info/PKG-INFO
|
|
8
|
+
faster_cron.egg-info/SOURCES.txt
|
|
9
|
+
faster_cron.egg-info/dependency_links.txt
|
|
10
|
+
faster_cron.egg-info/top_level.txt
|
|
11
|
+
test/test_async.py
|
|
12
|
+
test/test_sync.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
faster_cron
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "faster-cron"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Bernard Simon", email="bernardziyi@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "一个轻量、直观、支持异步与同步双模式的定时任务调度器"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.7"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
"Homepage" = "https://github.com/BernardSimon/faster-cron"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["."]
|
|
27
|
+
include = ["faster_cron*"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import pytest
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from faster_cron.async_cron import AsyncFastCron
|
|
5
|
+
|
|
6
|
+
@pytest.mark.asyncio
|
|
7
|
+
async def test_async_overlap_prevention():
|
|
8
|
+
"""
|
|
9
|
+
测试异步模式下的重叠控制:
|
|
10
|
+
注册一个每秒触发一次的任务,但任务耗时 1.5 秒且 allow_overlap=False。
|
|
11
|
+
在运行 3 秒的时间内,该任务应该只执行 2 次(第 1 秒触发,第 2 秒因为第 1 秒的没跑完被跳过)。
|
|
12
|
+
"""
|
|
13
|
+
cron = AsyncFastCron()
|
|
14
|
+
execution_counter = 0
|
|
15
|
+
|
|
16
|
+
@cron.schedule("* * * * * *", allow_overlap=False)
|
|
17
|
+
async def slow_task():
|
|
18
|
+
nonlocal execution_counter
|
|
19
|
+
execution_counter += 1
|
|
20
|
+
await asyncio.sleep(1.5)
|
|
21
|
+
|
|
22
|
+
# 启动调度器并运行 3.2 秒后停止
|
|
23
|
+
cron_task = asyncio.create_task(cron.start())
|
|
24
|
+
await asyncio.sleep(3.2)
|
|
25
|
+
cron._running = False
|
|
26
|
+
cron_task.cancel()
|
|
27
|
+
|
|
28
|
+
# 预期执行次数应为 2 (触发点: T+1, T+3)
|
|
29
|
+
# 如果允许重叠,3.2秒内会触发 3 次
|
|
30
|
+
assert execution_counter == 2, f"预期执行 2 次,实际执行了 {execution_counter} 次"
|
|
31
|
+
|
|
32
|
+
@pytest.mark.asyncio
|
|
33
|
+
async def test_async_context_injection():
|
|
34
|
+
"""测试上下文参数是否能正确注入"""
|
|
35
|
+
cron = AsyncFastCron()
|
|
36
|
+
received_context = None
|
|
37
|
+
|
|
38
|
+
@cron.schedule("* * * * * *")
|
|
39
|
+
async def task_with_ctx(context):
|
|
40
|
+
nonlocal received_context
|
|
41
|
+
received_context = context
|
|
42
|
+
|
|
43
|
+
cron_task = asyncio.create_task(cron.start())
|
|
44
|
+
await asyncio.sleep(1.1)
|
|
45
|
+
cron._running = False
|
|
46
|
+
cron_task.cancel()
|
|
47
|
+
|
|
48
|
+
assert received_context is not None
|
|
49
|
+
assert "task_name" in received_context
|
|
50
|
+
assert received_context["task_name"] == "task_with_ctx"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
import pytest
|
|
4
|
+
from faster_cron.sync_cron import FastCron
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_sync_overlap_prevention():
|
|
8
|
+
"""
|
|
9
|
+
测试同步模式下的重叠控制:
|
|
10
|
+
任务耗时 1.2 秒,触发间隔 1 秒,禁止重叠。
|
|
11
|
+
"""
|
|
12
|
+
cron = FastCron()
|
|
13
|
+
execution_counter = 0
|
|
14
|
+
lock = threading.Lock()
|
|
15
|
+
|
|
16
|
+
@cron.schedule("* * * * * *", allow_overlap=False)
|
|
17
|
+
def slow_sync_task():
|
|
18
|
+
nonlocal execution_counter
|
|
19
|
+
with lock:
|
|
20
|
+
execution_counter += 1
|
|
21
|
+
time.sleep(1.2)
|
|
22
|
+
|
|
23
|
+
# 在后台线程运行调度器
|
|
24
|
+
cron_thread = threading.Thread(target=cron.run, daemon=True)
|
|
25
|
+
cron_thread.start()
|
|
26
|
+
|
|
27
|
+
# 观察 2.5 秒
|
|
28
|
+
time.sleep(2.5)
|
|
29
|
+
cron._running = False
|
|
30
|
+
|
|
31
|
+
# 预期:第 1 秒启动,第 2 秒检测到线程 alive 于是跳过
|
|
32
|
+
with lock:
|
|
33
|
+
assert execution_counter < 3, "同步模式下禁止重叠失败,执行次数过多"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_sync_no_overlap_allowed():
|
|
37
|
+
"""
|
|
38
|
+
测试同步模式下的并发:
|
|
39
|
+
允许重叠时,即使任务慢,触发点到了也应该立即启动新线程。
|
|
40
|
+
"""
|
|
41
|
+
cron = FastCron()
|
|
42
|
+
execution_counter = 0
|
|
43
|
+
lock = threading.Lock()
|
|
44
|
+
|
|
45
|
+
@cron.schedule("* * * * * *", allow_overlap=True)
|
|
46
|
+
def slow_concurrent_task():
|
|
47
|
+
nonlocal execution_counter
|
|
48
|
+
with lock:
|
|
49
|
+
execution_counter += 1
|
|
50
|
+
time.sleep(2)
|
|
51
|
+
|
|
52
|
+
cron_thread = threading.Thread(target=cron.run, daemon=True)
|
|
53
|
+
cron_thread.start()
|
|
54
|
+
|
|
55
|
+
time.sleep(2.5)
|
|
56
|
+
cron._running = False
|
|
57
|
+
|
|
58
|
+
# 允许重叠,2.5秒内应该触发 2-3 次
|
|
59
|
+
with lock:
|
|
60
|
+
assert execution_counter >= 2
|