lesscode-flask 0.2.89__tar.gz → 0.2.92__tar.gz
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.
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/PKG-INFO +2 -2
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/__init__.py +1 -1
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/setup/__init__.py +147 -12
- lesscode_flask-0.2.92/lesscode_flask/utils/fs_util.py +68 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask.egg-info/PKG-INFO +2 -2
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask.egg-info/requires.txt +1 -1
- lesscode_flask-0.2.89/lesscode_flask/utils/fs_util.py +0 -55
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/README.md +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/app.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/db/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/db/datasource.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/db/executor.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/export_data/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/export_data/data_download_handler.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/log/access_log_handler.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/model/access_log.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/model/base_model.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/model/parameterized_query.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/model/resource_param_template.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/model/response_result.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/model/user.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/model/user_limit_policy.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/service/access_log_service.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/service/base_service.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/service/resource_param_template_service.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/setting/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/signals.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/static/swagger.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/decorator/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/decorator/cache.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/decorator/sql_injection.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/decorator/swagger.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/dify_utils.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/file/file_exporter.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/file/file_utils.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/helpers.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/json/NotSortJSONProvider.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/limit/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/limit/consecutive/consecutive_limiter_handler.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/limit/consecutive/redis_consecutive_limiter.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/limit/limit_util.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/limit/req/rate_limiter_handler.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/limit/req/redis_rate_limiter.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/limit/req_count/count_limiter_handler.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/limit/req_count/redis_count_limiter.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/oss/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/oss/aliyun_oss.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/oss/ks3_oss.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/oss/minio_oss.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/redis/redis_helper.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/request/request.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/sign/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/sign/signature.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/swagger/swagger_template.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/swagger/swagger_util.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/task/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/task/task_helper.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/thread/thread_utils.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/wsgi.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask.egg-info/SOURCES.txt +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask.egg-info/dependency_links.txt +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask.egg-info/top_level.txt +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/redash/query_runner/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/redash/query_runner/clickhouse.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/redash/query_runner/elasticsearch.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/redash/query_runner/kingbase.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/redash/query_runner/mysql.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/redash/query_runner/pg.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/redash/settings/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/redash/settings/helpers.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/redash/utils/__init__.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/redash/utils/requests_session.py +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/setup.cfg +0 -0
- {lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lesscode-flask
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.92
|
|
4
4
|
Summary: lesscode-flask 是基于flask的web开发脚手架项目,该项目初衷为简化开发过程,让研发人员更加关注业务。
|
|
5
5
|
Home-page: https://lesscode-flask
|
|
6
6
|
Author: Chao.yy
|
|
@@ -11,7 +11,7 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Requires-Python: >=3.9
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: Flask==3.0.3
|
|
14
|
-
Requires-Dist: lesscode-utils==0.0.
|
|
14
|
+
Requires-Dist: lesscode-utils==0.0.95
|
|
15
15
|
Requires-Dist: Flask-Login==0.6.3
|
|
16
16
|
Requires-Dist: redis==5.1.1
|
|
17
17
|
Requires-Dist: flask-swagger-ui==4.11.1
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import json
|
|
2
3
|
import uuid
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from importlib import import_module
|
|
@@ -6,6 +7,7 @@ from logging.handlers import TimedRotatingFileHandler
|
|
|
6
7
|
|
|
7
8
|
import requests
|
|
8
9
|
from flask import current_app, Response, request
|
|
10
|
+
from lesscode_utils.encryption_algorithm import AES
|
|
9
11
|
|
|
10
12
|
from lesscode_flask import download_func_dict, SQ_Blueprint
|
|
11
13
|
from lesscode_flask.db import db
|
|
@@ -94,6 +96,7 @@ def setup_blueprint(app, path=None, pkg_name="handlers", blueprint_map=None):
|
|
|
94
96
|
while f"{blueprint_name}-{index}" in blueprint_map:
|
|
95
97
|
index += 1
|
|
96
98
|
return f"{blueprint_name}-{index}"
|
|
99
|
+
|
|
97
100
|
if path is None:
|
|
98
101
|
# 项目内Handler的文件路径,使用当前工作目录作为根
|
|
99
102
|
path = os.path.join(os.getcwd(), pkg_name)
|
|
@@ -169,7 +172,122 @@ def _normalize_methods(raw_methods, supported_methods):
|
|
|
169
172
|
return [item for item in methods if item in supported_methods] or ["POST"]
|
|
170
173
|
|
|
171
174
|
|
|
172
|
-
def
|
|
175
|
+
def _build_forward_headers(app):
|
|
176
|
+
headers = {}
|
|
177
|
+
for key, value in request.headers:
|
|
178
|
+
if key.lower() not in {"host", "content-length"}:
|
|
179
|
+
headers[key] = value
|
|
180
|
+
# 转调时按配置补充自定义请求头,用于能力平台鉴权/路由。
|
|
181
|
+
data_source_key = app.config.get("DATA_SOURCE_KEY")
|
|
182
|
+
data_source_value = app.config.get("DATA_SOURCE_VALUE")
|
|
183
|
+
if data_source_key and data_source_value is not None:
|
|
184
|
+
headers[str(data_source_key)] = str(data_source_value)
|
|
185
|
+
return headers
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _build_download_key(target_url, request_method):
|
|
189
|
+
key_seed = f"{str(request_method).upper()}::{target_url}"
|
|
190
|
+
# 保持原有 AES 风格,同时拼接首尾片段,避免只截前缀导致不同接口冲突。
|
|
191
|
+
encrypted = AES.encrypt(key_seed)
|
|
192
|
+
return f"{encrypted[:8]}{encrypted[-8:]}"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _is_auto_export_route(route):
|
|
196
|
+
route = str(route or "").lower()
|
|
197
|
+
return "/list/" in route
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _apply_path_params(route, request_params):
|
|
201
|
+
target_route = str(route or "")
|
|
202
|
+
for param_name in list(request_params.keys()):
|
|
203
|
+
placeholder = f"<{param_name}>"
|
|
204
|
+
legacy_placeholder = f"{{{param_name}}}"
|
|
205
|
+
if placeholder not in target_route and legacy_placeholder not in target_route:
|
|
206
|
+
continue
|
|
207
|
+
value = str(request_params.pop(param_name))
|
|
208
|
+
target_route = target_route.replace(placeholder, value).replace(legacy_placeholder, value)
|
|
209
|
+
return target_route
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _inject_download_key(payload, download_key):
|
|
213
|
+
if not download_key or not isinstance(payload, dict):
|
|
214
|
+
return payload
|
|
215
|
+
# 自动注册接口仍然沿用老的 download_key 协议,优先回填到 data 节点。
|
|
216
|
+
data_payload = payload.get("data")
|
|
217
|
+
if isinstance(data_payload, dict):
|
|
218
|
+
data_payload["download_key"] = download_key
|
|
219
|
+
else:
|
|
220
|
+
payload["download_key"] = download_key
|
|
221
|
+
return payload
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _extract_auto_export_result(payload):
|
|
225
|
+
if isinstance(payload, list):
|
|
226
|
+
return {"dataSource": payload, "columns": []}
|
|
227
|
+
if not isinstance(payload, dict):
|
|
228
|
+
raise Exception("自动导出接口返回格式不支持")
|
|
229
|
+
|
|
230
|
+
# 兼容能力平台常见的列表返回结构,统一转换成 data_download 可消费的格式。
|
|
231
|
+
candidate_list = [payload]
|
|
232
|
+
data_payload = payload.get("data")
|
|
233
|
+
if isinstance(data_payload, dict):
|
|
234
|
+
candidate_list.insert(0, data_payload)
|
|
235
|
+
|
|
236
|
+
for candidate in candidate_list:
|
|
237
|
+
if not isinstance(candidate, dict):
|
|
238
|
+
continue
|
|
239
|
+
if isinstance(candidate.get("dataSource"), list):
|
|
240
|
+
return candidate
|
|
241
|
+
for key in ("list", "rows", "records", "items", "result"):
|
|
242
|
+
value = candidate.get(key)
|
|
243
|
+
if isinstance(value, list):
|
|
244
|
+
return {
|
|
245
|
+
"dataSource": value,
|
|
246
|
+
"columns": candidate.get("columns") or candidate.get("column") or []
|
|
247
|
+
}
|
|
248
|
+
if isinstance(value, dict) and isinstance(value.get("dataSource"), list):
|
|
249
|
+
return value
|
|
250
|
+
raise Exception("自动导出接口缺少可识别的数据列表")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _register_auto_export_handler(app, target_url, request_method):
|
|
254
|
+
download_key = _build_download_key(target_url, request_method)
|
|
255
|
+
|
|
256
|
+
def _auto_export_func(offset=0, size=10, **kwargs):
|
|
257
|
+
# 自动注册接口没有本地业务函数,这里动态构造一个“导出函数”给 data_download 调用。
|
|
258
|
+
request_params = {"offset": offset, "size": size}
|
|
259
|
+
request_params.update(kwargs)
|
|
260
|
+
export_url = _apply_path_params(target_url, request_params)
|
|
261
|
+
headers = _build_forward_headers(app)
|
|
262
|
+
request_method_upper = str(request_method).upper()
|
|
263
|
+
request_kwargs = {
|
|
264
|
+
"method": request_method_upper,
|
|
265
|
+
"url": export_url,
|
|
266
|
+
"headers": headers,
|
|
267
|
+
"timeout": 30,
|
|
268
|
+
}
|
|
269
|
+
if request_method_upper == "GET":
|
|
270
|
+
request_kwargs["params"] = request_params
|
|
271
|
+
resp = requests.request(**request_kwargs)
|
|
272
|
+
else:
|
|
273
|
+
request_kwargs["json"] = request_params
|
|
274
|
+
resp = requests.request(**request_kwargs)
|
|
275
|
+
# 某些 /list/ 接口可能不是 JSON 入参;仅在常见参数错误场景下回退到表单透传。
|
|
276
|
+
if resp.status_code in {400, 415, 422}:
|
|
277
|
+
request_kwargs.pop("json", None)
|
|
278
|
+
form_headers = {k: v for k, v in headers.items() if k.lower() != "content-type"}
|
|
279
|
+
request_kwargs["headers"] = form_headers
|
|
280
|
+
request_kwargs["data"] = request_params
|
|
281
|
+
resp = requests.request(**request_kwargs)
|
|
282
|
+
resp.raise_for_status()
|
|
283
|
+
return _extract_auto_export_result(resp.json())
|
|
284
|
+
|
|
285
|
+
_auto_export_func.__name__ = f"auto_export_{uuid.uuid4().hex}"
|
|
286
|
+
download_func_dict[download_key] = _auto_export_func
|
|
287
|
+
return download_key
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _build_proxy_view(app, target_url, title, description=None, dynamic_params=None, download_key=None):
|
|
173
291
|
# 生成一个代理视图:把当前请求透明转发到能力平台目标地址。
|
|
174
292
|
def _proxy_view(**path_params):
|
|
175
293
|
url = target_url
|
|
@@ -177,15 +295,7 @@ def _build_proxy_view(app, target_url, title, description=None, dynamic_params=N
|
|
|
177
295
|
value = str(value)
|
|
178
296
|
url = url.replace(f"<{key}>", value).replace(f"{{{key}}}", value)
|
|
179
297
|
|
|
180
|
-
headers =
|
|
181
|
-
for key, value in request.headers:
|
|
182
|
-
if key.lower() not in {"host", "content-length"}:
|
|
183
|
-
headers[key] = value
|
|
184
|
-
# 转调时按配置补充自定义请求头,用于能力平台鉴权/路由。
|
|
185
|
-
data_source_key = app.config.get("DATA_SOURCE_KEY")
|
|
186
|
-
data_source_value = app.config.get("DATA_SOURCE_VALUE")
|
|
187
|
-
if data_source_key and data_source_value is not None:
|
|
188
|
-
headers[str(data_source_key)] = str(data_source_value)
|
|
298
|
+
headers = _build_forward_headers(app)
|
|
189
299
|
|
|
190
300
|
resp = requests.request(
|
|
191
301
|
method=request.method,
|
|
@@ -198,6 +308,19 @@ def _build_proxy_view(app, target_url, title, description=None, dynamic_params=N
|
|
|
198
308
|
|
|
199
309
|
excluded_headers = {"content-encoding", "content-length", "transfer-encoding", "connection"}
|
|
200
310
|
response_headers = [(k, v) for k, v in resp.headers.items() if k.lower() not in excluded_headers]
|
|
311
|
+
if download_key:
|
|
312
|
+
try:
|
|
313
|
+
# 列表代理接口返回 JSON 时,把 download_key 挂回响应,前端可直接复用原导出流程。
|
|
314
|
+
payload = json.loads(resp.content)
|
|
315
|
+
payload = _inject_download_key(payload, download_key)
|
|
316
|
+
return Response(
|
|
317
|
+
json.dumps(payload, ensure_ascii=False),
|
|
318
|
+
status=resp.status_code,
|
|
319
|
+
headers=response_headers,
|
|
320
|
+
content_type=resp.headers.get("Content-Type", "application/json")
|
|
321
|
+
)
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
201
324
|
return Response(resp.content, status=resp.status_code, headers=response_headers)
|
|
202
325
|
|
|
203
326
|
_proxy_view._title = title
|
|
@@ -388,8 +511,17 @@ def setup_api_blueprint(app, exclude_route_method_keys=None, existing_blueprint_
|
|
|
388
511
|
title = item.get("title") or f"{bp_name}-{index + 1}"
|
|
389
512
|
endpoint = f"auto_dynamic_{group_id}_{index}_{'_'.join(available_methods).lower()}"
|
|
390
513
|
description = item.get("description") or title
|
|
514
|
+
download_key = None
|
|
515
|
+
if _is_auto_export_route(remote_route):
|
|
516
|
+
# 约定 /list/ 路由支持导出,因此额外挂载 download_key -> 动态导出函数。
|
|
517
|
+
download_key = _register_auto_export_handler(
|
|
518
|
+
app,
|
|
519
|
+
target_url=target_url,
|
|
520
|
+
request_method=available_methods[0]
|
|
521
|
+
)
|
|
391
522
|
view_func = _build_proxy_view(app, target_url=target_url, title=title,
|
|
392
|
-
|
|
523
|
+
description=description, dynamic_params=item.get("dynamic_params"),
|
|
524
|
+
download_key=download_key)
|
|
393
525
|
dynamic_bp.add_url_rule(local_route, endpoint=endpoint, view_func=view_func, methods=available_methods)
|
|
394
526
|
group_route_count += 1
|
|
395
527
|
total_route_count += 1
|
|
@@ -628,7 +760,7 @@ def setup_resource_register(app):
|
|
|
628
760
|
url_rules_dict = {}
|
|
629
761
|
for blueprint_name, blueprint in app.blueprints.items():
|
|
630
762
|
group_key = f'{blueprint_name}|{blueprint.url_prefix}'
|
|
631
|
-
if
|
|
763
|
+
if "swagger-ui" not in blueprint.url_prefix:
|
|
632
764
|
url_rules_dict[group_key] = []
|
|
633
765
|
# 遍历全局 URL 规则
|
|
634
766
|
for rule in app.url_map.iter_rules():
|
|
@@ -715,6 +847,9 @@ def setup_data_download(app):
|
|
|
715
847
|
func = download_func_dict.get(download_key, {})
|
|
716
848
|
request_param = {"offset": offset, "size": size}
|
|
717
849
|
signature = inspect.signature(func)
|
|
850
|
+
if any(parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()):
|
|
851
|
+
# 动态导出函数统一使用 **kwargs,直接透传前端筛选条件即可。
|
|
852
|
+
request_param.update(params)
|
|
718
853
|
for parameter_name, parameter in signature.parameters.items():
|
|
719
854
|
if params.get(parameter_name):
|
|
720
855
|
request_param[parameter_name] = params[parameter_name]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from lesscode_flask.utils.helpers import app_config
|
|
9
|
+
from lesscode_flask.utils.redis.redis_helper import RedisHelper
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
_FS_WEBHOOK_CACHE_TTL = 3 * 60 * 60 # 3小时,单位秒
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def fs_webhook(webhook_url: str, title: str, fs_content: list):
|
|
17
|
+
"""
|
|
18
|
+
发送飞书 webhook 消息。3小时内相同内容不重复发送。
|
|
19
|
+
|
|
20
|
+
:param webhook_url: 飞书 webhook 的 URL 地址
|
|
21
|
+
:param title: 消息的标题
|
|
22
|
+
:param fs_content: 消息内容列表
|
|
23
|
+
"""
|
|
24
|
+
# 构建缓存 key:对 webhook_url + title + content 做 MD5 哈希
|
|
25
|
+
cache_raw = (
|
|
26
|
+
f"{webhook_url}:{title}:"
|
|
27
|
+
f"{json.dumps(fs_content, ensure_ascii=False, sort_keys=True)}"
|
|
28
|
+
)
|
|
29
|
+
cache_key = "fs_webhook:" + hashlib.md5(cache_raw.encode("utf-8")).hexdigest()
|
|
30
|
+
|
|
31
|
+
# 检查 Redis 中是否已存在该 key(3小时内已发送过)
|
|
32
|
+
redis = None
|
|
33
|
+
try:
|
|
34
|
+
redis_oauth_key = app_config.get("REDIS_OAUTH_KEY", "redis")
|
|
35
|
+
redis = RedisHelper(redis_oauth_key)
|
|
36
|
+
if redis.sync_exists(cache_key):
|
|
37
|
+
logger.info("fs_webhook 重复内容,跳过发送,cache_key=%s", cache_key)
|
|
38
|
+
return
|
|
39
|
+
except Exception: # pylint: disable=broad-except
|
|
40
|
+
logger.warning("fs_webhook Redis 检查失败,继续发送")
|
|
41
|
+
|
|
42
|
+
headers = {'Content-Type': 'application/json;charset=utf-8'}
|
|
43
|
+
|
|
44
|
+
# 构造飞书消息的 JSON 结构
|
|
45
|
+
json_text = {
|
|
46
|
+
"msg_type": "post",
|
|
47
|
+
"content": {
|
|
48
|
+
"post": {
|
|
49
|
+
"zh_cn": {
|
|
50
|
+
"title": title,
|
|
51
|
+
"content": [
|
|
52
|
+
fs_content
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# 发送 POST 请求并记录响应结果
|
|
60
|
+
result = requests.post(webhook_url, json.dumps(json_text), headers=headers, timeout=10).content
|
|
61
|
+
logger.info(result)
|
|
62
|
+
|
|
63
|
+
# 发送成功后写入 Redis 缓存,3小时内不再重复发送
|
|
64
|
+
if redis is not None:
|
|
65
|
+
try:
|
|
66
|
+
redis.sync_set(cache_key, "1", ex=_FS_WEBHOOK_CACHE_TTL)
|
|
67
|
+
except Exception: # pylint: disable=broad-except
|
|
68
|
+
logger.warning("fs_webhook Redis 写入缓存失败")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lesscode-flask
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.92
|
|
4
4
|
Summary: lesscode-flask 是基于flask的web开发脚手架项目,该项目初衷为简化开发过程,让研发人员更加关注业务。
|
|
5
5
|
Home-page: https://lesscode-flask
|
|
6
6
|
Author: Chao.yy
|
|
@@ -11,7 +11,7 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Requires-Python: >=3.9
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: Flask==3.0.3
|
|
14
|
-
Requires-Dist: lesscode-utils==0.0.
|
|
14
|
+
Requires-Dist: lesscode-utils==0.0.95
|
|
15
15
|
Requires-Dist: Flask-Login==0.6.3
|
|
16
16
|
Requires-Dist: redis==5.1.1
|
|
17
17
|
Requires-Dist: flask-swagger-ui==4.11.1
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
import requests
|
|
6
|
-
|
|
7
|
-
logger = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
def fs_webhook(webhook_url:str,title:str, fs_content:list):
|
|
10
|
-
"""
|
|
11
|
-
发送飞书 webhook 消息
|
|
12
|
-
|
|
13
|
-
参数:
|
|
14
|
-
webhook_url (str): 飞书 webhook 的 URL 地址
|
|
15
|
-
title (str): 消息的标题
|
|
16
|
-
fs_content (str or list): 消息内容,可以是字符串或字符串列表
|
|
17
|
-
|
|
18
|
-
返回值:
|
|
19
|
-
无返回值,直接发送 HTTP 请求
|
|
20
|
-
"""
|
|
21
|
-
headers = {'Content-Type': 'application/json;charset=utf-8'}
|
|
22
|
-
|
|
23
|
-
# # 根据内容类型构建飞书消息格式
|
|
24
|
-
# if isinstance(content, list):
|
|
25
|
-
# fs_content = []
|
|
26
|
-
# for i in content:
|
|
27
|
-
# fs_content.append({
|
|
28
|
-
# "tag": "text",
|
|
29
|
-
# "text": f"{i}\n"
|
|
30
|
-
# })
|
|
31
|
-
# else:
|
|
32
|
-
# fs_content = [{
|
|
33
|
-
# "tag": "text",
|
|
34
|
-
# "text": f"{content}\n"
|
|
35
|
-
# }]
|
|
36
|
-
|
|
37
|
-
# 构造飞书消息的 JSON 结构
|
|
38
|
-
json_text = {
|
|
39
|
-
"msg_type": "post",
|
|
40
|
-
"content": {
|
|
41
|
-
"post": {
|
|
42
|
-
"zh_cn": {
|
|
43
|
-
"title": title,
|
|
44
|
-
"content": [
|
|
45
|
-
fs_content
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
# 发送 POST 请求并记录响应结果
|
|
53
|
-
result = requests.post(webhook_url, json.dumps(json_text), headers=headers).content
|
|
54
|
-
logger.info(result)
|
|
55
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/export_data/data_download_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/model/resource_param_template.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/service/access_log_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/decorator/sql_injection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/json/NotSortJSONProvider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/limit/req/redis_rate_limiter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/swagger/swagger_template.py
RENAMED
|
File without changes
|
{lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask/utils/swagger/swagger_util.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lesscode_flask-0.2.89 → lesscode_flask-0.2.92}/lesscode_flask.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|