gomyck-tools 1.4.1__py3-none-any.whl → 1.4.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 +21 -0
- ctools/ai/env_config.py +18 -1
- ctools/ai/llm_chat.py +8 -8
- ctools/ai/llm_client.py +26 -24
- ctools/ai/mcp/mcp_client.py +33 -17
- ctools/ai/tools/json_extract.py +3 -2
- ctools/ai/tools/quick_tools.py +71 -22
- ctools/ai/tools/tool_use_xml_parse.py +2 -1
- ctools/ai/tools/xml_extract.py +3 -0
- ctools/application.py +21 -19
- ctools/aspect.py +65 -0
- ctools/auto/browser_element.py +11 -3
- ctools/auto/plan_area.py +2 -2
- ctools/auto/pty_process.py +0 -1
- ctools/auto/screenshot.py +3 -4
- ctools/auto/win_canvas.py +10 -4
- ctools/auto/win_control.py +8 -4
- ctools/call.py +32 -47
- ctools/cdate.py +43 -2
- ctools/cid.py +6 -4
- ctools/cipher/aes_util.py +2 -2
- ctools/cipher/b64.py +2 -0
- ctools/cipher/czip.py +3 -1
- ctools/cipher/rsa.py +6 -1
- ctools/cipher/sign.py +1 -0
- ctools/cipher/sm_util.py +3 -0
- ctools/cjson.py +5 -0
- ctools/cron_lite.py +10 -4
- ctools/database/database.py +52 -22
- ctools/dict_wrapper.py +1 -0
- ctools/ex.py +4 -0
- ctools/geo/coord_trans.py +94 -94
- ctools/geo/douglas_rarefy.py +13 -9
- ctools/metrics.py +6 -0
- ctools/office/cword.py +7 -7
- ctools/office/word_fill.py +1 -4
- ctools/patch.py +88 -0
- ctools/path_info.py +29 -0
- ctools/pkg/__init__.py +4 -0
- ctools/pkg/dynamic_imp.py +38 -0
- ctools/pools/process_pool.py +6 -1
- ctools/pools/thread_pool.py +6 -2
- ctools/similar.py +3 -0
- ctools/stream/ckafka.py +11 -5
- ctools/stream/credis.py +37 -23
- ctools/stream/mqtt_utils.py +2 -2
- ctools/sys_info.py +8 -0
- ctools/sys_log.py +4 -1
- ctools/util/cftp.py +4 -2
- ctools/util/cklock.py +118 -0
- ctools/util/config_util.py +52 -0
- ctools/util/http_util.py +1 -0
- ctools/util/image_process.py +8 -0
- ctools/util/jb_cut.py +53 -0
- ctools/util/snow_id.py +3 -2
- ctools/web/__init__.py +2 -2
- ctools/web/aio_web_server.py +19 -9
- ctools/web/api_result.py +3 -2
- ctools/web/bottle_web_base.py +134 -70
- ctools/web/bottle_webserver.py +41 -35
- ctools/web/bottle_websocket.py +4 -0
- ctools/web/ctoken.py +81 -13
- ctools/web/download_util.py +1 -1
- ctools/web/params_util.py +4 -0
- ctools/web/upload_util.py +1 -1
- {gomyck_tools-1.4.1.dist-info → gomyck_tools-1.4.3.dist-info}/METADATA +9 -11
- gomyck_tools-1.4.3.dist-info/RECORD +88 -0
- ctools/auto/pacth.py +0 -74
- gomyck_tools-1.4.1.dist-info/RECORD +0 -82
- {gomyck_tools-1.4.1.dist-info → gomyck_tools-1.4.3.dist-info}/WHEEL +0 -0
- {gomyck_tools-1.4.1.dist-info → gomyck_tools-1.4.3.dist-info}/licenses/LICENSE +0 -0
- {gomyck_tools-1.4.1.dist-info → gomyck_tools-1.4.3.dist-info}/top_level.txt +0 -0
ctools/office/cword.py
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
from docx import Document
|
|
14
14
|
|
|
15
|
+
|
|
15
16
|
def merge_word_files(input_files: [], output_file: str):
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
merged_doc = Document()
|
|
18
|
+
for file in input_files:
|
|
19
|
+
doc = Document(file)
|
|
20
|
+
for element in doc.element.body:
|
|
21
|
+
merged_doc.element.body.append(element)
|
|
22
|
+
merged_doc.save(output_file)
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def read_word_file(input_file: str):
|
|
@@ -27,4 +28,3 @@ def read_word_file(input_file: str):
|
|
|
27
28
|
for paragraph in doc.paragraphs:
|
|
28
29
|
text.append(paragraph.text)
|
|
29
30
|
return "\n".join(text)
|
|
30
|
-
|
ctools/office/word_fill.py
CHANGED
|
@@ -469,6 +469,7 @@ def check_format(input_path: str):
|
|
|
469
469
|
res = False
|
|
470
470
|
return res
|
|
471
471
|
|
|
472
|
+
|
|
472
473
|
def excel_optimize(input_path: str):
|
|
473
474
|
"""
|
|
474
475
|
优化excel内容清除无法使用的隐患
|
|
@@ -492,7 +493,6 @@ def excel_optimize(input_path: str):
|
|
|
492
493
|
except Exception as e:
|
|
493
494
|
print("优化模板内容清除无法使用的隐患异常: %s" % e)
|
|
494
495
|
|
|
495
|
-
|
|
496
496
|
# 示例用法:从第1行到第5行、从第1列到第3列的区域复制到Word文档中
|
|
497
497
|
# excel_file = r'C:\Users\DELL\xxx/xxx-rpa/document\test-2024-04-01_09-05-26.xlsx' # Excel文件名
|
|
498
498
|
# excel_file_1 = r'E:\test\c.xlsx' # Excel文件名
|
|
@@ -557,6 +557,3 @@ def excel_optimize(input_path: str):
|
|
|
557
557
|
# print(check_format(input_path))
|
|
558
558
|
|
|
559
559
|
# from docxtpl import DocxTemplate, RichText
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
ctools/patch.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from sqlalchemy.sql import text
|
|
4
|
+
|
|
5
|
+
from ctools import path_info
|
|
6
|
+
from ctools.database import database
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
from ctools import patch
|
|
10
|
+
def xx():
|
|
11
|
+
print('hello world')
|
|
12
|
+
def xx1():
|
|
13
|
+
print('hello world1')
|
|
14
|
+
def xx2():
|
|
15
|
+
print('hello world2')
|
|
16
|
+
patch_funcs = {
|
|
17
|
+
'V1.0.2': xx,
|
|
18
|
+
'V1.0.3': xx1,
|
|
19
|
+
'V1.1.4': xx2
|
|
20
|
+
}
|
|
21
|
+
patch.sync_version("kwc", "V1.1.5", patch_funcs)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
class Patch:
|
|
25
|
+
|
|
26
|
+
def __init__(self, oldVersion, newVersion, patch_func: dict) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
if oldVersion:
|
|
29
|
+
self.oldV = version_to_int(oldVersion)
|
|
30
|
+
else:
|
|
31
|
+
self.oldV = 0
|
|
32
|
+
self.currentV = version_to_int(newVersion)
|
|
33
|
+
self.snapshot = '-snapshot' in newVersion or (oldVersion is not None and '-snapshot' in oldVersion)
|
|
34
|
+
self.patch_func = patch_func
|
|
35
|
+
|
|
36
|
+
def apply_patch(self):
|
|
37
|
+
patch_methods = [method for method in self.patch_func.keys() if method and (method.startswith('V') or method.startswith('v'))]
|
|
38
|
+
patch_methods.sort(key=lambda x: version_to_int(x))
|
|
39
|
+
max_method_name = patch_methods[-1]
|
|
40
|
+
exec_max_method = False
|
|
41
|
+
for method_name in patch_methods:
|
|
42
|
+
slVersion = version_to_int(method_name)
|
|
43
|
+
if self.currentV > slVersion >= self.oldV:
|
|
44
|
+
if max_method_name == method_name: exec_max_method = True
|
|
45
|
+
method = self.patch_func[method_name]
|
|
46
|
+
print('start exec patch {}'.format(method_name))
|
|
47
|
+
method()
|
|
48
|
+
print('patch {} update success'.format(method_name))
|
|
49
|
+
if self.snapshot and not exec_max_method:
|
|
50
|
+
print('start exec snapshot patch {}'.format(max_method_name))
|
|
51
|
+
method = self.patch_func[max_method_name]
|
|
52
|
+
method()
|
|
53
|
+
print('snapshot patch {} update success'.format(max_method_name))
|
|
54
|
+
|
|
55
|
+
def version_to_int(version):
|
|
56
|
+
return int(version.replace('V', '').replace('v', '').replace('.', '').replace('-snapshot', ''))
|
|
57
|
+
|
|
58
|
+
def run_sqls(sqls):
|
|
59
|
+
with database.get_session() as s:
|
|
60
|
+
for sql in sqls.split(";"):
|
|
61
|
+
try:
|
|
62
|
+
s.execute(text(sql.strip()))
|
|
63
|
+
s.commit()
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print('结构升级错误, 请检查!!! {}'.format(e.__cause__))
|
|
66
|
+
|
|
67
|
+
def sync_version(app_name, new_version, patch_func: dict):
|
|
68
|
+
destFilePath = os.path.join(path_info.get_user_work_path(".ck/{}".format(app_name), mkdir=True), "version")
|
|
69
|
+
if not os.path.exists(destFilePath):
|
|
70
|
+
patch = Patch(oldVersion=None, newVersion=new_version, patch_func=patch_func)
|
|
71
|
+
patch.apply_patch()
|
|
72
|
+
with open(destFilePath, 'w') as nv:
|
|
73
|
+
nv.write(new_version)
|
|
74
|
+
print('初始化安装, 版本信息为: {}'.format(new_version))
|
|
75
|
+
nv.flush()
|
|
76
|
+
else:
|
|
77
|
+
with open(destFilePath, 'r') as oldVersion:
|
|
78
|
+
oldV = oldVersion.readline()
|
|
79
|
+
print('本地版本信息为: {}, 程序版本信息为: {}'.format(oldV, new_version))
|
|
80
|
+
oldVersion.close()
|
|
81
|
+
if oldV >= new_version and '-snapshot' not in oldV: return
|
|
82
|
+
print('开始升级本地程序..')
|
|
83
|
+
patch = Patch(oldVersion=oldV, newVersion=new_version, patch_func=patch_func)
|
|
84
|
+
patch.apply_patch()
|
|
85
|
+
with open(destFilePath, 'w') as newVersion:
|
|
86
|
+
newVersion.write(new_version)
|
|
87
|
+
print('程序升级成功, 更新版本信息为: {}'.format(new_version))
|
|
88
|
+
newVersion.flush()
|
ctools/path_info.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
|
+
import importlib
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
from pathlib import Path
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def get_current_path(subPath: str = '') -> str:
|
|
@@ -67,3 +70,29 @@ def get_install_path(subPath: str = '') -> str:
|
|
|
67
70
|
:return: 安装包安装的路径
|
|
68
71
|
"""
|
|
69
72
|
return os.path.join(os.getcwd(), subPath)
|
|
73
|
+
|
|
74
|
+
def get_package_path(package: str | ModuleType) -> Path:
|
|
75
|
+
"""
|
|
76
|
+
获取包所在目录
|
|
77
|
+
:param package: 包名字符串或已导入模块
|
|
78
|
+
:return: 包目录的 Path 对象
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(package, str):
|
|
81
|
+
module = importlib.import_module(package)
|
|
82
|
+
elif isinstance(package, ModuleType):
|
|
83
|
+
module = package
|
|
84
|
+
else:
|
|
85
|
+
raise TypeError("package 必须是字符串或模块对象")
|
|
86
|
+
|
|
87
|
+
return Path(module.__file__).resolve().parent
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_resource_path(package: str | ModuleType, *subpaths: str) -> Path:
|
|
91
|
+
"""
|
|
92
|
+
获取包内资源文件路径(自动拼接子路径)
|
|
93
|
+
:param package: 包名或模块
|
|
94
|
+
:param subpaths: 可变参数,表示路径中的子目录或文件名
|
|
95
|
+
:return: 资源的绝对路径
|
|
96
|
+
"""
|
|
97
|
+
base = get_package_path(package)
|
|
98
|
+
return base.joinpath(*subpaths)
|
ctools/pkg/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: UTF-8 -*-
|
|
3
|
+
__author__ = 'haoyang'
|
|
4
|
+
__date__ = '2025/7/15 11:03'
|
|
5
|
+
|
|
6
|
+
import importlib
|
|
7
|
+
import pkgutil
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_modules_from_package(package, exclude=None, recursive=True):
|
|
11
|
+
"""
|
|
12
|
+
递归加载指定包下所有模块(不包括包本身)
|
|
13
|
+
|
|
14
|
+
:param package: 要加载模块的包对象(如 mypkg.plugins)
|
|
15
|
+
:param exclude: 排除的模块完整路径列表(如 ['mypkg.plugins.demo.mod2'])
|
|
16
|
+
:param recursive: 是否递归子包
|
|
17
|
+
:return: 模块列表(不含子包本身,只包含模块)
|
|
18
|
+
"""
|
|
19
|
+
if exclude is None: exclude = []
|
|
20
|
+
modules = []
|
|
21
|
+
for finder, modname, ispkg in pkgutil.iter_modules(package.__path__):
|
|
22
|
+
full_modname = f"{package.__name__}.{modname}"
|
|
23
|
+
if ispkg and recursive:
|
|
24
|
+
try:
|
|
25
|
+
subpkg = importlib.import_module(full_modname)
|
|
26
|
+
modules.extend(load_modules_from_package(subpkg, exclude, recursive))
|
|
27
|
+
except Exception as e:
|
|
28
|
+
print(f"递归子包 {full_modname} 失败:{e}")
|
|
29
|
+
continue
|
|
30
|
+
if full_modname in exclude:
|
|
31
|
+
continue
|
|
32
|
+
try:
|
|
33
|
+
module = importlib.import_module(full_modname)
|
|
34
|
+
modules.append(module)
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(f"!!!!!!加载模块 {full_modname} 失败:{e}!!!!!!")
|
|
37
|
+
continue
|
|
38
|
+
return modules
|
ctools/pools/process_pool.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# -*- coding: UTF-8 -*-
|
|
3
3
|
__author__ = 'haoyang'
|
|
4
4
|
__date__ = '2024/9/20 12:00'
|
|
5
|
+
|
|
5
6
|
import os
|
|
6
7
|
import time
|
|
7
8
|
from concurrent.futures import ProcessPoolExecutor
|
|
@@ -10,12 +11,14 @@ from ctools import call
|
|
|
10
11
|
|
|
11
12
|
_process_pool: ProcessPoolExecutor = None
|
|
12
13
|
|
|
14
|
+
|
|
13
15
|
@call.init
|
|
14
16
|
def init():
|
|
15
17
|
global _process_pool
|
|
16
|
-
max_workers = min(32, os.cpu_count())
|
|
18
|
+
max_workers = min(32, os.cpu_count()) # 最多 32 个
|
|
17
19
|
_process_pool = ProcessPoolExecutor(max_workers=max_workers)
|
|
18
20
|
|
|
21
|
+
|
|
19
22
|
def cb(f, callback):
|
|
20
23
|
exc = f.exception()
|
|
21
24
|
if exc:
|
|
@@ -24,6 +27,7 @@ def cb(f, callback):
|
|
|
24
27
|
else:
|
|
25
28
|
if callback: callback(f.result())
|
|
26
29
|
|
|
30
|
+
|
|
27
31
|
def submit(func, *args, callback=None, **kwargs):
|
|
28
32
|
if _process_pool is None: raise Exception('process pool is not init')
|
|
29
33
|
future = _process_pool.submit(func, *args, **kwargs)
|
|
@@ -31,6 +35,7 @@ def submit(func, *args, callback=None, **kwargs):
|
|
|
31
35
|
time.sleep(0.01)
|
|
32
36
|
return future
|
|
33
37
|
|
|
38
|
+
|
|
34
39
|
def shutdown(wait=True):
|
|
35
40
|
if _process_pool is None: raise Exception('process pool is not init')
|
|
36
41
|
_process_pool.shutdown(wait=wait)
|
ctools/pools/thread_pool.py
CHANGED
|
@@ -9,11 +9,13 @@ thread_local = threading.local()
|
|
|
9
9
|
|
|
10
10
|
_threadPool: ThreadPoolExecutor = None
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
@call.init
|
|
13
14
|
def init():
|
|
14
15
|
global _threadPool
|
|
15
|
-
max_work_num = min(32, (os.cpu_count() or 1) + 4)
|
|
16
|
-
_threadPool = ThreadPoolExecutor(max_workers=
|
|
16
|
+
max_work_num = min(32, (os.cpu_count() or 1) + 4) # 最多 32 个
|
|
17
|
+
_threadPool = ThreadPoolExecutor(max_workers=max_work_num, thread_name_prefix='ck-')
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
def cb(f, callback):
|
|
19
21
|
exc = f.exception()
|
|
@@ -23,6 +25,7 @@ def cb(f, callback):
|
|
|
23
25
|
else:
|
|
24
26
|
if callback: callback(f.result())
|
|
25
27
|
|
|
28
|
+
|
|
26
29
|
def submit(func, *args, callback=None, **kwargs):
|
|
27
30
|
if _threadPool is None: raise Exception('thread pool is not init')
|
|
28
31
|
future = _threadPool.submit(func, *args, **kwargs)
|
|
@@ -30,6 +33,7 @@ def submit(func, *args, callback=None, **kwargs):
|
|
|
30
33
|
time.sleep(0.01)
|
|
31
34
|
return future
|
|
32
35
|
|
|
36
|
+
|
|
33
37
|
def shutdown(wait=True):
|
|
34
38
|
if _threadPool is None: raise Exception('thread pool is not init')
|
|
35
39
|
_threadPool.shutdown(wait=wait)
|
ctools/similar.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from fuzzywuzzy import fuzz, process
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
def best_match(query: str, choices: list[str], score_cutoff: int = 70):
|
|
4
5
|
"""
|
|
5
6
|
获取最接近 query 的匹配项
|
|
@@ -7,6 +8,7 @@ def best_match(query: str, choices: list[str], score_cutoff: int = 70):
|
|
|
7
8
|
"""
|
|
8
9
|
return process.extractOne(query, choices, scorer=fuzz.ratio, score_cutoff=score_cutoff)
|
|
9
10
|
|
|
11
|
+
|
|
10
12
|
def all_matches(query: str, choices: list[str], limit: int = 5):
|
|
11
13
|
"""
|
|
12
14
|
获取多个相似匹配项
|
|
@@ -14,6 +16,7 @@ def all_matches(query: str, choices: list[str], limit: int = 5):
|
|
|
14
16
|
"""
|
|
15
17
|
return process.extract(query, choices, scorer=fuzz.ratio, limit=limit)
|
|
16
18
|
|
|
19
|
+
|
|
17
20
|
def is_similar(s1: str, s2: str, threshold: int = 85):
|
|
18
21
|
"""
|
|
19
22
|
判断两个字符串是否相似
|
ctools/stream/ckafka.py
CHANGED
|
@@ -41,6 +41,8 @@ consumer.receive_msg('jqxx', callBack=consumer_callback)
|
|
|
41
41
|
|
|
42
42
|
while True: time.sleep(1)
|
|
43
43
|
"""
|
|
44
|
+
|
|
45
|
+
|
|
44
46
|
class KafkaInstance:
|
|
45
47
|
def __init__(self, producer: KafkaProducer, consumer: KafkaConsumer):
|
|
46
48
|
self.start_consumer = False
|
|
@@ -52,7 +54,7 @@ class KafkaInstance:
|
|
|
52
54
|
# FutureRecordMetadata 可以添加回调, 来监听是否发送成功
|
|
53
55
|
# r.add_callback(lambda x: print(x))
|
|
54
56
|
# r.get() 可以同步获取结果
|
|
55
|
-
def send_msg(self, topic, msg, key: str=None, partition:int=None) -> FutureRecordMetadata:
|
|
57
|
+
def send_msg(self, topic, msg, key: str = None, partition: int = None) -> FutureRecordMetadata:
|
|
56
58
|
if self.producer is None: raise RuntimeError("Producer is not initialized")
|
|
57
59
|
if self.quited: return
|
|
58
60
|
return self.producer.send(topic=topic, value=msg, key=None if key is None else key.encode('utf-8'), partition=partition)
|
|
@@ -85,10 +87,14 @@ class KafkaInstance:
|
|
|
85
87
|
|
|
86
88
|
def shutdown(self):
|
|
87
89
|
self.quited = True
|
|
88
|
-
try:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
try:
|
|
91
|
+
self.consumer.close()
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
try:
|
|
95
|
+
self.producer.close()
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
92
98
|
|
|
93
99
|
|
|
94
100
|
class CKafka:
|
ctools/stream/credis.py
CHANGED
|
@@ -6,53 +6,64 @@ __date__ = '2025/2/14 11:09'
|
|
|
6
6
|
import redis
|
|
7
7
|
from redis import Redis
|
|
8
8
|
|
|
9
|
-
from ctools.pools import thread_pool
|
|
10
9
|
from ctools import cdate, cid
|
|
10
|
+
from ctools.pools import thread_pool
|
|
11
11
|
|
|
12
|
+
# 最后一次连接的redis
|
|
13
|
+
_ck_redis: Redis = None
|
|
14
|
+
|
|
15
|
+
def get_redis(): return _ck_redis
|
|
12
16
|
|
|
13
17
|
def init_pool(host: str = 'localhost', port: int = 6379, db: int = 0, password: str = None,
|
|
14
18
|
username: str = None, decode_responses: bool = True, max_connections: int = 75,
|
|
15
19
|
health_check_interval: int = 30, retry_count: int = 3) -> Redis:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
for attempt in range(retry_count):
|
|
21
|
+
try:
|
|
22
|
+
r: Redis = redis.StrictRedis(
|
|
23
|
+
host=host, port=port, db=db,
|
|
24
|
+
username=username, password=password,
|
|
25
|
+
retry_on_timeout=True,
|
|
26
|
+
max_connections=max_connections,
|
|
27
|
+
decode_responses=decode_responses,
|
|
28
|
+
health_check_interval=health_check_interval,
|
|
29
|
+
socket_connect_timeout=5,
|
|
30
|
+
socket_timeout=5
|
|
31
|
+
)
|
|
32
|
+
if r.ping():
|
|
33
|
+
print('CRedis connect {} {} success!'.format(host, port))
|
|
34
|
+
global _ck_redis
|
|
35
|
+
_ck_redis = r
|
|
36
|
+
return _ck_redis
|
|
37
|
+
except redis.ConnectionError as e:
|
|
38
|
+
if attempt == retry_count - 1:
|
|
39
|
+
raise Exception(f"Failed to connect to Redis after {retry_count} attempts: {str(e)}")
|
|
40
|
+
print(f"Connection attempt {attempt + 1} failed, retrying...")
|
|
41
|
+
|
|
35
42
|
|
|
36
43
|
def add_lock(r: Redis, key: str, timeout: int = 30):
|
|
37
44
|
if r.exists(key):
|
|
38
45
|
expire_time = r.get(key)
|
|
39
|
-
if cdate.time_diff_in_seconds(expire_time, cdate.get_date_time()) > 0:
|
|
40
|
-
return
|
|
46
|
+
if expire_time and cdate.time_diff_in_seconds(expire_time, cdate.get_date_time()) > 0:
|
|
47
|
+
return False
|
|
41
48
|
else:
|
|
42
49
|
r.delete(key)
|
|
43
50
|
return r.set(key, cdate.opt_time(seconds=timeout), nx=True, ex=timeout) is not None
|
|
44
51
|
|
|
52
|
+
|
|
45
53
|
def remove_lock(r: Redis, key: str):
|
|
46
54
|
r.delete(key)
|
|
47
55
|
|
|
56
|
+
|
|
48
57
|
def subscribe(r: Redis, channel_name, callback):
|
|
49
58
|
def thread_func():
|
|
50
59
|
pubsub = r.pubsub()
|
|
51
60
|
pubsub.subscribe(channel_name)
|
|
52
61
|
for message in pubsub.listen():
|
|
53
62
|
callback(message)
|
|
63
|
+
|
|
54
64
|
thread_pool.submit(thread_func)
|
|
55
65
|
|
|
66
|
+
|
|
56
67
|
def _process_pending_messages(r: Redis, stream_name: str, group_name: str, consumer_name: str, callback):
|
|
57
68
|
"""
|
|
58
69
|
处理未确认的消息
|
|
@@ -84,7 +95,8 @@ def _process_pending_messages(r: Redis, stream_name: str, group_name: str, consu
|
|
|
84
95
|
else:
|
|
85
96
|
print("No pending messages found.")
|
|
86
97
|
|
|
87
|
-
|
|
98
|
+
|
|
99
|
+
def stream_subscribe(r: Redis, stream_name, group_name, callback, from_id: str = '$', noack: bool = False):
|
|
88
100
|
def thread_func():
|
|
89
101
|
try:
|
|
90
102
|
# $表示从最后面消费, 0表示从开始消费
|
|
@@ -107,7 +119,9 @@ def stream_subscribe(r: Redis, stream_name, group_name, callback, from_id: str='
|
|
|
107
119
|
if res: r.xack(stream_name, group_name, message_id)
|
|
108
120
|
except Exception as e:
|
|
109
121
|
print('stream_subscribe error: ', e)
|
|
122
|
+
|
|
110
123
|
thread_pool.submit(thread_func)
|
|
111
124
|
|
|
125
|
+
|
|
112
126
|
def stream_publish(r: Redis, stream_name, message):
|
|
113
127
|
r.xadd(stream_name, message)
|
ctools/stream/mqtt_utils.py
CHANGED
|
@@ -5,17 +5,17 @@ from typing import Dict
|
|
|
5
5
|
from paho.mqtt import client as mqtt
|
|
6
6
|
from paho.mqtt.enums import CallbackAPIVersion
|
|
7
7
|
|
|
8
|
+
from ctools import cid, cdate
|
|
8
9
|
from ctools import sys_log, cjson, sys_info
|
|
9
10
|
from ctools.cipher import sm_util
|
|
10
11
|
from ctools.dict_wrapper import DictWrapper as DictToObj
|
|
11
12
|
from ctools.pools import thread_pool
|
|
12
|
-
from ctools import cid, cdate
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class MQTTEvent(Enum):
|
|
16
|
-
|
|
17
16
|
pass
|
|
18
17
|
|
|
18
|
+
|
|
19
19
|
'''
|
|
20
20
|
MQTT服务使用示例:
|
|
21
21
|
|
ctools/sys_info.py
CHANGED
|
@@ -7,13 +7,16 @@ from ctools.cipher import sm_util
|
|
|
7
7
|
|
|
8
8
|
MACHINE_KEY = b'EnrGffoorbFyTYoS0902YyT1Fhehj4InpbezIDUuPOg='
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
class MachineInfo:
|
|
11
12
|
machine_code = None
|
|
12
13
|
|
|
14
|
+
|
|
13
15
|
def get_user():
|
|
14
16
|
import getpass
|
|
15
17
|
return getpass.getuser()
|
|
16
18
|
|
|
19
|
+
|
|
17
20
|
def get_machine_code():
|
|
18
21
|
if MachineInfo.machine_code: return MachineInfo.machine_code
|
|
19
22
|
destPath = os.path.join(path_info.get_user_work_path(), "AppData/Local/machine")
|
|
@@ -74,6 +77,7 @@ def get_hash_machine_code(origin_code):
|
|
|
74
77
|
machine_code = hashlib.md5(code.encode()).hexdigest()
|
|
75
78
|
return machine_code.upper()
|
|
76
79
|
|
|
80
|
+
|
|
77
81
|
def get_public_ip():
|
|
78
82
|
import requests
|
|
79
83
|
try:
|
|
@@ -83,6 +87,7 @@ def get_public_ip():
|
|
|
83
87
|
except Exception as e:
|
|
84
88
|
return f"Failed to get public IP: {e}"
|
|
85
89
|
|
|
90
|
+
|
|
86
91
|
def get_local_ipv4():
|
|
87
92
|
import psutil
|
|
88
93
|
import socket
|
|
@@ -102,6 +107,7 @@ def get_local_ipv4():
|
|
|
102
107
|
s.close()
|
|
103
108
|
return ip
|
|
104
109
|
|
|
110
|
+
|
|
105
111
|
def get_remote_ipv4():
|
|
106
112
|
from bottle import request
|
|
107
113
|
try:
|
|
@@ -109,6 +115,7 @@ def get_remote_ipv4():
|
|
|
109
115
|
except:
|
|
110
116
|
return '127.0.0.1'
|
|
111
117
|
|
|
118
|
+
|
|
112
119
|
def get_proc_pid_by(cmdline):
|
|
113
120
|
import psutil
|
|
114
121
|
"""
|
|
@@ -127,6 +134,7 @@ def get_proc_pid_by(cmdline):
|
|
|
127
134
|
pass
|
|
128
135
|
return pid_list
|
|
129
136
|
|
|
137
|
+
|
|
130
138
|
def get_os_architecture():
|
|
131
139
|
if '64' in platform.machine():
|
|
132
140
|
return '64'
|
ctools/sys_log.py
CHANGED
|
@@ -48,6 +48,7 @@ def _console_log(log_level: int = logging.INFO) -> logging:
|
|
|
48
48
|
import io
|
|
49
49
|
import logging
|
|
50
50
|
|
|
51
|
+
|
|
51
52
|
class StreamToLogger(io.StringIO):
|
|
52
53
|
def __init__(self, logger: logging.Logger, level: int = logging.INFO):
|
|
53
54
|
super().__init__()
|
|
@@ -80,14 +81,16 @@ class StreamToLogger(io.StringIO):
|
|
|
80
81
|
def fileno(self):
|
|
81
82
|
return sys.__stdout__.fileno()
|
|
82
83
|
|
|
84
|
+
|
|
83
85
|
@call.init
|
|
84
86
|
def _init_log() -> None:
|
|
85
87
|
global flog, clog
|
|
86
|
-
flog = _file_log(
|
|
88
|
+
flog = _file_log(path_info.get_user_work_path(".ck/ck-py-log", mkdir=True), mixin=True, log_level=logging.DEBUG)
|
|
87
89
|
clog = _console_log()
|
|
88
90
|
sys.stdout = StreamToLogger(flog, level=logging.INFO)
|
|
89
91
|
sys.stderr = StreamToLogger(flog, level=logging.ERROR)
|
|
90
92
|
|
|
93
|
+
|
|
91
94
|
def setLevel(log_level=logging.INFO):
|
|
92
95
|
flog.setLevel(log_level)
|
|
93
96
|
clog.setLevel(log_level)
|
ctools/util/cftp.py
CHANGED
|
@@ -7,6 +7,7 @@ import io
|
|
|
7
7
|
import time
|
|
8
8
|
from ftplib import FTP, error_perm, error_temp
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
class CFTP:
|
|
11
12
|
"""
|
|
12
13
|
with open('xx/xx.md', 'rb') as file:
|
|
@@ -15,16 +16,17 @@ class CFTP:
|
|
|
15
16
|
ftp_password = 'x'
|
|
16
17
|
CFTP(ftp_host, ftp_username, ftp_password).upload_file_to_ftp('xx.md', file)
|
|
17
18
|
"""
|
|
19
|
+
|
|
18
20
|
def __init__(self, host, username, password, timeout=30, max_retries=3, retry_delay=5):
|
|
19
21
|
self.host = host
|
|
20
22
|
self.username = username
|
|
21
23
|
self.password = password
|
|
22
|
-
self.ftp:FTP = None
|
|
24
|
+
self.ftp: FTP = None
|
|
23
25
|
self.timeout = timeout
|
|
24
26
|
self.max_retries = max_retries
|
|
25
27
|
self.retry_delay = retry_delay
|
|
26
28
|
|
|
27
|
-
def upload_file_to_ftp(self, file_name, file:io.BytesIO, ftp_directory='/'):
|
|
29
|
+
def upload_file_to_ftp(self, file_name, file: io.BytesIO, ftp_directory='/'):
|
|
28
30
|
if not file_name: raise Exception('文件名不能为空')
|
|
29
31
|
if not file: raise Exception('文件不能为空')
|
|
30
32
|
for attempt in range(self.max_retries):
|