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.
Files changed (64) hide show
  1. ctools/__init__.py +0 -0
  2. ctools/aes_tools.py +35 -0
  3. ctools/api_result.py +55 -0
  4. ctools/application.py +386 -0
  5. ctools/b64.py +7 -0
  6. ctools/bashPath.py +13 -0
  7. ctools/bottle_web_base.py +169 -0
  8. ctools/bottle_webserver.py +143 -0
  9. ctools/bottle_websocket.py +75 -0
  10. ctools/browser_element_tools.py +314 -0
  11. ctools/call.py +71 -0
  12. ctools/cftp.py +74 -0
  13. ctools/cjson.py +54 -0
  14. ctools/ckafka.py +159 -0
  15. ctools/compile_tools.py +18 -0
  16. ctools/console.py +55 -0
  17. ctools/coord_trans.py +127 -0
  18. ctools/credis.py +111 -0
  19. ctools/cron_lite.py +252 -0
  20. ctools/ctoken.py +34 -0
  21. ctools/cword.py +30 -0
  22. ctools/czip.py +130 -0
  23. ctools/database.py +185 -0
  24. ctools/date_utils.py +43 -0
  25. ctools/dict_wrapper.py +20 -0
  26. ctools/douglas_rarefy.py +136 -0
  27. ctools/download_tools.py +57 -0
  28. ctools/enums.py +4 -0
  29. ctools/ex.py +31 -0
  30. ctools/excelOpt.py +36 -0
  31. ctools/html_soup.py +35 -0
  32. ctools/http_utils.py +24 -0
  33. ctools/images_tools.py +27 -0
  34. ctools/imgDialog.py +44 -0
  35. ctools/metrics.py +131 -0
  36. ctools/mqtt_utils.py +289 -0
  37. ctools/obj.py +20 -0
  38. ctools/pacth.py +74 -0
  39. ctools/plan_area_tools.py +97 -0
  40. ctools/process_pool.py +36 -0
  41. ctools/pty_tools.py +72 -0
  42. ctools/resource_bundle_tools.py +121 -0
  43. ctools/rsa.py +70 -0
  44. ctools/screenshot_tools.py +127 -0
  45. ctools/sign.py +20 -0
  46. ctools/sm_tools.py +49 -0
  47. ctools/snow_id.py +76 -0
  48. ctools/str_diff.py +20 -0
  49. ctools/string_tools.py +85 -0
  50. ctools/sys_info.py +157 -0
  51. ctools/sys_log.py +89 -0
  52. ctools/thread_pool.py +35 -0
  53. ctools/upload_tools.py +40 -0
  54. ctools/win_canvas.py +83 -0
  55. ctools/win_control.py +106 -0
  56. ctools/word_fill.py +562 -0
  57. ctools/word_fill_entity.py +46 -0
  58. ctools/work_path.py +69 -0
  59. {gomyck_tools-1.3.1.dist-info → gomyck_tools-1.3.2.dist-info}/METADATA +1 -1
  60. gomyck_tools-1.3.2.dist-info/RECORD +62 -0
  61. gomyck_tools-1.3.2.dist-info/top_level.txt +1 -0
  62. gomyck_tools-1.3.1.dist-info/RECORD +0 -4
  63. gomyck_tools-1.3.1.dist-info/top_level.txt +0 -1
  64. {gomyck_tools-1.3.1.dist-info → gomyck_tools-1.3.2.dist-info}/WHEEL +0 -0
