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.
Files changed (72) hide show
  1. ctools/__init__.py +21 -0
  2. ctools/ai/env_config.py +18 -1
  3. ctools/ai/llm_chat.py +8 -8
  4. ctools/ai/llm_client.py +26 -24
  5. ctools/ai/mcp/mcp_client.py +33 -17
  6. ctools/ai/tools/json_extract.py +3 -2
  7. ctools/ai/tools/quick_tools.py +71 -22
  8. ctools/ai/tools/tool_use_xml_parse.py +2 -1
  9. ctools/ai/tools/xml_extract.py +3 -0
  10. ctools/application.py +21 -19
  11. ctools/aspect.py +65 -0
  12. ctools/auto/browser_element.py +11 -3
  13. ctools/auto/plan_area.py +2 -2
  14. ctools/auto/pty_process.py +0 -1
  15. ctools/auto/screenshot.py +3 -4
  16. ctools/auto/win_canvas.py +10 -4
  17. ctools/auto/win_control.py +8 -4
  18. ctools/call.py +32 -47
  19. ctools/cdate.py +43 -2
  20. ctools/cid.py +6 -4
  21. ctools/cipher/aes_util.py +2 -2
  22. ctools/cipher/b64.py +2 -0
  23. ctools/cipher/czip.py +3 -1
  24. ctools/cipher/rsa.py +6 -1
  25. ctools/cipher/sign.py +1 -0
  26. ctools/cipher/sm_util.py +3 -0
  27. ctools/cjson.py +5 -0
  28. ctools/cron_lite.py +10 -4
  29. ctools/database/database.py +52 -22
  30. ctools/dict_wrapper.py +1 -0
  31. ctools/ex.py +4 -0
  32. ctools/geo/coord_trans.py +94 -94
  33. ctools/geo/douglas_rarefy.py +13 -9
  34. ctools/metrics.py +6 -0
  35. ctools/office/cword.py +7 -7
  36. ctools/office/word_fill.py +1 -4
  37. ctools/patch.py +88 -0
  38. ctools/path_info.py +29 -0
  39. ctools/pkg/__init__.py +4 -0
  40. ctools/pkg/dynamic_imp.py +38 -0
  41. ctools/pools/process_pool.py +6 -1
  42. ctools/pools/thread_pool.py +6 -2
  43. ctools/similar.py +3 -0
  44. ctools/stream/ckafka.py +11 -5
  45. ctools/stream/credis.py +37 -23
  46. ctools/stream/mqtt_utils.py +2 -2
  47. ctools/sys_info.py +8 -0
  48. ctools/sys_log.py +4 -1
  49. ctools/util/cftp.py +4 -2
  50. ctools/util/cklock.py +118 -0
  51. ctools/util/config_util.py +52 -0
  52. ctools/util/http_util.py +1 -0
  53. ctools/util/image_process.py +8 -0
  54. ctools/util/jb_cut.py +53 -0
  55. ctools/util/snow_id.py +3 -2
  56. ctools/web/__init__.py +2 -2
  57. ctools/web/aio_web_server.py +19 -9
  58. ctools/web/api_result.py +3 -2
  59. ctools/web/bottle_web_base.py +134 -70
  60. ctools/web/bottle_webserver.py +41 -35
  61. ctools/web/bottle_websocket.py +4 -0
  62. ctools/web/ctoken.py +81 -13
  63. ctools/web/download_util.py +1 -1
  64. ctools/web/params_util.py +4 -0
  65. ctools/web/upload_util.py +1 -1
  66. {gomyck_tools-1.4.1.dist-info → gomyck_tools-1.4.3.dist-info}/METADATA +9 -11
  67. gomyck_tools-1.4.3.dist-info/RECORD +88 -0
  68. ctools/auto/pacth.py +0 -74
  69. gomyck_tools-1.4.1.dist-info/RECORD +0 -82
  70. {gomyck_tools-1.4.1.dist-info → gomyck_tools-1.4.3.dist-info}/WHEEL +0 -0
  71. {gomyck_tools-1.4.1.dist-info → gomyck_tools-1.4.3.dist-info}/licenses/LICENSE +0 -0
  72. {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
@@ -1,5 +1,6 @@
1
1
  import requests
2
2
 
3
+
3
4
  def get(url, params=None, headers=None):
4
5
  result = ""
5
6
  try:
@@ -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
- print('clock is moving backwards. Rejecting requests until {}'.format(self.last_timestamp))
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
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: UTF-8 -*-
3
- __author__='haoyang'
4
- __date__='2025/6/11 09:12'
3
+ __author__ = 'haoyang'
4
+ __date__ = '2025/6/11 09:12'
@@ -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
- raise http_exc
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 = None):
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
- await web._run_app(
152
- self.app,
153
- port=self.port,
154
- host='0.0.0.0'
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
- def init_server(routes: Optional[web.RouteTableDef] = None, app: Optional[web.Application] = None, port: int = DEFAULT_PORT, async_func = None) -> AioHttpServer:
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 = _ResEnum(400, "失败")
25
- ERROR = _ResEnum(500, "异常")
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
@@ -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
- allowRemoteCallURI = [
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 init_app(context_path=None):
30
- app = Bottle()
31
- app.context_path = context_path
32
-
33
- @app.hook('before_request')
34
- def before_request():
35
- for interceptor in GlobalState.interceptors:
36
- res: R = interceptor['func']()
37
- if res.code != 200: bottle.abort(res.code, res.message)
38
-
39
- @app.error(401)
40
- def unauthorized(error):
41
- response.status = 401
42
- log.error("系统未授权: {} {} {}".format(error.body, request.method, request.fullpath))
43
- return R.error(resp=R.Code.cus_code(401, "系统未授权! {}".format(error.body)))
44
-
45
- @app.error(403)
46
- def unauthorized(error):
47
- response.status = 403
48
- log.error("访问受限: {} {} {}".format(error.body, request.method, request.fullpath))
49
- return R.error(resp=R.Code.cus_code(403, "访问受限: {}".format(error.body)))
50
-
51
- @app.error(404)
52
- def not_found(error):
53
- response.status = 404
54
- log.error("404 not found : {} {} {}".format(error.body, request.method, request.fullpath))
55
- return R.error(resp=R.Code.cus_code(404, "资源未找到: {}".format(error.body)))
56
-
57
- @app.error(405)
58
- def method_not_allow(error):
59
- response.status = 405
60
- log.error("请求方法错误: {} {} {}".format(error.status_line, request.method, request.fullpath))
61
- return R.error(resp=R.Code.cus_code(405, '请求方法错误: {}'.format(error.status_line)))
62
-
63
- @app.error(500)
64
- def internal_error(error):
65
- response.status = 500
66
- log.error("系统发生错误: {} {} {}".format(error.body, request.method, request.fullpath))
67
- return R.error(msg='系统发生错误: {}'.format(error.exception))
68
-
69
- @app.hook('after_request')
70
- def after_request():
71
- enable_cors()
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
- # if GlobalState.licenseInfo is not None and key not in GlobalState.licenseInfo.access_module:
97
- # log.error("系统未授权! {} {}".format(request.fullpath, '当前请求的模块未授权!请联系管理员!'))
98
- # return R.error(resp=R.Code.cus_code(9999, "系统未授权! {}".format('当前请求的模块未授权!请联系管理员!')))
99
- return func(*args, **kwargs)
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(aes_key):
161
- if request.path.startswith('/static') or request.path in GlobalState.withOutLoginURI:
162
- return R.ok(to_json_str=False)
163
- auth_token = request.get_header('Authorization')
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)