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.
- ctools/__init__.py +21 -0
- ctools/ai/__init__.py +4 -0
- ctools/ai/llm_chat.py +184 -0
- ctools/ai/llm_client.py +163 -0
- ctools/ai/llm_exception.py +17 -0
- ctools/ai/mcp/__init__.py +4 -0
- ctools/ai/mcp/mcp_client.py +326 -0
- ctools/ai/tools/__init__.py +4 -0
- ctools/ai/tools/json_extract.py +202 -0
- ctools/ai/tools/quick_tools.py +149 -0
- ctools/ai/tools/think_process.py +11 -0
- ctools/ai/tools/tool_use_xml_parse.py +40 -0
- ctools/ai/tools/xml_extract.py +15 -0
- ctools/application.py +50 -47
- ctools/aspect.py +65 -0
- ctools/auto/__init__.py +4 -0
- ctools/{browser_element_tools.py → auto/browser_element.py} +18 -8
- ctools/{plan_area_tools.py → auto/plan_area.py} +5 -7
- ctools/{pty_tools.py → auto/pty_process.py} +6 -3
- ctools/{resource_bundle_tools.py → auto/resource_bundle.py} +4 -4
- ctools/{screenshot_tools.py → auto/screenshot.py} +7 -6
- ctools/{win_canvas.py → auto/win_canvas.py} +10 -4
- ctools/{win_control.py → auto/win_control.py} +14 -5
- ctools/call.py +34 -49
- ctools/cdate.py +84 -0
- ctools/cdebug.py +146 -0
- ctools/cid.py +20 -0
- ctools/cipher/__init__.py +4 -0
- ctools/{aes_tools.py → cipher/aes_util.py} +10 -0
- ctools/{b64.py → cipher/b64.py} +2 -0
- ctools/cipher/czip.py +133 -0
- ctools/cipher/rsa.py +75 -0
- ctools/{sign.py → cipher/sign.py} +2 -1
- ctools/{sm_tools.py → cipher/sm_util.py} +20 -4
- ctools/cjson.py +10 -10
- ctools/cron_lite.py +109 -97
- ctools/database/__init__.py +4 -0
- ctools/{database.py → database/database.py} +93 -26
- ctools/dict_wrapper.py +21 -0
- ctools/ex.py +4 -0
- ctools/geo/__init__.py +4 -0
- ctools/geo/coord_trans.py +127 -0
- ctools/geo/douglas_rarefy.py +139 -0
- ctools/metrics.py +56 -63
- ctools/office/__init__.py +4 -0
- ctools/office/cword.py +30 -0
- ctools/{word_fill.py → office/word_fill.py} +3 -6
- ctools/patch.py +88 -0
- ctools/{work_path.py → path_info.py} +34 -2
- ctools/pkg/__init__.py +4 -0
- ctools/pkg/dynamic_imp.py +38 -0
- ctools/pools/__init__.py +4 -0
- ctools/pools/process_pool.py +41 -0
- ctools/{thread_pool.py → pools/thread_pool.py} +13 -4
- ctools/similar.py +25 -0
- ctools/stream/__init__.py +4 -0
- ctools/stream/ckafka.py +164 -0
- ctools/stream/credis.py +127 -0
- ctools/{mqtt_utils.py → stream/mqtt_utils.py} +20 -12
- ctools/sys_info.py +36 -13
- ctools/sys_log.py +46 -27
- ctools/util/__init__.py +4 -0
- ctools/util/cftp.py +76 -0
- ctools/util/cklock.py +118 -0
- ctools/util/config_util.py +52 -0
- ctools/util/env_config.py +63 -0
- ctools/{html_soup.py → util/html_soup.py} +0 -7
- ctools/{http_utils.py → util/http_util.py} +4 -2
- ctools/{images_tools.py → util/image_process.py} +10 -1
- ctools/util/jb_cut.py +54 -0
- ctools/{id_worker_tools.py → util/snow_id.py} +8 -23
- ctools/web/__init__.py +4 -0
- ctools/web/aio_web_server.py +186 -0
- ctools/web/api_result.py +56 -0
- ctools/web/bottle_web_base.py +239 -0
- ctools/web/bottle_webserver.py +191 -0
- ctools/web/bottle_websocket.py +79 -0
- ctools/web/ctoken.py +103 -0
- ctools/{download_tools.py → web/download_util.py} +14 -12
- ctools/web/params_util.py +46 -0
- ctools/{upload_tools.py → web/upload_util.py} +3 -2
- gomyck_tools-1.4.7.dist-info/METADATA +70 -0
- gomyck_tools-1.4.7.dist-info/RECORD +88 -0
- {gomyck_tools-1.0.0.dist-info → gomyck_tools-1.4.7.dist-info}/WHEEL +1 -1
- gomyck_tools-1.4.7.dist-info/licenses/LICENSE +13 -0
- ctools/bashPath.py +0 -13
- ctools/bottle_server.py +0 -49
- ctools/console.py +0 -55
- ctools/date_utils.py +0 -44
- ctools/enums.py +0 -4
- ctools/excelOpt.py +0 -36
- ctools/imgDialog.py +0 -44
- ctools/license.py +0 -37
- ctools/log.py +0 -28
- ctools/mvc.py +0 -56
- ctools/obj.py +0 -20
- ctools/pacth.py +0 -73
- ctools/ssh.py +0 -9
- ctools/strDiff.py +0 -20
- ctools/string_tools.py +0 -101
- ctools/token_tools.py +0 -13
- ctools/wordFill.py +0 -24
- gomyck_tools-1.0.0.dist-info/METADATA +0 -20
- gomyck_tools-1.0.0.dist-info/RECORD +0 -54
- /ctools/{word_fill_entity.py → office/word_fill_entity.py} +0 -0
- /ctools/{compile_tools.py → util/compile_util.py} +0 -0
- {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
|
ctools/web/api_result.py
ADDED
|
@@ -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()
|