ctools/cjson.py ADDED
@@ -0,0 +1,54 @@
1
+ import jsonpickle
2
+
3
+ # 需要转换成str的属性
4
+ str_value_keys = []
5
+ jsonpickle.set_preferred_backend('json')
6
+ jsonpickle.set_encoder_options('json', ensure_ascii=False)
7
+ jsonpickle.set_decoder_options('json')
8
+
9
+ def dumps(obj, **kwargs) -> str:
10
+ """
11
+ 将对象转换为json字符串
12
+ :param obj: 对象
13
+ :return: json 字符串
14
+ """
15
+ # indent = 2 可以美化输出
16
+ if obj is None: return None
17
+ if type(obj) == str: return obj
18
+ return f'{jsonpickle.encode(obj, unpicklable=False, make_refs=False, **kwargs)}'
19
+
20
+ def loads(json_str: str, **kwargs) -> dict:
21
+ """
22
+ 将json字符串转换为对象
23
+ :param json_str: json 字符串
24
+ :return: 对象
25
+ """
26
+ return jsonpickle.decode(json_str, **kwargs)
27
+
28
+ def unify_to_str(json_str: str) -> str:
29
+ if not str_value_keys and len(str_value_keys) == 0: return json_str
30
+ obj = loads(json_str)
31
+ if isinstance(obj, list):
32
+ _handle_list(obj)
33
+ elif isinstance(obj, dict):
34
+ _handle_dict(obj)
35
+ return dumps(obj)
36
+
37
+ def _handle_list(data):
38
+ for o in data:
39
+ if isinstance(o, list):
40
+ _handle_list(o)
41
+ elif isinstance(o, dict):
42
+ _handle_dict(o)
43
+
44
+ def _handle_dict(data):
45
+ for k, v in data.items():
46
+ if isinstance(v, list):
47
+ _handle_list(v)
48
+ elif isinstance(v, dict):
49
+ _handle_dict(v)
50
+ elif k in str_value_keys:
51
+ try:
52
+ data[k] = str(v)
53
+ except Exception:
54
+ pass
ctools/ckafka.py ADDED
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ __author__ = 'haoyang'
4
+ __date__ = '2024/9/5 10:39'
5
+
6
+ import time
7
+ from threading import Thread, Lock
8
+
9
+ from kafka import KafkaProducer, errors, KafkaConsumer
10
+ from kafka.producer.future import FutureRecordMetadata
11
+
12
+ from ctools import thread_pool
13
+ from ctools.cjson import dumps
14
+
15
+ """
16
+ import time
17
+ from datetime import datetime
18
+
19
+ from ctools import thread_pool, string_tools
20
+ from ctools.ckafka import CKafka
21
+
22
+ c = CKafka(kafka_url='192.168.3.160:9094', secure=True)
23
+
24
+ producer = c.init_producer()
25
+ consumer = c.init_consumer(enable_auto_commit=False)
26
+
27
+ def send_msg():
28
+ while True:
29
+ command = input('发送消息: Y/n \n')
30
+ if command.strip() not in ['N', 'n']:
31
+ producer.send_msg('jqxx', '{{"jqid": "{}", "xxxx": "{}"}}'.format(string_tools.get_snowflake_id(), datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S')))
32
+ else:
33
+ break
34
+
35
+ thread_pool.submit(send_msg)
36
+
37
+ def consumer_callback(msg):
38
+ print(msg)
39
+ return True
40
+
41
+ consumer.receive_msg('jqxx', callBack=consumer_callback)
42
+
43
+ while True: time.sleep(1)
44
+ """
45
+ class KafkaInstance:
46
+ def __init__(self, producer: KafkaProducer, consumer: KafkaConsumer):
47
+ self.start_consumer = False
48
+ self.quited = False
49
+ self.producer = producer
50
+ self.consumer = consumer
51
+ self.consumer_callback = {"topic_key": []}
52
+
53
+ # FutureRecordMetadata 可以添加回调, 来监听是否发送成功
54
+ # r.add_callback(lambda x: print(x))
55
+ # r.get() 可以同步获取结果
56
+ def send_msg(self, topic, msg, key: str=None, partition:int=None) -> FutureRecordMetadata:
57
+ if self.producer is None: raise RuntimeError("Producer is not initialized")
58
+ if self.quited: return
59
+ return self.producer.send(topic=topic, value=msg, key=None if key is None else key.encode('utf-8'), partition=partition)
60
+
61
+ def receive_msg(self, topics: str, callBack=print):
62
+ if self.consumer is None: raise RuntimeError("Consumer is not initialized")
63
+ for topic in topics.split(','):
64
+ if topic not in self.consumer_callback.keys():
65
+ self.consumer_callback[topic] = []
66
+ self.consumer.subscribe(self.consumer_callback.keys())
67
+ self.consumer_callback[topic].append(callBack)
68
+ if not self.start_consumer:
69
+ t = Thread(target=self._start_consumer_poll, daemon=True)
70
+ t.start()
71
+
72
+ def _start_consumer_poll(self):
73
+ self.start_consumer = True
74
+ for msg in self.consumer:
75
+ if self.quited: break
76
+ funcList = []
77
+ begin_time = time.time()
78
+ for func in self.consumer_callback[msg.topic]:
79
+ if self.quited: break
80
+ res = func(msg)
81
+ if not self.consumer.config['enable_auto_commit'] and res: self.consumer.commit()
82
+ funcList.append(func.__name__)
83
+ end_time = time.time()
84
+ if end_time - begin_time > 1: print(f"kafka consume too slow!!! {funcList} time cost: ", f'{round(end_time - begin_time, 2)}s')
85
+ funcList.clear()
86
+
87
+ def shutdown(self):
88
+ self.quited = True
89
+ try: self.consumer.close()
90
+ except Exception: pass
91
+ try: self.producer.close()
92
+ except Exception: pass
93
+
94
+
95
+ class CKafka:
96
+
97
+ def __init__(self, kafka_url: str = '127.0.0.1:9092', secure: bool = False, username: str = 'client', password: str = 'hylink_user_password'):
98
+ self.kafka_url = kafka_url
99
+ self.secure = secure
100
+ self.username = username
101
+ self.password = password
102
+
103
+ def init_producer(self, acks=1) -> KafkaInstance:
104
+ print("[ Producer ] Connecting to Kafka [{}]".format(self.kafka_url))
105
+ for i in range(0, 6):
106
+ try:
107
+ if self.secure:
108
+ producer = KafkaProducer(
109
+ acks=acks,
110
+ bootstrap_servers=self.kafka_url,
111
+ value_serializer=lambda x: dumps(x).encode('utf-8'),
112
+ sasl_plain_username=self.username,
113
+ sasl_plain_password=self.password,
114
+ security_protocol='SASL_PLAINTEXT',
115
+ sasl_mechanism='PLAIN'
116
+ )
117
+ else:
118
+ producer = KafkaProducer(
119
+ acks=acks,
120
+ bootstrap_servers=self.kafka_url,
121
+ value_serializer=lambda x: dumps(x).encode('utf-8')
122
+ )
123
+ print("[ Producer ] Success Connected to Kafka [{}]".format(self.kafka_url))
124
+ return KafkaInstance(producer=producer, consumer=None)
125
+ except errors.NoBrokersAvailable:
126
+ print("[ Producer ] Waiting for Kafka [{}] to become available...".format(self.kafka_url))
127
+ time.sleep(3)
128
+ raise RuntimeError("[ Producer ] Failed to connect to Kafka [{}] within 60 seconds".format(self.kafka_url))
129
+
130
+ def init_consumer(self, client_id: str = 'ck-py-kafka-consumer', consumer_group: str = 'ck-py-kafka-consumer', enable_auto_commit: bool = True) -> KafkaInstance:
131
+ print("[ Consumer ] Connecting to Kafka [{}]".format(self.kafka_url))
132
+ for i in range(0, 6):
133
+ try:
134
+ if self.secure:
135
+ consumer = KafkaConsumer(
136
+ client_id=client_id,
137
+ group_id=consumer_group,
138
+ enable_auto_commit=enable_auto_commit,
139
+ bootstrap_servers=self.kafka_url,
140
+ value_deserializer=lambda x: x.decode('utf-8'),
141
+ sasl_plain_username=self.username,
142
+ sasl_plain_password=self.password,
143
+ security_protocol='SASL_PLAINTEXT',
144
+ sasl_mechanism='PLAIN'
145
+ )
146
+ else:
147
+ consumer = KafkaProducer(
148
+ client_id=client_id,
149
+ group_id=consumer_group,
150
+ enable_auto_commit=enable_auto_commit,
151
+ bootstrap_servers=self.kafka_url,
152
+ value_deserializer=lambda x: x.decode('utf-8')
153
+ )
154
+ print("[ Consumer ] Success Connected to Kafka [{}]".format(self.kafka_url))
155
+ return KafkaInstance(producer=None, consumer=consumer)
156
+ except errors.NoBrokersAvailable:
157
+ print("[ Consumer ] Waiting for Kafka [{}] to become available...".format(self.kafka_url))
158
+ time.sleep(3)
159
+ raise RuntimeError("[ Consumer ] Failed to connect to Kafka [{}] within 60 seconds".format(self.kafka_url))
@@ -0,0 +1,18 @@
1
+ import importlib
2
+ import os
3
+ import time
4
+
5
+
6
+ def code_to_pyc(code: str, out_file_path: str):
7
+ file_name = os.path.split(out_file_path)[-1]
8
+ compiled_code = compile(code, file_name, 'exec')
9
+ bytecode = importlib._bootstrap_external._code_to_timestamp_pyc(compiled_code, time.time(), len(code))
10
+ with open(out_file_path, 'wb') as f:
11
+ f.write(bytecode)
12
+
13
+
14
+ def file_to_pyc(file_path: str, out_file_path: str):
15
+ with open(file_path, 'r') as f:
16
+ code = f.read()
17
+ if code:
18
+ code_to_pyc(code, out_file_path)
ctools/console.py ADDED
@@ -0,0 +1,55 @@
1
+ import logging
2
+ import sys
3
+ import tkinter as tk
4
+
5
+
6
+ class Console:
7
+
8
+ def __init__(self, master):
9
+ self.master = master
10
+
11
+ # 创建文本框和滚动条
12
+ self.textbox = tk.Text(self.master, wrap=tk.NONE)
13
+
14
+ self.vertical_scrollbar = tk.Scrollbar(self.textbox, command=self.textbox.yview)
15
+ self.horizontal_scrollbar = tk.Scrollbar(self.textbox, command=self.textbox.xview, orient=tk.HORIZONTAL)
16
+
17
+ self.textbox.configure(yscrollcommand=self.vertical_scrollbar.set, xscrollcommand=self.horizontal_scrollbar.set)
18
+ self.textbox.pack(side=tk.LEFT, pady=10, padx=10, ipadx=10, ipady=10, fill=tk.BOTH, expand=True)
19
+
20
+ self.vertical_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
21
+ self.horizontal_scrollbar.pack(side=tk.BOTTOM, fill=tk.X)
22
+
23
+ # 将标准输出和标准错误输出重定向到文本框中
24
+ sys.stdout = self
25
+ sys.stderr = self
26
+
27
+ # # 创建输入框和按钮
28
+ # self.entry = tk.Entry(self.master)
29
+ # self.entry.pack(side=tk.BOTTOM, fill=tk.X, expand=True)
30
+ # self.button = tk.Button(self.master, text="Send", command=self.send)
31
+ # self.button.pack(side=tk.BOTTOM)
32
+
33
+ # 将日志输出到文本框中
34
+ self.log_handler = logging.StreamHandler(self)
35
+ self.log_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
36
+ logging.getLogger().addHandler(self.log_handler)
37
+ # logging.getLogger().setLevel(logging.INFO)
38
+
39
+ def write(self, message):
40
+ # 在文本框中输出消息
41
+ self.textbox.insert(tk.END, message + '\n')
42
+ self.textbox.see(tk.END)
43
+
44
+ def flush(self):
45
+ pass
46
+
47
+ def send(self):
48
+ # 获取输入框中的文本并打印到控制台
49
+ text = self.entry.get()
50
+ print(text)
51
+ self.entry.delete(0, tk.END)
52
+
53
+ def __del__(self):
54
+ # 关闭日志处理器
55
+ logging.getLogger().removeHandler(self.log_handler)
ctools/coord_trans.py ADDED
@@ -0,0 +1,127 @@
1
+ # -*- coding: utf-8 -*-
2
+ import math
3
+
4
+ x_pi = 3.14159265358979324 * 3000.0 / 180.0
5
+ pi = 3.1415926535897932384626 # π
6
+ a = 6378245.0 # 长半轴
7
+ ee = 0.00669342162296594323 # 偏心率平方
8
+
9
+ def gcj02_to_bd09(lng, lat):
10
+ """
11
+ 火星坐标系(GCJ-02)转百度坐标系(BD-09)
12
+ 谷歌、高德——>百度
13
+ :param lng:火星坐标经度
14
+ :param lat:火星坐标纬度
15
+ :return:
16
+ """
17
+ z = math.sqrt(lng * lng + lat * lat) + 0.00002 * math.sin(lat * x_pi)
18
+ theta = math.atan2(lat, lng) + 0.000003 * math.cos(lng * x_pi)
19
+ bd_lng = z * math.cos(theta) + 0.0065
20
+ bd_lat = z * math.sin(theta) + 0.006
21
+ return [bd_lng, bd_lat]
22
+
23
+
24
+ def bd09_to_gcj02(bd_lon, bd_lat):
25
+ """
26
+ 百度坐标系(BD-09)转火星坐标系(GCJ-02)
27
+ 百度——>谷歌、高德
28
+ :param bd_lat:百度坐标纬度
29
+ :param bd_lon:百度坐标经度
30
+ :return:转换后的坐标列表形式
31
+ """
32
+ x = bd_lon - 0.0065
33
+ y = bd_lat - 0.006
34
+ z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * x_pi)
35
+ theta = math.atan2(y, x) - 0.000003 * math.cos(x * x_pi)
36
+ gg_lng = z * math.cos(theta)
37
+ gg_lat = z * math.sin(theta)
38
+ return [gg_lng, gg_lat]
39
+
40
+
41
+ def wgs84_to_gcj02(lng, lat):
42
+ """
43
+ WGS84转GCJ02(火星坐标系)
44
+ :param lng:WGS84坐标系的经度
45
+ :param lat:WGS84坐标系的纬度
46
+ :return:
47
+ """
48
+ if out_of_china(lng, lat): # 判断是否在国内
49
+ return [lng, lat]
50
+ dlat = _transformlat(lng - 105.0, lat - 35.0)
51
+ dlng = _transformlng(lng - 105.0, lat - 35.0)
52
+ radlat = lat / 180.0 * pi
53
+ magic = math.sin(radlat)
54
+ magic = 1 - ee * magic * magic
55
+ sqrtmagic = math.sqrt(magic)
56
+ dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
57
+ dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
58
+ mglat = lat + dlat
59
+ mglng = lng + dlng
60
+ return [mglng, mglat]
61
+
62
+
63
+ def gcj02_to_wgs84(lng, lat):
64
+ """
65
+ GCJ02(火星坐标系)转GPS84
66
+ :param lng:火星坐标系的经度
67
+ :param lat:火星坐标系纬度
68
+ :return:
69
+ """
70
+ if out_of_china(lng, lat):
71
+ return [lng, lat]
72
+ dlat = _transformlat(lng - 105.0, lat - 35.0)
73
+ dlng = _transformlng(lng - 105.0, lat - 35.0)
74
+ radlat = lat / 180.0 * pi
75
+ magic = math.sin(radlat)
76
+ magic = 1 - ee * magic * magic
77
+ sqrtmagic = math.sqrt(magic)
78
+ dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
79
+ dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
80
+ mglat = lat + dlat
81
+ mglng = lng + dlng
82
+ return [lng * 2 - mglng, lat * 2 - mglat]
83
+
84
+
85
+ def bd09_to_wgs84(bd_lon, bd_lat):
86
+ lon, lat = bd09_to_gcj02(bd_lon, bd_lat)
87
+ return gcj02_to_wgs84(lon, lat)
88
+
89
+
90
+ def wgs84_to_bd09(lon, lat):
91
+ lon, lat = wgs84_to_gcj02(lon, lat)
92
+ return gcj02_to_bd09(lon, lat)
93
+
94
+
95
+ def _transformlat(lng, lat):
96
+ ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + \
97
+ 0.1 * lng * lat + 0.2 * math.sqrt(math.fabs(lng))
98
+ ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 *
99
+ math.sin(2.0 * lng * pi)) * 2.0 / 3.0
100
+ ret += (20.0 * math.sin(lat * pi) + 40.0 *
101
+ math.sin(lat / 3.0 * pi)) * 2.0 / 3.0
102
+ ret += (160.0 * math.sin(lat / 12.0 * pi) + 320 *
103
+ math.sin(lat * pi / 30.0)) * 2.0 / 3.0
104
+ return ret
105
+
106
+
107
+ def _transformlng(lng, lat):
108
+ ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + \
109
+ 0.1 * lng * lat + 0.1 * math.sqrt(math.fabs(lng))
110
+ ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 *
111
+ math.sin(2.0 * lng * pi)) * 2.0 / 3.0
112
+ ret += (20.0 * math.sin(lng * pi) + 40.0 *
113
+ math.sin(lng / 3.0 * pi)) * 2.0 / 3.0
114
+ ret += (150.0 * math.sin(lng / 12.0 * pi) + 300.0 *
115
+ math.sin(lng / 30.0 * pi)) * 2.0 / 3.0
116
+ return ret
117
+
118
+
119
+ def out_of_china(lng, lat):
120
+ """
121
+ 判断是否在国内,不在国内不做偏移
122
+ :param lng:
123
+ :param lat:
124
+ :return:
125
+ """
126
+ return not (lng > 73.66 and lng < 135.05 and lat > 3.86 and lat < 53.55)
127
+
ctools/credis.py ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ __author__ = 'haoyang'
4
+ __date__ = '2025/2/14 11:09'
5
+
6
+ import redis
7
+ from redis import Redis
8
+
9
+ from ctools import date_utils, thread_pool, string_tools
10
+
11
+ def init_pool(host: str = 'localhost', port: int = 6379, db: int = 0, password: str = None,
12
+ username: str = None, decode_responses: bool = True, max_connections: int = 75,
13
+ health_check_interval: int = 30, retry_count: int = 3) -> Redis:
14
+ for attempt in range(retry_count):
15
+ try:
16
+ r: Redis = redis.StrictRedis(
17
+ host=host, port=port, db=db,
18
+ username=username, password=password,
19
+ retry_on_timeout=True,
20
+ max_connections=max_connections,
21
+ decode_responses=decode_responses,
22
+ health_check_interval=health_check_interval,
23
+ socket_connect_timeout=5,
24
+ socket_timeout=5
25
+ )
26
+ if r.ping():
27
+ print('CRedis connect {} {} success!'.format(host, port))
28
+ return r
29
+ except redis.ConnectionError as e:
30
+ if attempt == retry_count - 1:
31
+ raise Exception(f"Failed to connect to Redis after {retry_count} attempts: {str(e)}")
32
+ print(f"Connection attempt {attempt + 1} failed, retrying...")
33
+
34
+ def add_lock(r: Redis, key: str, timeout: int = 30):
35
+ if r.exists(key):
36
+ expire_time = r.get(key)
37
+ if date_utils.time_diff_in_seconds(expire_time, date_utils.get_date_time()) > 0:
38
+ return True
39
+ else:
40
+ r.delete(key)
41
+ return r.set(key, date_utils.opt_time(seconds=timeout), nx=True, ex=timeout) is not None
42
+
43
+ def remove_lock(r: Redis, key: str):
44
+ r.delete(key)
45
+
46
+ def subscribe(r: Redis, channel_name, callback):
47
+ def thread_func():
48
+ pubsub = r.pubsub()
49
+ pubsub.subscribe(channel_name)
50
+ for message in pubsub.listen():
51
+ callback(message)
52
+ thread_pool.submit(thread_func)
53
+
54
+ def _process_pending_messages(r: Redis, stream_name: str, group_name: str, consumer_name: str, callback):
55
+ """
56
+ 处理未确认的消息
57
+ :param r: Redis 连接
58
+ :param stream_name: 流名称
59
+ :param group_name: 消费者组名称
60
+ :param consumer_name: 消费者名称
61
+ :param callback: 消息处理回调函数
62
+ """
63
+ # 检查未确认的消息
64
+ pending_messages = r.xpending(stream_name, group_name)
65
+ if pending_messages['pending'] > 0:
66
+ print(f"Found {pending_messages['pending']} pending messages.")
67
+ # 获取未确认的消息列表
68
+ pending_list = r.xpending_range(stream_name, group_name, min='-', max='+', count=pending_messages['pending'])
69
+ for message in pending_list:
70
+ message_id = message['message_id']
71
+ claimed_messages = r.xclaim(stream_name, group_name, consumer_name, min_idle_time=0, message_ids=[message_id])
72
+ if claimed_messages:
73
+ # 处理消息
74
+ for claimed_message in claimed_messages:
75
+ message_id, data = claimed_message
76
+ print(f"Processing pending message: {message_id}, data: {data}")
77
+ try:
78
+ if callback(message_id, data):
79
+ r.xack(stream_name, group_name, message_id)
80
+ except Exception as e:
81
+ print(f"Error processing message {message_id}: {e}")
82
+ else:
83
+ print("No pending messages found.")
84
+
85
+ def stream_subscribe(r: Redis, stream_name, group_name, callback, from_id: str='$', noack: bool = False):
86
+ def thread_func():
87
+ try:
88
+ # $表示从最后面消费, 0表示从开始消费
89
+ r.xgroup_create(name=stream_name, groupname=group_name, id=from_id, mkstream=True)
90
+ print(f"Consumer group '{group_name}' created successfully.")
91
+ except Exception as e:
92
+ if "already exists" in str(e):
93
+ print(f"Consumer group '{group_name}' already exists.")
94
+ else:
95
+ print(f"Error creating consumer group '{group_name}': {e}")
96
+ consumer_name = 'consumer-{}'.format(string_tools.get_uuid())
97
+ # 处理未确认的消息
98
+ _process_pending_messages(r, stream_name, group_name, consumer_name, callback)
99
+ while True:
100
+ messages = r.xreadgroup(group_name, consumer_name, {stream_name: '>'}, block=1000, noack=noack)
101
+ for message in messages:
102
+ try:
103
+ message_id, data = message[1][0]
104
+ res = callback(message_id, data)
105
+ if res: r.xack(stream_name, group_name, message_id)
106
+ except Exception as e:
107
+ print('stream_subscribe error: ', e)
108
+ thread_pool.submit(thread_func)
109
+
110
+ def stream_publish(r: Redis, stream_name, message):
111
+ r.xadd(stream_name, message)