fiuai-sdk-python 0.7.0__py3-none-any.whl → 0.7.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.
- fiuai_sdk_python/resp.py +51 -3
- {fiuai_sdk_python-0.7.0.dist-info → fiuai_sdk_python-0.7.2.dist-info}/METADATA +1 -1
- {fiuai_sdk_python-0.7.0.dist-info → fiuai_sdk_python-0.7.2.dist-info}/RECORD +5 -12
- fiuai_sdk_python/pkg/db/__init__.py +0 -35
- fiuai_sdk_python/pkg/db/config.py +0 -25
- fiuai_sdk_python/pkg/db/errors.py +0 -27
- fiuai_sdk_python/pkg/db/manager.py +0 -439
- fiuai_sdk_python/pkg/db/utils.py +0 -78
- fiuai_sdk_python/pkg/vector/__init__.py +0 -23
- fiuai_sdk_python/pkg/vector/vector.py +0 -853
- {fiuai_sdk_python-0.7.0.dist-info → fiuai_sdk_python-0.7.2.dist-info}/WHEEL +0 -0
- {fiuai_sdk_python-0.7.0.dist-info → fiuai_sdk_python-0.7.2.dist-info}/licenses/LICENSE +0 -0
fiuai_sdk_python/resp.py
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# Copyright (c) 2025 FiuAI
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
+
from httpx import Response
|
|
9
10
|
from typing import List, Dict, Any, Optional, Union
|
|
10
11
|
from pydantic import BaseModel, Field
|
|
11
12
|
|
|
@@ -30,7 +31,41 @@ class ApiResponse(BaseModel):
|
|
|
30
31
|
return self.http_success and self.api_success
|
|
31
32
|
|
|
32
33
|
|
|
33
|
-
def
|
|
34
|
+
def _get_response_body_preview(response: Response, max_chars: int = 300) -> str:
|
|
35
|
+
"""
|
|
36
|
+
安全读取响应体前若干字符,用于错误信息预览。兼容编码问题及中文。
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
raw = response.content
|
|
40
|
+
if not raw:
|
|
41
|
+
return ""
|
|
42
|
+
text = raw.decode("utf-8", errors="replace")
|
|
43
|
+
text = text.strip()
|
|
44
|
+
if len(text) > max_chars:
|
|
45
|
+
text = text[:max_chars] + "..."
|
|
46
|
+
return text
|
|
47
|
+
except Exception:
|
|
48
|
+
return ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_response_body_for_log(response: Response, max_chars: int = 2000) -> str:
|
|
52
|
+
"""
|
|
53
|
+
安全读取完整响应体用于日志,避免编码异常,支持中文。
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
raw = response.content
|
|
57
|
+
if not raw:
|
|
58
|
+
return "(empty)"
|
|
59
|
+
text = raw.decode("utf-8", errors="replace")
|
|
60
|
+
text = text.strip()
|
|
61
|
+
if len(text) > max_chars:
|
|
62
|
+
text = text[:max_chars] + "...(truncated)"
|
|
63
|
+
return text
|
|
64
|
+
except Exception as ex:
|
|
65
|
+
return f"(decode error: {ex})"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_response(response: Response) -> ApiResponse:
|
|
34
69
|
"""
|
|
35
70
|
解析HTTP响应,返回结构化的API响应
|
|
36
71
|
|
|
@@ -67,12 +102,25 @@ def parse_response(response) -> ApiResponse:
|
|
|
67
102
|
try:
|
|
68
103
|
response_data = response.json()
|
|
69
104
|
except Exception as e:
|
|
105
|
+
# 安全读取响应体,兼容非 UTF-8 及中文等,便于日志和错误信息完整
|
|
106
|
+
raw_preview = _get_response_body_preview(response, max_chars=300)
|
|
107
|
+
err_msg = f"Invalid JSON response (status={response.status_code}): {e}"
|
|
108
|
+
if raw_preview:
|
|
109
|
+
err_msg += f"; body preview: {raw_preview}"
|
|
110
|
+
else:
|
|
111
|
+
err_msg += "; response body empty (e.g. gateway/upstream error)"
|
|
112
|
+
logger.error(
|
|
113
|
+
"parse_response json failed, status=%s, error=%s, body=%s",
|
|
114
|
+
response.status_code,
|
|
115
|
+
str(e),
|
|
116
|
+
_get_response_body_for_log(response),
|
|
117
|
+
)
|
|
70
118
|
return ApiResponse(
|
|
71
119
|
http_success=True,
|
|
72
120
|
api_success=False,
|
|
73
121
|
status_code=response.status_code,
|
|
74
|
-
error_message=[
|
|
75
|
-
error_code=["API_INVALID_JSON"]
|
|
122
|
+
error_message=[err_msg],
|
|
123
|
+
error_code=["API_INVALID_JSON"],
|
|
76
124
|
)
|
|
77
125
|
|
|
78
126
|
# 检查API业务是否成功
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fiuai_sdk_python
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: FiuAI Python SDK - 企业级AI服务集成开发工具包
|
|
5
5
|
Project-URL: Homepage, https://github.com/fiuai/fiuai-sdk-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/fiuai/fiuai-sdk-python#readme
|
|
@@ -11,7 +11,7 @@ fiuai_sdk_python/error.py,sha256=YYsqP39vY8N7wWD4ervsx7ngcdXIMR59Wc4A4h4Rb-k,235
|
|
|
11
11
|
fiuai_sdk_python/item.py,sha256=RPvUYaJDWTyUBdqdrPKkgFR4txYXQVqoDPPhv0iBUac,1434
|
|
12
12
|
fiuai_sdk_python/perm.py,sha256=BEQjgJeW4JqG8mhNndRd8TBipnjCh2kBEMWEYXQgths,150
|
|
13
13
|
fiuai_sdk_python/profile.py,sha256=hcbJIYHAR-OEkFDRp-o1c91aVDinIwNsv7oAspFcxxQ,5526
|
|
14
|
-
fiuai_sdk_python/resp.py,sha256=
|
|
14
|
+
fiuai_sdk_python/resp.py,sha256=UiagvtsX8SQRRA226dCS9ggQ7tKWa1SLE4DaqCXGIKc,11861
|
|
15
15
|
fiuai_sdk_python/setup.py,sha256=ER0IPAouHhrVSzG0Iu87Ky0R5c4kCgOF77kRAOO-1MI,8025
|
|
16
16
|
fiuai_sdk_python/type.py,sha256=vinZKflNvmQNhqO5mDARAE6O133k0LiR1s1ZvexN_q4,28940
|
|
17
17
|
fiuai_sdk_python/util.py,sha256=x3TkNsC8_nzA-8x6ndIGrIpE9sRKpn3vlxnj2Hqpxwo,2326
|
|
@@ -25,18 +25,11 @@ fiuai_sdk_python/http/__init__.py,sha256=RMtrf0O-iuAGMIqfpQg6sQHj_O9Lo4Lhn7Kejca
|
|
|
25
25
|
fiuai_sdk_python/http/client.py,sha256=omcM8R8NGSszZlKBPikZr-NJXHgwpNYu7qDa_9H1Jug,14321
|
|
26
26
|
fiuai_sdk_python/pkg/cache/__init__.py,sha256=7mVRUKkAxCAHIWVZrIslel_kr0S5Je_l0I9Fy_iZjzI,112
|
|
27
27
|
fiuai_sdk_python/pkg/cache/redis_manager.py,sha256=APSRRmsJKRWfDvzIJiwNMtIqjzbheOdULskj5mI55Fk,6114
|
|
28
|
-
fiuai_sdk_python/pkg/db/__init__.py,sha256=IK-zw5tTiSpVMtT3zdVGMaqup08TACIWcEYWpe1htkc,709
|
|
29
|
-
fiuai_sdk_python/pkg/db/config.py,sha256=tId1db3209oZGVQKDwm-cvKu_GCbpbPerR8rgdeD3xE,645
|
|
30
|
-
fiuai_sdk_python/pkg/db/errors.py,sha256=JJwfM_sHXYAOfP5peg9rg8n2BsCMnPXIKVLrOFMPHOs,489
|
|
31
|
-
fiuai_sdk_python/pkg/db/manager.py,sha256=HapTkFOfCzQLCXWBAOlGHsjGCWVBNBLEMCDH7Kty7tI,14855
|
|
32
|
-
fiuai_sdk_python/pkg/db/utils.py,sha256=qqZ14eV7fy9x_mwcEDKJDz1W54xPndpJ1QfM_D0E3FI,2419
|
|
33
|
-
fiuai_sdk_python/pkg/vector/__init__.py,sha256=fvbetFxbHiFeojnTfKT3roA1hOcoNHF_eCRuG6HwMpI,404
|
|
34
|
-
fiuai_sdk_python/pkg/vector/vector.py,sha256=hdwhDSiKp4a7s6dVwFNTRk1BPqhZppywD8LnOudoMdo,28990
|
|
35
28
|
fiuai_sdk_python/utils/__init__.py,sha256=UwwsvqBsaRCHbWdx-wvM48szT3j50h95k9MZdbfawRQ,72
|
|
36
29
|
fiuai_sdk_python/utils/ids.py,sha256=ZDtEqt_Woth8ytPB2tdnnTIv7noWr8XYhSsUvkZ7Hc0,6448
|
|
37
30
|
fiuai_sdk_python/utils/logger.py,sha256=RuKn9TFmV1IfsRTV3ZdtXLZkM44GkHs32ehbJdrdu-8,3657
|
|
38
31
|
fiuai_sdk_python/utils/text.py,sha256=bnob_W0nj_Vj8Hp93B0cYmFOY8IhUWF0C8UedOYCNvs,1667
|
|
39
|
-
fiuai_sdk_python-0.7.
|
|
40
|
-
fiuai_sdk_python-0.7.
|
|
41
|
-
fiuai_sdk_python-0.7.
|
|
42
|
-
fiuai_sdk_python-0.7.
|
|
32
|
+
fiuai_sdk_python-0.7.2.dist-info/METADATA,sha256=875jpAqWZXx7CphAj7wDdLjgrfHX1YOvu9vj939rzVM,1523
|
|
33
|
+
fiuai_sdk_python-0.7.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
34
|
+
fiuai_sdk_python-0.7.2.dist-info/licenses/LICENSE,sha256=PFMF0dFErrBFqU-rryEby0yW8GBagYqrdbyZQHMUCJg,1062
|
|
35
|
+
fiuai_sdk_python-0.7.2.dist-info/RECORD,,
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# -- coding: utf-8 --
|
|
2
|
-
# Project: fiuai-world
|
|
3
|
-
# Created Date: 2025-01-27
|
|
4
|
-
# Author: liming
|
|
5
|
-
# Email: lmlala@aliyun.com
|
|
6
|
-
# Copyright (c) 2025 FiuAI
|
|
7
|
-
|
|
8
|
-
from pkg.db.errors import (
|
|
9
|
-
PostgresError,
|
|
10
|
-
PostgresConnectionError,
|
|
11
|
-
PostgresQueryError,
|
|
12
|
-
PostgresPoolError,
|
|
13
|
-
)
|
|
14
|
-
from pkg.db.config import PostgresConfig
|
|
15
|
-
from pkg.db.manager import (
|
|
16
|
-
PostgresManager,
|
|
17
|
-
postgres_manager,
|
|
18
|
-
)
|
|
19
|
-
from pkg.db.utils import (
|
|
20
|
-
escape_table_name,
|
|
21
|
-
build_frappe_table_name,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
__all__ = [
|
|
25
|
-
'PostgresManager',
|
|
26
|
-
'PostgresConfig',
|
|
27
|
-
'PostgresError',
|
|
28
|
-
'PostgresConnectionError',
|
|
29
|
-
'PostgresQueryError',
|
|
30
|
-
'PostgresPoolError',
|
|
31
|
-
'postgres_manager',
|
|
32
|
-
'escape_table_name',
|
|
33
|
-
'build_frappe_table_name',
|
|
34
|
-
]
|
|
35
|
-
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# -- coding: utf-8 --
|
|
2
|
-
# Project: fiuai-world
|
|
3
|
-
# Created Date: 2025-01-27
|
|
4
|
-
# Author: liming
|
|
5
|
-
# Email: lmlala@aliyun.com
|
|
6
|
-
# Copyright (c) 2025 FiuAI
|
|
7
|
-
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass
|
|
12
|
-
class PostgresConfig:
|
|
13
|
-
"""PostgreSQL 配置"""
|
|
14
|
-
host: str
|
|
15
|
-
port: int
|
|
16
|
-
user: str
|
|
17
|
-
password: str
|
|
18
|
-
database: str
|
|
19
|
-
pool_minconn: int = 1 # 连接池最小连接数
|
|
20
|
-
pool_maxconn: int = 10 # 连接池最大连接数
|
|
21
|
-
pool_timeout: int = 30 # 获取连接超时时间(秒)
|
|
22
|
-
connect_timeout: int = 10 # 连接超时时间(秒)
|
|
23
|
-
retry_count: int = 3 # 重试次数
|
|
24
|
-
retry_delay: int = 1 # 重试延迟(秒)
|
|
25
|
-
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# -- coding: utf-8 --
|
|
2
|
-
# Project: fiuai-world
|
|
3
|
-
# Created Date: 2025-01-27
|
|
4
|
-
# Author: liming
|
|
5
|
-
# Email: lmlala@aliyun.com
|
|
6
|
-
# Copyright (c) 2025 FiuAI
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class PostgresError(Exception):
|
|
10
|
-
"""PostgreSQL 基础错误类"""
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class PostgresConnectionError(PostgresError):
|
|
15
|
-
"""PostgreSQL 连接错误"""
|
|
16
|
-
pass
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class PostgresQueryError(PostgresError):
|
|
20
|
-
"""PostgreSQL 查询错误"""
|
|
21
|
-
pass
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class PostgresPoolError(PostgresError):
|
|
25
|
-
"""PostgreSQL 连接池错误"""
|
|
26
|
-
pass
|
|
27
|
-
|
|
@@ -1,439 +0,0 @@
|
|
|
1
|
-
# -- coding: utf-8 --
|
|
2
|
-
# Project: fiuai-world
|
|
3
|
-
# Created Date: 2025-01-27
|
|
4
|
-
# Author: liming
|
|
5
|
-
# Email: lmlala@aliyun.com
|
|
6
|
-
# Copyright (c) 2025 FiuAI
|
|
7
|
-
|
|
8
|
-
import time
|
|
9
|
-
import threading
|
|
10
|
-
from typing import Optional, Dict, Any, List, Union
|
|
11
|
-
from contextlib import contextmanager
|
|
12
|
-
|
|
13
|
-
import psycopg2
|
|
14
|
-
from psycopg2 import pool
|
|
15
|
-
from psycopg2.extras import RealDictCursor, DictCursor
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
from logging import getLogger
|
|
19
|
-
from .errors import (
|
|
20
|
-
PostgresError,
|
|
21
|
-
PostgresConnectionError,
|
|
22
|
-
PostgresQueryError,
|
|
23
|
-
PostgresPoolError,
|
|
24
|
-
)
|
|
25
|
-
from .config import PostgresConfig
|
|
26
|
-
|
|
27
|
-
logger = getLogger(__name__)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class PostgresManager:
|
|
31
|
-
"""PostgreSQL 连接池管理器单例类
|
|
32
|
-
|
|
33
|
-
提供连接池管理、重连、重试等稳定性机制
|
|
34
|
-
提供基础 CRUD 接口
|
|
35
|
-
"""
|
|
36
|
-
_instance: Optional['PostgresManager'] = None
|
|
37
|
-
_lock = threading.Lock()
|
|
38
|
-
_initialized = False
|
|
39
|
-
|
|
40
|
-
def __new__(cls):
|
|
41
|
-
if cls._instance is None:
|
|
42
|
-
with cls._lock:
|
|
43
|
-
if cls._instance is None:
|
|
44
|
-
cls._instance = super().__new__(cls)
|
|
45
|
-
return cls._instance
|
|
46
|
-
|
|
47
|
-
def __init__(self):
|
|
48
|
-
if hasattr(self, '_initialized') and self._initialized:
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
self._config: Optional[PostgresConfig] = None
|
|
52
|
-
self._pool: Optional[pool.ThreadedConnectionPool] = None
|
|
53
|
-
self._is_connected_ref = {'connected': False}
|
|
54
|
-
self._initialized = True
|
|
55
|
-
|
|
56
|
-
def initialize(self, config: PostgresConfig):
|
|
57
|
-
"""初始化 PostgreSQL 连接池
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
config: PostgreSQL 配置
|
|
61
|
-
|
|
62
|
-
Raises:
|
|
63
|
-
PostgresPoolError: 初始化失败时抛出
|
|
64
|
-
"""
|
|
65
|
-
if self._is_connected_ref['connected']:
|
|
66
|
-
logger.warning("PostgreSQL 已经初始化,跳过重复初始化")
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
self._config = config
|
|
70
|
-
|
|
71
|
-
try:
|
|
72
|
-
# 创建连接池
|
|
73
|
-
self._pool = pool.ThreadedConnectionPool(
|
|
74
|
-
minconn=config.pool_minconn,
|
|
75
|
-
maxconn=config.pool_maxconn,
|
|
76
|
-
host=config.host,
|
|
77
|
-
port=config.port,
|
|
78
|
-
user=config.user,
|
|
79
|
-
password=config.password,
|
|
80
|
-
database=config.database,
|
|
81
|
-
connect_timeout=config.connect_timeout,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
# 测试连接
|
|
85
|
-
test_conn = self._pool.getconn()
|
|
86
|
-
if not test_conn:
|
|
87
|
-
raise PostgresPoolError("无法从连接池获取连接")
|
|
88
|
-
|
|
89
|
-
# 执行简单查询测试连接
|
|
90
|
-
with test_conn.cursor() as cursor:
|
|
91
|
-
cursor.execute("SELECT 1")
|
|
92
|
-
cursor.fetchone()
|
|
93
|
-
|
|
94
|
-
self._pool.putconn(test_conn)
|
|
95
|
-
self._is_connected_ref['connected'] = True
|
|
96
|
-
|
|
97
|
-
logger.info(
|
|
98
|
-
f"PostgreSQL 连接池初始化成功: {config.host}:{config.port}/{config.database}, "
|
|
99
|
-
f"pool_size={config.pool_maxconn}"
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
except Exception as e:
|
|
103
|
-
self._is_connected_ref['connected'] = False
|
|
104
|
-
if self._pool:
|
|
105
|
-
try:
|
|
106
|
-
self._pool.closeall()
|
|
107
|
-
except Exception:
|
|
108
|
-
pass
|
|
109
|
-
self._pool = None
|
|
110
|
-
logger.error(f"PostgreSQL 初始化失败: {str(e)}")
|
|
111
|
-
raise PostgresPoolError(f"初始化失败: {str(e)}")
|
|
112
|
-
|
|
113
|
-
def _check_connection(self) -> bool:
|
|
114
|
-
"""检查连接是否有效
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
bool: 连接是否有效
|
|
118
|
-
"""
|
|
119
|
-
if not self._pool or not self._is_connected_ref.get('connected', False):
|
|
120
|
-
return False
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
conn = self._pool.getconn()
|
|
124
|
-
if not conn:
|
|
125
|
-
return False
|
|
126
|
-
|
|
127
|
-
with conn.cursor() as cursor:
|
|
128
|
-
cursor.execute("SELECT 1")
|
|
129
|
-
cursor.fetchone()
|
|
130
|
-
|
|
131
|
-
self._pool.putconn(conn)
|
|
132
|
-
return True
|
|
133
|
-
|
|
134
|
-
except Exception as e:
|
|
135
|
-
logger.warning(f"连接检查失败: {str(e)}")
|
|
136
|
-
return False
|
|
137
|
-
|
|
138
|
-
def _reconnect(self) -> bool:
|
|
139
|
-
"""重新连接
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
bool: 重连是否成功
|
|
143
|
-
"""
|
|
144
|
-
if not self._config:
|
|
145
|
-
return False
|
|
146
|
-
|
|
147
|
-
logger.info("尝试重新连接 PostgreSQL...")
|
|
148
|
-
|
|
149
|
-
try:
|
|
150
|
-
# 关闭旧连接池
|
|
151
|
-
if self._pool:
|
|
152
|
-
try:
|
|
153
|
-
self._pool.closeall()
|
|
154
|
-
except Exception:
|
|
155
|
-
pass
|
|
156
|
-
self._pool = None
|
|
157
|
-
|
|
158
|
-
# 重新创建连接池
|
|
159
|
-
self._pool = pool.ThreadedConnectionPool(
|
|
160
|
-
minconn=self._config.pool_minconn,
|
|
161
|
-
maxconn=self._config.pool_maxconn,
|
|
162
|
-
host=self._config.host,
|
|
163
|
-
port=self._config.port,
|
|
164
|
-
user=self._config.user,
|
|
165
|
-
password=self._config.password,
|
|
166
|
-
database=self._config.database,
|
|
167
|
-
connect_timeout=self._config.connect_timeout,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
# 测试连接
|
|
171
|
-
test_conn = self._pool.getconn()
|
|
172
|
-
if not test_conn:
|
|
173
|
-
raise PostgresPoolError("无法从连接池获取连接")
|
|
174
|
-
|
|
175
|
-
with test_conn.cursor() as cursor:
|
|
176
|
-
cursor.execute("SELECT 1")
|
|
177
|
-
cursor.fetchone()
|
|
178
|
-
|
|
179
|
-
self._pool.putconn(test_conn)
|
|
180
|
-
self._is_connected_ref['connected'] = True
|
|
181
|
-
|
|
182
|
-
return True
|
|
183
|
-
|
|
184
|
-
except Exception as e:
|
|
185
|
-
logger.error(f"重连失败: {str(e)}")
|
|
186
|
-
self._is_connected_ref['connected'] = False
|
|
187
|
-
return False
|
|
188
|
-
|
|
189
|
-
@contextmanager
|
|
190
|
-
def _get_connection(self):
|
|
191
|
-
"""获取连接(带重连机制)
|
|
192
|
-
|
|
193
|
-
Yields:
|
|
194
|
-
connection: PostgreSQL 连接对象
|
|
195
|
-
|
|
196
|
-
Raises:
|
|
197
|
-
PostgresConnectionError: 获取连接失败时抛出
|
|
198
|
-
"""
|
|
199
|
-
if not self._is_connected_ref['connected']:
|
|
200
|
-
if not self._reconnect():
|
|
201
|
-
raise PostgresConnectionError("无法连接到 PostgreSQL,请检查配置和网络")
|
|
202
|
-
|
|
203
|
-
conn = None
|
|
204
|
-
try:
|
|
205
|
-
conn = self._pool.getconn()
|
|
206
|
-
if not conn:
|
|
207
|
-
# 尝试重连一次
|
|
208
|
-
if self._reconnect():
|
|
209
|
-
conn = self._pool.getconn()
|
|
210
|
-
|
|
211
|
-
if not conn:
|
|
212
|
-
raise PostgresConnectionError("无法从连接池获取连接")
|
|
213
|
-
|
|
214
|
-
yield conn
|
|
215
|
-
|
|
216
|
-
except psycopg2.Error as e:
|
|
217
|
-
if conn:
|
|
218
|
-
try:
|
|
219
|
-
self._pool.putconn(conn, close=True)
|
|
220
|
-
except Exception:
|
|
221
|
-
pass
|
|
222
|
-
self._is_connected_ref['connected'] = False
|
|
223
|
-
raise PostgresConnectionError(f"数据库连接错误: {str(e)}")
|
|
224
|
-
except Exception as e:
|
|
225
|
-
if conn:
|
|
226
|
-
try:
|
|
227
|
-
self._pool.putconn(conn)
|
|
228
|
-
except Exception:
|
|
229
|
-
pass
|
|
230
|
-
raise PostgresConnectionError(f"获取连接时发生错误: {str(e)}")
|
|
231
|
-
finally:
|
|
232
|
-
if conn:
|
|
233
|
-
try:
|
|
234
|
-
self._pool.putconn(conn)
|
|
235
|
-
except Exception:
|
|
236
|
-
pass
|
|
237
|
-
|
|
238
|
-
def _execute_with_retry(
|
|
239
|
-
self,
|
|
240
|
-
query: str,
|
|
241
|
-
params: Optional[tuple] = None,
|
|
242
|
-
retry_count: Optional[int] = None,
|
|
243
|
-
fetch: bool = True,
|
|
244
|
-
fetch_one: bool = False,
|
|
245
|
-
as_dict: bool = False,
|
|
246
|
-
) -> Optional[Union[List[Dict[str, Any]], List[tuple], int]]:
|
|
247
|
-
"""执行查询(带重试机制)
|
|
248
|
-
|
|
249
|
-
Args:
|
|
250
|
-
query: 查询语句
|
|
251
|
-
params: 查询参数
|
|
252
|
-
retry_count: 重试次数,None 则使用配置中的值
|
|
253
|
-
fetch: 是否获取结果
|
|
254
|
-
fetch_one: 是否只获取一条结果
|
|
255
|
-
as_dict: 是否返回字典格式
|
|
256
|
-
|
|
257
|
-
Returns:
|
|
258
|
-
Optional[List[Dict[str, Any]]]: 查询结果,如果 fetch=False 则返回受影响的行数(int)
|
|
259
|
-
|
|
260
|
-
Raises:
|
|
261
|
-
PostgresQueryError: 执行失败时抛出
|
|
262
|
-
"""
|
|
263
|
-
if retry_count is None:
|
|
264
|
-
retry_count = self._config.retry_count if self._config else 3
|
|
265
|
-
|
|
266
|
-
last_error = None
|
|
267
|
-
for attempt in range(retry_count + 1):
|
|
268
|
-
try:
|
|
269
|
-
with self._get_connection() as conn:
|
|
270
|
-
cursor_factory = RealDictCursor if as_dict else None
|
|
271
|
-
with conn.cursor(cursor_factory=cursor_factory) as cursor:
|
|
272
|
-
logger.debug(f"[postgres-sql] {query}")
|
|
273
|
-
if params:
|
|
274
|
-
logger.debug(f"[postgres-params] {params}")
|
|
275
|
-
cursor.execute(query, params)
|
|
276
|
-
else:
|
|
277
|
-
cursor.execute(query)
|
|
278
|
-
|
|
279
|
-
if not fetch:
|
|
280
|
-
rowcount = cursor.rowcount
|
|
281
|
-
conn.commit()
|
|
282
|
-
return rowcount
|
|
283
|
-
|
|
284
|
-
if fetch_one:
|
|
285
|
-
row = cursor.fetchone()
|
|
286
|
-
if as_dict and row:
|
|
287
|
-
return [dict(row)]
|
|
288
|
-
return [row] if row else []
|
|
289
|
-
else:
|
|
290
|
-
rows = cursor.fetchall()
|
|
291
|
-
if as_dict:
|
|
292
|
-
return [dict(row) for row in rows]
|
|
293
|
-
return rows
|
|
294
|
-
|
|
295
|
-
except psycopg2.Error as e:
|
|
296
|
-
last_error = PostgresQueryError(f"数据库查询错误: {str(e)}")
|
|
297
|
-
# 如果是连接相关错误,尝试重连
|
|
298
|
-
error_msg = str(e).lower()
|
|
299
|
-
if "connection" in error_msg or "timeout" in error_msg or "server closed" in error_msg:
|
|
300
|
-
self._is_connected_ref['connected'] = False
|
|
301
|
-
if attempt < retry_count:
|
|
302
|
-
time.sleep(self._config.retry_delay if self._config else 1)
|
|
303
|
-
continue
|
|
304
|
-
|
|
305
|
-
if attempt < retry_count:
|
|
306
|
-
time.sleep(self._config.retry_delay if self._config else 1)
|
|
307
|
-
continue
|
|
308
|
-
|
|
309
|
-
except Exception as e:
|
|
310
|
-
last_error = PostgresQueryError(f"执行查询时发生错误: {str(e)}")
|
|
311
|
-
if attempt < retry_count:
|
|
312
|
-
time.sleep(self._config.retry_delay if self._config else 1)
|
|
313
|
-
continue
|
|
314
|
-
|
|
315
|
-
raise last_error or PostgresQueryError("查询执行失败")
|
|
316
|
-
|
|
317
|
-
def execute(
|
|
318
|
-
self,
|
|
319
|
-
query: str,
|
|
320
|
-
params: Optional[tuple] = None,
|
|
321
|
-
retry_count: Optional[int] = None,
|
|
322
|
-
) -> int:
|
|
323
|
-
"""执行 SQL 语句(不返回结果)
|
|
324
|
-
|
|
325
|
-
Args:
|
|
326
|
-
query: SQL 语句
|
|
327
|
-
params: 查询参数
|
|
328
|
-
retry_count: 重试次数
|
|
329
|
-
|
|
330
|
-
Returns:
|
|
331
|
-
int: 受影响的行数
|
|
332
|
-
|
|
333
|
-
Raises:
|
|
334
|
-
PostgresQueryError: 执行失败时抛出
|
|
335
|
-
"""
|
|
336
|
-
if not self._is_connected_ref['connected']:
|
|
337
|
-
raise PostgresQueryError("PostgreSQL 未初始化,请先调用 initialize()")
|
|
338
|
-
|
|
339
|
-
result = self._execute_with_retry(query, params, retry_count, fetch=False)
|
|
340
|
-
return result if result is not None else 0
|
|
341
|
-
|
|
342
|
-
def query(
|
|
343
|
-
self,
|
|
344
|
-
query: str,
|
|
345
|
-
params: Optional[tuple] = None,
|
|
346
|
-
retry_count: Optional[int] = None,
|
|
347
|
-
as_dict: bool = True,
|
|
348
|
-
) -> Union[List[Dict[str, Any]], List[tuple]]:
|
|
349
|
-
"""执行查询(返回结果)
|
|
350
|
-
|
|
351
|
-
Args:
|
|
352
|
-
query: 查询语句
|
|
353
|
-
params: 查询参数
|
|
354
|
-
retry_count: 重试次数
|
|
355
|
-
as_dict: 是否返回字典格式
|
|
356
|
-
|
|
357
|
-
Returns:
|
|
358
|
-
Union[List[Dict[str, Any]], List[tuple]]: 查询结果列表,as_dict=True 时返回字典列表,否则返回元组列表
|
|
359
|
-
|
|
360
|
-
Raises:
|
|
361
|
-
PostgresQueryError: 执行失败时抛出
|
|
362
|
-
"""
|
|
363
|
-
if not self._is_connected_ref['connected']:
|
|
364
|
-
raise PostgresQueryError("PostgreSQL 未初始化,请先调用 initialize()")
|
|
365
|
-
|
|
366
|
-
result = self._execute_with_retry(query, params, retry_count, fetch=True, as_dict=as_dict)
|
|
367
|
-
return result or []
|
|
368
|
-
|
|
369
|
-
def query_one(
|
|
370
|
-
self,
|
|
371
|
-
query: str,
|
|
372
|
-
params: Optional[tuple] = None,
|
|
373
|
-
retry_count: Optional[int] = None,
|
|
374
|
-
as_dict: bool = True,
|
|
375
|
-
) -> Optional[Union[Dict[str, Any], tuple]]:
|
|
376
|
-
"""执行查询(返回单条结果)
|
|
377
|
-
|
|
378
|
-
Args:
|
|
379
|
-
query: 查询语句
|
|
380
|
-
params: 查询参数
|
|
381
|
-
retry_count: 重试次数
|
|
382
|
-
as_dict: 是否返回字典格式
|
|
383
|
-
|
|
384
|
-
Returns:
|
|
385
|
-
Optional[Union[Dict[str, Any], tuple]]: 查询结果,as_dict=True 时返回字典,否则返回元组,如果不存在则返回 None
|
|
386
|
-
|
|
387
|
-
Raises:
|
|
388
|
-
PostgresQueryError: 执行失败时抛出
|
|
389
|
-
"""
|
|
390
|
-
if not self._is_connected_ref['connected']:
|
|
391
|
-
raise PostgresQueryError("PostgreSQL 未初始化,请先调用 initialize()")
|
|
392
|
-
|
|
393
|
-
result = self._execute_with_retry(
|
|
394
|
-
query, params, retry_count, fetch=True, fetch_one=True, as_dict=as_dict
|
|
395
|
-
)
|
|
396
|
-
return result[0] if result else None
|
|
397
|
-
|
|
398
|
-
@contextmanager
|
|
399
|
-
def transaction(self):
|
|
400
|
-
"""事务上下文管理器
|
|
401
|
-
|
|
402
|
-
Yields:
|
|
403
|
-
connection: PostgreSQL 连接对象
|
|
404
|
-
|
|
405
|
-
Raises:
|
|
406
|
-
PostgresConnectionError: 获取连接失败时抛出
|
|
407
|
-
"""
|
|
408
|
-
with self._get_connection() as conn:
|
|
409
|
-
try:
|
|
410
|
-
yield conn
|
|
411
|
-
conn.commit()
|
|
412
|
-
except Exception as e:
|
|
413
|
-
conn.rollback()
|
|
414
|
-
raise PostgresQueryError(f"事务执行失败: {str(e)}")
|
|
415
|
-
|
|
416
|
-
def close(self):
|
|
417
|
-
"""关闭连接池"""
|
|
418
|
-
if self._pool:
|
|
419
|
-
try:
|
|
420
|
-
self._pool.closeall()
|
|
421
|
-
except Exception as e:
|
|
422
|
-
logger.warning(f"关闭连接池时发生错误: {str(e)}")
|
|
423
|
-
finally:
|
|
424
|
-
self._pool = None
|
|
425
|
-
self._is_connected_ref['connected'] = False
|
|
426
|
-
logger.info("PostgreSQL 连接池已关闭")
|
|
427
|
-
|
|
428
|
-
def is_connected(self) -> bool:
|
|
429
|
-
"""检查是否已连接
|
|
430
|
-
|
|
431
|
-
Returns:
|
|
432
|
-
bool: 是否已连接
|
|
433
|
-
"""
|
|
434
|
-
return self._is_connected_ref['connected'] and self._check_connection()
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
# 创建全局单例实例
|
|
438
|
-
postgres_manager = PostgresManager()
|
|
439
|
-
|