gomyck-tools 1.3.1__py3-none-any.whl → 1.3.2__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 +0 -0
- ctools/aes_tools.py +35 -0
- ctools/api_result.py +55 -0
- ctools/application.py +386 -0
- ctools/b64.py +7 -0
- ctools/bashPath.py +13 -0
- ctools/bottle_web_base.py +169 -0
- ctools/bottle_webserver.py +143 -0
- ctools/bottle_websocket.py +75 -0
- ctools/browser_element_tools.py +314 -0
- ctools/call.py +71 -0
- ctools/cftp.py +74 -0
- ctools/cjson.py +54 -0
- ctools/ckafka.py +159 -0
- ctools/compile_tools.py +18 -0
- ctools/console.py +55 -0
- ctools/coord_trans.py +127 -0
- ctools/credis.py +111 -0
- ctools/cron_lite.py +252 -0
- ctools/ctoken.py +34 -0
- ctools/cword.py +30 -0
- ctools/czip.py +130 -0
- ctools/database.py +185 -0
- ctools/date_utils.py +43 -0
- ctools/dict_wrapper.py +20 -0
- ctools/douglas_rarefy.py +136 -0
- ctools/download_tools.py +57 -0
- ctools/enums.py +4 -0
- ctools/ex.py +31 -0
- ctools/excelOpt.py +36 -0
- ctools/html_soup.py +35 -0
- ctools/http_utils.py +24 -0
- ctools/images_tools.py +27 -0
- ctools/imgDialog.py +44 -0
- ctools/metrics.py +131 -0
- ctools/mqtt_utils.py +289 -0
- ctools/obj.py +20 -0
- ctools/pacth.py +74 -0
- ctools/plan_area_tools.py +97 -0
- ctools/process_pool.py +36 -0
- ctools/pty_tools.py +72 -0
- ctools/resource_bundle_tools.py +121 -0
- ctools/rsa.py +70 -0
- ctools/screenshot_tools.py +127 -0
- ctools/sign.py +20 -0
- ctools/sm_tools.py +49 -0
- ctools/snow_id.py +76 -0
- ctools/str_diff.py +20 -0
- ctools/string_tools.py +85 -0
- ctools/sys_info.py +157 -0
- ctools/sys_log.py +89 -0
- ctools/thread_pool.py +35 -0
- ctools/upload_tools.py +40 -0
- ctools/win_canvas.py +83 -0
- ctools/win_control.py +106 -0
- ctools/word_fill.py +562 -0
- ctools/word_fill_entity.py +46 -0
- ctools/work_path.py +69 -0
- {gomyck_tools-1.3.1.dist-info → gomyck_tools-1.3.2.dist-info}/METADATA +1 -1
- gomyck_tools-1.3.2.dist-info/RECORD +62 -0
- gomyck_tools-1.3.2.dist-info/top_level.txt +1 -0
- gomyck_tools-1.3.1.dist-info/RECORD +0 -4
- gomyck_tools-1.3.1.dist-info/top_level.txt +0 -1
- {gomyck_tools-1.3.1.dist-info → gomyck_tools-1.3.2.dist-info}/WHEEL +0 -0
ctools/douglas_rarefy.py
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: UTF-8 -*-
|
3
|
+
__author__ = 'haoyang'
|
4
|
+
__date__ = '2024/9/19 14:02'
|
5
|
+
|
6
|
+
import math
|
7
|
+
|
8
|
+
from jsonpath_ng import parser
|
9
|
+
|
10
|
+
from ctools import cjson
|
11
|
+
from ctools.sys_log import flog as log
|
12
|
+
|
13
|
+
"""
|
14
|
+
douglas_rarefy.DouglasRarefy(res, level=3).sparse_points()
|
15
|
+
"""
|
16
|
+
|
17
|
+
class THIN_LEVEL:
|
18
|
+
L1 = 0.00001
|
19
|
+
L2 = 0.00003
|
20
|
+
L3 = 0.00009
|
21
|
+
L4 = 0.0002
|
22
|
+
L5 = 0.0004
|
23
|
+
L6 = 0.0007
|
24
|
+
L7 = 0.0011
|
25
|
+
L8 = 0.0017
|
26
|
+
L9 = 0.0022
|
27
|
+
|
28
|
+
class Point:
|
29
|
+
def __init__(self, lng, lat, origin_data):
|
30
|
+
self.lng = lng
|
31
|
+
self.lat = lat
|
32
|
+
self.origin_data = origin_data
|
33
|
+
|
34
|
+
def _get_line_by_point(xy1, xy2):
|
35
|
+
"""
|
36
|
+
根据两个点求直线方程 ax + by + c = 0
|
37
|
+
:param xy1: 点1, 例如 {'lat': 1, 'lng': 1}
|
38
|
+
:param xy2: 点2, 例如 {'lat': 2, 'lng': 2}
|
39
|
+
:return: 直线方程的三个参数 [a, b, c]
|
40
|
+
"""
|
41
|
+
x1 = xy1.lng
|
42
|
+
y1 = xy1.lat
|
43
|
+
x2 = xy2.lng
|
44
|
+
y2 = xy2.lat
|
45
|
+
a = y2 - y1
|
46
|
+
b = x1 - x2
|
47
|
+
c = (y1 - y2) * x1 - y1 * (x1 - x2)
|
48
|
+
return [a, b, c]
|
49
|
+
|
50
|
+
|
51
|
+
def _get_distance_from_point_to_line(a, b, c, xy):
|
52
|
+
"""
|
53
|
+
点到直线的距离,直线方程为 ax + by + c = 0
|
54
|
+
:param a: 直线参数a
|
55
|
+
:param b: 直线参数b
|
56
|
+
:param c: 直线参数c
|
57
|
+
:param xy: 点坐标,例如 {'lat': 2, 'lng': 2}
|
58
|
+
:return: 距离
|
59
|
+
"""
|
60
|
+
x = xy.lng
|
61
|
+
y = xy.lat
|
62
|
+
return abs((a * x + b * y + c) / math.sqrt(a * a + b * b))
|
63
|
+
|
64
|
+
|
65
|
+
class DouglasRarefy:
|
66
|
+
"""
|
67
|
+
DouglasRarefy Use Guide:
|
68
|
+
points must be arrays, element can be dict or arrays, when element is arrays, index 0 must be lng, index 1 must be lat, and element can be use max column num is 2 (lng, lat) in ret_tpl
|
69
|
+
level default is L2, this level can be hold most of the points detail
|
70
|
+
ret_tpl is the result tpl, can be arrays and json or some can be json loads, exp: [{lng}, {lat}] OR {{"lng": {lng}, "lat": {lat}}}
|
71
|
+
"""
|
72
|
+
def __init__(self, points:[], level=THIN_LEVEL.L2, ret_tpl=None, get_lng=None, get_lat=None):
|
73
|
+
if not isinstance(points, list): raise Exception('points must be list obj !!')
|
74
|
+
if len(points) < 3: raise Exception('points length must be gt 2 !!')
|
75
|
+
self.points = points
|
76
|
+
self.threshold = THIN_LEVEL.L2 if level is None else (getattr(THIN_LEVEL, "L{}".format(int(level))) if int(level) >= 1 else level)
|
77
|
+
log.debug("threshold is: {}".format(self.threshold))
|
78
|
+
self.is_json = isinstance(points[0], dict)
|
79
|
+
self.get_lng = get_lng
|
80
|
+
self.get_lat = get_lat
|
81
|
+
if self.is_json:
|
82
|
+
if not self.get_lng: self.get_lng = '$.lng'
|
83
|
+
if not self.get_lat: self.get_lat = '$.lat'
|
84
|
+
else:
|
85
|
+
if not self.get_lng: self.get_lng = '$.[0]'
|
86
|
+
if not self.get_lat: self.get_lat = '$.[1]'
|
87
|
+
log.debug("get_lng is: {}, get_lat is: {}".format(self.get_lng, self.get_lat))
|
88
|
+
self.lng_parser = parser.parse(self.get_lng)
|
89
|
+
self.lat_parser = parser.parse(self.get_lat)
|
90
|
+
log.debug("is_json is: {}".format(self.is_json))
|
91
|
+
self.ret_tpl = ret_tpl
|
92
|
+
log.debug("ret_tpl is: {}".format(self.ret_tpl))
|
93
|
+
self.data = [Point(self.lng_parser.find(p)[0].value, self.lat_parser.find(p)[0].value, p) for p in self.points]
|
94
|
+
|
95
|
+
def _sparse_points(self, points):
|
96
|
+
"""
|
97
|
+
点位压缩
|
98
|
+
:return: 稀疏后的点集
|
99
|
+
"""
|
100
|
+
if len(points) < 3:
|
101
|
+
if not self.ret_tpl:
|
102
|
+
return [points[0].origin_data, points[-1].origin_data]
|
103
|
+
else:
|
104
|
+
if self.is_json:
|
105
|
+
return [cjson.loads(self.ret_tpl.format(**points[0].origin_data)), cjson.loads(self.ret_tpl.format(**points[-1].origin_data))]
|
106
|
+
else:
|
107
|
+
return [cjson.loads(self.ret_tpl.format(lng=points[0].lng, lat=points[0].lat)), cjson.loads(self.ret_tpl.format(lng=points[-1].lng, lat=points[-1].lat))]
|
108
|
+
|
109
|
+
xy_first = points[0] # 第一个点
|
110
|
+
xy_end = points[-1] # 最后一个点
|
111
|
+
a, b, c = _get_line_by_point(xy_first, xy_end) # 获取直线方程的 a, b, c 值
|
112
|
+
d_max = 0 # 记录点到直线的最大距离
|
113
|
+
split = 0 # 分割位置
|
114
|
+
for i in range(1, len(points) - 1):
|
115
|
+
d = _get_distance_from_point_to_line(a, b, c, points[i])
|
116
|
+
if d > d_max:
|
117
|
+
split = i
|
118
|
+
d_max = d
|
119
|
+
if d_max > self.threshold:
|
120
|
+
# 如果存在点到首位点连成直线的距离大于 max_distance 的, 即需要再次划分
|
121
|
+
child_left = self._sparse_points(points[:split + 1]) # 递归处理左边部分
|
122
|
+
child_right = self._sparse_points(points[split:]) # 递归处理右边部分
|
123
|
+
# 合并结果,避免重复
|
124
|
+
return child_left + child_right[1:]
|
125
|
+
else:
|
126
|
+
if not self.ret_tpl:
|
127
|
+
return [points[0].origin_data, points[-1].origin_data]
|
128
|
+
else:
|
129
|
+
if self.is_json:
|
130
|
+
return [cjson.loads(self.ret_tpl.format(**points[0].origin_data)), cjson.loads(self.ret_tpl.format(**points[-1].origin_data))]
|
131
|
+
else:
|
132
|
+
return [cjson.loads(self.ret_tpl.format(lng=points[0].lng, lat=points[0].lat)), cjson.loads(self.ret_tpl.format(lng=points[-1].lng, lat=points[-1].lat))]
|
133
|
+
|
134
|
+
def sparse_points(self):
|
135
|
+
return self._sparse_points(self.data)
|
136
|
+
|
ctools/download_tools.py
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
import os
|
2
|
+
from urllib.parse import urlencode
|
3
|
+
|
4
|
+
from bottle import static_file, HTTPResponse
|
5
|
+
|
6
|
+
from ctools import sys_log, http_utils
|
7
|
+
|
8
|
+
log = sys_log.flog
|
9
|
+
|
10
|
+
"""
|
11
|
+
文件下载服务
|
12
|
+
"""
|
13
|
+
|
14
|
+
|
15
|
+
def download(file_path: str, download_name:str=None):
|
16
|
+
"""
|
17
|
+
文件下载
|
18
|
+
:param file_path: 静态文件路径
|
19
|
+
:param download_name: 下载文件名
|
20
|
+
:return:
|
21
|
+
"""
|
22
|
+
if os.path.exists(file_path):
|
23
|
+
root_path = os.path.split(file_path)[0]
|
24
|
+
file_name = os.path.split(file_path)[1]
|
25
|
+
download_filename = urlencode({'filename': download_name or file_name}).split("=")[-1] # 对文件名进行URL编码
|
26
|
+
response = static_file(file_name, root=root_path, download=True)
|
27
|
+
# 设置响应头,告诉浏览器这是一个文件下载
|
28
|
+
response.headers['Content-Type'] = 'application/octet-stream;charset=utf-8'
|
29
|
+
response.headers['Content-Disposition'] = f'attachment; filename={download_filename}'
|
30
|
+
log.debug(f"下载文件成功, file_path: {file_path}, file_name: {file_name}, download_name: {download_name}")
|
31
|
+
else:
|
32
|
+
response = None
|
33
|
+
log.info("下载文件失败, 此文件不存在, file_path: %s" % file_path)
|
34
|
+
return response
|
35
|
+
|
36
|
+
|
37
|
+
def download_bytes(file_bytes: bytes, download_name: str):
|
38
|
+
"""
|
39
|
+
文件下载
|
40
|
+
:param file_bytes: file_bytes
|
41
|
+
:param download_name: download_name
|
42
|
+
:return:
|
43
|
+
"""
|
44
|
+
download_filename = urlencode({'filename': download_name}).split("=")[-1] # 对文件名进行URL编码
|
45
|
+
# 设置响应头,告诉浏览器这是一个文件下载
|
46
|
+
headers = {"Accept-Ranges": "bytes", "Content-Length": len(file_bytes),
|
47
|
+
'Content-Type': 'application/octet-stream;charset=utf-8',
|
48
|
+
'Content-Disposition': f'attachment; filename={download_filename}'}
|
49
|
+
return HTTPResponse(file_bytes, **headers)
|
50
|
+
|
51
|
+
|
52
|
+
def download_url(url: str, save_path: str):
|
53
|
+
content = http_utils.get(url)
|
54
|
+
if content:
|
55
|
+
with open(save_path, "wb") as f:
|
56
|
+
f.write(content)
|
57
|
+
return save_path
|
ctools/enums.py
ADDED
ctools/ex.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
import time
|
2
|
+
import traceback
|
3
|
+
from functools import wraps
|
4
|
+
|
5
|
+
# annotation
|
6
|
+
def exception_handler(fail_return, retry_num=0, delay=3, catch_e=Exception, print_exc=False):
|
7
|
+
def decorator(func):
|
8
|
+
@wraps(func)
|
9
|
+
def wrapper(*args, **kwargs):
|
10
|
+
try:
|
11
|
+
return func(*args, **kwargs)
|
12
|
+
except catch_e as e:
|
13
|
+
print(f"{func.__name__} runtime exception: {str(e)}")
|
14
|
+
if print_exc: traceback.print_exc()
|
15
|
+
nonlocal retry_num
|
16
|
+
renum = retry_num
|
17
|
+
if renum == 0:
|
18
|
+
return fail_return
|
19
|
+
else:
|
20
|
+
while renum > 0:
|
21
|
+
time.sleep(delay)
|
22
|
+
renum -= 1
|
23
|
+
try:
|
24
|
+
return func(*args, **kwargs)
|
25
|
+
except catch_e:
|
26
|
+
pass
|
27
|
+
return fail_return
|
28
|
+
|
29
|
+
return wrapper
|
30
|
+
|
31
|
+
return decorator
|
ctools/excelOpt.py
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
from openpyxl import load_workbook
|
2
|
+
from openpyxl.worksheet.datavalidation import DataValidation
|
3
|
+
|
4
|
+
|
5
|
+
class excelUtil:
|
6
|
+
wb = None
|
7
|
+
sourcePath = None
|
8
|
+
savePath = None
|
9
|
+
|
10
|
+
def __init__(self, path, save_path):
|
11
|
+
# 创建一个 Workbook 对象
|
12
|
+
self.wb = load_workbook(path)
|
13
|
+
# 在 Workbook 中创建一个 Worksheet 对象
|
14
|
+
self.ws = self.wb.active
|
15
|
+
self.sourcePath = path
|
16
|
+
self.savePath = save_path
|
17
|
+
|
18
|
+
def makeDropData(self, col, drop_data):
|
19
|
+
# 定义下拉框的数据
|
20
|
+
dropdown_items = drop_data
|
21
|
+
# 将下拉框数据转换成字符串
|
22
|
+
dropdown_items_str = ','.join(dropdown_items)
|
23
|
+
# 在第一列中添加下拉框
|
24
|
+
dropdown_col = col
|
25
|
+
dropdown_start_row = 2
|
26
|
+
dropdown_end_row = 200
|
27
|
+
# 配置下拉框参数
|
28
|
+
dropdown = DataValidation(type="list", formula1=f'"{dropdown_items_str}"', allow_blank=True)
|
29
|
+
# 添加下拉框到指定的单元格区域
|
30
|
+
dropdown_range = f"{dropdown_col}{dropdown_start_row}:{dropdown_col}{dropdown_end_row}"
|
31
|
+
self.ws.add_data_validation(dropdown)
|
32
|
+
dropdown.add(dropdown_range)
|
33
|
+
|
34
|
+
def save(self):
|
35
|
+
# 保存工作簿
|
36
|
+
self.wb.save(self.savePath)
|
ctools/html_soup.py
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
from bs4 import BeautifulSoup
|
2
|
+
|
3
|
+
from ctools.ex import exception_handler
|
4
|
+
|
5
|
+
|
6
|
+
@exception_handler(fail_return=['解析错误'], print_exc=True)
|
7
|
+
def table2list(html, include_header=True, recursive_find=True,
|
8
|
+
table_tag='table', table_class=None, table_attrs: dict = {},
|
9
|
+
row_tag='tr', row_class=None, row_attrs: dict = {},
|
10
|
+
header_cell_tag='th', header_cell_class=None, header_cell_attrs: dict = {},
|
11
|
+
cell_tag='td', cell_class=None, cell_attrs: dict = {}):
|
12
|
+
soup = BeautifulSoup(markup=html, features='html.parser')
|
13
|
+
if table_class:
|
14
|
+
table = soup.find(table_tag, class_=table_class, **table_attrs)
|
15
|
+
else:
|
16
|
+
table = soup.find(table_tag, **table_attrs)
|
17
|
+
if row_class:
|
18
|
+
all_row = table.find_all(row_tag, class_=row_class, recursive=recursive_find, **row_attrs)
|
19
|
+
else:
|
20
|
+
all_row = table.find_all(row_tag, recursive=recursive_find, **row_attrs)
|
21
|
+
rows = []
|
22
|
+
if include_header:
|
23
|
+
if header_cell_class:
|
24
|
+
header = [i.text for i in all_row[0].find_all(header_cell_tag, class_=header_cell_class, recursive=recursive_find, **header_cell_attrs)]
|
25
|
+
else:
|
26
|
+
header = [i.text for i in all_row[0].find_all(header_cell_tag, recursive=recursive_find, **header_cell_attrs)]
|
27
|
+
rows.append(header)
|
28
|
+
for tr in all_row[1 if include_header else 0:]:
|
29
|
+
if cell_class:
|
30
|
+
td = tr.find_all(cell_tag, class_=cell_class, recursive=recursive_find, **cell_attrs)
|
31
|
+
else:
|
32
|
+
td = tr.find_all(cell_tag, recursive=recursive_find, **cell_attrs)
|
33
|
+
row = [i.text for i in td]
|
34
|
+
rows.append(row)
|
35
|
+
return rows
|
ctools/http_utils.py
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
import requests
|
2
|
+
|
3
|
+
|
4
|
+
def get(url, params=None, headers=None):
|
5
|
+
result = ""
|
6
|
+
try:
|
7
|
+
response = requests.get(url, params=params, headers=headers, timeout=60, verify=False)
|
8
|
+
response.raise_for_status()
|
9
|
+
if response.status_code == 200:
|
10
|
+
result = response.content
|
11
|
+
except Exception as e:
|
12
|
+
print("GET请求异常:", e)
|
13
|
+
if isinstance(result, bytes): return result.decode('utf-8')
|
14
|
+
return result
|
15
|
+
|
16
|
+
|
17
|
+
def post(url, data=None, json=None, headers=None, files=None):
|
18
|
+
result = ""
|
19
|
+
response = requests.post(url, data=data, json=json, files=files, headers=headers, timeout=60, verify=False)
|
20
|
+
response.raise_for_status()
|
21
|
+
if response.status_code == 200:
|
22
|
+
result = response.content
|
23
|
+
if isinstance(result, bytes): return result.decode('utf-8')
|
24
|
+
return result
|
ctools/images_tools.py
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
from io import BytesIO
|
2
|
+
|
3
|
+
from PIL import Image
|
4
|
+
|
5
|
+
|
6
|
+
def get_size(image_path):
|
7
|
+
return Image.open(image_path).size
|
8
|
+
|
9
|
+
def change_color(image_path, area=None, rgb_color=None):
|
10
|
+
"""
|
11
|
+
修改图片指定区域颜色
|
12
|
+
:param image_path: 图片路径
|
13
|
+
:param area: 修改区域: (x1, y1, x2, y2)
|
14
|
+
:param rgb_color: 入盘颜色 (255, 0, 0)
|
15
|
+
:return:
|
16
|
+
"""
|
17
|
+
with Image.open(image_path) as img:
|
18
|
+
if area:
|
19
|
+
pixels = img.load()
|
20
|
+
for x in range(area[0], area[2]):
|
21
|
+
for y in range(area[1], area[3]):
|
22
|
+
pixels[x, y] = rgb_color
|
23
|
+
img_bytes = BytesIO()
|
24
|
+
img.save(img_bytes, format='JPEG')
|
25
|
+
img_binary = img_bytes.getvalue()
|
26
|
+
return img_binary
|
27
|
+
|
ctools/imgDialog.py
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
import tkinter
|
2
|
+
import tkinter as tk
|
3
|
+
from io import BytesIO
|
4
|
+
from tkinter import ttk
|
5
|
+
|
6
|
+
import requests
|
7
|
+
from PIL import Image, ImageTk
|
8
|
+
|
9
|
+
def showImageTip(root, title, imagePath, tips):
|
10
|
+
# 创建一个Tk对象
|
11
|
+
if root:
|
12
|
+
window = root
|
13
|
+
else:
|
14
|
+
window = tk.Tk()
|
15
|
+
# 设置窗口大小和位置
|
16
|
+
win_width = 400
|
17
|
+
win_height = 480
|
18
|
+
screen_width = window.winfo_screenwidth()
|
19
|
+
screen_height = window.winfo_screenheight()
|
20
|
+
x = int((screen_width - win_width) / 2)
|
21
|
+
y = int((screen_height - win_height) / 2)
|
22
|
+
window.geometry("{}x{}+{}+{}".format(win_width, win_height, x, y))
|
23
|
+
|
24
|
+
# 设置窗口大小和标题
|
25
|
+
window.title(title)
|
26
|
+
|
27
|
+
# 创建一个Label控件用于显示图片
|
28
|
+
resp = requests.get(imagePath)
|
29
|
+
image = Image.open(BytesIO(resp.content)) # 替换你自己的图片路径
|
30
|
+
image = image.resize((400, 400))
|
31
|
+
photo = ImageTk.PhotoImage(image)
|
32
|
+
label1 = ttk.Label(window, image=photo)
|
33
|
+
label1.pack(side=tkinter.TOP)
|
34
|
+
|
35
|
+
# 创建一个Label控件用于显示提示文字
|
36
|
+
label2 = ttk.Label(window, text=tips, font=("Arial Bold", 16))
|
37
|
+
label2.config(anchor='center', justify='center')
|
38
|
+
label2.pack(side=tkinter.BOTTOM)
|
39
|
+
# 显示窗口
|
40
|
+
window.mainloop()
|
41
|
+
|
42
|
+
|
43
|
+
if __name__ == '__main__':
|
44
|
+
showImageTip(root=None, title='在线授权', imagePath='https://blog.gomyck.com/img/pay-img/wechatPay2Me.jpg', tips='{}\n授权已失效,请联系微信:\n{}'.format(123, 123))
|
ctools/metrics.py
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
import os
|
2
|
+
import threading
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
from prometheus_client import Counter, Gauge, Summary, Histogram, start_http_server
|
6
|
+
from ctools import call, cjson, sys_log, work_path
|
7
|
+
|
8
|
+
log = sys_log.flog
|
9
|
+
|
10
|
+
_metrics_port = 8889
|
11
|
+
_persistent_json = {}
|
12
|
+
_metrics_initial = {}
|
13
|
+
_lock = threading.Lock()
|
14
|
+
_metrics_persistent_path = os.path.join(work_path.get_user_work_path('metrics', True), 'persistent.json')
|
15
|
+
|
16
|
+
# 认证中间件
|
17
|
+
# @app.before_request
|
18
|
+
# def check_authentication():
|
19
|
+
# auth = request.authorization
|
20
|
+
# if not auth or auth.username != USERNAME or auth.password != PASSWORD:
|
21
|
+
# return Response(
|
22
|
+
# "Unauthorized", 401, {"WWW-Authenticate": 'Basic realm="Login Required"'}
|
23
|
+
# )
|
24
|
+
|
25
|
+
class MetricType(Enum):
|
26
|
+
COUNTER = 'counter'
|
27
|
+
GAUGE = 'gauge'
|
28
|
+
SUMMARY = 'summary'
|
29
|
+
HISTOGRAM = 'histogram'
|
30
|
+
|
31
|
+
class Metric:
|
32
|
+
def __init__(self, metric_type: MetricType, metric_key: str, metric_labels: [], persistent: bool = False, buckets: [] = None, reset: bool = False, desc: str = ""):
|
33
|
+
self.metric_type = metric_type
|
34
|
+
self.metric_key = metric_key
|
35
|
+
self.metric_labels = metric_labels
|
36
|
+
self.buckets = buckets
|
37
|
+
self.metric = None
|
38
|
+
self.persistent = persistent
|
39
|
+
self.reset = reset
|
40
|
+
if metric_type == MetricType.COUNTER:
|
41
|
+
self.metric = Counter(metric_key, desc, metric_labels)
|
42
|
+
elif metric_type == MetricType.GAUGE:
|
43
|
+
self.metric = Gauge(metric_key, desc, metric_labels)
|
44
|
+
elif metric_type == MetricType.SUMMARY:
|
45
|
+
self.metric = Summary(metric_key, desc, metric_labels)
|
46
|
+
elif metric_type == MetricType.HISTOGRAM:
|
47
|
+
if buckets is None: raise Exception('histogram buckets can not empty')
|
48
|
+
self.metric = Histogram(metric_key, desc, metric_labels, buckets=self.buckets)
|
49
|
+
else:
|
50
|
+
raise Exception('metric type not found')
|
51
|
+
_metrics_initial[metric_key] = self
|
52
|
+
|
53
|
+
@call.once
|
54
|
+
def init(reset_persistent: bool = False):
|
55
|
+
if os.path.exists(_metrics_persistent_path) and not reset_persistent:
|
56
|
+
with open(_metrics_persistent_path, 'r') as persistent_file:
|
57
|
+
global _persistent_json
|
58
|
+
try:
|
59
|
+
content = persistent_file.readline()
|
60
|
+
log.info("persistent初始化: %s" % content)
|
61
|
+
_persistent_json = cjson.loads(content)
|
62
|
+
except Exception:
|
63
|
+
log.error('persistent.json is not valid json!!!!!')
|
64
|
+
_persistent_json = {}
|
65
|
+
_init_all_metrics()
|
66
|
+
_persistent_json = _persistent_json or {}
|
67
|
+
for key, item in _persistent_json.items():
|
68
|
+
metrics_key = key.split("-")[0]
|
69
|
+
if '_labels' in key or metrics_key not in _metrics_initial: continue
|
70
|
+
opt(metrics_key, _persistent_json[key + '_labels'], _persistent_json[key])
|
71
|
+
persistent_metrics()
|
72
|
+
start_http_server(port=_metrics_port)
|
73
|
+
|
74
|
+
@call.schd(5, start_by_call=True)
|
75
|
+
def persistent_metrics():
|
76
|
+
if _persistent_json and not _lock.locked():
|
77
|
+
log.info('begin persistent metrics to file...')
|
78
|
+
with open(_metrics_persistent_path, 'w') as persistent_file:
|
79
|
+
persistent_file.write(cjson.dumps(_persistent_json))
|
80
|
+
persistent_file.flush()
|
81
|
+
|
82
|
+
def opt(metric_key: str, label_values: [], metric_value: int):
|
83
|
+
_lock.acquire(timeout=5)
|
84
|
+
try:
|
85
|
+
persistent_key = "%s-%s" % (metric_key, "_".join(map(str, label_values)))
|
86
|
+
metric_entity: Metric = _metrics_initial[metric_key]
|
87
|
+
if metric_entity.persistent:
|
88
|
+
if not metric_entity.reset and persistent_key in _persistent_json:
|
89
|
+
_persistent_json[persistent_key] += metric_value
|
90
|
+
else:
|
91
|
+
_persistent_json[persistent_key] = metric_value
|
92
|
+
_persistent_json[persistent_key + '_labels'] = label_values
|
93
|
+
|
94
|
+
if _persistent_json[persistent_key] < 0:
|
95
|
+
_persistent_json[persistent_key] = 0
|
96
|
+
metric_value = 0
|
97
|
+
|
98
|
+
if metric_entity.metric_type == MetricType.COUNTER or metric_entity.metric_type == MetricType.GAUGE:
|
99
|
+
if label_values is None or len(label_values) == 0:
|
100
|
+
if metric_entity.metric_type == MetricType.COUNTER and metric_entity.reset:
|
101
|
+
metric_entity.metric.labels('').reset()
|
102
|
+
if metric_entity.metric_type == MetricType.GAUGE and metric_entity.reset:
|
103
|
+
metric_entity.metric.labels('').set(0)
|
104
|
+
metric_entity.metric.labels('').inc(metric_value)
|
105
|
+
else:
|
106
|
+
if metric_entity.reset:
|
107
|
+
if metric_entity.metric_type == MetricType.COUNTER and metric_entity.reset:
|
108
|
+
metric_entity.metric.labels(*label_values).reset()
|
109
|
+
if metric_entity.metric_type == MetricType.GAUGE and metric_entity.reset:
|
110
|
+
metric_entity.metric.labels(*label_values).set(0)
|
111
|
+
metric_entity.metric.labels(*label_values).inc(metric_value)
|
112
|
+
else:
|
113
|
+
if label_values is None or len(label_values) == 0:
|
114
|
+
metric_entity.metric.observe(metric_value)
|
115
|
+
else:
|
116
|
+
metric_entity.metric.labels(*label_values).observe(metric_value)
|
117
|
+
except Exception as e:
|
118
|
+
log.error("添加指标信息异常: %s" % e)
|
119
|
+
_lock.release()
|
120
|
+
|
121
|
+
def _init_all_metrics():
|
122
|
+
Metric(MetricType.GAUGE, 'gomyck', ['g_label1', 'g_label2'], persistent=True, reset=True)
|
123
|
+
|
124
|
+
# if __name__ == '__main__':
|
125
|
+
# init()
|
126
|
+
# import random
|
127
|
+
# while True:
|
128
|
+
# opt('data_reported_time', ['123', '123'], random.randint(1, 10))
|
129
|
+
# opt('data_received_time', ['123', '123'], random.randint(1, 10))
|
130
|
+
# opt('data_insert_time', ['123', '123'], random.randint(1, 10))
|
131
|
+
# time.sleep(1)
|