yxsdk 0.4.0__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.
yxsdk-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: yxsdk
3
+ Version: 0.4.0
4
+ Summary: A Python SDK for common utilities
5
+ Author-email: Yi Chen <sgrchen@163.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sgrchen/yxsdk
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Requires-Python: >=3.7
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: mysql-connector-python>=8.0.0
19
+ Requires-Dist: redis>=3.0.0
20
+ Requires-Dist: psutil>=7.0.0
21
+ Requires-Dist: requests>=2.0.0
22
+ Requires-Dist: PyJWT>=2.3.0
23
+ Provides-Extra: dev
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "yxsdk"
7
+ dynamic = ["version", "readme"] # 版本和 README 动态读取
8
+ authors = [
9
+ { name = "Yi Chen", email = "sgrchen@163.com" },
10
+ ]
11
+ description = "A Python SDK for common utilities"
12
+ license = "MIT"
13
+ requires-python = ">=3.7"
14
+ dependencies = [
15
+ "mysql-connector-python>=8.0.0",
16
+ "redis>=3.0.0",
17
+ "psutil>=7.0.0",
18
+ "requests>=2.0.0",
19
+ "PyJWT>=2.3.0",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 3 - Alpha",
23
+ "Intended Audience :: Developers",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.7",
26
+ "Programming Language :: Python :: 3.8",
27
+ "Programming Language :: Python :: 3.9",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/sgrchen/yxsdk"
34
+
35
+ [project.optional-dependencies]
36
+ dev = []
37
+
38
+ [tool.setuptools]
39
+ include-package-data = true
40
+
41
+ [tool.setuptools.dynamic]
42
+ version = { file = "src/version.txt" }
43
+ readme = { file = "README.md", content-type = "text/markdown" }
44
+
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
yxsdk-0.4.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ 0.4.0
@@ -0,0 +1,29 @@
1
+ from pathlib import Path
2
+ __version__ = (Path(__file__).parent.parent / "version.txt").read_text().strip()
3
+
4
+ from .mysql_client import MysqlClient
5
+ from .redis_client import RedisClient
6
+ from .sqlite_client import SqliteClient
7
+ from .protocol_converter import ProtocolConverterManager, ProtocolConverterInterface
8
+ from .event_worker import EventWorker
9
+ from .ollama_client import OllamaClient
10
+ from .utils import datetime_str, log_debug, json_dumps, uuid_generate
11
+ # 第三方接口
12
+ from .aliyun_client import AliyunClient
13
+
14
+
15
+ __all__ = [
16
+ '__version__',
17
+ 'MysqlClient',
18
+ 'RedisClient',
19
+ 'ProtocolConverterManager',
20
+ 'ProtocolConverterInterface',
21
+ 'EventWorker',
22
+ 'datetime_str',
23
+ 'log_debug',
24
+ 'json_dumps',
25
+ 'uuid_generate',
26
+ 'SqliteClient',
27
+ 'OllamaClient',
28
+ 'AliyunClient'
29
+ ]
@@ -0,0 +1,146 @@
1
+ import hashlib
2
+ import hmac
3
+ import json
4
+ import urllib.parse
5
+ from datetime import datetime, timezone
6
+
7
+ import requests
8
+
9
+
10
+ class AliyunSMSClient:
11
+ """阿里云短信服务客户端(纯 HTTP + V3 签名)"""
12
+
13
+ def __init__(self, access_key_id: str, access_key_secret: str, region_id: str = "cn-hangzhou"):
14
+ self.access_key_id = access_key_id
15
+ self.access_key_secret = access_key_secret
16
+ self.region_id = region_id
17
+
18
+ # 短信服务域名
19
+ self.endpoint = "dysmsapi.aliyuncs.com"
20
+ # API 版本
21
+ self.api_version = "2017-05-25"
22
+ # 签名算法
23
+ self.algorithm = "HMAC-SHA256"
24
+
25
+ def _sign(self, key: bytes, msg: str) -> bytes:
26
+ return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
27
+
28
+ def _get_hashed_payload(self, body: str) -> str:
29
+ """计算请求体的 SHA256 哈希值(小写十六进制)"""
30
+ return hashlib.sha256(body.encode("utf-8")).hexdigest()
31
+
32
+ def _get_canonical_headers(self, headers: dict) -> tuple[str, str]:
33
+ """构造规范化请求头,返回 (CanonicalHeaders, SignedHeaders)"""
34
+ # 必须包含 host 和 x-acs-* 头
35
+ canonical_headers = ""
36
+ signed_headers = ""
37
+ for key in sorted(headers.keys(), key=lambda x: x.lower()):
38
+ lower_key = key.lower()
39
+ value = headers[key].strip()
40
+ canonical_headers += f"{lower_key}:{value}\n"
41
+ if signed_headers:
42
+ signed_headers += ";"
43
+ signed_headers += lower_key
44
+ return canonical_headers, signed_headers
45
+
46
+ def _build_authorization(
47
+ self,
48
+ method: str,
49
+ canonical_uri: str,
50
+ canonical_query_string: str,
51
+ headers: dict,
52
+ body: str,
53
+ ) -> str:
54
+ # 1. 构建规范化请求
55
+ canonical_headers, signed_headers = self._get_canonical_headers(headers)
56
+ hashed_payload = self._get_hashed_payload(body)
57
+
58
+ canonical_request = (
59
+ f"{method.upper()}\n"
60
+ f"{canonical_uri}\n"
61
+ f"{canonical_query_string}\n"
62
+ f"{canonical_headers}\n"
63
+ f"{signed_headers}\n"
64
+ f"{hashed_payload}"
65
+ )
66
+
67
+ # 2. 构建待签字符串
68
+ hashed_canonical_request = hashlib.sha256(
69
+ canonical_request.encode("utf-8")
70
+ ).hexdigest()
71
+ string_to_sign = f"{self.algorithm}\n{hashed_canonical_request}"
72
+
73
+ # 3. 计算签名密钥
74
+ secret = self.access_key_secret + "&"
75
+ signature = hmac.new(
76
+ secret.encode("utf-8"),
77
+ string_to_sign.encode("utf-8"),
78
+ hashlib.sha256,
79
+ ).hexdigest()
80
+
81
+ # 4. 构造 Authorization 头
82
+ return (
83
+ f"{self.algorithm} "
84
+ f"Credential={self.access_key_id},"
85
+ f"SignedHeaders={signed_headers},"
86
+ f"Signature={signature}"
87
+ )
88
+
89
+ def _request(self, action: str, params: dict) -> dict:
90
+ """发送阿里云 OpenAPI 请求"""
91
+ # 请求时间(UTC)
92
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
93
+
94
+ # 请求头
95
+ headers = {
96
+ "host": self.endpoint,
97
+ "x-acs-action": action,
98
+ "x-acs-version": self.api_version,
99
+ "x-acs-date": timestamp,
100
+ "x-acs-signature-nonce": hashlib.md5(
101
+ timestamp.encode("utf-8")
102
+ ).hexdigest(), # 简单生成唯一值
103
+ "content-type": "application/json; charset=utf-8",
104
+ }
105
+
106
+ # 请求体
107
+ body = json.dumps(params)
108
+
109
+ # 生成签名
110
+ headers["authorization"] = self._build_authorization(
111
+ method="POST",
112
+ canonical_uri="/",
113
+ canonical_query_string="", # 查询参数为空
114
+ headers=headers,
115
+ body=body,
116
+ )
117
+
118
+ # 发送请求
119
+ url = f"https://{self.endpoint}/"
120
+ response = requests.post(url, headers=headers, data=body.encode("utf-8"))
121
+ return response.json()
122
+
123
+ def send_sms(
124
+ self,
125
+ phone_numbers: str,
126
+ sign_name: str,
127
+ template_code: str,
128
+ template_param: dict = None,
129
+ ) -> dict:
130
+ """
131
+ 发送短信
132
+ :param phone_numbers: 手机号码,多个用逗号分隔
133
+ :param sign_name: 短信签名
134
+ :param template_code: 短信模板 CODE
135
+ :param template_param: 模板变量字典,例如 {"code":"1234"}
136
+ :return: API 返回的 JSON
137
+ """
138
+ params = {
139
+ "PhoneNumbers": phone_numbers,
140
+ "SignName": sign_name,
141
+ "TemplateCode": template_code,
142
+ }
143
+ if template_param:
144
+ params["TemplateParam"] = json.dumps(template_param)
145
+ return self._request("SendSms", params)
146
+
@@ -0,0 +1,123 @@
1
+ import sys
2
+ import json
3
+ import threading
4
+ import time
5
+ import requests
6
+ from abc import ABC, abstractmethod
7
+ from .redis_client import RedisClient
8
+
9
+ class EventWorker(ABC):
10
+ """
11
+ Redis 消费组事件处理器基类
12
+ """
13
+
14
+ def __init__(self, group_name, stream_name, redis_host='localhost',
15
+ redis_port=6379, redis_db=0, num_consumers=2,
16
+ messages_per_batch=5, block_timeout=2000):
17
+ """
18
+ 初始化事件处理器
19
+
20
+ Args:
21
+ group_name: 消费组名称
22
+ stream_name: Redis 流名称
23
+ redis_host: Redis 主机地址
24
+ redis_port: Redis 端口
25
+ redis_db: Redis 数据库编号
26
+ num_consumers: 消费者数量
27
+ messages_per_batch: 每次读取消息数量
28
+ block_timeout: 阻塞时间(毫秒)
29
+ """
30
+ self.group_name = group_name
31
+ self.stream_name = stream_name
32
+ self.redis_host = redis_host
33
+ self.redis_port = redis_port
34
+ self.redis_db = redis_db
35
+ self.num_consumers = num_consumers
36
+ self.messages_per_batch = messages_per_batch
37
+ self.block_timeout = block_timeout
38
+
39
+ # 创建 Redis 客户端
40
+ self.redis_client = RedisClient(
41
+ host=self.redis_host,
42
+ port=self.redis_port,
43
+ db=self.redis_db
44
+ )
45
+
46
+ self.threads = []
47
+ self.running = False
48
+
49
+ @abstractmethod
50
+ def process_event(self, consumer_id, message_id, message_data):
51
+ """
52
+ 处理事件的抽象方法,子类必须实现
53
+
54
+ Args:
55
+ consumer_id: 消费者ID
56
+ message_id: 消息ID
57
+ message_data: 消息数据
58
+
59
+ Returns:
60
+ bool: 处理成功返回 True,失败返回 False
61
+ """
62
+ pass
63
+
64
+ def worker(self, consumer_id):
65
+ """
66
+ 工作线程方法
67
+ """
68
+ # 每个工作线程创建独立的 Redis 连接
69
+ worker_redis_client = RedisClient(
70
+ host=self.redis_host,
71
+ port=self.redis_port,
72
+ db=self.redis_db
73
+ )
74
+
75
+ print(f"事件处理工作线程 {consumer_id} 启动,等待消息...")
76
+
77
+ while self.running:
78
+ try:
79
+ messages = worker_redis_client.xreadgroup(self.stream_name, self.group_name, consumer_id, block=self.block_timeout)
80
+ if messages:
81
+ for stream_name, message_list in messages:
82
+ for message_id, message_data in message_list:
83
+ success = self.process_event(consumer_id, message_id, message_data)
84
+ if not success:
85
+ print(f"[消费者 {consumer_id}] 处理消息 {message_id} 失败")
86
+ worker_redis_client.xack(self.stream_name, self.group_name, message_id)
87
+ except Exception as e:
88
+ if self.running: # 只有在运行状态才打印错误
89
+ print(f"[消费者 {consumer_id}] 读取消息出错: {e}")
90
+ time.sleep(5) # 异常发生时暂停一下再继续
91
+
92
+ def start(self):
93
+ try:
94
+ self.running = True
95
+
96
+ # 启动多个工作线程
97
+ for i in range(self.num_consumers):
98
+ t = threading.Thread(target=self.worker, args=(i+1,))
99
+ self.threads.append(t)
100
+ t.daemon = True # 设置为守护线程
101
+ t.start()
102
+ print(f"启动消费者 {i+1}")
103
+
104
+ print(f"事件处理器启动完成,共 {self.num_consumers} 个消费者")
105
+
106
+ # 等待所有线程
107
+ for t in self.threads:
108
+ t.join()
109
+
110
+ except KeyboardInterrupt:
111
+ print("程序中断,正在退出...")
112
+ self.stop()
113
+
114
+ def stop(self):
115
+ print("正在停止事件处理器...")
116
+ self.running = False
117
+
118
+ # 等待所有线程结束
119
+ for t in self.threads:
120
+ if t.is_alive():
121
+ t.join(timeout=5)
122
+
123
+ print("事件处理器已停止")
@@ -0,0 +1,117 @@
1
+ import time
2
+ from datetime import datetime, date
3
+ from decimal import Decimal
4
+ import mysql.connector
5
+
6
+ class Row(dict):
7
+ def __init__(self, data):
8
+ super().__init__()
9
+ for key,value in data:
10
+ self[key] = value
11
+
12
+ def __getattr__(self, key):
13
+ try:
14
+ return self[key]
15
+ except KeyError:
16
+ raise AttributeError(key)
17
+ def __setitem__(self, key, value):
18
+ if isinstance(value, datetime):
19
+ value = value.strftime('%Y-%m-%d %H:%M:%S')
20
+ elif isinstance(value, date):
21
+ value = value.strftime('%Y-%m-%d')
22
+ elif isinstance(value, Decimal):
23
+ # 将 Decimal 转换为 int 或 float
24
+ if value % 1 == 0: # 如果是整数
25
+ value = int(value)
26
+ else: # 如果是小数
27
+ value = float(value)
28
+ super().__setitem__(key, value)
29
+
30
+ class MysqlClient:
31
+ def __init__(self, host, port, user, password, database):
32
+ self._db = None
33
+ self._host = host
34
+ self._port = port or 3306
35
+ self._user = user
36
+ self._password = password
37
+ self._database = database
38
+ self._max_idle_time = 7 * 3600
39
+ self._last_use_time = time.time()
40
+ try:
41
+ self.reconnect()
42
+ except Exception:
43
+ raise Exception("Cannot connect to MySQL server")
44
+
45
+ def __del__(self):
46
+ self.close()
47
+
48
+ def close(self):
49
+ if self._db:
50
+ try:
51
+ self._db.close()
52
+ except Exception:
53
+ pass
54
+ self._db = None
55
+
56
+ def reconnect(self):
57
+ self.close()
58
+ self._db = mysql.connector.connect(host=self._host, port=self._port, user=self._user, password=self._password, database=self._database, time_zone='+08:00')
59
+ self._db.autocommit = True
60
+
61
+ def _ensure_connected(self):
62
+ if not self._db or (time.time() - self._last_use_time > self._max_idle_time):
63
+ self.reconnect()
64
+ self._last_use_time = time.time()
65
+
66
+ def _cursor(self):
67
+ self._ensure_connected()
68
+ return self._db.cursor()
69
+
70
+ def _execute(self, cursor, query, parameters, kwparameters):
71
+ try:
72
+ cursor.execute(query, kwparameters or parameters)
73
+ except Exception:
74
+ self.close()
75
+ raise
76
+
77
+ def query(self, query, *parameters, **kwparameters):
78
+ cursor = self._cursor()
79
+ try:
80
+ self._execute(cursor, query, parameters, kwparameters)
81
+ columns = [d[0] for d in cursor.description]
82
+ return [Row(zip(columns, row)) for row in cursor.fetchall()]
83
+ finally:
84
+ cursor.close()
85
+
86
+ def get(self, query, *parameters, **kwparameters):
87
+ rows = self.query(query, *parameters, **kwparameters)
88
+ if not rows:
89
+ return None
90
+ elif len(rows) > 1:
91
+ raise Exception("Multiple rows returned for DBI.get() query")
92
+ else:
93
+ return rows[0]
94
+
95
+ def execute_lastrowid(self, query, *parameters, **kwparameters):
96
+ cursor = self._cursor()
97
+ try:
98
+ self._execute(cursor, query, parameters, kwparameters)
99
+ return cursor.lastrowid
100
+ finally:
101
+ cursor.close()
102
+
103
+ def execute_rowcount(self, query, *parameters, **kwparameters):
104
+ cursor = self._cursor()
105
+ try:
106
+ self._execute(cursor, query, parameters, kwparameters)
107
+ return cursor.rowcount
108
+ finally:
109
+ cursor.close()
110
+
111
+ def execute(self, query, *parameters, **kwparameters):
112
+ return self.execute_lastrowid(query, *parameters, **kwparameters)
113
+
114
+ update = execute_rowcount
115
+ insert = execute_lastrowid
116
+ delete = execute_lastrowid
117
+
@@ -0,0 +1,234 @@
1
+ import requests
2
+ import json
3
+ import re
4
+
5
+ class OllamaClient:
6
+ def __init__(self, model=None, base_url="http://localhost:11434", key=None):
7
+ self.base_url = base_url
8
+ self.model = model
9
+ self.key = key
10
+
11
+ def chat(self, messages, stream=False, model=None):
12
+ url = f"{self.base_url}/api/chat"
13
+
14
+ # 使用传入的model参数,如果没有则使用实例的model
15
+ used_model = model or self.model
16
+ if not used_model:
17
+ raise ValueError("未指定模型名称")
18
+
19
+ payload = {
20
+ "model": used_model,
21
+ "messages": messages,
22
+ "stream": stream,
23
+ "options": {
24
+ "temperature": 0.0
25
+ }
26
+ }
27
+
28
+ try:
29
+ response = requests.post(url, json=payload, stream=stream)
30
+ response.raise_for_status()
31
+
32
+ if stream:
33
+ return self._handle_stream_response(response)
34
+ else:
35
+ return response.json()
36
+
37
+ except requests.exceptions.RequestException as e:
38
+ print(f"请求错误: {e}")
39
+ return None
40
+
41
+ def generate(self, prompt, stream=False, system=None, model=None, temperature=0.7,
42
+ max_tokens=None, top_p=None, top_k=None, repeat_penalty=None,
43
+ stop=None, context=None, raw=False):
44
+ """使用 /api/generate 端点进行单次文本生成"""
45
+ url = f"{self.base_url}/api/generate"
46
+
47
+ # 使用传入的model参数,如果没有则使用实例的model
48
+ used_model = model or self.model
49
+ if not used_model:
50
+ raise ValueError("未指定模型名称")
51
+
52
+ payload = {
53
+ "model": used_model,
54
+ "prompt": prompt,
55
+ "stream": stream,
56
+ "think": False,
57
+ "raw": raw # 是否返回原始响应,不进行任何格式化
58
+ }
59
+
60
+ # 构建 options 参数
61
+ options = {"temperature": temperature, "think": False}
62
+
63
+ if max_tokens is not None:
64
+ options["num_predict"] = max_tokens # 最大生成token数
65
+
66
+ if top_p is not None:
67
+ options["top_p"] = top_p # nucleus sampling参数
68
+
69
+ if top_k is not None:
70
+ options["top_k"] = top_k # top-k sampling参数
71
+
72
+ if repeat_penalty is not None:
73
+ options["repeat_penalty"] = repeat_penalty # 重复惩罚
74
+
75
+ if stop is not None:
76
+ options["stop"] = stop if isinstance(stop, list) else [stop] # 停止词
77
+
78
+ payload["options"] = options
79
+
80
+ # 如果提供了系统提示,添加到payload中
81
+ if system:
82
+ payload["system"] = system
83
+
84
+ # 如果提供了上下文,添加到payload中
85
+ if context is not None:
86
+ payload["context"] = context
87
+
88
+ try:
89
+ headers = {}
90
+ if self.key:
91
+ headers['Authorization'] = f"Bearer {self.key}"
92
+ response = requests.post(url, headers=headers, json=payload, stream=stream)
93
+ response.raise_for_status()
94
+
95
+ if stream:
96
+ return self._handle_generate_stream_response(response)
97
+ else:
98
+ content = response.json()
99
+ if 'response' in content:
100
+ return content['response']
101
+ else:
102
+ print("响应格式错误,未找到 'response' 字段")
103
+ return None
104
+
105
+ except requests.exceptions.RequestException as e:
106
+ print(f"请求错误: {e}")
107
+ return None
108
+
109
+ def embed_text(self, text, model=None):
110
+ """对单个文本进行嵌入向量化"""
111
+ url = f"{self.base_url}/api/embeddings"
112
+
113
+ # 使用传入的model参数,如果没有则使用实例的model
114
+ used_model = model or self.model
115
+ if not used_model:
116
+ raise ValueError("未指定模型名称")
117
+
118
+ payload = {
119
+ "model": used_model,
120
+ "prompt": text
121
+ }
122
+
123
+ try:
124
+ headers = {}
125
+ if self.key:
126
+ headers['Authorization'] = f"Bearer {self.key}"
127
+ response = requests.post(url, headers=headers, json=payload)
128
+ response.raise_for_status()
129
+
130
+ content = response.json()
131
+ if 'embedding' in content:
132
+ return content['embedding']
133
+ else:
134
+ print("响应格式错误,未找到 'embedding' 字段")
135
+ return None
136
+
137
+ except requests.exceptions.RequestException as e:
138
+ print(f"请求错误: {e}")
139
+ return None
140
+
141
+ def embed_documents(self, documents, model=None):
142
+ """对多个文档进行批量嵌入向量化
143
+
144
+ Args:
145
+ documents: 文档列表,可以是字符串列表
146
+ model: 模型名称
147
+
148
+ Returns:
149
+ list: 嵌入向量列表,每个元素对应一个文档的嵌入向量
150
+ """
151
+ if not isinstance(documents, list):
152
+ raise ValueError("documents 参数必须是列表")
153
+
154
+ embeddings = []
155
+ for i, doc in enumerate(documents):
156
+ try:
157
+ embedding = self.embed_text(doc, model)
158
+ if embedding is not None:
159
+ embeddings.append(embedding)
160
+ else:
161
+ print(f"文档 {i+1} 嵌入失败")
162
+ embeddings.append(None)
163
+ except Exception as e:
164
+ print(f"处理文档 {i+1} 时出错: {e}")
165
+ embeddings.append(None)
166
+
167
+ return embeddings
168
+
169
+ def _handle_stream_response(self, response):
170
+ """处理流式响应"""
171
+ for line in response.iter_lines():
172
+ if line:
173
+ try:
174
+ # 解码字节为字符串
175
+ if isinstance(line, bytes):
176
+ line = line.decode('utf-8')
177
+
178
+ # 移除可能的 "data: " 前缀
179
+ if line.startswith('data: '):
180
+ line = line[6:]
181
+
182
+ data = json.loads(line)
183
+ if 'message' in data and 'content' in data['message']:
184
+ content = data['message']['content']
185
+ if content: # 只返回非空内容
186
+ yield content
187
+ except json.JSONDecodeError:
188
+ continue
189
+ except Exception as e:
190
+ print(f"解析响应错误: {e}")
191
+ continue
192
+
193
+ def _handle_generate_stream_response(self, response):
194
+ """处理 generate 端点的流式响应,支持思考模型的 <thinking> 标签输出"""
195
+ in_thinking = False
196
+
197
+ for line in response.iter_lines():
198
+ if not line:
199
+ continue
200
+ try:
201
+ if isinstance(line, bytes):
202
+ line = line.decode('utf-8')
203
+
204
+ if line.startswith('data: '):
205
+ line = line[6:]
206
+
207
+ data = json.loads(line)
208
+
209
+ thinking_content = data.get('thinking', '')
210
+ response_content = data.get('response', '')
211
+
212
+ if thinking_content:
213
+ if not in_thinking:
214
+ yield "<thinking>\n"
215
+ in_thinking = True
216
+ yield thinking_content
217
+ elif in_thinking:
218
+ # thinking 字段变空,代表思考阶段结束
219
+ yield "</thinking>\n"
220
+ in_thinking = False
221
+
222
+ if response_content:
223
+ yield response_content
224
+
225
+ except json.JSONDecodeError:
226
+ continue
227
+ except Exception as e:
228
+ print(f"解析响应错误: {e}")
229
+ continue
230
+
231
+ # 确保流异常结束时 thinking 标签正确闭合
232
+ if in_thinking:
233
+ yield "</thinking>\n"
234
+
@@ -0,0 +1,37 @@
1
+ from abc import ABC, abstractmethod
2
+ import importlib
3
+
4
+ class ProtocolConverterInterface(ABC):
5
+ @abstractmethod
6
+ def convert(self, message):
7
+ pass
8
+
9
+ @abstractmethod
10
+ def get_input_protocol(self):
11
+ pass
12
+
13
+ @abstractmethod
14
+ def get_output_protocol(self):
15
+ pass
16
+
17
+ class ProtocolConverterManager:
18
+ def __init__(self):
19
+ self.protocol_converters = {}
20
+
21
+ def load_converters(self, converter_names):
22
+ for name in converter_names:
23
+ try:
24
+ module = importlib.import_module(name)
25
+ converter_class = getattr(module, 'ProtocolConverter')
26
+ converter = converter_class()
27
+ input_protocol = converter.get_input_protocol()
28
+ output_protocol = converter.get_output_protocol()
29
+ key = (input_protocol, output_protocol)
30
+ self.protocol_converters[key] = converter
31
+ except Exception as e:
32
+ print(f"加载协议转换器 {name} 失败: {e}")
33
+
34
+ def get_converter(self, input_protocol, output_protocol):
35
+ key = (input_protocol, output_protocol)
36
+ return self.protocol_converters.get(key)
37
+
@@ -0,0 +1,80 @@
1
+ import redis
2
+ import json
3
+
4
+ class RedisClient:
5
+ def __init__(self, host, port, db):
6
+ self.redisClient = redis.StrictRedis(host=host, port=port, db=db, decode_responses=True)
7
+
8
+ def selectdb(self, db):
9
+ self.redisClient.select(db)
10
+
11
+ def set(self, key, value):
12
+ if not isinstance(value, str):
13
+ value = json.dumps(value, ensure_ascii=False)
14
+ self.redisClient.set(key, value)
15
+
16
+ def get(self, key):
17
+ value = self.redisClient.get(key)
18
+ if value is not None:
19
+ try:
20
+ value = json.loads(value)
21
+ except json.JSONDecodeError:
22
+ pass
23
+ return value
24
+
25
+ def exists(self, key):
26
+ return self.redisClient.exists(key)
27
+
28
+ def setex(self, key, value, seconds):
29
+ if not isinstance(value, str):
30
+ value = json.dumps(value, ensure_ascii=False)
31
+ self.redisClient.setex(key, value, seconds)
32
+
33
+ def delete(self, key):
34
+ self.redisClient.delete(key)
35
+
36
+ def keys(self, pattern):
37
+ return self.redisClient.keys(pattern)
38
+
39
+ def llen(self, key):
40
+ return self.redisClient.llen(key)
41
+
42
+ def rpush(self, key, value):
43
+ if not isinstance(value, str):
44
+ value = json.dumps(value, ensure_ascii=False)
45
+ self.redisClient.rpush(key, value)
46
+
47
+ def lrange(self, key, start, end):
48
+ values = self.redisClient.lrange(key, start, end)
49
+ return [json.loads(value) if isinstance(value, str) else value for value in values]
50
+
51
+ def lpop(self, key):
52
+ value = self.redisClient.lpop(key)
53
+ if value is not None:
54
+ try:
55
+ value = json.loads(value)
56
+ except json.JSONDecodeError:
57
+ pass
58
+ return value
59
+
60
+ def xgroup_create(self, key, group):
61
+ self.redisClient.xgroup_create(key, group, id='0', mkstream=True)
62
+
63
+ def xadd(self, key, value):
64
+ self.redisClient.xadd(key, value)
65
+
66
+ def xreadgroup(self, stream, group, consumer, block=0):
67
+ return self.redisClient.xreadgroup(group, consumer, {stream: '>'}, block=block)
68
+
69
+ def xack(self, stream, group, id):
70
+ self.redisClient.xack(stream, group, id)
71
+
72
+ def incr(self, key):
73
+ return self.redisClient.incr(key)
74
+
75
+ def expire(self, key, seconds):
76
+ self.redisClient.expire(key, seconds)
77
+
78
+
79
+
80
+
@@ -0,0 +1,161 @@
1
+ import time
2
+ import os
3
+ from datetime import datetime, date
4
+ from decimal import Decimal
5
+ import sqlite3
6
+
7
+ class Row(dict):
8
+ def __init__(self, data):
9
+ super().__init__()
10
+ for key, value in data:
11
+ self[key] = value
12
+
13
+ def __getattr__(self, key):
14
+ try:
15
+ return self[key]
16
+ except KeyError:
17
+ raise AttributeError(key)
18
+
19
+ def __setitem__(self, key, value):
20
+ if isinstance(value, datetime):
21
+ value = value.strftime('%Y-%m-%d %H:%M:%S')
22
+ elif isinstance(value, date):
23
+ value = value.strftime('%Y-%m-%d')
24
+ elif isinstance(value, Decimal):
25
+ # 将 Decimal 转换为 int 或 float
26
+ if value % 1 == 0: # 如果是整数
27
+ value = int(value)
28
+ else: # 如果是小数
29
+ value = float(value)
30
+ super().__setitem__(key, value)
31
+
32
+ class SqliteClient:
33
+ def __init__(self, database_path):
34
+ self._db = None
35
+ self._database_path = database_path
36
+ self._max_idle_time = 7 * 3600
37
+ self._last_use_time = time.time()
38
+
39
+ # 确保数据库目录存在
40
+ db_dir = os.path.dirname(database_path)
41
+ if db_dir and not os.path.exists(db_dir):
42
+ os.makedirs(db_dir)
43
+
44
+ try:
45
+ self.reconnect()
46
+ except Exception as e:
47
+ raise Exception(f"Cannot connect to SQLite database: {e}")
48
+
49
+ def __del__(self):
50
+ self.close()
51
+
52
+ def close(self):
53
+ if self._db:
54
+ try:
55
+ self._db.close()
56
+ except Exception:
57
+ pass
58
+ self._db = None
59
+
60
+ def reconnect(self):
61
+ self.close()
62
+ self._db = sqlite3.connect(self._database_path)
63
+ # 启用外键约束
64
+ self._db.execute("PRAGMA foreign_keys = ON")
65
+ # 配置行工厂函数,让查询结果支持字典访问
66
+ self._db.row_factory = sqlite3.Row
67
+
68
+ def _ensure_connected(self):
69
+ if not self._db or (time.time() - self._last_use_time > self._max_idle_time):
70
+ self.reconnect()
71
+ self._last_use_time = time.time()
72
+
73
+ def _cursor(self):
74
+ self._ensure_connected()
75
+ return self._db.cursor()
76
+
77
+ def _execute(self, cursor, query, parameters, kwparameters):
78
+ try:
79
+ if kwparameters:
80
+ cursor.execute(query, kwparameters)
81
+ else:
82
+ cursor.execute(query, parameters)
83
+ except Exception:
84
+ self.close()
85
+ raise
86
+
87
+ def query(self, query, *parameters, **kwparameters):
88
+ cursor = self._cursor()
89
+ try:
90
+ self._execute(cursor, query, parameters, kwparameters)
91
+ if cursor.description:
92
+ columns = [d[0] for d in cursor.description]
93
+ return [Row(zip(columns, row)) for row in cursor.fetchall()]
94
+ return []
95
+ finally:
96
+ cursor.close()
97
+
98
+ def get(self, query, *parameters, **kwparameters):
99
+ rows = self.query(query, *parameters, **kwparameters)
100
+ if not rows:
101
+ return None
102
+ elif len(rows) > 1:
103
+ raise Exception("Multiple rows returned for SqliteClient.get() query")
104
+ else:
105
+ return rows[0]
106
+
107
+ def execute_lastrowid(self, query, *parameters, **kwparameters):
108
+ cursor = self._cursor()
109
+ try:
110
+ self._execute(cursor, query, parameters, kwparameters)
111
+ self._db.commit()
112
+ return cursor.lastrowid
113
+ finally:
114
+ cursor.close()
115
+
116
+ def execute_rowcount(self, query, *parameters, **kwparameters):
117
+ cursor = self._cursor()
118
+ try:
119
+ self._execute(cursor, query, parameters, kwparameters)
120
+ self._db.commit()
121
+ return cursor.rowcount
122
+ finally:
123
+ cursor.close()
124
+
125
+ def execute(self, query, *parameters, **kwparameters):
126
+ return self.execute_lastrowid(query, *parameters, **kwparameters)
127
+
128
+ def execute_script(self, script):
129
+ """执行多条SQL语句"""
130
+ self._ensure_connected()
131
+ try:
132
+ self._db.executescript(script)
133
+ self._db.commit()
134
+ except Exception:
135
+ self._db.rollback()
136
+ raise
137
+
138
+ def transaction(self):
139
+ """返回事务上下文管理器"""
140
+ return Transaction(self)
141
+
142
+ update = execute_rowcount
143
+ insert = execute_lastrowid
144
+ delete = execute_rowcount
145
+
146
+
147
+ class Transaction:
148
+ """SQLite事务上下文管理器"""
149
+
150
+ def __init__(self, client):
151
+ self.client = client
152
+
153
+ def __enter__(self):
154
+ self.client._ensure_connected()
155
+ return self
156
+
157
+ def __exit__(self, exc_type, exc_val, exc_tb):
158
+ if exc_type is None:
159
+ self.client._db.commit()
160
+ else:
161
+ self.client._db.rollback()
@@ -0,0 +1,24 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import json
5
+ import uuid
6
+
7
+
8
+ def datetime_str(timestamp = None):
9
+ if timestamp is None:
10
+ return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
11
+ return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp))
12
+
13
+ def log_debug(appliaction, msg):
14
+ dt_str = datetime_str(None)
15
+ print(f'[{dt_str}] {appliaction}: {msg}')
16
+ sys.stdout.flush()
17
+
18
+ def uuid_generate():
19
+ return str(uuid.uuid4().hex.upper())
20
+
21
+ def json_dumps(data, **kwargs):
22
+ return json.dumps(data, ensure_ascii=False, **kwargs)
23
+
24
+
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: yxsdk
3
+ Version: 0.4.0
4
+ Summary: A Python SDK for common utilities
5
+ Author-email: Yi Chen <sgrchen@163.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sgrchen/yxsdk
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Requires-Python: >=3.7
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: mysql-connector-python>=8.0.0
19
+ Requires-Dist: redis>=3.0.0
20
+ Requires-Dist: psutil>=7.0.0
21
+ Requires-Dist: requests>=2.0.0
22
+ Requires-Dist: PyJWT>=2.3.0
23
+ Provides-Extra: dev
@@ -0,0 +1,16 @@
1
+ pyproject.toml
2
+ src/version.txt
3
+ src/yxsdk/__init__.py
4
+ src/yxsdk/aliyun_client.py
5
+ src/yxsdk/event_worker.py
6
+ src/yxsdk/mysql_client.py
7
+ src/yxsdk/ollama_client.py
8
+ src/yxsdk/protocol_converter.py
9
+ src/yxsdk/redis_client.py
10
+ src/yxsdk/sqlite_client.py
11
+ src/yxsdk/utils.py
12
+ src/yxsdk.egg-info/PKG-INFO
13
+ src/yxsdk.egg-info/SOURCES.txt
14
+ src/yxsdk.egg-info/dependency_links.txt
15
+ src/yxsdk.egg-info/requires.txt
16
+ src/yxsdk.egg-info/top_level.txt
@@ -0,0 +1,7 @@
1
+ mysql-connector-python>=8.0.0
2
+ redis>=3.0.0
3
+ psutil>=7.0.0
4
+ requests>=2.0.0
5
+ PyJWT>=2.3.0
6
+
7
+ [dev]
@@ -0,0 +1 @@
1
+ yxsdk