gomyck-tools 1.3.1__py3-none-any.whl → 1.3.3__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.
Files changed (66) hide show
  1. ctools/__init__.py +0 -0
  2. ctools/aes_tools.py +35 -0
  3. ctools/api_result.py +55 -0
  4. ctools/application.py +386 -0
  5. ctools/b64.py +7 -0
  6. ctools/bashPath.py +13 -0
  7. ctools/bottle_web_base.py +169 -0
  8. ctools/bottle_webserver.py +143 -0
  9. ctools/bottle_websocket.py +75 -0
  10. ctools/browser_element_tools.py +314 -0
  11. ctools/call.py +71 -0
  12. ctools/cdebug.py +143 -0
  13. ctools/cftp.py +74 -0
  14. ctools/cjson.py +54 -0
  15. ctools/ckafka.py +159 -0
  16. ctools/compile_tools.py +18 -0
  17. ctools/console.py +55 -0
  18. ctools/coord_trans.py +127 -0
  19. ctools/credis.py +111 -0
  20. ctools/cron_lite.py +245 -0
  21. ctools/ctoken.py +34 -0
  22. ctools/cword.py +30 -0
  23. ctools/czip.py +130 -0
  24. ctools/database.py +185 -0
  25. ctools/date_utils.py +43 -0
  26. ctools/dict_wrapper.py +20 -0
  27. ctools/douglas_rarefy.py +136 -0
  28. ctools/download_tools.py +57 -0
  29. ctools/enums.py +4 -0
  30. ctools/ex.py +31 -0
  31. ctools/excelOpt.py +36 -0
  32. ctools/html_soup.py +35 -0
  33. ctools/http_utils.py +24 -0
  34. ctools/images_tools.py +27 -0
  35. ctools/imgDialog.py +44 -0
  36. ctools/metrics.py +131 -0
  37. ctools/mqtt_utils.py +289 -0
  38. ctools/obj.py +20 -0
  39. ctools/pacth.py +74 -0
  40. ctools/plan_area_tools.py +97 -0
  41. ctools/process_pool.py +36 -0
  42. ctools/pty_tools.py +72 -0
  43. ctools/resource_bundle_tools.py +121 -0
  44. ctools/rsa.py +70 -0
  45. ctools/screenshot_tools.py +127 -0
  46. ctools/sign.py +20 -0
  47. ctools/sm_tools.py +49 -0
  48. ctools/snow_id.py +76 -0
  49. ctools/str_diff.py +20 -0
  50. ctools/string_tools.py +85 -0
  51. ctools/sys_info.py +157 -0
  52. ctools/sys_log.py +89 -0
  53. ctools/thread_pool.py +35 -0
  54. ctools/upload_tools.py +40 -0
  55. ctools/win_canvas.py +83 -0
  56. ctools/win_control.py +106 -0
  57. ctools/word_fill.py +562 -0
  58. ctools/word_fill_entity.py +46 -0
  59. ctools/work_path.py +69 -0
  60. {gomyck_tools-1.3.1.dist-info → gomyck_tools-1.3.3.dist-info}/METADATA +4 -2
  61. gomyck_tools-1.3.3.dist-info/RECORD +64 -0
  62. {gomyck_tools-1.3.1.dist-info → gomyck_tools-1.3.3.dist-info}/WHEEL +1 -1
  63. gomyck_tools-1.3.3.dist-info/licenses/LICENSE +13 -0
  64. gomyck_tools-1.3.3.dist-info/top_level.txt +1 -0
  65. gomyck_tools-1.3.1.dist-info/RECORD +0 -4
  66. gomyck_tools-1.3.1.dist-info/top_level.txt +0 -1
