gomyck-tools 1.0.0__py3-none-any.whl → 1.4.7__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 (107) hide show
  1. ctools/__init__.py +21 -0
  2. ctools/ai/__init__.py +4 -0
  3. ctools/ai/llm_chat.py +184 -0
  4. ctools/ai/llm_client.py +163 -0
  5. ctools/ai/llm_exception.py +17 -0
  6. ctools/ai/mcp/__init__.py +4 -0
  7. ctools/ai/mcp/mcp_client.py +326 -0
  8. ctools/ai/tools/__init__.py +4 -0
  9. ctools/ai/tools/json_extract.py +202 -0
  10. ctools/ai/tools/quick_tools.py +149 -0
  11. ctools/ai/tools/think_process.py +11 -0
  12. ctools/ai/tools/tool_use_xml_parse.py +40 -0
  13. ctools/ai/tools/xml_extract.py +15 -0
  14. ctools/application.py +50 -47
  15. ctools/aspect.py +65 -0
  16. ctools/auto/__init__.py +4 -0
  17. ctools/{browser_element_tools.py → auto/browser_element.py} +18 -8
  18. ctools/{plan_area_tools.py → auto/plan_area.py} +5 -7
  19. ctools/{pty_tools.py → auto/pty_process.py} +6 -3
  20. ctools/{resource_bundle_tools.py → auto/resource_bundle.py} +4 -4
  21. ctools/{screenshot_tools.py → auto/screenshot.py} +7 -6
  22. ctools/{win_canvas.py → auto/win_canvas.py} +10 -4
  23. ctools/{win_control.py → auto/win_control.py} +14 -5
  24. ctools/call.py +34 -49
  25. ctools/cdate.py +84 -0
  26. ctools/cdebug.py +146 -0
  27. ctools/cid.py +20 -0
  28. ctools/cipher/__init__.py +4 -0
  29. ctools/{aes_tools.py → cipher/aes_util.py} +10 -0
  30. ctools/{b64.py → cipher/b64.py} +2 -0
  31. ctools/cipher/czip.py +133 -0
  32. ctools/cipher/rsa.py +75 -0
  33. ctools/{sign.py → cipher/sign.py} +2 -1
  34. ctools/{sm_tools.py → cipher/sm_util.py} +20 -4
  35. ctools/cjson.py +10 -10
  36. ctools/cron_lite.py +109 -97
  37. ctools/database/__init__.py +4 -0
  38. ctools/{database.py → database/database.py} +93 -26
  39. ctools/dict_wrapper.py +21 -0
  40. ctools/ex.py +4 -0
  41. ctools/geo/__init__.py +4 -0
  42. ctools/geo/coord_trans.py +127 -0
  43. ctools/geo/douglas_rarefy.py +139 -0
  44. ctools/metrics.py +56 -63
  45. ctools/office/__init__.py +4 -0
  46. ctools/office/cword.py +30 -0
  47. ctools/{word_fill.py → office/word_fill.py} +3 -6
  48. ctools/patch.py +88 -0
  49. ctools/{work_path.py → path_info.py} +34 -2
  50. ctools/pkg/__init__.py +4 -0
  51. ctools/pkg/dynamic_imp.py +38 -0
  52. ctools/pools/__init__.py +4 -0
  53. ctools/pools/process_pool.py +41 -0
  54. ctools/{thread_pool.py → pools/thread_pool.py} +13 -4
  55. ctools/similar.py +25 -0
  56. ctools/stream/__init__.py +4 -0
  57. ctools/stream/ckafka.py +164 -0
  58. ctools/stream/credis.py +127 -0
  59. ctools/{mqtt_utils.py → stream/mqtt_utils.py} +20 -12
  60. ctools/sys_info.py +36 -13
  61. ctools/sys_log.py +46 -27
  62. ctools/util/__init__.py +4 -0
  63. ctools/util/cftp.py +76 -0
  64. ctools/util/cklock.py +118 -0
  65. ctools/util/config_util.py +52 -0
  66. ctools/util/env_config.py +63 -0
  67. ctools/{html_soup.py → util/html_soup.py} +0 -7
  68. ctools/{http_utils.py → util/http_util.py} +4 -2
  69. ctools/{images_tools.py → util/image_process.py} +10 -1
  70. ctools/util/jb_cut.py +54 -0
  71. ctools/{id_worker_tools.py → util/snow_id.py} +8 -23
  72. ctools/web/__init__.py +4 -0
  73. ctools/web/aio_web_server.py +186 -0
  74. ctools/web/api_result.py +56 -0
  75. ctools/web/bottle_web_base.py +239 -0
  76. ctools/web/bottle_webserver.py +191 -0
  77. ctools/web/bottle_websocket.py +79 -0
  78. ctools/web/ctoken.py +103 -0
  79. ctools/{download_tools.py → web/download_util.py} +14 -12
  80. ctools/web/params_util.py +46 -0
  81. ctools/{upload_tools.py → web/upload_util.py} +3 -2
  82. gomyck_tools-1.4.7.dist-info/METADATA +70 -0
  83. gomyck_tools-1.4.7.dist-info/RECORD +88 -0
  84. {gomyck_tools-1.0.0.dist-info → gomyck_tools-1.4.7.dist-info}/WHEEL +1 -1
  85. gomyck_tools-1.4.7.dist-info/licenses/LICENSE +13 -0
  86. ctools/bashPath.py +0 -13
  87. ctools/bottle_server.py +0 -49
  88. ctools/console.py +0 -55
  89. ctools/date_utils.py +0 -44
  90. ctools/enums.py +0 -4
  91. ctools/excelOpt.py +0 -36
  92. ctools/imgDialog.py +0 -44
  93. ctools/license.py +0 -37
  94. ctools/log.py +0 -28
  95. ctools/mvc.py +0 -56
  96. ctools/obj.py +0 -20
  97. ctools/pacth.py +0 -73
  98. ctools/ssh.py +0 -9
  99. ctools/strDiff.py +0 -20
  100. ctools/string_tools.py +0 -101
  101. ctools/token_tools.py +0 -13
  102. ctools/wordFill.py +0 -24
  103. gomyck_tools-1.0.0.dist-info/METADATA +0 -20
  104. gomyck_tools-1.0.0.dist-info/RECORD +0 -54
  105. /ctools/{word_fill_entity.py → office/word_fill_entity.py} +0 -0
  106. /ctools/{compile_tools.py → util/compile_util.py} +0 -0
  107. {gomyck_tools-1.0.0.dist-info → gomyck_tools-1.4.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ """A lightweight async HTTP server based on aiohttp."""
4
+
5
+ __author__ = 'haoyang'
6
+ __date__ = '2025/5/30 09:54'
7
+
8
+ import asyncio
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Optional, Dict, Any
12
+
13
+ from aiohttp import web
14
+ from ctools import sys_info, cjson
15
+ from ctools.sys_log import flog as log
16
+ from ctools.web.api_result import R
17
+
18
+ DEFAULT_PORT = 8888
19
+
20
+ @web.middleware
21
+ async def response_wrapper_middleware(request, handler):
22
+ try:
23
+ result = await handler(request)
24
+ if isinstance(result, web.Response):
25
+ return result
26
+ elif isinstance(result, str):
27
+ return web.Response(text=result, content_type='application/json')
28
+ elif isinstance(result, dict):
29
+ return web.Response(text=cjson.dumps(result), content_type='application/json')
30
+ else:
31
+ return result
32
+ except web.HTTPException as http_exc:
33
+ raise http_exc
34
+ except Exception as e:
35
+ log.error(f"Error in response_wrapper_middleware: {e}", exc_info=True)
36
+ return web.json_response(text=R.error(str(e)), status=500, content_type='application/json')
37
+
38
+
39
+ class AioHttpServer:
40
+ def __init__(self, port: int = DEFAULT_PORT, app: Optional[web.Application] = None, routes: Optional[web.RouteTableDef] = None, async_func=None):
41
+ """
42
+ Initialize the HTTP server.
43
+
44
+ Args:
45
+ port: Port number to listen on
46
+ app: Optional existing aiohttp Application instance
47
+ """
48
+ self.app = app or web.Application(middlewares=[response_wrapper_middleware])
49
+ self.port = port
50
+ self.index_root = Path('./')
51
+ self.index_filename = 'index.html'
52
+ self.is_tpl = False
53
+ self.template_args: Dict[str, Any] = {}
54
+ self.redirect_url: Optional[str] = None
55
+ self.static_root = Path('./static')
56
+ self.download_root = Path('./download')
57
+ self.routes = routes
58
+ self.async_func = async_func
59
+
60
+ # Register routes
61
+ self.app.add_routes([
62
+ web.get('/', self.handle_index),
63
+ web.get('/index', self.handle_index),
64
+ web.get('/static/{filepath:.*}', self.handle_static),
65
+ web.get('/download/{filepath:.*}', self.handle_download)
66
+ ])
67
+ if self.routes:
68
+ self.app.add_routes(self.routes)
69
+
70
+ async def handle_index(self, request: web.Request) -> web.StreamResponse:
71
+ """Handle requests to the index page."""
72
+ if self.redirect_url:
73
+ return web.HTTPFound(self.redirect_url)
74
+
75
+ index_path = self.index_root / self.index_filename
76
+
77
+ if not index_path.exists():
78
+ return web.HTTPNotFound()
79
+
80
+ if self.is_tpl:
81
+ return web.FileResponse(
82
+ index_path,
83
+ headers={'Content-Type': 'text/html'}
84
+ )
85
+ return web.FileResponse(index_path)
86
+
87
+ async def handle_static(self, request: web.Request) -> web.StreamResponse:
88
+ """Handle static file requests."""
89
+ filepath = Path(request.match_info['filepath'])
90
+ full_path = self.static_root / filepath
91
+
92
+ if not full_path.exists():
93
+ return web.HTTPNotFound()
94
+
95
+ return web.FileResponse(full_path)
96
+
97
+ async def handle_download(self, request: web.Request) -> web.StreamResponse:
98
+ """Handle file download requests."""
99
+ filepath = Path(request.match_info['filepath'])
100
+ full_path = self.download_root / filepath
101
+
102
+ if not full_path.exists():
103
+ return web.HTTPNotFound()
104
+
105
+ return web.FileResponse(
106
+ full_path,
107
+ headers={'Content-Disposition': f'attachment; filename="{filepath.name}"'}
108
+ )
109
+
110
+ def set_index(
111
+ self,
112
+ filename: str = 'index.html',
113
+ root: str = './',
114
+ is_tpl: bool = False,
115
+ redirect_url: Optional[str] = None,
116
+ **kwargs: Any
117
+ ) -> None:
118
+ """
119
+ Configure index page settings.
120
+
121
+ Args:
122
+ filename: Name of the index file
123
+ root: Root directory for index file
124
+ is_tpl: Whether the file is a template
125
+ redirect_url: URL to redirect to instead of serving index
126
+ kwargs: Additional template arguments
127
+ """
128
+ self.index_root = Path(root)
129
+ self.index_filename = filename
130
+ self.is_tpl = is_tpl
131
+ self.redirect_url = redirect_url
132
+ self.template_args = kwargs
133
+
134
+ def set_static(self, root: str = './static') -> None:
135
+ """Set the root directory for static files."""
136
+ self.static_root = Path(root)
137
+
138
+ def set_download(self, root: str = './download') -> None:
139
+ """Set the root directory for downloadable files."""
140
+ self.download_root = Path(root)
141
+
142
+ async def run(self) -> None:
143
+ """Run the server."""
144
+ if self.async_func:
145
+ await self.async_func()
146
+
147
+ print(
148
+ 'Server running at:\n'
149
+ f'\tLocal: http://localhost:{self.port}\n'
150
+ f'\tNetwork: http://{sys_info.get_local_ipv4()}:{self.port}',
151
+ file=sys.stderr
152
+ )
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()
164
+
165
+
166
+ def init_routes() -> web.RouteTableDef:
167
+ return web.RouteTableDef()
168
+
169
+
170
+ def init_server(routes: Optional[web.RouteTableDef] = None, app: Optional[web.Application] = None, port: int = DEFAULT_PORT, async_func=None) -> AioHttpServer:
171
+ """Initialize and return a new AioHttpServer instance."""
172
+ return AioHttpServer(port=port, app=app, routes=routes, async_func=async_func)
173
+
174
+
175
+ async def get_stream_resp(request, content_type: str = 'text/event-stream') -> web.StreamResponse:
176
+ resp = web.StreamResponse(
177
+ status=200,
178
+ reason='OK',
179
+ headers={
180
+ 'Content-Type': content_type,
181
+ 'Cache-Control': 'no-cache',
182
+ 'X-Accel-Buffering': 'no',
183
+ }
184
+ )
185
+ await resp.prepare(request)
186
+ return resp
@@ -0,0 +1,56 @@
1
+ from ctools import cjson
2
+
3
+ cjson.str_value_keys = [
4
+ "obj_id",
5
+ ]
6
+
7
+
8
+ class _ResEnum(object):
9
+ def __init__(self, code: int, message: str):
10
+ self.code = code
11
+ self.message = message
12
+
13
+ def __eq__(self, o: object) -> bool:
14
+ return self.code == o
15
+
16
+
17
+ class R(object):
18
+ class Code:
19
+
20
+ @staticmethod
21
+ def cus_code(code, msg):
22
+ return _ResEnum(code, msg)
23
+
24
+ SUCCESS = _ResEnum(200, "成功")
25
+ FAIL = _ResEnum(400, "失败")
26
+ ERROR = _ResEnum(500, "异常")
27
+
28
+ def __init__(self, code: int, message: str, data=""):
29
+ self.code = code
30
+ self.message = message
31
+ self.data = data
32
+
33
+ def _to_json_str(self):
34
+ return cjson.unify_to_str(cjson.dumps(self))
35
+
36
+ @staticmethod
37
+ def parser(r_json: str):
38
+ return R(**cjson.loads(r_json))
39
+
40
+ @staticmethod
41
+ def ok(data=None, resp=Code.SUCCESS, msg=None, to_json_str=True):
42
+ if not to_json_str:
43
+ return R(resp.code, msg if msg is not None else resp.message, data)
44
+ return R(resp.code, msg if msg is not None else resp.message, data)._to_json_str()
45
+
46
+ @staticmethod
47
+ def fail(msg=None, resp=Code.FAIL, data=None, to_json_str=True):
48
+ if not to_json_str:
49
+ return R(resp.code, msg if msg is not None else resp.message, data)
50
+ return R(resp.code, msg if msg is not None else resp.message, data)._to_json_str()
51
+
52
+ @staticmethod
53
+ def error(msg=None, resp=Code.ERROR, data=None, to_json_str=True):
54
+ if not to_json_str:
55
+ return R(resp.code, msg if msg is not None else resp.message, data)
56
+ return R(resp.code, msg if msg is not None else resp.message, data)._to_json_str()
@@ -0,0 +1,239 @@
1
+ import inspect
2
+ import threading
3
+ from functools import wraps
4
+
5
+ import bottle
6
+ from bottle import response, Bottle, request
7
+
8
+ from ctools import call
9
+ from ctools.dict_wrapper import DictWrapper
10
+ from ctools.sys_log import flog as log
11
+ from ctools.util.cklock import try_lock
12
+ from ctools.web import ctoken
13
+ from ctools.web.api_result import R
14
+
15
+ bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 * 50
16
+ func_has_params = {}
17
+ app_cache = {}
18
+
19
+ class GlobalState:
20
+ lock = threading.Lock()
21
+ withOutLoginURI = {
22
+ '/',
23
+ '/index',
24
+ '/login',
25
+ '/favicon.ico',
26
+ '/static/',
27
+ }
28
+ allowRemoteCallURI = set()
29
+ auth_ignore_func = set()
30
+ token = {}
31
+ interceptors = []
32
+
33
+ def join_path(*parts):
34
+ """拼接 url"""
35
+ cleaned_parts = [p.strip('/') for p in parts if p and p.strip('/')]
36
+ return '/' + '/'.join(cleaned_parts)
37
+
38
+ @call.once
39
+ def cache_white_list(app):
40
+ """缓存白名单"""
41
+ for route in app.routes:
42
+ real_func = route.config.get('mountpoint.target')
43
+ if not real_func: continue
44
+ for method, routes in real_func.router.static.items():
45
+ for path, tuples in routes.items():
46
+ req_func = inspect.getmodule(tuples[0].callback).__name__ + "." + tuples[0].callback.__name__
47
+ if req_func in GlobalState.auth_ignore_func:
48
+ print("add white list: {}".format(route.rule))
49
+ GlobalState.withOutLoginURI.add(join_path(app.context_path, real_func.context_path, path))
50
+
51
+ def init_app(context_path="/", main_app=False):
52
+ with try_lock(block=True):
53
+ cache_app = app_cache.get(context_path)
54
+ if cache_app: return cache_app
55
+ app = Bottle()
56
+ app_cache[context_path] = app
57
+ app.context_path = context_path if context_path else "/"
58
+
59
+ def init_main_app():
60
+ @app.hook('before_request')
61
+ def before_request():
62
+ for v in GlobalState.withOutLoginURI:
63
+ if v.endswith('/'):
64
+ if request.path.startswith(v): return
65
+ else:
66
+ if v in request.path: return
67
+ for interceptor in GlobalState.interceptors:
68
+ res: R = interceptor['func']()
69
+ if res.code != 200: bottle.abort(res.code, res.message)
70
+
71
+ @app.error(401)
72
+ def unauthorized(error):
73
+ response.status = 401
74
+ log.error("系统未授权: {} {} {}".format(error.body, request.method, request.fullpath))
75
+ return R.error(resp=R.Code.cus_code(401, "系统未授权! {}".format(error.body)))
76
+
77
+ @app.error(403)
78
+ def unauthorized(error):
79
+ response.status = 403
80
+ log.error("访问受限: {} {} {}".format(error.body, request.method, request.fullpath))
81
+ return R.error(resp=R.Code.cus_code(403, "访问受限: {}".format(error.body)))
82
+
83
+ @app.error(404)
84
+ def not_found(error):
85
+ response.status = 404
86
+ log.error("404 not found : {} {} {}".format(error.body, request.method, request.fullpath))
87
+ return R.error(resp=R.Code.cus_code(404, "资源未找到: {}".format(error.body)))
88
+
89
+ @app.error(405)
90
+ def method_not_allow(error):
91
+ response.status = 405
92
+ log.error("请求方法错误: {} {} {}".format(error.status_line, request.method, request.fullpath))
93
+ return R.error(resp=R.Code.cus_code(405, '请求方法错误: {}'.format(error.status_line)))
94
+
95
+ @app.error(500)
96
+ def internal_error(error):
97
+ response.status = 500
98
+ log.error("系统发生错误: {} {} {}".format(error.body, request.method, request.fullpath))
99
+ return R.error(msg='系统发生错误: {}'.format(error.exception))
100
+
101
+ @app.hook('after_request')
102
+ def after_request():
103
+ enable_cors()
104
+
105
+ if main_app: init_main_app()
106
+ app.install(params_resolve)
107
+ return app
108
+
109
+
110
+ def enable_cors():
111
+ response.headers['Access-Control-Allow-Origin'] = '*'
112
+ response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
113
+ request_headers = request.headers.get('Access-Control-Request-Headers')
114
+ response.headers['Access-Control-Allow-Headers'] = request_headers if request_headers else ''
115
+ response.headers['Access-Control-Expose-Headers'] = '*'
116
+
117
+
118
+ # annotation
119
+ def before_intercept(order=0):
120
+ def decorator(func):
121
+ for interceptor in GlobalState.interceptors:
122
+ if interceptor['func'].__name__ == func.__name__:
123
+ log.info("duplicate interceptor: {}".format(func.__name__))
124
+ return
125
+ log.info("add before interceptor: {}".format(func.__name__))
126
+ GlobalState.interceptors.append({'order': order, 'func': func})
127
+ GlobalState.interceptors = sorted(GlobalState.interceptors, key=lambda x: x['order'])
128
+
129
+ return decorator
130
+
131
+ # annotation
132
+ # 接口请求地址后面带不带斜杠都会影响: /api/xxx/ 和 /api/xxx 是不一样的
133
+ def auth_ignore(func):
134
+ """忽略登录验证的接口"""
135
+ ignore_req_func = inspect.getmodule(func).__name__ + "." + func.__name__
136
+ if ignore_req_func in GlobalState.auth_ignore_func: raise Exception("duplicate ignore func: {}".format(ignore_req_func))
137
+ GlobalState.auth_ignore_func.add(ignore_req_func)
138
+ @wraps(func)
139
+ def decorated(*args, **kwargs):
140
+ return func(*args, **kwargs)
141
+ return decorated
142
+
143
+ # annotation
144
+ def rule(key):
145
+ def return_func(func):
146
+ @wraps(func)
147
+ def decorated(*args, **kwargs):
148
+ rules = ctoken.get_token_attr("rules") or []
149
+ if _match_rule_by_prefix(key, rules):
150
+ return func(*args, **kwargs)
151
+ else:
152
+ return R.error("权限不足, 请联系管理员: {}".format(key))
153
+ return decorated
154
+ return return_func
155
+
156
+ def _match_rule_by_prefix(key, rules):
157
+ for r in rules:
158
+ if key.startswith(r):
159
+ return True
160
+ return False
161
+
162
+ # annotation or plugins, has auto install, don't need to call
163
+ def params_resolve(func):
164
+ @wraps(func)
165
+ def decorated(*args, **kwargs):
166
+ if func_has_params.get(request.fullpath) is not None and not func_has_params.get(request.fullpath):
167
+ return func(*args, **kwargs)
168
+ if func_has_params.get(request.fullpath) is None:
169
+ sig = inspect.signature(func)
170
+ params = sig.parameters
171
+ if not params.get('params'):
172
+ func_has_params[request.fullpath] = False
173
+ return func(*args, **kwargs)
174
+ else:
175
+ func_has_params[request.fullpath] = True
176
+ if request.method == 'GET' or request.method == 'DELETE':
177
+ queryStr = request.query.decode('utf-8')
178
+ page_info = PageInfo(
179
+ page_size=int(request.query.page_size) if request.headers.get('page_size') is None else int(request.headers.get('page_size')),
180
+ page_index=int(request.query.page_index) if request.headers.get('page_index') is None else int(request.headers.get('page_index'))
181
+ )
182
+ queryStr = auto_exchange(func, queryStr)
183
+ queryStr.page_info = page_info
184
+ return func(params=queryStr, *args, **kwargs)
185
+ elif request.method == 'POST' or request.method == 'PUT':
186
+ query_params = request.query.decode('utf-8')
187
+ content_type = request.get_header('content-type')
188
+ if content_type == 'application/json':
189
+ params = request.json or {}
190
+ dict_wrapper = DictWrapper(params)
191
+ dict_wrapper.update(query_params.dict)
192
+ return func(params=auto_exchange(func, dict_wrapper), *args, **kwargs)
193
+ elif 'multipart/form-data' in content_type:
194
+ form_data = request.forms.decode()
195
+ form_files = request.files.decode()
196
+ dict_wrapper = DictWrapper(form_data)
197
+ dict_wrapper.update(query_params.dict)
198
+ dict_wrapper.files = form_files
199
+ return func(params=auto_exchange(func, dict_wrapper), *args, **kwargs)
200
+ elif 'application/x-www-form-urlencoded' in content_type:
201
+ params = request.forms.decode()
202
+ dict_wrapper = DictWrapper(params.dict)
203
+ dict_wrapper.update(query_params.dict)
204
+ return func(params=auto_exchange(func, dict_wrapper), *args, **kwargs)
205
+ elif 'text/plain' in content_type:
206
+ params = request.body.read().decode('utf-8')
207
+ dict_wrapper = DictWrapper({'body': params})
208
+ dict_wrapper.update(query_params.dict)
209
+ return func(params=auto_exchange(func, dict_wrapper), *args, **kwargs)
210
+ else:
211
+ return func(*args, **kwargs)
212
+
213
+ return decorated
214
+
215
+ # 自动转换参数类型
216
+ def auto_exchange(func, dict_wrapper):
217
+ model_class = func.__annotations__.get('params')
218
+ if model_class:
219
+ try:
220
+ model_instance = model_class(**dict_wrapper)
221
+ return model_instance
222
+ except Exception as e:
223
+ log.exception(e)
224
+ return dict_wrapper
225
+ else:
226
+ return dict_wrapper
227
+
228
+ # 分页信息对象
229
+ class PageInfo:
230
+ def __init__(self, page_size, page_index):
231
+ self.page_size = page_size
232
+ self.page_index = page_index
233
+
234
+
235
+ # 通用的鉴权方法
236
+ def common_auth_verify() -> R:
237
+ valid = ctoken.is_valid()
238
+ if valid: return R.ok(to_json_str=False)
239
+ return R.error(resp=R.Code.cus_code(401, "请登录!"), to_json_str=False)
@@ -0,0 +1,191 @@
1
+ import os
2
+ import sys
3
+ from socketserver import ThreadingMixIn
4
+ from wsgiref.simple_server import WSGIServer, WSGIRequestHandler, make_server
5
+
6
+ from bottle import ServerAdapter, Bottle, template, static_file, abort, redirect, response, request
7
+
8
+ from ctools import sys_info
9
+ from ctools.pkg.dynamic_imp import load_modules_from_package
10
+ from ctools.web.api_result import R
11
+ from ctools.web.bottle_web_base import cache_white_list, auth_ignore, GlobalState
12
+
13
+ """
14
+ import controllers
15
+ from ctools import patch
16
+ from ctools.database import database
17
+ from ctools.util.config_util import load_config
18
+ from ctools.web import bottle_web_base, bottle_webserver
19
+ from key_word_cloud.db_core.db_init import init_partitions
20
+ from patch_manager import patch_funcs
21
+
22
+ database.init_db('postgresql://postgres:123123@192.168.xx.xx:5432/xxx', default_schema='xxx', auto_gen_table=False, echo=False)
23
+
24
+ config = load_config('application.ini')
25
+
26
+ patch.sync_version(config.base.app_name, config.base.version, patch_funcs)
27
+ init_partitions()
28
+
29
+ app = bottle_web_base.init_app("/api", True)
30
+
31
+ @bottle_web_base.before_intercept(0)
32
+ def token_check():
33
+ return bottle_web_base.common_auth_verify(config.base.secret_key)
34
+
35
+ if __name__ == '__main__':
36
+ main_server = bottle_webserver.init_bottle(app)
37
+ main_server.auto_mount(controllers)
38
+ main_server.run()
39
+ """
40
+
41
+ _default_port = 8888
42
+
43
+
44
+ class CBottle:
45
+
46
+ def __init__(self, bottle: Bottle, port=_default_port, quiet=False):
47
+ self.port = port
48
+ self.quiet = quiet
49
+ self.bottle = bottle
50
+ self.index_root = './'
51
+ self.index_filename = 'index.html'
52
+ self.is_tpl = False
53
+ self.tmp_args = {}
54
+ self.redirect_url = None
55
+ self.static_root = './static'
56
+ self.download_root = './download'
57
+ self.template_root = './templates'
58
+
59
+ @self.bottle.route(['/', '/index'])
60
+ def index():
61
+ try:
62
+ if self.redirect_url: return redirect(self.redirect_url)
63
+ if self.is_tpl: return template(f"{self.index_root}/{self.index_filename}", self.tmp_args)
64
+ return static_file(filename=self.index_filename, root=self.index_root)
65
+ except FileNotFoundError:
66
+ abort(404, "File not found...")
67
+
68
+ @self.bottle.route('/template/<filepath:path>')
69
+ def tpl(filepath, tmp_args):
70
+ try:
71
+ return template(f"{self.template_root}/{filepath}", tmp_args)
72
+ except FileNotFoundError:
73
+ abort(404, "File not found...")
74
+
75
+ @self.bottle.route('/static/<filepath:path>')
76
+ def static(filepath):
77
+ try:
78
+ return static_file(filepath, root=self.static_root)
79
+ except FileNotFoundError:
80
+ abort(404, "File not found...")
81
+
82
+ @self.bottle.route('/download/<filepath:path>')
83
+ def download(filepath):
84
+ return static_file(filepath, root=self.download_root, download=True)
85
+
86
+ @self.bottle.route('/favicon.ico')
87
+ def favicon():
88
+ response.content_type = 'image/svg+xml'
89
+ svg_icon = '''<?xml version="1.0" encoding="UTF-8"?>
90
+ <svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
91
+ <circle cx="16" cy="16" r="14" fill="#007bff"/>
92
+ <path d="M16 8a8 8 0 0 0-8 8h2a6 6 0 0 1 12 0h2a8 8 0 0 0-8-8z" fill="white"/>
93
+ <circle cx="16" cy="20" r="2" fill="white"/>
94
+ </svg>
95
+ '''
96
+ return svg_icon
97
+
98
+ def auto_register_statics(self, dist_root='./static'):
99
+ for d in os.listdir(dist_root):
100
+ full_path = os.path.join(dist_root, d)
101
+ if os.path.isdir(full_path):
102
+ self.add_static_route(f'/{d}', full_path)
103
+
104
+ def add_static_route(self, url_prefix, root):
105
+ """动态添加静态资源映射"""
106
+ if not url_prefix.startswith('/'):
107
+ url_prefix = '/' + url_prefix
108
+ if not url_prefix.endswith('/'):
109
+ url_prefix += '/'
110
+ route_path = f"{url_prefix}<filepath:path>"
111
+ @self.bottle.route(route_path)
112
+ def static_handler(filepath):
113
+ try:
114
+ return static_file(filepath, root=root)
115
+ except FileNotFoundError:
116
+ abort(404, f"{filepath} not found in {root}")
117
+ GlobalState.withOutLoginURI.add(url_prefix)
118
+ print(f"[static] mounted {url_prefix} -> {root}")
119
+
120
+ def run(self):
121
+ http_server = WSGIRefServer(port=self.port)
122
+ 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)
123
+ cache_white_list(self.bottle)
124
+ self.bottle.run(server=http_server, quiet=self.quiet)
125
+
126
+ def enable_spa_mode(self):
127
+ @self.bottle.error(404)
128
+ def error_404_handler(error):
129
+ if request.path.startswith('/api/'): return R.error(resp=R.Code.cus_code(404, "资源未找到: {}".format(error.body)))
130
+ return static_file(filename=self.index_filename, root=self.index_root)
131
+
132
+ def set_index(self, filename='index.html', root='./', is_tpl=False, redirect_url=None, spa=False, **kwargs):
133
+ self.index_root = root
134
+ self.index_filename = filename
135
+ self.is_tpl = is_tpl
136
+ self.redirect_url = redirect_url
137
+ if spa: self.enable_spa_mode()
138
+ self.tmp_args = kwargs
139
+
140
+ def set_static(self, root='./static'):
141
+ self.static_root = root
142
+
143
+ def set_template(self, root='./templates'):
144
+ self.template_root = root
145
+
146
+ def set_download(self, root='./download'):
147
+ self.download_root = root
148
+
149
+ def mount(self, context_path, app, **kwargs):
150
+ if not context_path: return
151
+ self.bottle.mount(context_path, app, **kwargs)
152
+
153
+ def auto_mount(self, package, exclude=None, recursive=True):
154
+ for module in load_modules_from_package(package, exclude, recursive):
155
+ if self.bottle.context_path != '/':
156
+ if module.app.context_path == '/':
157
+ ctx_path = self.bottle.context_path
158
+ else:
159
+ ctx_path = self.bottle.context_path + module.app.context_path
160
+ else:
161
+ ctx_path = module.app.context_path
162
+ print("mount: %s on %s" % (ctx_path, module.__name__))
163
+ self.bottle.mount(ctx_path, module.app)
164
+
165
+ def init_bottle(app: Bottle = None, port=_default_port, quiet=False) -> CBottle:
166
+ bottle = app or Bottle()
167
+ return CBottle(bottle, port, quiet)
168
+
169
+
170
+ class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
171
+ daemon_threads = True
172
+
173
+
174
+ class CustomWSGIHandler(WSGIRequestHandler):
175
+ def log_request(*args, **kw): pass
176
+
177
+
178
+ class WSGIRefServer(ServerAdapter):
179
+
180
+ def __init__(self, host='0.0.0.0', port=_default_port):
181
+ super().__init__(host, port)
182
+ self.server = None
183
+
184
+ def run(self, handler):
185
+ req_handler = WSGIRequestHandler
186
+ if self.quiet: req_handler = CustomWSGIHandler
187
+ self.server = make_server(self.host, self.port, handler, server_class=ThreadedWSGIServer, handler_class=req_handler)
188
+ self.server.serve_forever()
189
+
190
+ def stop(self):
191
+ self.server.shutdown()