faster-cron 0.1.1__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.
@@ -0,0 +1,9 @@
1
+ """
2
+ FasterCron: 一个轻量、直观、支持异步与同步双模式的定时任务调度器。
3
+ """
4
+
5
+ from .async_cron import AsyncFasterCron
6
+ from .sync_cron import FasterCron
7
+
8
+ __version__ = "0.1.0"
9
+ __all__ = ["AsyncFasterCron", "FasterCron"]
@@ -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)
faster_cron/base.py ADDED
@@ -0,0 +1,93 @@
1
+ import datetime
2
+ import logging
3
+
4
+
5
+ class CronBase:
6
+ """提供符合标准 Cron 规范的解析逻辑"""
7
+
8
+ @staticmethod
9
+ def is_time_match(expression: str, now: datetime.datetime) -> bool:
10
+ """
11
+ 判断当前时间是否匹配 Cron 表达式
12
+ 逻辑参考标准 Unix Cron:当日期和星期同时被指定时,采用 OR 关系。
13
+ """
14
+ parts = expression.split()
15
+ if len(parts) == 5:
16
+ # 分 时 日 月 周 -> 补齐秒为 0
17
+ sec_part, min_part, hour_part, day_part, month_part, weekday_part = "0", *parts
18
+ elif len(parts) == 6:
19
+ sec_part, min_part, hour_part, day_part, month_part, weekday_part = parts
20
+ else:
21
+ return False
22
+
23
+ # 1. 转换星期逻辑 (Python 0=Mon, 6=Sun -> Cron 0或7=Sun, 1=Mon...)
24
+ # 转换公式:(now.weekday() + 1) % 7 -> 结果 0=Sun, 1=Mon, ..., 6=Sat
25
+ cron_weekday = (now.weekday() + 1) % 7
26
+
27
+ try:
28
+ # 2. 基础字段匹配
29
+ sec_match = CronBase._match_field(sec_part, now.second)
30
+ min_match = CronBase._match_field(min_part, now.minute)
31
+ hour_match = CronBase._match_field(hour_part, now.hour)
32
+ month_match = CronBase._match_field(month_part, now.month)
33
+
34
+ day_matches = CronBase._match_field(day_part, now.day)
35
+ weekday_matches = CronBase._match_field(weekday_part, cron_weekday)
36
+
37
+ # 3. 处理 Day 和 Weekday 的特殊关系 (Standard Cron Logic)
38
+ # 如果两个字段都有限制(不是 *),则为 OR 关系;否则为 AND 关系。
39
+ day_is_star = (day_part == "*")
40
+ weekday_is_star = (weekday_part == "*")
41
+
42
+ if not day_is_star and not weekday_is_star:
43
+ day_weekday_ok = (day_matches or weekday_matches)
44
+ else:
45
+ day_weekday_ok = (day_matches and weekday_matches)
46
+
47
+ return (
48
+ sec_match and
49
+ min_match and
50
+ hour_match and
51
+ month_match and
52
+ day_weekday_ok
53
+ )
54
+ except Exception:
55
+ # 如果表达式解析失败(如格式错误),返回 False 避免程序崩溃
56
+ return False
57
+
58
+ @staticmethod
59
+ def _match_field(pattern: str, value: int) -> bool:
60
+ """解析单个 Cron 字段"""
61
+ if pattern == "*":
62
+ return True
63
+
64
+ # 处理列表: "1,2,3"
65
+ if "," in pattern:
66
+ return any(CronBase._match_field(p, value) for p in pattern.split(","))
67
+
68
+ # 处理步长: "*/5" 或 "1-10/2"
69
+ if "/" in pattern:
70
+ r, s = pattern.split("/")
71
+ step = int(s)
72
+ if r in ["*", ""]:
73
+ return value % step == 0
74
+ if "-" in r:
75
+ start, end = map(int, r.split("-"))
76
+ return start <= value <= end and (value - start) % step == 0
77
+ # 固定点开始的步长: "5/10"
78
+ return value >= int(r) and (value - int(r)) % step == 0
79
+
80
+ # 处理范围: "10-20"
81
+ if "-" in pattern:
82
+ start, end = map(int, pattern.split("-"))
83
+ return start <= value <= end
84
+
85
+ # 处理精确数值: "5"
86
+ try:
87
+ target_val = int(pattern)
88
+ # 兼容性处理:Cron 中 7 经常作为周日的另一种写法
89
+ if target_val == 7:
90
+ target_val = 0
91
+ return target_val == value
92
+ except ValueError:
93
+ return False
@@ -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,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: faster-cron
3
+ Version: 0.1.1
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
+ [English](./README_EN.md) | 中文版
16
+
17
+ **FasterCron** 是一个轻量级、直观且功能强大的 Python 定时任务调度工具库。它完美支持 **Asyncio (异步)** 和 **Threading (多线程)** 双模式,专为需要高可靠性、简单配置和任务并发控制的场景设计。
18
+
19
+ ---
20
+
21
+ ## 🌟 核心特性
22
+
23
+ * **双模式支持**:一套逻辑同时提供 `AsyncFasterCron`(异步)和 `FasterCron`(同步多线程)两种实现。
24
+ * **任务级并发控制**:通过 `allow_overlap` 参数精准控制同一个任务是否允许重叠执行(单例模式 vs 并发模式)。
25
+ * **智能参数注入**:自动检测任务函数签名,按需注入包含调度时间、任务名称的 `context` 上下文。
26
+ * **标准 Cron 支持**:兼容 5 位(分时日月周)和 6 位(秒分时日月周)Cron 表达式。
27
+ * **健壮性**:内置异常捕获机制,单个任务崩溃不影响调度器运行。
28
+ * **无外部依赖**:仅使用 Python 标准库实现,轻量无负担。
29
+
30
+ ---
31
+
32
+ ## 📦 安装
33
+
34
+ 您可以直接通过 pip 安装:
35
+
36
+ ```bash
37
+ pip install faster-cron
38
+
39
+ ```
40
+
41
+ 或者直接将源码放入您的项目中。
42
+
43
+ ---
44
+
45
+ ## 🚀 快速上手
46
+
47
+ ### 1. 异步模式 (Async Mode)
48
+
49
+ 适用于使用了 `aiohttp`, `httpx` 或 `tortoise-orm` 等异步库的项目。
50
+
51
+ ```python
52
+ import asyncio
53
+ from faster_cron import AsyncFasterCron
54
+
55
+ cron = AsyncFasterCron()
56
+
57
+
58
+ # 示例:每 5 秒执行一次,禁止重叠(若上一个任务没跑完,则跳过本次)
59
+ @cron.schedule("*/5 * * * * *", allow_overlap=False)
60
+ async def my_async_job(context):
61
+ print(f"正在执行任务: {context['task_name']}, 计划时间: {context['scheduled_at']}")
62
+ await asyncio.sleep(6) # 模拟长耗时任务
63
+
64
+
65
+ async def main():
66
+ await cron.start()
67
+
68
+
69
+ if __name__ == "__main__":
70
+ asyncio.run(main())
71
+
72
+ ```
73
+
74
+ ### 2. 同步模式 (Sync Mode)
75
+
76
+ 适用于传统的阻塞式脚本或爬虫。
77
+
78
+ ```python
79
+ from faster_cron import FasterCron
80
+ import time
81
+
82
+ cron = FasterCron()
83
+
84
+
85
+ # 示例:每秒执行一次,允许并发执行
86
+ @cron.schedule("* * * * * *", allow_overlap=True)
87
+ def my_sync_job():
88
+ print("滴答,同步任务正在运行...")
89
+ time.sleep(2)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ cron.run()
94
+
95
+ ```
96
+
97
+ ---
98
+
99
+ ## 🛠 核心 API 说明
100
+
101
+ ### 调度装饰器 `schedule`
102
+
103
+ | 参数 | 类型 | 默认值 | 说明 |
104
+ | --- | --- | --- | --- |
105
+ | `expression` | `str` | - | Cron 表达式。支持 `*`, `,`, `-`, `/`。 |
106
+ | `allow_overlap` | `bool` | `True` | **关键参数**。`True`: 时间点到达即执行;`False`: 若该任务的上一个实例未结束,则跳过本次执行循环。 |
107
+
108
+ ### 上下文参数 `context`
109
+
110
+ 如果您的任务函数接收名为 `context` 的参数,FasterCron 会自动注入以下字典:
111
+
112
+ * `task_name`: 函数名称。
113
+ * `scheduled_at`: 任务触发的精确 `datetime` 对象。
114
+ * `expression`: 该任务使用的 Cron 表达式。
115
+
116
+ ---
117
+
118
+ ## 📅 Cron 表达式参考
119
+
120
+ FasterCron 支持灵活的表达式定义:
121
+
122
+ * `* * * * * *` : 每秒执行。
123
+ * `*/5 * * * * *` : 每 5 秒执行。
124
+ * `0 0 * * * *` : 每整小时执行。
125
+ * `0 30 9-17 * * *` : 每天 9:00 到 17:00 之间的每半小时执行。
126
+ * `0 0 0 * * 0` : 每周日凌晨执行。
127
+
128
+ ---
129
+
130
+ ## 🧪 运行测试
131
+
132
+ 本项目包含完善的单元测试。您可以使用 `pytest` 来验证功能:
133
+
134
+ ```bash
135
+ # 安装测试依赖
136
+ pip install pytest pytest-asyncio
137
+
138
+ # 运行所有测试
139
+ pytest
140
+
141
+ ```
142
+
143
+ ---
144
+
145
+ ## 📄 开源协议
146
+
147
+ MIT License.
148
+
149
+ ---
150
+
151
+ **如果您觉得好用,欢迎点一个 Star!🌟**
@@ -0,0 +1,8 @@
1
+ faster_cron/__init__.py,sha256=9TQeFkow--g4AlkZpemak_vODrkQI6E2zIrbIdgDg80,243
2
+ faster_cron/async_cron.py,sha256=VMD5BqagBvbfhKI4Eo_T1nyYUlcpdmCEKYwnfoLETeE,2084
3
+ faster_cron/base.py,sha256=lIK0kOnt33DD_H9bXT0pEONbPZthBzinivETbrek5EM,3525
4
+ faster_cron/sync_cron.py,sha256=xbyRDJFo2wUvpmlXI9VB6UWUet3mZngHd5XRrTOKR3k,3775
5
+ faster_cron-0.1.1.dist-info/METADATA,sha256=3kMUQMDawwuDKvfPM8beqswm6SK6GPc7I0DpujCw0BQ,4004
6
+ faster_cron-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ faster_cron-0.1.1.dist-info/top_level.txt,sha256=lQbktVSdGKKyocrLDqk8V_bFLBuWML4squauwEdZuik,12
8
+ faster_cron-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ faster_cron