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 +23 -0
- yxsdk-0.4.0/pyproject.toml +48 -0
- yxsdk-0.4.0/setup.cfg +4 -0
- yxsdk-0.4.0/src/version.txt +1 -0
- yxsdk-0.4.0/src/yxsdk/__init__.py +29 -0
- yxsdk-0.4.0/src/yxsdk/aliyun_client.py +146 -0
- yxsdk-0.4.0/src/yxsdk/event_worker.py +123 -0
- yxsdk-0.4.0/src/yxsdk/mysql_client.py +117 -0
- yxsdk-0.4.0/src/yxsdk/ollama_client.py +234 -0
- yxsdk-0.4.0/src/yxsdk/protocol_converter.py +37 -0
- yxsdk-0.4.0/src/yxsdk/redis_client.py +80 -0
- yxsdk-0.4.0/src/yxsdk/sqlite_client.py +161 -0
- yxsdk-0.4.0/src/yxsdk/utils.py +24 -0
- yxsdk-0.4.0/src/yxsdk.egg-info/PKG-INFO +23 -0
- yxsdk-0.4.0/src/yxsdk.egg-info/SOURCES.txt +16 -0
- yxsdk-0.4.0/src/yxsdk.egg-info/dependency_links.txt +1 -0
- yxsdk-0.4.0/src/yxsdk.egg-info/requires.txt +7 -0
- yxsdk-0.4.0/src/yxsdk.egg-info/top_level.txt +1 -0
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 @@
|
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
yxsdk
|