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.
- ctools/__init__.py +0 -0
- ctools/aes_tools.py +35 -0
- ctools/api_result.py +55 -0
- ctools/application.py +386 -0
- ctools/b64.py +7 -0
- ctools/bashPath.py +13 -0
- ctools/bottle_web_base.py +169 -0
- ctools/bottle_webserver.py +143 -0
- ctools/bottle_websocket.py +75 -0
- ctools/browser_element_tools.py +314 -0
- ctools/call.py +71 -0
- ctools/cdebug.py +143 -0
- ctools/cftp.py +74 -0
- ctools/cjson.py +54 -0
- ctools/ckafka.py +159 -0
- ctools/compile_tools.py +18 -0
- ctools/console.py +55 -0
- ctools/coord_trans.py +127 -0
- ctools/credis.py +111 -0
- ctools/cron_lite.py +245 -0
- ctools/ctoken.py +34 -0
- ctools/cword.py +30 -0
- ctools/czip.py +130 -0
- ctools/database.py +185 -0
- ctools/date_utils.py +43 -0
- ctools/dict_wrapper.py +20 -0
- ctools/douglas_rarefy.py +136 -0
- ctools/download_tools.py +57 -0
- ctools/enums.py +4 -0
- ctools/ex.py +31 -0
- ctools/excelOpt.py +36 -0
- ctools/html_soup.py +35 -0
- ctools/http_utils.py +24 -0
- ctools/images_tools.py +27 -0
- ctools/imgDialog.py +44 -0
- ctools/metrics.py +131 -0
- ctools/mqtt_utils.py +289 -0
- ctools/obj.py +20 -0
- ctools/pacth.py +74 -0
- ctools/plan_area_tools.py +97 -0
- ctools/process_pool.py +36 -0
- ctools/pty_tools.py +72 -0
- ctools/resource_bundle_tools.py +121 -0
- ctools/rsa.py +70 -0
- ctools/screenshot_tools.py +127 -0
- ctools/sign.py +20 -0
- ctools/sm_tools.py +49 -0
- ctools/snow_id.py +76 -0
- ctools/str_diff.py +20 -0
- ctools/string_tools.py +85 -0
- ctools/sys_info.py +157 -0
- ctools/sys_log.py +89 -0
- ctools/thread_pool.py +35 -0
- ctools/upload_tools.py +40 -0
- ctools/win_canvas.py +83 -0
- ctools/win_control.py +106 -0
- ctools/word_fill.py +562 -0
- ctools/word_fill_entity.py +46 -0
- ctools/work_path.py +69 -0
- {gomyck_tools-1.3.1.dist-info → gomyck_tools-1.3.3.dist-info}/METADATA +4 -2
- gomyck_tools-1.3.3.dist-info/RECORD +64 -0
- {gomyck_tools-1.3.1.dist-info → gomyck_tools-1.3.3.dist-info}/WHEEL +1 -1
- gomyck_tools-1.3.3.dist-info/licenses/LICENSE +13 -0
- gomyck_tools-1.3.3.dist-info/top_level.txt +1 -0
- gomyck_tools-1.3.1.dist-info/RECORD +0 -4
- 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]
|
ctools/douglas_rarefy.py
ADDED
@@ -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
|
+
|
ctools/download_tools.py
ADDED
@@ -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
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))
|