gomyck-tools 1.4.1__py3-none-any.whl → 1.4.2__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 +68 -21
- ctools/ai/tools/tool_use_xml_parse.py +2 -1
- ctools/ai/tools/xml_extract.py +3 -0
- ctools/application.py +19 -17
- 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 +18 -22
- ctools/cdate.py +43 -2
- ctools/cid.py +3 -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 +37 -17
- ctools/dict_wrapper.py +1 -0
- ctools/ex.py +1 -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/path_info.py +29 -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 +29 -21
- ctools/stream/mqtt_utils.py +2 -2
- ctools/sys_info.py +8 -0
- ctools/sys_log.py +3 -0
- ctools/util/cftp.py +4 -2
- ctools/util/http_util.py +1 -0
- ctools/util/image_process.py +1 -1
- 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 +15 -2
- ctools/web/bottle_webserver.py +14 -8
- ctools/web/bottle_websocket.py +4 -0
- ctools/web/ctoken.py +5 -1
- 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.2.dist-info}/METADATA +2 -1
- gomyck_tools-1.4.2.dist-info/RECORD +82 -0
- gomyck_tools-1.4.1.dist-info/RECORD +0 -82
- {gomyck_tools-1.4.1.dist-info → gomyck_tools-1.4.2.dist-info}/WHEEL +0 -0
- {gomyck_tools-1.4.1.dist-info → gomyck_tools-1.4.2.dist-info}/licenses/LICENSE +0 -0
- {gomyck_tools-1.4.1.dist-info → gomyck_tools-1.4.2.dist-info}/top_level.txt +0 -0
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,32 +6,33 @@ __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
12
|
|
13
13
|
def init_pool(host: str = 'localhost', port: int = 6379, db: int = 0, password: str = None,
|
14
14
|
username: str = None, decode_responses: bool = True, max_connections: int = 75,
|
15
15
|
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
|
-
|
16
|
+
for attempt in range(retry_count):
|
17
|
+
try:
|
18
|
+
r: Redis = redis.StrictRedis(
|
19
|
+
host=host, port=port, db=db,
|
20
|
+
username=username, password=password,
|
21
|
+
retry_on_timeout=True,
|
22
|
+
max_connections=max_connections,
|
23
|
+
decode_responses=decode_responses,
|
24
|
+
health_check_interval=health_check_interval,
|
25
|
+
socket_connect_timeout=5,
|
26
|
+
socket_timeout=5
|
27
|
+
)
|
28
|
+
if r.ping():
|
29
|
+
print('CRedis connect {} {} success!'.format(host, port))
|
30
|
+
return r
|
31
|
+
except redis.ConnectionError as e:
|
32
|
+
if attempt == retry_count - 1:
|
33
|
+
raise Exception(f"Failed to connect to Redis after {retry_count} attempts: {str(e)}")
|
34
|
+
print(f"Connection attempt {attempt + 1} failed, retrying...")
|
35
|
+
|
35
36
|
|
36
37
|
def add_lock(r: Redis, key: str, timeout: int = 30):
|
37
38
|
if r.exists(key):
|
@@ -42,17 +43,21 @@ def add_lock(r: Redis, key: str, timeout: int = 30):
|
|
42
43
|
r.delete(key)
|
43
44
|
return r.set(key, cdate.opt_time(seconds=timeout), nx=True, ex=timeout) is not None
|
44
45
|
|
46
|
+
|
45
47
|
def remove_lock(r: Redis, key: str):
|
46
48
|
r.delete(key)
|
47
49
|
|
50
|
+
|
48
51
|
def subscribe(r: Redis, channel_name, callback):
|
49
52
|
def thread_func():
|
50
53
|
pubsub = r.pubsub()
|
51
54
|
pubsub.subscribe(channel_name)
|
52
55
|
for message in pubsub.listen():
|
53
56
|
callback(message)
|
57
|
+
|
54
58
|
thread_pool.submit(thread_func)
|
55
59
|
|
60
|
+
|
56
61
|
def _process_pending_messages(r: Redis, stream_name: str, group_name: str, consumer_name: str, callback):
|
57
62
|
"""
|
58
63
|
处理未确认的消息
|
@@ -84,7 +89,8 @@ def _process_pending_messages(r: Redis, stream_name: str, group_name: str, consu
|
|
84
89
|
else:
|
85
90
|
print("No pending messages found.")
|
86
91
|
|
87
|
-
|
92
|
+
|
93
|
+
def stream_subscribe(r: Redis, stream_name, group_name, callback, from_id: str = '$', noack: bool = False):
|
88
94
|
def thread_func():
|
89
95
|
try:
|
90
96
|
# $表示从最后面消费, 0表示从开始消费
|
@@ -107,7 +113,9 @@ def stream_subscribe(r: Redis, stream_name, group_name, callback, from_id: str='
|
|
107
113
|
if res: r.xack(stream_name, group_name, message_id)
|
108
114
|
except Exception as e:
|
109
115
|
print('stream_subscribe error: ', e)
|
116
|
+
|
110
117
|
thread_pool.submit(thread_func)
|
111
118
|
|
119
|
+
|
112
120
|
def stream_publish(r: Redis, stream_name, message):
|
113
121
|
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,6 +81,7 @@ 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
|
@@ -88,6 +90,7 @@ def _init_log() -> None:
|
|
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):
|
ctools/util/http_util.py
CHANGED
ctools/util/image_process.py
CHANGED
@@ -6,6 +6,7 @@ from PIL import Image
|
|
6
6
|
def get_size(image_path):
|
7
7
|
return Image.open(image_path).size
|
8
8
|
|
9
|
+
|
9
10
|
def change_color(image_path, area=None, rgb_color=None):
|
10
11
|
"""
|
11
12
|
修改图片指定区域颜色
|
@@ -24,4 +25,3 @@ def change_color(image_path, area=None, rgb_color=None):
|
|
24
25
|
img.save(img_bytes, format='JPEG')
|
25
26
|
img_binary = img_bytes.getvalue()
|
26
27
|
return img_binary
|
27
|
-
|
ctools/util/snow_id.py
CHANGED
@@ -16,10 +16,12 @@ SEQUENCE_MASK = -1 ^ (-1 << SEQUENCE_BITS)
|
|
16
16
|
# Twitter元年时间戳
|
17
17
|
TWEPOCH = 1288834974657
|
18
18
|
|
19
|
+
|
19
20
|
class SnowId(object):
|
20
21
|
"""
|
21
22
|
用于生成IDs
|
22
23
|
"""
|
24
|
+
|
23
25
|
def __init__(self, datacenter_id=0, worker_id=0, sequence=0):
|
24
26
|
"""
|
25
27
|
初始化
|
@@ -53,8 +55,7 @@ class SnowId(object):
|
|
53
55
|
timestamp = self._gen_timestamp()
|
54
56
|
# 时钟回拨
|
55
57
|
if timestamp < self.last_timestamp:
|
56
|
-
|
57
|
-
raise
|
58
|
+
timestamp = self._til_next_millis(self.last_timestamp)
|
58
59
|
if timestamp == self.last_timestamp:
|
59
60
|
self.sequence = (self.sequence + 1) & SEQUENCE_MASK
|
60
61
|
if self.sequence == 0:
|
ctools/web/__init__.py
CHANGED
ctools/web/aio_web_server.py
CHANGED
@@ -5,12 +5,12 @@
|
|
5
5
|
__author__ = 'haoyang'
|
6
6
|
__date__ = '2025/5/30 09:54'
|
7
7
|
|
8
|
+
import asyncio
|
8
9
|
import sys
|
9
10
|
from pathlib import Path
|
10
11
|
from typing import Optional, Dict, Any
|
11
12
|
|
12
13
|
from aiohttp import web
|
13
|
-
|
14
14
|
from ctools import sys_info, cjson
|
15
15
|
from ctools.sys_log import flog as log
|
16
16
|
from ctools.web.api_result import R
|
@@ -30,13 +30,14 @@ async def response_wrapper_middleware(request, handler):
|
|
30
30
|
else:
|
31
31
|
return result
|
32
32
|
except web.HTTPException as http_exc:
|
33
|
-
|
33
|
+
raise http_exc
|
34
34
|
except Exception as e:
|
35
35
|
log.error(f"Error in response_wrapper_middleware: {e}", exc_info=True)
|
36
36
|
return web.json_response(text=R.error(str(e)), status=500, content_type='application/json')
|
37
37
|
|
38
|
+
|
38
39
|
class AioHttpServer:
|
39
|
-
def __init__(self, port: int = DEFAULT_PORT, app: Optional[web.Application] = None, routes: Optional[web.RouteTableDef] = None, async_func
|
40
|
+
def __init__(self, port: int = DEFAULT_PORT, app: Optional[web.Application] = None, routes: Optional[web.RouteTableDef] = None, async_func=None):
|
40
41
|
"""
|
41
42
|
Initialize the HTTP server.
|
42
43
|
|
@@ -142,26 +143,35 @@ class AioHttpServer:
|
|
142
143
|
"""Run the server."""
|
143
144
|
if self.async_func:
|
144
145
|
await self.async_func()
|
146
|
+
|
145
147
|
print(
|
146
148
|
'Server running at:\n'
|
147
149
|
f'\tLocal: http://localhost:{self.port}\n'
|
148
150
|
f'\tNetwork: http://{sys_info.get_local_ipv4()}:{self.port}',
|
149
151
|
file=sys.stderr
|
150
152
|
)
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
153
|
+
runner = None
|
154
|
+
try:
|
155
|
+
runner = web.AppRunner(self.app)
|
156
|
+
await runner.setup()
|
157
|
+
site = web.TCPSite(runner, host='0.0.0.0', port=self.port)
|
158
|
+
await site.start()
|
159
|
+
while True: await asyncio.sleep(3600)
|
160
|
+
except Exception as e:
|
161
|
+
print(f"Server failed to start: {e}", file=sys.stderr)
|
162
|
+
finally:
|
163
|
+
await runner.cleanup()
|
156
164
|
|
157
165
|
|
158
166
|
def init_routes() -> web.RouteTableDef:
|
159
167
|
return web.RouteTableDef()
|
160
168
|
|
161
|
-
|
169
|
+
|
170
|
+
def init_server(routes: Optional[web.RouteTableDef] = None, app: Optional[web.Application] = None, port: int = DEFAULT_PORT, async_func=None) -> AioHttpServer:
|
162
171
|
"""Initialize and return a new AioHttpServer instance."""
|
163
172
|
return AioHttpServer(port=port, app=app, routes=routes, async_func=async_func)
|
164
173
|
|
174
|
+
|
165
175
|
async def get_stream_resp(request, content_type: str = 'text/event-stream') -> web.StreamResponse:
|
166
176
|
resp = web.StreamResponse(
|
167
177
|
status=200,
|
ctools/web/api_result.py
CHANGED
@@ -13,6 +13,7 @@ class _ResEnum(object):
|
|
13
13
|
def __eq__(self, o: object) -> bool:
|
14
14
|
return self.code == o
|
15
15
|
|
16
|
+
|
16
17
|
class R(object):
|
17
18
|
class Code:
|
18
19
|
|
@@ -21,8 +22,8 @@ class R(object):
|
|
21
22
|
return _ResEnum(code, msg)
|
22
23
|
|
23
24
|
SUCCESS = _ResEnum(200, "成功")
|
24
|
-
FAIL
|
25
|
-
ERROR
|
25
|
+
FAIL = _ResEnum(400, "失败")
|
26
|
+
ERROR = _ResEnum(500, "异常")
|
26
27
|
|
27
28
|
def __init__(self, code: int, message: str, data=""):
|
28
29
|
self.code = code
|
ctools/web/bottle_web_base.py
CHANGED
@@ -13,12 +13,14 @@ from ctools.web.api_result import R
|
|
13
13
|
bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 * 50
|
14
14
|
func_has_params = {}
|
15
15
|
|
16
|
+
|
16
17
|
class GlobalState:
|
17
18
|
lock = threading.Lock()
|
18
19
|
withOutLoginURI = [
|
19
20
|
'/',
|
20
21
|
'/index',
|
21
|
-
'/login'
|
22
|
+
'/login',
|
23
|
+
'/favicon.ico',
|
22
24
|
]
|
23
25
|
allowRemoteCallURI = [
|
24
26
|
|
@@ -26,6 +28,7 @@ class GlobalState:
|
|
26
28
|
token = {}
|
27
29
|
interceptors = []
|
28
30
|
|
31
|
+
|
29
32
|
def init_app(context_path=None):
|
30
33
|
app = Bottle()
|
31
34
|
app.context_path = context_path
|
@@ -73,6 +76,7 @@ def init_app(context_path=None):
|
|
73
76
|
app.install(params_resolve)
|
74
77
|
return app
|
75
78
|
|
79
|
+
|
76
80
|
def enable_cors():
|
77
81
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
78
82
|
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
@@ -80,14 +84,17 @@ def enable_cors():
|
|
80
84
|
response.headers['Access-Control-Allow-Headers'] = request_headers if request_headers else ''
|
81
85
|
response.headers['Access-Control-Expose-Headers'] = '*'
|
82
86
|
|
87
|
+
|
83
88
|
# annotation
|
84
89
|
def before_intercept(order=0):
|
85
90
|
def decorator(func):
|
86
91
|
log.info("add before interceptor: {}".format(func.__name__))
|
87
92
|
GlobalState.interceptors.append({'order': order, 'func': func})
|
88
93
|
GlobalState.interceptors = sorted(GlobalState.interceptors, key=lambda x: x['order'])
|
94
|
+
|
89
95
|
return decorator
|
90
96
|
|
97
|
+
|
91
98
|
# annotation
|
92
99
|
def rule(key):
|
93
100
|
def return_func(func):
|
@@ -97,9 +104,12 @@ def rule(key):
|
|
97
104
|
# log.error("系统未授权! {} {}".format(request.fullpath, '当前请求的模块未授权!请联系管理员!'))
|
98
105
|
# return R.error(resp=R.Code.cus_code(9999, "系统未授权! {}".format('当前请求的模块未授权!请联系管理员!')))
|
99
106
|
return func(*args, **kwargs)
|
107
|
+
|
100
108
|
return decorated
|
109
|
+
|
101
110
|
return return_func
|
102
111
|
|
112
|
+
|
103
113
|
# annotation or plugins, has auto install, don't need to call
|
104
114
|
def params_resolve(func):
|
105
115
|
@wraps(func)
|
@@ -149,13 +159,16 @@ def params_resolve(func):
|
|
149
159
|
return func(params=dict_wrapper, *args, **kwargs)
|
150
160
|
else:
|
151
161
|
return func(*args, **kwargs)
|
162
|
+
|
152
163
|
return decorated
|
153
164
|
|
165
|
+
|
154
166
|
class PageInfo:
|
155
167
|
def __init__(self, page_size, page_index):
|
156
168
|
self.page_size = page_size
|
157
169
|
self.page_index = page_index
|
158
170
|
|
171
|
+
|
159
172
|
# 通用的鉴权方法
|
160
173
|
def common_auth_verify(aes_key):
|
161
174
|
if request.path.startswith('/static') or request.path in GlobalState.withOutLoginURI:
|
@@ -166,4 +179,4 @@ def common_auth_verify(aes_key):
|
|
166
179
|
payload = ctoken.get_payload(auth_token, aes_key)
|
167
180
|
if payload:
|
168
181
|
return R.ok(to_json_str=False)
|
169
|
-
return R.error(resp=R.Code.cus_code(401, "
|
182
|
+
return R.error(resp=R.Code.cus_code(401, "请登录!"), to_json_str=False)
|
ctools/web/bottle_webserver.py
CHANGED
@@ -22,8 +22,8 @@ def get_ws_modules():
|
|
22
22
|
"""
|
23
23
|
|
24
24
|
"""
|
25
|
-
from ctools import bottle_web_base, ctoken, bottle_webserver
|
26
|
-
from ctools.api_result import R
|
25
|
+
from ctools.web import bottle_web_base, ctoken, bottle_webserver
|
26
|
+
from ctools.web.api_result import R
|
27
27
|
|
28
28
|
secret_key = "xxx"
|
29
29
|
app = bottle_web_base.init_app('子模块写 context_path, 主模块就不用写任何东西')
|
@@ -42,14 +42,16 @@ def login(params):
|
|
42
42
|
def demo(params):
|
43
43
|
print(params)
|
44
44
|
|
45
|
-
|
46
|
-
main_app.
|
47
|
-
main_app.
|
48
|
-
main_app.
|
45
|
+
if __name__ == '__main__':
|
46
|
+
main_app = bottle_webserver.init_bottle() # 这里可以传 APP 当做主模块, 但是 context_path 就不好使了, 上下文必须是 /
|
47
|
+
main_app.mount(app.context_path, app)
|
48
|
+
main_app.set_index(r'index.html')
|
49
|
+
main_app.run()
|
49
50
|
"""
|
50
51
|
|
51
52
|
_default_port = 8888
|
52
53
|
|
54
|
+
|
53
55
|
class CBottle:
|
54
56
|
|
55
57
|
def __init__(self, bottle: Bottle, port=_default_port, quiet=False):
|
@@ -98,7 +100,7 @@ class CBottle:
|
|
98
100
|
|
99
101
|
def run(self):
|
100
102
|
http_server = WSGIRefServer(port=self.port)
|
101
|
-
print('Click the link below to open the service homepage %s' % '\n \t\t http://localhost:%s \n \t\t http://%s:%s' %
|
103
|
+
print('Click the link below to open the service homepage %s' % '\n \t\t http://localhost:%s \n \t\t http://%s:%s' % (self.port, sys_info.get_local_ipv4(), self.port), file=sys.stderr)
|
102
104
|
self.bottle.run(server=http_server, quiet=self.quiet)
|
103
105
|
|
104
106
|
def set_index(self, filename='index.html', root='./', is_tpl=False, redirect_url=None, **kwargs):
|
@@ -118,16 +120,20 @@ class CBottle:
|
|
118
120
|
if not context_path: return
|
119
121
|
self.bottle.mount(context_path, app, **kwargs)
|
120
122
|
|
121
|
-
|
123
|
+
|
124
|
+
def init_bottle(app: Bottle = None, port=_default_port, quiet=False) -> CBottle:
|
122
125
|
bottle = app or Bottle()
|
123
126
|
return CBottle(bottle, port, quiet)
|
124
127
|
|
128
|
+
|
125
129
|
class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
|
126
130
|
daemon_threads = True
|
127
131
|
|
132
|
+
|
128
133
|
class CustomWSGIHandler(WSGIRequestHandler):
|
129
134
|
def log_request(*args, **kw): pass
|
130
135
|
|
136
|
+
|
131
137
|
class WSGIRefServer(ServerAdapter):
|
132
138
|
|
133
139
|
def __init__(self, host='0.0.0.0', port=_default_port):
|
ctools/web/bottle_websocket.py
CHANGED
@@ -33,6 +33,7 @@ socket_app.run()
|
|
33
33
|
|
34
34
|
_default_port = 8887
|
35
35
|
|
36
|
+
|
36
37
|
class CBottle:
|
37
38
|
|
38
39
|
def __init__(self, bottle: Bottle, port=_default_port, quiet=False):
|
@@ -47,10 +48,12 @@ class CBottle:
|
|
47
48
|
def mount(self, context_path, app):
|
48
49
|
self.bottle.mount(context_path, app)
|
49
50
|
|
51
|
+
|
50
52
|
def init_bottle(port=_default_port, quiet=False) -> CBottle:
|
51
53
|
bottle = Bottle()
|
52
54
|
return CBottle(bottle, port, quiet)
|
53
55
|
|
56
|
+
|
54
57
|
class CustomWebSocketHandler(WebSocketHandler):
|
55
58
|
def log_request(self):
|
56
59
|
if '101' not in str(self.status):
|
@@ -60,6 +63,7 @@ class CustomWebSocketHandler(WebSocketHandler):
|
|
60
63
|
return
|
61
64
|
self.logger.info(log_msg)
|
62
65
|
|
66
|
+
|
63
67
|
class WebSocketServer(ServerAdapter):
|
64
68
|
|
65
69
|
def __init__(self, host='0.0.0.0', port=_default_port):
|
ctools/web/ctoken.py
CHANGED
@@ -12,10 +12,12 @@ from ctools.dict_wrapper import DictWrapper
|
|
12
12
|
|
13
13
|
token_header = 'Authorization'
|
14
14
|
|
15
|
-
|
15
|
+
|
16
|
+
def gen_token(payload: {}, secret_key, expired: int = 3600) -> str:
|
16
17
|
payload.update({'exp': time.time() + expired})
|
17
18
|
return jwt.encode(payload, secret_key, algorithm='HS256')
|
18
19
|
|
20
|
+
|
19
21
|
def get_payload(token, secret_key):
|
20
22
|
try:
|
21
23
|
payload = jwt.decode(token, secret_key, algorithms=['HS256'])
|
@@ -23,9 +25,11 @@ def get_payload(token, secret_key):
|
|
23
25
|
except Exception as e:
|
24
26
|
return None
|
25
27
|
|
28
|
+
|
26
29
|
def get_token(key):
|
27
30
|
return get_payload(request.get_header(token_header), key)
|
28
31
|
|
32
|
+
|
29
33
|
def is_valid(key):
|
30
34
|
return get_payload(request.get_header(token_header), key) is not None
|
31
35
|
|
ctools/web/download_util.py
CHANGED
ctools/web/params_util.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
__author__ = 'haoyang'
|
4
4
|
__date__ = '2025/6/11 09:35'
|
5
5
|
|
6
|
+
|
6
7
|
def is_list(v: str):
|
7
8
|
try:
|
8
9
|
list(v)
|
@@ -13,6 +14,7 @@ def is_list(v: str):
|
|
13
14
|
except Exception:
|
14
15
|
return False
|
15
16
|
|
17
|
+
|
16
18
|
def is_digit(v: str):
|
17
19
|
try:
|
18
20
|
float(v)
|
@@ -20,12 +22,14 @@ def is_digit(v: str):
|
|
20
22
|
except Exception:
|
21
23
|
return False
|
22
24
|
|
25
|
+
|
23
26
|
def is_bool(v: str):
|
24
27
|
if v in ["False", "True"]:
|
25
28
|
return True
|
26
29
|
else:
|
27
30
|
return False
|
28
31
|
|
32
|
+
|
29
33
|
def dict_to_params(obj: dict):
|
30
34
|
params = ""
|
31
35
|
for k, v in obj.items():
|