terryutils 1.0.5__py3-none-any.whl → 1.0.7__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.
terryutils/__init__.py CHANGED
@@ -1,2 +1,3 @@
1
1
  from .mysql_util import MysqlUtil
2
- from .log import setup_logger
2
+ from .log import setup_logger
3
+ from .date_util import DateUtils
@@ -0,0 +1,228 @@
1
+ from datetime import datetime, date, timedelta
2
+ import calendar
3
+ import re
4
+
5
+ class DateUtils:
6
+ """企业级 Python 日期工具类"""
7
+
8
+ COMMON_FORMATS = [
9
+ "%Y-%m-%d %H:%M:%S",
10
+ "%Y-%m-%d %H:%M",
11
+ "%Y-%m-%d",
12
+ "%d/%m/%Y",
13
+ "%d-%m-%Y",
14
+ "%Y/%m/%d",
15
+ ]
16
+
17
+ # -------------------- 基础获取 --------------------
18
+ @staticmethod
19
+ def now(fmt="%Y-%m-%d %H:%M:%S"):
20
+ """当前时间"""
21
+ return datetime.now().strftime(fmt)
22
+
23
+ @staticmethod
24
+ def today(fmt="%Y-%m-%d"):
25
+ """今天"""
26
+ return date.today().strftime(fmt)
27
+
28
+ @staticmethod
29
+ def datetime_now():
30
+ """返回datetime对象"""
31
+ return datetime.now()
32
+
33
+ @staticmethod
34
+ def date_today():
35
+ """返回date对象"""
36
+ return date.today()
37
+
38
+ # -------------------- 字符串与datetime互转 --------------------
39
+ @staticmethod
40
+ def parse(date_str, fmt=None):
41
+ """尝试多种格式解析字符串"""
42
+ if isinstance(date_str, (int, float)):
43
+ return datetime.fromtimestamp(date_str)
44
+ if isinstance(date_str, datetime):
45
+ return date_str
46
+ if fmt:
47
+ return datetime.strptime(date_str, fmt)
48
+ for f in DateUtils.COMMON_FORMATS:
49
+ try:
50
+ return datetime.strptime(date_str, f)
51
+ except:
52
+ continue
53
+ raise ValueError(f"无法解析日期: {date_str}")
54
+
55
+ @staticmethod
56
+ def format(dt, fmt="%Y-%m-%d %H:%M:%S"):
57
+ if isinstance(dt, str):
58
+ dt = DateUtils.parse(dt)
59
+ return dt.strftime(fmt)
60
+
61
+ @staticmethod
62
+ def to_timestamp(dt):
63
+ dt = DateUtils.parse(dt) if isinstance(dt, (str, int, float)) else dt
64
+ return int(dt.timestamp())
65
+
66
+ @staticmethod
67
+ def from_timestamp(ts):
68
+ return datetime.fromtimestamp(ts)
69
+
70
+ # -------------------- 日期加减 --------------------
71
+ @staticmethod
72
+ def add_days(dt, days):
73
+ dt = DateUtils.parse(dt)
74
+ return dt + timedelta(days=days)
75
+
76
+ @staticmethod
77
+ def add_seconds(dt, seconds, ft):
78
+ dt = DateUtils.parse(dt, ft)
79
+ return dt + timedelta(seconds=seconds)
80
+
81
+ @staticmethod
82
+ def add_months(dt, months):
83
+ dt = DateUtils.parse(dt)
84
+ month = dt.month - 1 + months
85
+ year = dt.year + month // 12
86
+ month = month % 12 + 1
87
+ day = min(dt.day, calendar.monthrange(year, month)[1])
88
+ return dt.replace(year=year, month=month, day=day)
89
+
90
+ @staticmethod
91
+ def add_years(dt, years):
92
+ dt = DateUtils.parse(dt)
93
+ try:
94
+ return dt.replace(year=dt.year + years)
95
+ except ValueError:
96
+ # 闰年2月29日处理
97
+ return dt.replace(month=2, day=28, year=dt.year + years)
98
+
99
+ # -------------------- 周/月/季度起止 --------------------
100
+ @staticmethod
101
+ def start_of_day(dt=None):
102
+ dt = DateUtils.parse(dt) if dt else datetime.now()
103
+ return datetime(dt.year, dt.month, dt.day, 0, 0, 0)
104
+
105
+ @staticmethod
106
+ def end_of_day(dt=None):
107
+ dt = DateUtils.parse(dt) if dt else datetime.now()
108
+ return datetime(dt.year, dt.month, dt.day, 23, 59, 59)
109
+
110
+ @staticmethod
111
+ def start_of_week(dt=None):
112
+ dt = DateUtils.parse(dt) if dt else datetime.now()
113
+ start = dt - timedelta(days=dt.weekday())
114
+ return datetime(start.year, start.month, start.day)
115
+
116
+ @staticmethod
117
+ def end_of_week(dt=None):
118
+ dt = DateUtils.parse(dt) if dt else datetime.now()
119
+ end = dt + timedelta(days=6 - dt.weekday())
120
+ return datetime(end.year, end.month, end.day, 23, 59, 59)
121
+
122
+ @staticmethod
123
+ def start_of_month(dt=None):
124
+ dt = DateUtils.parse(dt) if dt else datetime.now()
125
+ return datetime(dt.year, dt.month, 1)
126
+
127
+ @staticmethod
128
+ def end_of_month(dt=None):
129
+ dt = DateUtils.parse(dt) if dt else datetime.now()
130
+ last_day = calendar.monthrange(dt.year, dt.month)[1]
131
+ return datetime(dt.year, dt.month, last_day, 23, 59, 59)
132
+
133
+ @staticmethod
134
+ def start_of_quarter(dt=None):
135
+ dt = DateUtils.parse(dt) if dt else datetime.now()
136
+ month = ((dt.month - 1) // 3) * 3 + 1
137
+ return datetime(dt.year, month, 1)
138
+
139
+ @staticmethod
140
+ def end_of_quarter(dt=None):
141
+ dt = DateUtils.parse(dt) if dt else datetime.now()
142
+ month = ((dt.month - 1) // 3 + 1) * 3
143
+ last_day = calendar.monthrange(dt.year, month)[1]
144
+ return datetime(dt.year, month, last_day, 23, 59, 59)
145
+
146
+ # -------------------- 日期判断 --------------------
147
+ @staticmethod
148
+ def is_leap_year(year):
149
+ return calendar.isleap(year)
150
+
151
+ @staticmethod
152
+ def weekday(dt=None):
153
+ dt = DateUtils.parse(dt) if dt else datetime.now()
154
+ return dt.weekday() # 0=周一
155
+
156
+ @staticmethod
157
+ def is_weekend(dt=None):
158
+ return DateUtils.weekday(dt) >= 5
159
+
160
+ @staticmethod
161
+ def days_between(start, end):
162
+ start = DateUtils.parse(start)
163
+ end = DateUtils.parse(end)
164
+ return (end - start).days
165
+
166
+ # -------------------- 日期区间 --------------------
167
+ @staticmethod
168
+ def date_range(start, end, step=1):
169
+ start = DateUtils.parse(start)
170
+ end = DateUtils.parse(end)
171
+ days = []
172
+ delta = timedelta(days=step)
173
+ current = start
174
+ while current <= end:
175
+ days.append(current)
176
+ current += delta
177
+ return days
178
+
179
+ # -------------------- 自然语言解析 --------------------
180
+ @staticmethod
181
+ def smart_parse(text):
182
+ """支持 '今天','明天','昨天','上周一','下个月15号'"""
183
+ today = datetime.now()
184
+ text = text.strip()
185
+ if text == "今天":
186
+ return today
187
+ elif text == "明天":
188
+ return today + timedelta(days=1)
189
+ elif text == "昨天":
190
+ return today - timedelta(days=1)
191
+ m = re.match(r"上周([一二三四五六日])", text)
192
+ if m:
193
+ weekday_map = {"一":0,"二":1,"三":2,"四":3,"五":4,"六":5,"日":6}
194
+ target_weekday = weekday_map[m.group(1)]
195
+ delta_days = today.weekday() - target_weekday + 7
196
+ return today - timedelta(days=delta_days)
197
+ # TODO: 可继续扩展自然语言解析
198
+ return DateUtils.parse(text)
199
+
200
+ @staticmethod
201
+ def add_days_str(date_str, days, fmt="%Y-%m-%d"):
202
+ """直接对字符串日期加减天数,返回字符串"""
203
+ dt = datetime.strptime(date_str, fmt)
204
+ dt += timedelta(days=days)
205
+ return dt.strftime(fmt)
206
+
207
+ @staticmethod
208
+ def format_day(date_str, fmt="%Y-%m-%d"):
209
+ """直接对字符串日期加减天数,返回字符串"""
210
+ dt = datetime.strptime(date_str, fmt)
211
+ return dt.strftime("%Y年%m月%d日")
212
+
213
+ @staticmethod
214
+ def to_timestamp(dt):
215
+ dt = DateUtils.parse(dt) if isinstance(dt, (str, int, float)) else dt
216
+ return int(dt.timestamp())
217
+ # ------------------------ 测试 ------------------------
218
+ if __name__ == "__main__":
219
+ print("现在时间:", DateUtils.now())
220
+ print("今天:", DateUtils.today())
221
+ print("月初:", DateUtils.start_of_month())
222
+ print("月末:", DateUtils.end_of_month())
223
+ print("季度开始:", DateUtils.start_of_quarter())
224
+ print("季度结束:", DateUtils.end_of_quarter())
225
+ print("加3天:", DateUtils.add_days(datetime.now(), 3))
226
+ print("是否周末:", DateUtils.is_weekend())
227
+ print("自然语言解析 明天:", DateUtils.smart_parse("明天"))
228
+ print("日期区间:", [d.strftime("%Y-%m-%d") for d in DateUtils.date_range("2025-12-01", "2025-12-05")])
@@ -0,0 +1,63 @@
1
+ from dbutils.pooled_db import PooledDB
2
+ import pymysql
3
+ from pymysql.cursors import DictCursor
4
+
5
+ MYSQL_POOL = PooledDB(
6
+ creator=pymysql,
7
+ maxconnections=20, # 最大连接数(重点)
8
+ mincached=2, # 启动时创建
9
+ maxcached=10, # 池中最大空闲
10
+ blocking=True, # 连接耗尽时等待
11
+ host="localhost",
12
+ user="user",
13
+ password="password",
14
+ database="db",
15
+ port=3306,
16
+ charset="utf8mb4",
17
+ cursorclass=DictCursor,
18
+ autocommit=False
19
+ )
20
+
21
+
22
+ class MysqlClient:
23
+
24
+ def __init__(self, pool):
25
+ self.pool = pool
26
+
27
+ def _execute(self, fn):
28
+ conn = self.pool.connection()
29
+ try:
30
+ result = fn(conn)
31
+ conn.commit()
32
+ return result
33
+ except Exception:
34
+ conn.rollback()
35
+ raise
36
+ finally:
37
+ conn.close() # ❗归还给 pool,不是真 close
38
+
39
+ def execute(self, sql, params=None):
40
+ def run(conn):
41
+ with conn.cursor() as cursor:
42
+ return cursor.execute(sql, params or ())
43
+ return self._execute(run)
44
+
45
+ def executemany(self, sql, params_list):
46
+ def run(conn):
47
+ with conn.cursor() as cursor:
48
+ return cursor.executemany(sql, params_list)
49
+ return self._execute(run)
50
+
51
+ def fetchone(self, sql, params=None):
52
+ def run(conn):
53
+ with conn.cursor() as cursor:
54
+ cursor.execute(sql, params or ())
55
+ return cursor.fetchone()
56
+ return self._execute(run)
57
+
58
+ def fetchall(self, sql, params=None):
59
+ def run(conn):
60
+ with conn.cursor() as cursor:
61
+ cursor.execute(sql, params or ())
62
+ return cursor.fetchall()
63
+ return self._execute(run)
@@ -0,0 +1,222 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import asyncio
5
+ import inspect
6
+ import logging
7
+ import traceback
8
+ import threading
9
+ from queue import Queue, Empty
10
+ from dataclasses import dataclass, field
11
+ from typing import Callable, Optional
12
+ from logging.handlers import RotatingFileHandler
13
+
14
+ log_dir = "logs"
15
+ os.makedirs(log_dir, exist_ok=True)
16
+
17
+ log_file = os.path.join(log_dir, "request_framework.log")
18
+
19
+ formatter = logging.Formatter(
20
+ '%(asctime)s | %(levelname)-5s | %(threadName)s | %(message)s',
21
+ datefmt='%Y-%m-%d %H:%M:%S'
22
+ )
23
+ # --- 文件 handler ---
24
+ file_handler = RotatingFileHandler(
25
+ log_file,
26
+ maxBytes=50 * 1024 * 1024,
27
+ backupCount=5,
28
+ encoding='utf-8'
29
+ )
30
+ file_handler.setFormatter(formatter)
31
+ file_handler.setLevel(logging.INFO)
32
+
33
+ # --- 控制台 handler ---
34
+ console_handler = logging.StreamHandler()
35
+ console_handler.setFormatter(formatter)
36
+ console_handler.setLevel(logging.INFO)
37
+
38
+ logger = logging.getLogger("RequestFramework")
39
+ logger.setLevel(logging.INFO)
40
+
41
+ # 防止重复 add
42
+ if not logger.handlers:
43
+ logger.addHandler(file_handler)
44
+ logger.addHandler(console_handler)
45
+
46
+ logger.propagate = False
47
+
48
+
49
+
50
+
51
+ # --- 1. 任务实体:统一封装请求 ---
52
+ @dataclass
53
+ class RequestTask:
54
+ name: str # 任务名称(日志用)
55
+ func: Callable # 执行函数 (requests.get 或 sdk.call)
56
+ args: tuple = field(default_factory=tuple)
57
+ kwargs: dict = field(default_factory=dict)
58
+ callback: Optional[Callable] = None # 成功回调
59
+ max_retries: int = 3 # 最大重试次数
60
+ retry_count: int = 0 # 当前重试计数
61
+ priority: int = 10 # 优先级(数字越小越高)
62
+
63
+
64
+ # --- 2. 频率控制器:令牌桶思想 ---
65
+ class RateLimiter:
66
+ def __init__(self, qps: int):
67
+ self.interval = 1.0 / qps
68
+ self.last_run = 0.0
69
+ self._lock = threading.Lock()
70
+
71
+ def wait(self):
72
+ with self._lock:
73
+ elapsed = time.time() - self.last_run
74
+ wait_time = self.interval - elapsed
75
+ if wait_time > 0:
76
+ time.sleep(wait_time)
77
+ self.last_run = time.time()
78
+
79
+
80
+ # --- 3. 调度引擎 ---
81
+ class RequestEngine:
82
+ def __init__(self, worker_num: int = 3, qps: int = 5):
83
+ self.task_queue = Queue() # 待处理队列
84
+ self.error_queue = Queue() # 错误队列
85
+ self.limiter = RateLimiter(qps) # 频率控制
86
+ self.worker_num = worker_num
87
+ self.running = False
88
+
89
+ def add_task(self, task: RequestTask):
90
+ """外部提交任务接口"""
91
+ self.task_queue.put(task)
92
+
93
+ def start(self):
94
+ """启动引擎"""
95
+ self.running = True
96
+ for i in range(self.worker_num):
97
+ t = threading.Thread(target=self._worker_loop, name=f"Worker-{i}")
98
+ t.daemon = True
99
+ t.start()
100
+ logger.info(f"引擎启动:工作线程={self.worker_num}, QPS限制={1 / self.limiter.interval}")
101
+
102
+ def _worker_loop(self):
103
+ """工作线程主循环"""
104
+ while self.running:
105
+ try:
106
+ # 1. 获取任务 (阻塞1秒以便检查running状态)
107
+ task: RequestTask = self.task_queue.get(timeout=1)
108
+
109
+ # 2. 频率控制
110
+ self.limiter.wait()
111
+
112
+ # 3. 执行任务
113
+ self._execute(task)
114
+
115
+ self.task_queue.task_done()
116
+ except Empty:
117
+ continue
118
+
119
+ def _execute(self, task: RequestTask):
120
+ """执行逻辑:包含重试和异常处理"""
121
+ try:
122
+ logger.info(f"正在执行: {task.name}, 参数: {task.args}")
123
+ # 判断并处理异步函数
124
+ if inspect.iscoroutinefunction(task.func):
125
+ # 如果是 async def 定义的函数,启动一个临时的事件循环运行它
126
+ result = asyncio.run(task.func(*task.args, **task.kwargs))
127
+ else:
128
+ # 如果是普通的 def 定义的函数,直接调用
129
+ result = task.func(*task.args, **task.kwargs)
130
+
131
+ # 4. 执行回调
132
+ if task.callback:
133
+ task.callback(result)
134
+
135
+ logger.info(f"执行成功: {task.name}")
136
+
137
+ except Exception as e:
138
+ exc_type, exc_obj, tb = sys.exc_info()
139
+ filename = tb.tb_frame.f_code.co_filename
140
+ lineno = tb.tb_lineno
141
+ traceback.print_exc()
142
+ logger.error(
143
+ f"执行失败: {task.name}, "
144
+ f"{exc_type.__name__} at {filename}:{lineno}, "
145
+ f"错误信息: {e}"
146
+ )
147
+
148
+ # 5. 重试逻辑
149
+ if task.retry_count < task.max_retries:
150
+ task.retry_count += 1
151
+ wait_s = 2 ** task.retry_count # 指数退避
152
+ logger.warning(f"任务 {task.name} 将在 {wait_s}s 后进行第 {task.retry_count} 次重试")
153
+
154
+ # 异步延迟重试:不阻塞当前线程,放入定时器重新入队
155
+ threading.Timer(wait_s, self.add_task, args=[task]).start()
156
+ else:
157
+ # 6. 最终失败,放入错误队列
158
+ logger.critical(f"任务 {task.name} 达到最大重试次数,放弃处理")
159
+ self.error_queue.put({"task": task, "error": str(e)})
160
+
161
+
162
+
163
+
164
+ if __name__ == "__main__":
165
+ import requests
166
+
167
+ # 1. 初始化引擎(5个并发,每秒最多2个请求)
168
+ engine = RequestEngine(worker_num=3, qps=1)
169
+ # 2. 启动
170
+ engine.start()
171
+
172
+
173
+ # 示例 A: 封装 requests 请求
174
+ def get_weather(city):
175
+ resp = requests.get(f"https://httpbin.org/get?city={city}", timeout=5)
176
+ resp.raise_for_status()
177
+ return resp.json()
178
+
179
+ def callback_func(result):
180
+ print(result)
181
+
182
+ task1 = RequestTask(
183
+ name="天气查询-北京",
184
+ func=get_weather,
185
+ args=("Beijing",),
186
+ callback=callback_func # 成功后落库
187
+ )
188
+
189
+
190
+ # 示例 B: 封装 SDK 请求
191
+ class MySDK:
192
+ def upload_file(self, file_id):
193
+ if file_id == "bad_id": raise ValueError("SDK内部错误")
194
+ return {"status": "uploaded", "id": file_id}
195
+
196
+
197
+ sdk = MySDK()
198
+ task2 = RequestTask(
199
+ name="SDK上传-001",
200
+ func=sdk.upload_file,
201
+ kwargs={"file_id": "file_123"},
202
+ callback=callback_func
203
+ )
204
+
205
+ # 示例 C: 故意失败的任务(测试重试)
206
+ task3 = RequestTask(
207
+ name="必失败任务",
208
+ func=sdk.upload_file,
209
+ kwargs={"file_id": "bad_id"},
210
+ max_retries=2
211
+ )
212
+
213
+ # 3. 提交任务到队列
214
+ engine.add_task(task1)
215
+ engine.add_task(task2)
216
+ engine.add_task(task3)
217
+
218
+ # 保持主线程运行
219
+ try:
220
+ while True: time.sleep(1)
221
+ except KeyboardInterrupt:
222
+ print("退出中...")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: terryutils
3
- Version: 1.0.5
3
+ Version: 1.0.7
4
4
  Summary: My personal python toolkit
5
5
  Author-email: Terry Zhu <596480606@qq.com>
6
6
  License: MIT
@@ -0,0 +1,10 @@
1
+ terryutils/__init__.py,sha256=rDDyXE01Cmz90yMbiDhWa9AOdsB7JX-KqSmYBYt9PAY,99
2
+ terryutils/date_util.py,sha256=KdsTOd8EjKu7p9gbfBjm0Yd06zokM8WDAD2NfnogPu0,7832
3
+ terryutils/log.py,sha256=ul9DF7-6AwjH-vE7aPB96hFM0OKrYUjjUFmd4xP4678,1063
4
+ terryutils/mysql_pool_util.py,sha256=TI0MxEmdR5CZ4VtSS6lL9CAyzrANLM5-1caUJulYg24,1835
5
+ terryutils/mysql_util.py,sha256=UwnF8z-KNJlMuOnH03iHVPKkpSNj073YSsrvI3x8pkM,3292
6
+ terryutils/task_framework.py,sha256=040gMwDggAo2KFdtnHCNf4u9ctIJh7xCiHqJRTPB1Fo,6927
7
+ terryutils-1.0.7.dist-info/METADATA,sha256=gpy_i3tPNOPFksvc_OzO8ZBIaiN9EmUAQacq5yh4Q6o,344
8
+ terryutils-1.0.7.dist-info/WHEEL,sha256=WnJ8fYhv8N4SYVK2lLYNI6N0kVATA7b0piVUNvqIIJE,91
9
+ terryutils-1.0.7.dist-info/top_level.txt,sha256=LQtzsZsXViaVLHb_rJ4SJiT9ScUfK6dUoy89dP1mLIo,11
10
+ terryutils-1.0.7.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- terryutils/__init__.py,sha256=lxrRwy_RkvG1mcSs6DOv4SLRqZmwHVERKnT8CzgQ6t8,64
2
- terryutils/log.py,sha256=ul9DF7-6AwjH-vE7aPB96hFM0OKrYUjjUFmd4xP4678,1063
3
- terryutils/mysql_util.py,sha256=UwnF8z-KNJlMuOnH03iHVPKkpSNj073YSsrvI3x8pkM,3292
4
- terryutils-1.0.5.dist-info/METADATA,sha256=ZPiFz_6ifTjLHjTs-2TVERdpXkktNn-mRBqk1FRggMo,344
5
- terryutils-1.0.5.dist-info/WHEEL,sha256=WnJ8fYhv8N4SYVK2lLYNI6N0kVATA7b0piVUNvqIIJE,91
6
- terryutils-1.0.5.dist-info/top_level.txt,sha256=LQtzsZsXViaVLHb_rJ4SJiT9ScUfK6dUoy89dP1mLIo,11
7
- terryutils-1.0.5.dist-info/RECORD,,