ctools/database.py ADDED
@@ -0,0 +1,185 @@
1
+ import contextlib
2
+ import datetime
3
+ import math
4
+
5
+ from sqlalchemy import create_engine, Integer, Column, event
6
+ from sqlalchemy.ext.declarative import declarative_base
7
+ from sqlalchemy.orm import sessionmaker, Session
8
+ from sqlalchemy.sql import text
9
+
10
+ from ctools import call, string_tools
11
+ from ctools.thread_pool import thread_local
12
+
13
+ """
14
+ class XXXX(BaseMixin):
15
+ __tablename__ = 't_xxx_info'
16
+ __table_args__ = {'comment': 'xxx信息表'}
17
+ server_content: Column = Column(String(50), nullable=True, default='', comment='123123')
18
+ server_ip: Column = Column(String(30), index=True)
19
+ user_id: Column = Column(BigInteger)
20
+
21
+ database.init_db('postgresql://postgres:123456@192.168.3.107:32566/abc', default_schema='public', db_key='source', pool_size=100)
22
+ with database.get_session('source') as s:
23
+ s.execute(text('insert into xxx (name) values (:name)'), {'name': string_tools.get_random_str(5)})
24
+ s.commit()
25
+ """
26
+
27
+ Base = None
28
+ inited_db = {}
29
+ engines = {}
30
+ sessionMakers = {}
31
+
32
+ def getEngine(db_key: str='default'):
33
+ return engines[db_key]
34
+
35
+ @call.init
36
+ def _init():
37
+ global Base
38
+ Base = declarative_base()
39
+
40
+ """
41
+ The string form of the URL is
42
+ dialect[+driver]://user:password@host/dbname[?key=value..]
43
+ where ``dialect`` is a database name such as ``mysql``, ``oracle``, ``postgresql``, etc.
44
+ and ``driver`` the name of a DBAPI such as ``psycopg2``, ``pyodbc``, ``cx_oracle``, etc. Alternatively
45
+ """
46
+
47
+ # 密码里的@ 要替换成 %40
48
+
49
+ # sqlite connect_args={"check_same_thread": False} db_url=sqlite:///{}.format(db_url)
50
+ # sqlite 数据库, 初始化之后, 优化一下配置
51
+ # $ sqlite3 app.db
52
+ # > PRAGMA journal_mode=WAL; 设置事务的模式, wal 允许读写并发, 但是会额外创建俩文件
53
+ # > PRAGMA synchronous=NORMAL; 设置写盘策略, 默认是 FULL, 日志,数据都落, 设置成 NORMAL, 日志写完就算事务完成
54
+
55
+ def init_db(db_url: str, db_key: str='default', connect_args: dict={}, default_schema: str=None, pool_size: int=5, max_overflow: int=25, echo: bool=False):
56
+ if db_url.startswith('mysql'):
57
+ import pymysql
58
+ pymysql.install_as_MySQLdb()
59
+ if inited_db.get(db_key): raise Exception('db {} already init!!!'.format(db_key))
60
+ global engines, sessionMakers
61
+ engine, sessionMaker = _create_connection(db_url=db_url, connect_args=connect_args, pool_size=pool_size, max_overflow=max_overflow, echo=echo)
62
+ engines[db_key] = engine
63
+ sessionMakers[db_key] = sessionMaker
64
+ inited_db[db_key] = True
65
+ if default_schema: event.listen(engine, 'connect', lambda dbapi_connection, connection_record: _set_search_path(dbapi_connection, default_schema))
66
+ Base.metadata.create_all(engine)
67
+
68
+ def _set_search_path(dbapi_connection, default_schema):
69
+ with dbapi_connection.cursor() as cursor:
70
+ cursor.execute(f'SET search_path TO {default_schema}')
71
+
72
+ def _create_connection(db_url: str, pool_size: int=5, max_overflow: int=25, connect_args={}, echo: bool=False):
73
+ engine = create_engine('{}'.format(db_url),
74
+ echo=echo,
75
+ future=True,
76
+ pool_size=pool_size,
77
+ max_overflow=max_overflow,
78
+ pool_pre_ping=True,
79
+ pool_recycle=3600,
80
+ connect_args=connect_args)
81
+ sm = sessionmaker(bind=engine)
82
+ return engine, sm
83
+
84
+ def generate_custom_id():
85
+ return str(string_tools.get_snowflake_id())
86
+
87
+ class BaseMixin(Base):
88
+ __abstract__ = True
89
+ obj_id = Column(Integer, primary_key=True, default=generate_custom_id)
90
+
91
+ # ext1 = Column(String)
92
+ # ext2 = Column(String)
93
+ # ext3 = Column(String)
94
+ # create_time = Column(DateTime, nullable=False, default=datetime.datetime.now)
95
+ # update_time = Column(DateTime, nullable=False, default=datetime.datetime.now, onupdate=datetime.datetime.now, index=True)
96
+
97
+ def to_dict(self):
98
+ return self.__getstate__()
99
+
100
+ def from_dict(self, v):
101
+ self.__dict__.update(v)
102
+
103
+ def __getstate__(self):
104
+ ret_state = {}
105
+ state = self.__dict__.copy()
106
+ for key in state.keys():
107
+ if not key.startswith("_"):
108
+ if type(state[key]) == datetime.datetime:
109
+ ret_state[key] = state[key].strftime("%Y-%m-%d %H:%M:%S")
110
+ else:
111
+ ret_state[key] = state[key]
112
+ return ret_state
113
+
114
+ @contextlib.contextmanager
115
+ def get_session(db_key: str='default') -> Session:
116
+ thread_local.db_key = db_key
117
+ if sm:=sessionMakers.get(db_key):
118
+ s = sm()
119
+ else:
120
+ raise ValueError("Invalid db_key: {}".format(db_key))
121
+ try:
122
+ yield s
123
+ except Exception as e:
124
+ s.rollback()
125
+ raise e
126
+ finally:
127
+ s.close()
128
+
129
+ class PageInfoBuilder:
130
+
131
+ def __init__(self, pageInfo, total_count, records):
132
+ self.page_size = pageInfo.page_size
133
+ self.page_index = pageInfo.page_index
134
+ self.total_count = total_count
135
+ self.total_page = math.ceil(total_count / int(pageInfo.page_size))
136
+ self.records = records
137
+
138
+ def query_by_page(query, pageInfo):
139
+ records = query.offset((pageInfo.page_index - 1) * pageInfo.page_size).limit(pageInfo.page_size).all()
140
+ rs = []
141
+ for r in records:
142
+ rs.append(r)
143
+ return PageInfoBuilder(pageInfo, query.count(), rs)
144
+
145
+ def query4_crd_sql(session, sql: str, params: dict) -> []:
146
+ records = session.execute(text(sql), params).fetchall()
147
+ rs = []
148
+ for record in records:
149
+ data = {}
150
+ for index, key in enumerate(record._mapping):
151
+ data[key] = record[index]
152
+ rs.append(data)
153
+ return rs
154
+
155
+ sqlite_and_pg_page_sql = """
156
+ limit :limit offset :offset
157
+ """
158
+ mysql_page_sql = """
159
+ limit :offset, :limit
160
+ """
161
+
162
+ def query_by_page4_crd_sql(session, sql: str, params: dict, pageInfo) -> []:
163
+ db_name = engines[thread_local.db_key].name
164
+ if db_name == 'postgresql' or db_name == 'sqlite':
165
+ page_sql = sqlite_and_pg_page_sql
166
+ elif db_name == 'mysql':
167
+ page_sql = mysql_page_sql
168
+ else:
169
+ raise Exception('not support db: {}'.format(db_name))
170
+ wrapper_sql = """
171
+ select * from ({}) as t {}
172
+ """.format(sql, page_sql)
173
+ count_sql = """
174
+ select count(1) from ({}) as t
175
+ """.format(sql)
176
+ params["limit"] = pageInfo.page_size
177
+ params["offset"] = (pageInfo.page_index - 1) * pageInfo.page_size
178
+ records = session.execute(text(wrapper_sql), params).fetchall()
179
+ rs = []
180
+ for record in records:
181
+ data = {}
182
+ for index, key in enumerate(record._mapping):
183
+ data[key] = record[index]
184
+ rs.append(data)
185
+ return PageInfoBuilder(pageInfo, session.execute(text(count_sql), params).first()[0], rs)
ctools/date_utils.py ADDED
@@ -0,0 +1,43 @@
1
+ import time
2
+ from datetime import datetime, timedelta
3
+
4
+ def get_date():
5
+ return time.strftime('%Y-%m-%d', time.localtime(time.time()))
6
+
7
+ def get_time():
8
+ return time.strftime('%H-%M-%S', time.localtime(time.time()))
9
+
10
+ def get_date_time(fmt="%Y-%m-%d %H:%M:%S"):
11
+ return time.strftime(fmt, time.localtime(time.time()))
12
+
13
+ def str_to_datetime(val: str, fmt="%Y-%m-%d %H:%M:%S"):
14
+ return time.strptime(val, fmt)
15
+
16
+ def str_to_timestamp(val: str, fmt="%Y-%m-%d %H:%M:%S"):
17
+ return time.mktime(time.strptime(val, fmt))
18
+
19
+ def timestamp_to_str(timestamp: int=time.time(), fmt="%Y-%m-%d %H:%M:%S"):
20
+ return time.strftime(fmt, time.localtime(timestamp))
21
+
22
+ def get_today_start_end(now: datetime.now()):
23
+ start = datetime(now.year, now.month, now.day, 0, 0, 0)
24
+ end = datetime(now.year, now.month, now.day, 23, 59, 59)
25
+ return start.strftime("%Y-%m-%d %H:%M:%S"), end.strftime("%Y-%m-%d %H:%M:%S")
26
+
27
+ def get_week_start_end(now: datetime.now()):
28
+ start = now - timedelta(days=now.weekday()) # 本周一
29
+ end = start + timedelta(days=6) # 本周日
30
+ return start.strftime("%Y-%m-%d 00:00:00"), end.strftime("%Y-%m-%d 23:59:59")
31
+
32
+ def time_diff_in_seconds(sub_head: str=get_date_time(), sub_end: str=get_date_time()):
33
+ start_ts = str_to_timestamp(sub_head)
34
+ end_ts = str_to_timestamp(sub_end)
35
+ return int(start_ts - end_ts)
36
+
37
+ def opt_time(base_time=None, days=0, hours=0, minutes=0, seconds=0, weeks=0, fmt="%Y-%m-%d %H:%M:%S"):
38
+ if base_time is None:
39
+ base_time = datetime.now()
40
+ elif isinstance(base_time, str):
41
+ base_time = datetime.strptime(base_time, fmt)
42
+ new_time = base_time + timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, weeks=weeks)
43
+ return new_time.strftime(fmt)
ctools/dict_wrapper.py ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ __author__ = 'haoyang'
4
+ __date__ = '2024/10/25 09:42'
5
+
6
+ class DictWrapper(dict):
7
+
8
+ def __getattr__(self, key):
9
+ res = self.get(key)
10
+ if res is None:
11
+ raise AttributeError(f" ==>> {key} <<== Not Found In This Entity!!!")
12
+ if isinstance(res, dict):
13
+ return DictWrapper(res)
14
+ return res
15
+
16
+ def __setattr__(self, key, value):
17
+ self[key] = value
18
+
19
+ def __delattr__(self, key):
20
+ del self[key]
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ __author__ = 'haoyang'
4
+ __date__ = '2024/9/19 14:02'
5
+
6
+ import math
7
+
8
+ from jsonpath_ng import parser
9
+
10
+ from ctools import cjson
11
+ from ctools.sys_log import flog as log
12
+
13
+ """
14
+ douglas_rarefy.DouglasRarefy(res, level=3).sparse_points()
15
+ """
16
+
17
+ class THIN_LEVEL:
18
+ L1 = 0.00001
19
+ L2 = 0.00003
20
+ L3 = 0.00009
21
+ L4 = 0.0002
22
+ L5 = 0.0004
23
+ L6 = 0.0007
24
+ L7 = 0.0011
25
+ L8 = 0.0017
26
+ L9 = 0.0022
27
+
28
+ class Point:
29
+ def __init__(self, lng, lat, origin_data):
30
+ self.lng = lng
31
+ self.lat = lat
32
+ self.origin_data = origin_data
33
+
34
+ def _get_line_by_point(xy1, xy2):
35
+ """
36
+ 根据两个点求直线方程 ax + by + c = 0
37
+ :param xy1: 点1, 例如 {'lat': 1, 'lng': 1}
38
+ :param xy2: 点2, 例如 {'lat': 2, 'lng': 2}
39
+ :return: 直线方程的三个参数 [a, b, c]
40
+ """
41
+ x1 = xy1.lng
42
+ y1 = xy1.lat
43
+ x2 = xy2.lng
44
+ y2 = xy2.lat
45
+ a = y2 - y1
46
+ b = x1 - x2
47
+ c = (y1 - y2) * x1 - y1 * (x1 - x2)
48
+ return [a, b, c]
49
+
50
+
51
+ def _get_distance_from_point_to_line(a, b, c, xy):
52
+ """
53
+ 点到直线的距离,直线方程为 ax + by + c = 0
54
+ :param a: 直线参数a
55
+ :param b: 直线参数b
56
+ :param c: 直线参数c
57
+ :param xy: 点坐标,例如 {'lat': 2, 'lng': 2}
58
+ :return: 距离
59
+ """
60
+ x = xy.lng
61
+ y = xy.lat
62
+ return abs((a * x + b * y + c) / math.sqrt(a * a + b * b))
63
+
64
+
65
+ class DouglasRarefy:
66
+ """
67
+ DouglasRarefy Use Guide:
68
+ points must be arrays, element can be dict or arrays, when element is arrays, index 0 must be lng, index 1 must be lat, and element can be use max column num is 2 (lng, lat) in ret_tpl
69
+ level default is L2, this level can be hold most of the points detail
70
+ ret_tpl is the result tpl, can be arrays and json or some can be json loads, exp: [{lng}, {lat}] OR {{"lng": {lng}, "lat": {lat}}}
71
+ """
72
+ def __init__(self, points:[], level=THIN_LEVEL.L2, ret_tpl=None, get_lng=None, get_lat=None):
73
+ if not isinstance(points, list): raise Exception('points must be list obj !!')
74
+ if len(points) < 3: raise Exception('points length must be gt 2 !!')
75
+ self.points = points
76
+ self.threshold = THIN_LEVEL.L2 if level is None else (getattr(THIN_LEVEL, "L{}".format(int(level))) if int(level) >= 1 else level)
77
+ log.debug("threshold is: {}".format(self.threshold))
78
+ self.is_json = isinstance(points[0], dict)
79
+ self.get_lng = get_lng
80
+ self.get_lat = get_lat
81
+ if self.is_json:
82
+ if not self.get_lng: self.get_lng = '$.lng'
83
+ if not self.get_lat: self.get_lat = '$.lat'
84
+ else:
85
+ if not self.get_lng: self.get_lng = '$.[0]'
86
+ if not self.get_lat: self.get_lat = '$.[1]'
87
+ log.debug("get_lng is: {}, get_lat is: {}".format(self.get_lng, self.get_lat))
88
+ self.lng_parser = parser.parse(self.get_lng)
89
+ self.lat_parser = parser.parse(self.get_lat)
90
+ log.debug("is_json is: {}".format(self.is_json))
91
+ self.ret_tpl = ret_tpl
92
+ log.debug("ret_tpl is: {}".format(self.ret_tpl))
93
+ self.data = [Point(self.lng_parser.find(p)[0].value, self.lat_parser.find(p)[0].value, p) for p in self.points]
94
+
95
+ def _sparse_points(self, points):
96
+ """
97
+ 点位压缩
98
+ :return: 稀疏后的点集
99
+ """
100
+ if len(points) < 3:
101
+ if not self.ret_tpl:
102
+ return [points[0].origin_data, points[-1].origin_data]
103
+ else:
104
+ if self.is_json:
105
+ return [cjson.loads(self.ret_tpl.format(**points[0].origin_data)), cjson.loads(self.ret_tpl.format(**points[-1].origin_data))]
106
+ else:
107
+ return [cjson.loads(self.ret_tpl.format(lng=points[0].lng, lat=points[0].lat)), cjson.loads(self.ret_tpl.format(lng=points[-1].lng, lat=points[-1].lat))]
108
+
109
+ xy_first = points[0] # 第一个点
110
+ xy_end = points[-1] # 最后一个点
111
+ a, b, c = _get_line_by_point(xy_first, xy_end) # 获取直线方程的 a, b, c 值
112
+ d_max = 0 # 记录点到直线的最大距离
113
+ split = 0 # 分割位置
114
+ for i in range(1, len(points) - 1):
115
+ d = _get_distance_from_point_to_line(a, b, c, points[i])
116
+ if d > d_max:
117
+ split = i
118
+ d_max = d
119
+ if d_max > self.threshold:
120
+ # 如果存在点到首位点连成直线的距离大于 max_distance 的, 即需要再次划分
121
+ child_left = self._sparse_points(points[:split + 1]) # 递归处理左边部分
122
+ child_right = self._sparse_points(points[split:]) # 递归处理右边部分
123
+ # 合并结果,避免重复
124
+ return child_left + child_right[1:]
125
+ else:
126
+ if not self.ret_tpl:
127
+ return [points[0].origin_data, points[-1].origin_data]
128
+ else:
129
+ if self.is_json:
130
+ return [cjson.loads(self.ret_tpl.format(**points[0].origin_data)), cjson.loads(self.ret_tpl.format(**points[-1].origin_data))]
131
+ else:
132
+ return [cjson.loads(self.ret_tpl.format(lng=points[0].lng, lat=points[0].lat)), cjson.loads(self.ret_tpl.format(lng=points[-1].lng, lat=points[-1].lat))]
133
+
134
+ def sparse_points(self):
135
+ return self._sparse_points(self.data)
136
+
@@ -0,0 +1,57 @@
1
+ import os
2
+ from urllib.parse import urlencode
3
+
4
+ from bottle import static_file, HTTPResponse
5
+
6
+ from ctools import sys_log, http_utils
7
+
8
+ log = sys_log.flog
9
+
10
+ """
11
+ 文件下载服务
12
+ """
13
+
14
+
15
+ def download(file_path: str, download_name:str=None):
16
+ """
17
+ 文件下载
18
+ :param file_path: 静态文件路径
19
+ :param download_name: 下载文件名
20
+ :return:
21
+ """
22
+ if os.path.exists(file_path):
23
+ root_path = os.path.split(file_path)[0]
24
+ file_name = os.path.split(file_path)[1]
25
+ download_filename = urlencode({'filename': download_name or file_name}).split("=")[-1] # 对文件名进行URL编码
26
+ response = static_file(file_name, root=root_path, download=True)
27
+ # 设置响应头,告诉浏览器这是一个文件下载
28
+ response.headers['Content-Type'] = 'application/octet-stream;charset=utf-8'
29
+ response.headers['Content-Disposition'] = f'attachment; filename={download_filename}'
30
+ log.debug(f"下载文件成功, file_path: {file_path}, file_name: {file_name}, download_name: {download_name}")
31
+ else:
32
+ response = None
33
+ log.info("下载文件失败, 此文件不存在, file_path: %s" % file_path)
34
+ return response
35
+
36
+
37
+ def download_bytes(file_bytes: bytes, download_name: str):
38
+ """
39
+ 文件下载
40
+ :param file_bytes: file_bytes
41
+ :param download_name: download_name
42
+ :return:
43
+ """
44
+ download_filename = urlencode({'filename': download_name}).split("=")[-1] # 对文件名进行URL编码
45
+ # 设置响应头,告诉浏览器这是一个文件下载
46
+ headers = {"Accept-Ranges": "bytes", "Content-Length": len(file_bytes),
47
+ 'Content-Type': 'application/octet-stream;charset=utf-8',
48
+ 'Content-Disposition': f'attachment; filename={download_filename}'}
49
+ return HTTPResponse(file_bytes, **headers)
50
+
51
+
52
+ def download_url(url: str, save_path: str):
53
+ content = http_utils.get(url)
54
+ if content:
55
+ with open(save_path, "wb") as f:
56
+ f.write(content)
57
+ return save_path
ctools/enums.py ADDED
@@ -0,0 +1,4 @@
1
+ def value_of(e, v):
2
+ for member_name, member in e.__members__.items():
3
+ if member.value == v:
4
+ return member
ctools/ex.py ADDED
@@ -0,0 +1,31 @@
1
+ import time
2
+ import traceback
3
+ from functools import wraps
4
+
5
+ # annotation
6
+ def exception_handler(fail_return, retry_num=0, delay=3, catch_e=Exception, print_exc=False):
7
+ def decorator(func):
8
+ @wraps(func)
9
+ def wrapper(*args, **kwargs):
10
+ try:
11
+ return func(*args, **kwargs)
12
+ except catch_e as e:
13
+ print(f"{func.__name__} runtime exception: {str(e)}")
14
+ if print_exc: traceback.print_exc()
15
+ nonlocal retry_num
16
+ renum = retry_num
17
+ if renum == 0:
18
+ return fail_return
19
+ else:
20
+ while renum > 0:
21
+ time.sleep(delay)
22
+ renum -= 1
23
+ try:
24
+ return func(*args, **kwargs)
25
+ except catch_e:
26
+ pass
27
+ return fail_return
28
+
29
+ return wrapper
30
+
31
+ return decorator
ctools/excelOpt.py ADDED
@@ -0,0 +1,36 @@
1
+ from openpyxl import load_workbook
2
+ from openpyxl.worksheet.datavalidation import DataValidation
3
+
4
+
5
+ class excelUtil:
6
+ wb = None
7
+ sourcePath = None
8
+ savePath = None
9
+
10
+ def __init__(self, path, save_path):
11
+ # 创建一个 Workbook 对象
12
+ self.wb = load_workbook(path)
13
+ # 在 Workbook 中创建一个 Worksheet 对象
14
+ self.ws = self.wb.active
15
+ self.sourcePath = path
16
+ self.savePath = save_path
17
+
18
+ def makeDropData(self, col, drop_data):
19
+ # 定义下拉框的数据
20
+ dropdown_items = drop_data
21
+ # 将下拉框数据转换成字符串
22
+ dropdown_items_str = ','.join(dropdown_items)
23
+ # 在第一列中添加下拉框
24
+ dropdown_col = col
25
+ dropdown_start_row = 2
26
+ dropdown_end_row = 200
27
+ # 配置下拉框参数
28
+ dropdown = DataValidation(type="list", formula1=f'"{dropdown_items_str}"', allow_blank=True)
29
+ # 添加下拉框到指定的单元格区域
30
+ dropdown_range = f"{dropdown_col}{dropdown_start_row}:{dropdown_col}{dropdown_end_row}"
31
+ self.ws.add_data_validation(dropdown)
32
+ dropdown.add(dropdown_range)
33
+
34
+ def save(self):
35
+ # 保存工作簿
36
+ self.wb.save(self.savePath)
ctools/html_soup.py ADDED
@@ -0,0 +1,35 @@
1
+ from bs4 import BeautifulSoup
2
+
3
+ from ctools.ex import exception_handler
4
+
5
+
6
+ @exception_handler(fail_return=['解析错误'], print_exc=True)
7
+ def table2list(html, include_header=True, recursive_find=True,
8
+ table_tag='table', table_class=None, table_attrs: dict = {},
9
+ row_tag='tr', row_class=None, row_attrs: dict = {},
10
+ header_cell_tag='th', header_cell_class=None, header_cell_attrs: dict = {},
11
+ cell_tag='td', cell_class=None, cell_attrs: dict = {}):
12
+ soup = BeautifulSoup(markup=html, features='html.parser')
13
+ if table_class:
14
+ table = soup.find(table_tag, class_=table_class, **table_attrs)
15
+ else:
16
+ table = soup.find(table_tag, **table_attrs)
17
+ if row_class:
18
+ all_row = table.find_all(row_tag, class_=row_class, recursive=recursive_find, **row_attrs)
19
+ else:
20
+ all_row = table.find_all(row_tag, recursive=recursive_find, **row_attrs)
21
+ rows = []
22
+ if include_header:
23
+ if header_cell_class:
24
+ header = [i.text for i in all_row[0].find_all(header_cell_tag, class_=header_cell_class, recursive=recursive_find, **header_cell_attrs)]
25
+ else:
26
+ header = [i.text for i in all_row[0].find_all(header_cell_tag, recursive=recursive_find, **header_cell_attrs)]
27
+ rows.append(header)
28
+ for tr in all_row[1 if include_header else 0:]:
29
+ if cell_class:
30
+ td = tr.find_all(cell_tag, class_=cell_class, recursive=recursive_find, **cell_attrs)
31
+ else:
32
+ td = tr.find_all(cell_tag, recursive=recursive_find, **cell_attrs)
33
+ row = [i.text for i in td]
34
+ rows.append(row)
35
+ return rows
ctools/http_utils.py ADDED
@@ -0,0 +1,24 @@
1
+ import requests
2
+
3
+
4
+ def get(url, params=None, headers=None):
5
+ result = ""
6
+ try:
7
+ response = requests.get(url, params=params, headers=headers, timeout=60, verify=False)
8
+ response.raise_for_status()
9
+ if response.status_code == 200:
10
+ result = response.content
11
+ except Exception as e:
12
+ print("GET请求异常:", e)
13
+ if isinstance(result, bytes): return result.decode('utf-8')
14
+ return result
15
+
16
+
17
+ def post(url, data=None, json=None, headers=None, files=None):
18
+ result = ""
19
+ response = requests.post(url, data=data, json=json, files=files, headers=headers, timeout=60, verify=False)
20
+ response.raise_for_status()
21
+ if response.status_code == 200:
22
+ result = response.content
23
+ if isinstance(result, bytes): return result.decode('utf-8')
24
+ return result
ctools/images_tools.py ADDED
@@ -0,0 +1,27 @@
1
+ from io import BytesIO
2
+
3
+ from PIL import Image
4
+
5
+
6
+ def get_size(image_path):
7
+ return Image.open(image_path).size
8
+
9
+ def change_color(image_path, area=None, rgb_color=None):
10
+ """
11
+ 修改图片指定区域颜色
12
+ :param image_path: 图片路径
13
+ :param area: 修改区域: (x1, y1, x2, y2)
14
+ :param rgb_color: 入盘颜色 (255, 0, 0)
15
+ :return:
16
+ """
17
+ with Image.open(image_path) as img:
18
+ if area:
19
+ pixels = img.load()
20
+ for x in range(area[0], area[2]):
21
+ for y in range(area[1], area[3]):
22
+ pixels[x, y] = rgb_color
23
+ img_bytes = BytesIO()
24
+ img.save(img_bytes, format='JPEG')
25
+ img_binary = img_bytes.getvalue()
26
+ return img_binary
27
+
ctools/imgDialog.py ADDED
@@ -0,0 +1,44 @@
1
+ import tkinter
2
+ import tkinter as tk
3
+ from io import BytesIO
4
+ from tkinter import ttk
5
+
6
+ import requests
7
+ from PIL import Image, ImageTk
8
+
9
+ def showImageTip(root, title, imagePath, tips):
10
+ # 创建一个Tk对象
11
+ if root:
12
+ window = root
13
+ else:
14
+ window = tk.Tk()
15
+ # 设置窗口大小和位置
16
+ win_width = 400
17
+ win_height = 480
18
+ screen_width = window.winfo_screenwidth()
19
+ screen_height = window.winfo_screenheight()
20
+ x = int((screen_width - win_width) / 2)
21
+ y = int((screen_height - win_height) / 2)
22
+ window.geometry("{}x{}+{}+{}".format(win_width, win_height, x, y))
23
+
24
+ # 设置窗口大小和标题
25
+ window.title(title)
26
+
27
+ # 创建一个Label控件用于显示图片
28
+ resp = requests.get(imagePath)
29
+ image = Image.open(BytesIO(resp.content)) # 替换你自己的图片路径
30
+ image = image.resize((400, 400))
31
+ photo = ImageTk.PhotoImage(image)
32
+ label1 = ttk.Label(window, image=photo)
33
+ label1.pack(side=tkinter.TOP)
34
+
35
+ # 创建一个Label控件用于显示提示文字
36
+ label2 = ttk.Label(window, text=tips, font=("Arial Bold", 16))
37
+ label2.config(anchor='center', justify='center')
38
+ label2.pack(side=tkinter.BOTTOM)
39
+ # 显示窗口
40
+ window.mainloop()
41
+
42
+
43
+ if __name__ == '__main__':
44
+ showImageTip(root=None, title='在线授权', imagePath='https://blog.gomyck.com/img/pay-img/wechatPay2Me.jpg', tips='{}\n授权已失效,请联系微信:\n{}'.format(123, 123))