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/util/cklock.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: UTF-8 -*-
|
|
3
|
+
__author__ = 'haoyang'
|
|
4
|
+
__date__ = '2025/7/18 15:46'
|
|
5
|
+
|
|
6
|
+
import contextvars
|
|
7
|
+
import threading
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from functools import wraps
|
|
10
|
+
|
|
11
|
+
from ctools.stream.credis import get_redis, add_lock, remove_lock
|
|
12
|
+
from ctools.web import ctoken
|
|
13
|
+
from ctools.web.api_result import R
|
|
14
|
+
|
|
15
|
+
# 全局锁容器
|
|
16
|
+
_lock_dict = {}
|
|
17
|
+
_lock_dict_lock = threading.Lock()
|
|
18
|
+
|
|
19
|
+
def try_acquire_lock(key: str) -> bool:
|
|
20
|
+
with _lock_dict_lock:
|
|
21
|
+
if key not in _lock_dict:
|
|
22
|
+
_lock_dict[key] = threading.Lock()
|
|
23
|
+
return _lock_dict[key].acquire(blocking=False)
|
|
24
|
+
|
|
25
|
+
def try_acquire_lock_block(key: str):
|
|
26
|
+
with _lock_dict_lock:
|
|
27
|
+
if key not in _lock_dict:
|
|
28
|
+
_lock_dict[key] = threading.Lock()
|
|
29
|
+
_lock = _lock_dict[key]
|
|
30
|
+
_lock.acquire() # 这里是阻塞的
|
|
31
|
+
|
|
32
|
+
def release_lock(key: str):
|
|
33
|
+
with _lock_dict_lock:
|
|
34
|
+
_lock = _lock_dict.get(key)
|
|
35
|
+
if _lock and _lock.locked():
|
|
36
|
+
_lock.release()
|
|
37
|
+
if _lock and not _lock.locked():
|
|
38
|
+
_lock_dict.pop(key, None)
|
|
39
|
+
|
|
40
|
+
@contextmanager
|
|
41
|
+
def try_lock(key: str="sys_lock", block=False):
|
|
42
|
+
if not block:
|
|
43
|
+
acquired = try_acquire_lock(key)
|
|
44
|
+
try:
|
|
45
|
+
yield acquired
|
|
46
|
+
finally:
|
|
47
|
+
if acquired:
|
|
48
|
+
release_lock(key)
|
|
49
|
+
else:
|
|
50
|
+
try_acquire_lock_block(key)
|
|
51
|
+
try:
|
|
52
|
+
yield
|
|
53
|
+
finally:
|
|
54
|
+
release_lock(key)
|
|
55
|
+
|
|
56
|
+
#annotation
|
|
57
|
+
"""
|
|
58
|
+
@lock("params.attr")
|
|
59
|
+
"""
|
|
60
|
+
# 上下文保存锁key集合
|
|
61
|
+
current_locks = contextvars.ContextVar("current_locks", default=set())
|
|
62
|
+
|
|
63
|
+
def lock(lock_attrs=None):
|
|
64
|
+
def decorator(func):
|
|
65
|
+
@wraps(func)
|
|
66
|
+
def wrapper(*args, **kwargs):
|
|
67
|
+
lock_key = ""
|
|
68
|
+
nonlocal lock_attrs
|
|
69
|
+
user_level_lock = False
|
|
70
|
+
|
|
71
|
+
if not lock_attrs:
|
|
72
|
+
user_id = ctoken.get_user_id()
|
|
73
|
+
if user_id:
|
|
74
|
+
user_level_lock = True
|
|
75
|
+
lock_key = f"USER_ID_LOCK_{user_id}"
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError("请设置 lock_attrs 或使用 token!")
|
|
78
|
+
|
|
79
|
+
if not user_level_lock:
|
|
80
|
+
if isinstance(lock_attrs, str): lock_attrs = [lock_attrs]
|
|
81
|
+
try:
|
|
82
|
+
for attr in lock_attrs:
|
|
83
|
+
parts = attr.split(".")
|
|
84
|
+
if len(parts) != 2:
|
|
85
|
+
raise ValueError(f"lock_attr: {attr} 格式错误")
|
|
86
|
+
obj = kwargs.get(parts[0]) or args[0]
|
|
87
|
+
if obj is None:
|
|
88
|
+
raise ValueError(f"参数 {parts[0]} 不存在")
|
|
89
|
+
lock_key += f"_{getattr(obj, parts[1], None)}"
|
|
90
|
+
except Exception as e:
|
|
91
|
+
raise ValueError(f"生成锁键失败: {e}")
|
|
92
|
+
|
|
93
|
+
lock_set = current_locks.get()
|
|
94
|
+
if lock_key in lock_set:
|
|
95
|
+
return func(*args, **kwargs)
|
|
96
|
+
token = current_locks.set(lock_set | {lock_key})
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
if not get_redis():
|
|
100
|
+
with try_lock(lock_key) as locked:
|
|
101
|
+
if not locked:
|
|
102
|
+
return R.error("操作过于频繁, 请稍后再试")
|
|
103
|
+
return func(*args, **kwargs)
|
|
104
|
+
else:
|
|
105
|
+
locked = add_lock(get_redis(), lock_key)
|
|
106
|
+
try:
|
|
107
|
+
if locked:
|
|
108
|
+
return func(*args, **kwargs)
|
|
109
|
+
else:
|
|
110
|
+
return R.error("操作过于频繁, 请稍后再试")
|
|
111
|
+
finally:
|
|
112
|
+
if locked:
|
|
113
|
+
remove_lock(get_redis(), lock_key)
|
|
114
|
+
finally:
|
|
115
|
+
current_locks.reset(token)
|
|
116
|
+
return wrapper
|
|
117
|
+
return decorator
|
|
118
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: UTF-8 -*-
|
|
3
|
+
__author__ = 'haoyang'
|
|
4
|
+
__date__ = '2025/7/16 14:19'
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
config = load_config("application.ini")
|
|
8
|
+
print(config)
|
|
9
|
+
print(config.base.app_name)
|
|
10
|
+
print(config.base.version)
|
|
11
|
+
"""
|
|
12
|
+
from configparser import ConfigParser
|
|
13
|
+
|
|
14
|
+
cache = {}
|
|
15
|
+
|
|
16
|
+
class AttrNoneNamespace:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
pass
|
|
19
|
+
def __setattr__(self, key, value):
|
|
20
|
+
super().__setattr__(key, value)
|
|
21
|
+
def __getattr__(self, item):
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
def _convert_value(value: str):
|
|
25
|
+
val = value.strip()
|
|
26
|
+
if val.lower() in ('true', 'yes', 'on'):
|
|
27
|
+
return True
|
|
28
|
+
if val.lower() in ('false', 'no', 'off'):
|
|
29
|
+
return False
|
|
30
|
+
if val.isdigit():
|
|
31
|
+
return int(val)
|
|
32
|
+
try:
|
|
33
|
+
return float(val)
|
|
34
|
+
except ValueError:
|
|
35
|
+
return val
|
|
36
|
+
|
|
37
|
+
def _config_to_object(config: ConfigParser):
|
|
38
|
+
result = AttrNoneNamespace()
|
|
39
|
+
for section in config.sections():
|
|
40
|
+
section_obj = AttrNoneNamespace()
|
|
41
|
+
for key, value in config.items(section):
|
|
42
|
+
setattr(section_obj, key, _convert_value(value))
|
|
43
|
+
setattr(result, section, section_obj)
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
def load_config(file_path):
|
|
47
|
+
if file_path in cache: return cache[file_path]
|
|
48
|
+
config = ConfigParser()
|
|
49
|
+
config.read(file_path)
|
|
50
|
+
cf = _config_to_object(config)
|
|
51
|
+
cache[file_path] = cf
|
|
52
|
+
return cf
|
ctools/util/http_util.py
CHANGED
ctools/util/image_process.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
from io import BytesIO
|
|
2
3
|
|
|
3
4
|
from PIL import Image
|
|
@@ -6,6 +7,7 @@ from PIL import Image
|
|
|
6
7
|
def get_size(image_path):
|
|
7
8
|
return Image.open(image_path).size
|
|
8
9
|
|
|
10
|
+
|
|
9
11
|
def change_color(image_path, area=None, rgb_color=None):
|
|
10
12
|
"""
|
|
11
13
|
修改图片指定区域颜色
|
|
@@ -25,3 +27,9 @@ def change_color(image_path, area=None, rgb_color=None):
|
|
|
25
27
|
img_binary = img_bytes.getvalue()
|
|
26
28
|
return img_binary
|
|
27
29
|
|
|
30
|
+
|
|
31
|
+
def img2b64(img: Image, fmt="PNG"):
|
|
32
|
+
buf = BytesIO()
|
|
33
|
+
img.save(buf, format=fmt.upper())
|
|
34
|
+
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
|
35
|
+
|
ctools/util/jb_cut.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: UTF-8 -*-
|
|
3
|
+
__author__ = 'haoyang'
|
|
4
|
+
__date__ = '2025/7/15 13:08'
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import jieba
|
|
9
|
+
from jieba import posseg as pseg
|
|
10
|
+
|
|
11
|
+
jqfx_exclude = ('ul', 'uj', 'uz', 'a', 'c', 'm', 'f', 'ad', 'an', 'r', 'q', 'u', 't', 'd', 'p', 'x')
|
|
12
|
+
|
|
13
|
+
def add_dict(dic_word: str = None, dic_path: str = None):
|
|
14
|
+
"""
|
|
15
|
+
添加自定义词库(自定义词库添加之后, 如果不适用全模式切词, 有时也不好使, 因为权重没有默认的高)
|
|
16
|
+
:param dic_word: 一个单词
|
|
17
|
+
:param dic_path: 单词表文件地址
|
|
18
|
+
单词表文件格式:
|
|
19
|
+
单词 词频 标签
|
|
20
|
+
单词1 3 i
|
|
21
|
+
单词2 3 i
|
|
22
|
+
单词3 3 i
|
|
23
|
+
"""
|
|
24
|
+
if dic_word: jieba.add_word(dic_word)
|
|
25
|
+
if dic_path: jieba.load_userdict(dic_path)
|
|
26
|
+
|
|
27
|
+
def add_freq(word: ()):
|
|
28
|
+
"""
|
|
29
|
+
添加词频
|
|
30
|
+
:param word: 一个单词
|
|
31
|
+
"""
|
|
32
|
+
jieba.suggest_freq(word, True)
|
|
33
|
+
|
|
34
|
+
def get_declare_type_word(word: str, word_flag=(), use_paddle=False, exclude_flag="x"):
|
|
35
|
+
#import paddle
|
|
36
|
+
#paddle.enable_static()
|
|
37
|
+
#sys.stdout = sys.__stdout__
|
|
38
|
+
#sys.stderr = sys.__stderr__
|
|
39
|
+
"""
|
|
40
|
+
获取声明类型的单词
|
|
41
|
+
:param word: 单词
|
|
42
|
+
:param word_flag: 标签
|
|
43
|
+
:param use_paddle: 是否使用 paddle
|
|
44
|
+
:param exclude_flag: 排除的标签
|
|
45
|
+
:return: 筛选之后的单词(数组)
|
|
46
|
+
"""
|
|
47
|
+
# linux 可以开放下面的语句, 并发执行分词, windows 不支持
|
|
48
|
+
# if platform.system() == 'Linux': jieba.enable_parallel(4)
|
|
49
|
+
ret = []
|
|
50
|
+
for w, flag in pseg.cut(word, use_paddle=use_paddle):
|
|
51
|
+
if (flag in word_flag or len(word_flag) == 0) and flag not in exclude_flag:
|
|
52
|
+
ret.append((w, flag))
|
|
53
|
+
return ret
|
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
|
@@ -5,74 +5,103 @@ from functools import wraps
|
|
|
5
5
|
import bottle
|
|
6
6
|
from bottle import response, Bottle, request
|
|
7
7
|
|
|
8
|
+
from ctools import call
|
|
8
9
|
from ctools.dict_wrapper import DictWrapper
|
|
9
10
|
from ctools.sys_log import flog as log
|
|
11
|
+
from ctools.util.cklock import try_lock
|
|
10
12
|
from ctools.web import ctoken
|
|
11
13
|
from ctools.web.api_result import R
|
|
12
14
|
|
|
13
15
|
bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 * 50
|
|
14
16
|
func_has_params = {}
|
|
17
|
+
app_cache = {}
|
|
15
18
|
|
|
16
19
|
class GlobalState:
|
|
17
20
|
lock = threading.Lock()
|
|
18
|
-
withOutLoginURI =
|
|
21
|
+
withOutLoginURI = {
|
|
19
22
|
'/',
|
|
20
23
|
'/index',
|
|
21
|
-
'/login'
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
'/login',
|
|
25
|
+
'/favicon.ico',
|
|
26
|
+
}
|
|
27
|
+
allowRemoteCallURI = set()
|
|
28
|
+
auth_ignore_func = set()
|
|
26
29
|
token = {}
|
|
27
30
|
interceptors = []
|
|
28
31
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
32
|
+
def join_path(*parts):
|
|
33
|
+
"""拼接 url"""
|
|
34
|
+
cleaned_parts = [p.strip('/') for p in parts if p and p.strip('/')]
|
|
35
|
+
return '/' + '/'.join(cleaned_parts)
|
|
36
|
+
|
|
37
|
+
@call.once
|
|
38
|
+
def cache_white_list(app):
|
|
39
|
+
"""缓存白名单"""
|
|
40
|
+
for route in app.routes:
|
|
41
|
+
real_func = route.config.get('mountpoint.target')
|
|
42
|
+
if not real_func: continue
|
|
43
|
+
for method, routes in real_func.router.static.items():
|
|
44
|
+
for path, tuples in routes.items():
|
|
45
|
+
req_func = inspect.getmodule(tuples[0].callback).__name__ + "." + tuples[0].callback.__name__
|
|
46
|
+
if req_func in GlobalState.auth_ignore_func:
|
|
47
|
+
print("add white list: {}".format(join_path(app.context_path, real_func.context_path, path)))
|
|
48
|
+
GlobalState.withOutLoginURI.add(join_path(app.context_path, real_func.context_path, path))
|
|
49
|
+
|
|
50
|
+
def init_app(context_path="/", main_app=False):
|
|
51
|
+
with try_lock(block=True):
|
|
52
|
+
cache_app = app_cache.get(context_path)
|
|
53
|
+
if cache_app: return cache_app
|
|
54
|
+
app = Bottle()
|
|
55
|
+
app_cache[context_path] = app
|
|
56
|
+
app.context_path = context_path if context_path else "/"
|
|
57
|
+
|
|
58
|
+
def init_main_app():
|
|
59
|
+
@app.hook('before_request')
|
|
60
|
+
def before_request():
|
|
61
|
+
if request.path.startswith('/static') or request.path in GlobalState.withOutLoginURI: return
|
|
62
|
+
for interceptor in GlobalState.interceptors:
|
|
63
|
+
res: R = interceptor['func']()
|
|
64
|
+
if res.code != 200: bottle.abort(res.code, res.message)
|
|
65
|
+
|
|
66
|
+
@app.error(401)
|
|
67
|
+
def unauthorized(error):
|
|
68
|
+
response.status = 401
|
|
69
|
+
log.error("系统未授权: {} {} {}".format(error.body, request.method, request.fullpath))
|
|
70
|
+
return R.error(resp=R.Code.cus_code(401, "系统未授权! {}".format(error.body)))
|
|
71
|
+
|
|
72
|
+
@app.error(403)
|
|
73
|
+
def unauthorized(error):
|
|
74
|
+
response.status = 403
|
|
75
|
+
log.error("访问受限: {} {} {}".format(error.body, request.method, request.fullpath))
|
|
76
|
+
return R.error(resp=R.Code.cus_code(403, "访问受限: {}".format(error.body)))
|
|
77
|
+
|
|
78
|
+
@app.error(404)
|
|
79
|
+
def not_found(error):
|
|
80
|
+
response.status = 404
|
|
81
|
+
log.error("404 not found : {} {} {}".format(error.body, request.method, request.fullpath))
|
|
82
|
+
return R.error(resp=R.Code.cus_code(404, "资源未找到: {}".format(error.body)))
|
|
83
|
+
|
|
84
|
+
@app.error(405)
|
|
85
|
+
def method_not_allow(error):
|
|
86
|
+
response.status = 405
|
|
87
|
+
log.error("请求方法错误: {} {} {}".format(error.status_line, request.method, request.fullpath))
|
|
88
|
+
return R.error(resp=R.Code.cus_code(405, '请求方法错误: {}'.format(error.status_line)))
|
|
89
|
+
|
|
90
|
+
@app.error(500)
|
|
91
|
+
def internal_error(error):
|
|
92
|
+
response.status = 500
|
|
93
|
+
log.error("系统发生错误: {} {} {}".format(error.body, request.method, request.fullpath))
|
|
94
|
+
return R.error(msg='系统发生错误: {}'.format(error.exception))
|
|
95
|
+
|
|
96
|
+
@app.hook('after_request')
|
|
97
|
+
def after_request():
|
|
98
|
+
enable_cors()
|
|
99
|
+
|
|
100
|
+
if main_app: init_main_app()
|
|
73
101
|
app.install(params_resolve)
|
|
74
102
|
return app
|
|
75
103
|
|
|
104
|
+
|
|
76
105
|
def enable_cors():
|
|
77
106
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
|
78
107
|
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
|
@@ -80,26 +109,50 @@ def enable_cors():
|
|
|
80
109
|
response.headers['Access-Control-Allow-Headers'] = request_headers if request_headers else ''
|
|
81
110
|
response.headers['Access-Control-Expose-Headers'] = '*'
|
|
82
111
|
|
|
112
|
+
|
|
83
113
|
# annotation
|
|
84
114
|
def before_intercept(order=0):
|
|
85
115
|
def decorator(func):
|
|
116
|
+
for interceptor in GlobalState.interceptors:
|
|
117
|
+
if interceptor['func'].__name__ == func.__name__:
|
|
118
|
+
log.info("duplicate interceptor: {}".format(func.__name__))
|
|
119
|
+
return
|
|
86
120
|
log.info("add before interceptor: {}".format(func.__name__))
|
|
87
121
|
GlobalState.interceptors.append({'order': order, 'func': func})
|
|
88
122
|
GlobalState.interceptors = sorted(GlobalState.interceptors, key=lambda x: x['order'])
|
|
123
|
+
|
|
89
124
|
return decorator
|
|
90
125
|
|
|
126
|
+
# annotation
|
|
127
|
+
def auth_ignore(func):
|
|
128
|
+
"""忽略登录验证的接口"""
|
|
129
|
+
ignore_req_func = inspect.getmodule(func).__name__ + "." + func.__name__
|
|
130
|
+
if ignore_req_func in GlobalState.auth_ignore_func: raise Exception("duplicate ignore func: {}".format(ignore_req_func))
|
|
131
|
+
GlobalState.auth_ignore_func.add(ignore_req_func)
|
|
132
|
+
@wraps(func)
|
|
133
|
+
def decorated(*args, **kwargs):
|
|
134
|
+
return func(*args, **kwargs)
|
|
135
|
+
return decorated
|
|
136
|
+
|
|
91
137
|
# annotation
|
|
92
138
|
def rule(key):
|
|
93
139
|
def return_func(func):
|
|
94
140
|
@wraps(func)
|
|
95
141
|
def decorated(*args, **kwargs):
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
142
|
+
rules = ctoken.get_token_attr("rules") or []
|
|
143
|
+
if _match_rule_by_prefix(key, rules):
|
|
144
|
+
return func(*args, **kwargs)
|
|
145
|
+
else:
|
|
146
|
+
return R.error("权限不足, 请联系管理员: {}".format(key))
|
|
100
147
|
return decorated
|
|
101
148
|
return return_func
|
|
102
149
|
|
|
150
|
+
def _match_rule_by_prefix(key, rules):
|
|
151
|
+
for r in rules:
|
|
152
|
+
if key.startswith(r):
|
|
153
|
+
return True
|
|
154
|
+
return False
|
|
155
|
+
|
|
103
156
|
# annotation or plugins, has auto install, don't need to call
|
|
104
157
|
def params_resolve(func):
|
|
105
158
|
@wraps(func)
|
|
@@ -114,56 +167,67 @@ def params_resolve(func):
|
|
|
114
167
|
return func(*args, **kwargs)
|
|
115
168
|
else:
|
|
116
169
|
func_has_params[request.fullpath] = True
|
|
117
|
-
if request.method == 'GET':
|
|
170
|
+
if request.method == 'GET' or request.method == 'DELETE':
|
|
118
171
|
queryStr = request.query.decode('utf-8')
|
|
119
172
|
page_info = PageInfo(
|
|
120
173
|
page_size=10 if request.headers.get('page_size') is None else int(request.headers.get('page_size')),
|
|
121
174
|
page_index=1 if request.headers.get('page_index') is None else int(request.headers.get('page_index'))
|
|
122
175
|
)
|
|
176
|
+
queryStr = auto_exchange(func, queryStr)
|
|
123
177
|
queryStr.page_info = page_info
|
|
124
178
|
return func(params=queryStr, *args, **kwargs)
|
|
125
|
-
elif request.method == 'POST':
|
|
179
|
+
elif request.method == 'POST' or request.method == 'PUT':
|
|
126
180
|
query_params = request.query.decode('utf-8')
|
|
127
181
|
content_type = request.get_header('content-type')
|
|
128
182
|
if content_type == 'application/json':
|
|
129
183
|
params = request.json or {}
|
|
130
184
|
dict_wrapper = DictWrapper(params)
|
|
131
185
|
dict_wrapper.update(query_params.dict)
|
|
132
|
-
return func(params=dict_wrapper, *args, **kwargs)
|
|
186
|
+
return func(params=auto_exchange(func, dict_wrapper), *args, **kwargs)
|
|
133
187
|
elif 'multipart/form-data' in content_type:
|
|
134
188
|
form_data = request.forms.decode()
|
|
135
189
|
form_files = request.files.decode()
|
|
136
190
|
dict_wrapper = DictWrapper(form_data)
|
|
137
191
|
dict_wrapper.update(query_params.dict)
|
|
138
192
|
dict_wrapper.files = form_files
|
|
139
|
-
return func(params=dict_wrapper, *args, **kwargs)
|
|
193
|
+
return func(params=auto_exchange(func, dict_wrapper), *args, **kwargs)
|
|
140
194
|
elif 'application/x-www-form-urlencoded' in content_type:
|
|
141
195
|
params = request.forms.decode()
|
|
142
196
|
dict_wrapper = DictWrapper(params.dict)
|
|
143
197
|
dict_wrapper.update(query_params.dict)
|
|
144
|
-
return func(params=dict_wrapper, *args, **kwargs)
|
|
198
|
+
return func(params=auto_exchange(func, dict_wrapper), *args, **kwargs)
|
|
145
199
|
elif 'text/plain' in content_type:
|
|
146
200
|
params = request.body.read().decode('utf-8')
|
|
147
201
|
dict_wrapper = DictWrapper({'body': params})
|
|
148
202
|
dict_wrapper.update(query_params.dict)
|
|
149
|
-
return func(params=dict_wrapper, *args, **kwargs)
|
|
203
|
+
return func(params=auto_exchange(func, dict_wrapper), *args, **kwargs)
|
|
150
204
|
else:
|
|
151
205
|
return func(*args, **kwargs)
|
|
206
|
+
|
|
152
207
|
return decorated
|
|
153
208
|
|
|
209
|
+
# 自动转换参数类型
|
|
210
|
+
def auto_exchange(func, dict_wrapper):
|
|
211
|
+
model_class = func.__annotations__.get('params')
|
|
212
|
+
if model_class:
|
|
213
|
+
try:
|
|
214
|
+
model_instance = model_class(**dict_wrapper)
|
|
215
|
+
return model_instance
|
|
216
|
+
except Exception as e:
|
|
217
|
+
log.exception(e)
|
|
218
|
+
return dict_wrapper
|
|
219
|
+
else:
|
|
220
|
+
return dict_wrapper
|
|
221
|
+
|
|
222
|
+
# 分页信息对象
|
|
154
223
|
class PageInfo:
|
|
155
224
|
def __init__(self, page_size, page_index):
|
|
156
225
|
self.page_size = page_size
|
|
157
226
|
self.page_index = page_index
|
|
158
227
|
|
|
228
|
+
|
|
159
229
|
# 通用的鉴权方法
|
|
160
|
-
def common_auth_verify(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if auth_token is None:
|
|
165
|
-
auth_token = request.get_cookie('Authorization')
|
|
166
|
-
payload = ctoken.get_payload(auth_token, aes_key)
|
|
167
|
-
if payload:
|
|
168
|
-
return R.ok(to_json_str=False)
|
|
169
|
-
return R.error(resp=R.Code.cus_code(401, "未授权"), to_json_str=False)
|
|
230
|
+
def common_auth_verify() -> R:
|
|
231
|
+
valid = ctoken.is_valid()
|
|
232
|
+
if valid: return R.ok(to_json_str=False)
|
|
233
|
+
return R.error(resp=R.Code.cus_code(401, "请登录!"), to_json_str=False)
|