ronds-datastore-postgresql 0.0.1__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.
- datastore_postgresql/__init__.py +11 -0
- datastore_postgresql/connection.py +226 -0
- datastore_postgresql/driver.py +30 -0
- datastore_postgresql/pg_read_request.py +31 -0
- ronds_datastore_postgresql-0.0.1.dist-info/METADATA +8 -0
- ronds_datastore_postgresql-0.0.1.dist-info/RECORD +9 -0
- ronds_datastore_postgresql-0.0.1.dist-info/WHEEL +5 -0
- ronds_datastore_postgresql-0.0.1.dist-info/entry_points.txt +4 -0
- ronds_datastore_postgresql-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""PostgreSQL driver for DataStore SDK."""
|
|
2
|
+
from .driver import PgDataStoreDriver
|
|
3
|
+
from .connection import PgDataStoreConnection
|
|
4
|
+
from .pg_read_request import PgReadRequest, PgReadRequestBuilder
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"PgDataStoreDriver",
|
|
8
|
+
"PgDataStoreConnection",
|
|
9
|
+
"PgReadRequest",
|
|
10
|
+
"PgReadRequestBuilder",
|
|
11
|
+
]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
PostgreSQL 连接实现
|
|
4
|
+
支持 read(点查/扫描/自定义 SQL)、write、delete
|
|
5
|
+
URL 格式:pg://host:port/database 或 postgresql://host:port/database/schema/table
|
|
6
|
+
"""
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from datastore_client.exceptions import DataStoreException
|
|
10
|
+
from datastore_client.model.request import ReadRequest
|
|
11
|
+
from datastore_client.model.response import WriteResponse
|
|
12
|
+
from datastore_client.spi.connection import DataStoreConnection
|
|
13
|
+
from datastore_client.spi.config import ConnectionConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _quote_identifier(name: str) -> str:
|
|
17
|
+
return '"' + name.replace('"', '""') + '"'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _camel_to_snake(camel: str) -> str:
|
|
21
|
+
if not camel:
|
|
22
|
+
return camel
|
|
23
|
+
result = []
|
|
24
|
+
for i, c in enumerate(camel):
|
|
25
|
+
if c.isupper():
|
|
26
|
+
if i > 0:
|
|
27
|
+
result.append("_")
|
|
28
|
+
result.append(c.lower())
|
|
29
|
+
else:
|
|
30
|
+
result.append(c)
|
|
31
|
+
return "".join(result)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _snake_to_camel(snake: str) -> str:
|
|
35
|
+
if not snake:
|
|
36
|
+
return snake
|
|
37
|
+
result = []
|
|
38
|
+
cap = False
|
|
39
|
+
for c in snake:
|
|
40
|
+
if c == "_":
|
|
41
|
+
cap = True
|
|
42
|
+
else:
|
|
43
|
+
result.append(c.upper() if cap else c)
|
|
44
|
+
cap = False
|
|
45
|
+
return "".join(result)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PgDataStoreConnection(DataStoreConnection):
|
|
49
|
+
"""PostgreSQL 存储连接"""
|
|
50
|
+
|
|
51
|
+
DEFAULT_TABLE = "kv"
|
|
52
|
+
DEFAULT_SCHEMA = "public"
|
|
53
|
+
DEFAULT_PORT = 5432
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
connection_config: ConnectionConfig,
|
|
58
|
+
data_schema: Optional[str],
|
|
59
|
+
):
|
|
60
|
+
super().__init__(connection_config, data_schema)
|
|
61
|
+
config = connection_config
|
|
62
|
+
self._database = (
|
|
63
|
+
config.namespace.strip() if config.namespace and config.namespace.strip() else "postgres"
|
|
64
|
+
)
|
|
65
|
+
self._schema = config.get_parameter("schema") or self.DEFAULT_SCHEMA
|
|
66
|
+
self._table = (
|
|
67
|
+
config.entity.strip() if config.entity and config.entity.strip() else self.DEFAULT_TABLE
|
|
68
|
+
)
|
|
69
|
+
self._connection = None
|
|
70
|
+
self._conn_lock = __import__("threading").Lock()
|
|
71
|
+
|
|
72
|
+
def _get_connection(self):
|
|
73
|
+
import psycopg2
|
|
74
|
+
|
|
75
|
+
if self._connection is None or self._connection.closed:
|
|
76
|
+
with self._conn_lock:
|
|
77
|
+
if self._connection is None or self._connection.closed:
|
|
78
|
+
config = self.get_connection_config()
|
|
79
|
+
hosts = config.hosts
|
|
80
|
+
ports = config.ports
|
|
81
|
+
|
|
82
|
+
if hosts and len(hosts) > 0:
|
|
83
|
+
host = hosts[0]
|
|
84
|
+
port = (
|
|
85
|
+
ports[0]
|
|
86
|
+
if ports and len(ports) > 0 and ports[0] > 0
|
|
87
|
+
else self.DEFAULT_PORT
|
|
88
|
+
)
|
|
89
|
+
elif config.endpoints and config.endpoints.strip():
|
|
90
|
+
part = config.endpoints.split(",")[0].strip()
|
|
91
|
+
if ":" in part:
|
|
92
|
+
host, port_str = part.rsplit(":", 1)
|
|
93
|
+
host = host.strip()
|
|
94
|
+
port = int(port_str.strip())
|
|
95
|
+
else:
|
|
96
|
+
host = part
|
|
97
|
+
port = self.DEFAULT_PORT
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"PostgreSQL URL must specify host (e.g. pg://host:port/database)"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
conn_params = {
|
|
104
|
+
"host": host,
|
|
105
|
+
"port": port,
|
|
106
|
+
"dbname": self._database,
|
|
107
|
+
}
|
|
108
|
+
auth = config.auth_info
|
|
109
|
+
if auth and auth.username:
|
|
110
|
+
conn_params["user"] = auth.username
|
|
111
|
+
if auth and auth.password:
|
|
112
|
+
conn_params["password"] = auth.password
|
|
113
|
+
sslmode = config.get_parameter("sslmode")
|
|
114
|
+
if sslmode:
|
|
115
|
+
conn_params["sslmode"] = sslmode
|
|
116
|
+
|
|
117
|
+
self._connection = psycopg2.connect(**conn_params)
|
|
118
|
+
return self._connection
|
|
119
|
+
|
|
120
|
+
def read(self, request: ReadRequest) -> List[Any]:
|
|
121
|
+
where_clause = request.get_option(ReadRequest.OPT_WHERE_CLAUSE)
|
|
122
|
+
limit = request.get_limit() or 1000
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
conn = self._get_connection()
|
|
126
|
+
|
|
127
|
+
custom_sql = request.get_option(ReadRequest.OPT_SQL)
|
|
128
|
+
if custom_sql and custom_sql.strip():
|
|
129
|
+
return self._read_by_sql(conn, custom_sql)
|
|
130
|
+
|
|
131
|
+
full_table = f"{_quote_identifier(self._schema)}.{_quote_identifier(self._table)}"
|
|
132
|
+
|
|
133
|
+
if where_clause and where_clause.strip():
|
|
134
|
+
sql = f"SELECT * FROM {full_table} WHERE {where_clause} LIMIT {limit}"
|
|
135
|
+
else:
|
|
136
|
+
sql = f"SELECT * FROM {full_table} LIMIT {limit}"
|
|
137
|
+
|
|
138
|
+
with conn.cursor() as cur:
|
|
139
|
+
cur.execute(sql)
|
|
140
|
+
columns = [desc[0] for desc in cur.description]
|
|
141
|
+
rows = cur.fetchall()
|
|
142
|
+
result = []
|
|
143
|
+
for row in rows:
|
|
144
|
+
obj = {}
|
|
145
|
+
for i, col in enumerate(columns):
|
|
146
|
+
camel = _snake_to_camel(col) if "_" in col else col
|
|
147
|
+
obj[camel] = row[i]
|
|
148
|
+
result.append(obj)
|
|
149
|
+
return result
|
|
150
|
+
except DataStoreException:
|
|
151
|
+
raise
|
|
152
|
+
except Exception as e:
|
|
153
|
+
raise DataStoreException("PostgreSQL read failed", cause=e)
|
|
154
|
+
|
|
155
|
+
def _read_by_sql(self, conn, custom_sql: str) -> List[Any]:
|
|
156
|
+
with conn.cursor() as cur:
|
|
157
|
+
cur.execute(custom_sql)
|
|
158
|
+
columns = [desc[0] for desc in cur.description]
|
|
159
|
+
rows = cur.fetchall()
|
|
160
|
+
result = []
|
|
161
|
+
for row in rows:
|
|
162
|
+
obj = {}
|
|
163
|
+
for i, col in enumerate(columns):
|
|
164
|
+
camel = _snake_to_camel(col) if "_" in col else col
|
|
165
|
+
obj[camel] = row[i]
|
|
166
|
+
result.append(obj)
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
def write(self, data: Any, options: Optional[Dict[str, Any]] = None) -> WriteResponse:
|
|
170
|
+
if data is None:
|
|
171
|
+
return WriteResponse.failure("write data is null")
|
|
172
|
+
try:
|
|
173
|
+
if isinstance(data, dict):
|
|
174
|
+
cols = list(data.keys())
|
|
175
|
+
vals = list(data.values())
|
|
176
|
+
else:
|
|
177
|
+
import json
|
|
178
|
+
data_str = json.dumps(data, default=str)
|
|
179
|
+
data_dict = json.loads(data_str)
|
|
180
|
+
cols = list(data_dict.keys())
|
|
181
|
+
vals = list(data_dict.values())
|
|
182
|
+
|
|
183
|
+
if not cols:
|
|
184
|
+
return WriteResponse.failure("write data has no fields")
|
|
185
|
+
|
|
186
|
+
snake_cols = [_camel_to_snake(c) for c in cols]
|
|
187
|
+
col_list = ", ".join(_quote_identifier(c) for c in snake_cols)
|
|
188
|
+
placeholders = ", ".join(["%s"] * len(cols))
|
|
189
|
+
full_table = f"{_quote_identifier(self._schema)}.{_quote_identifier(self._table)}"
|
|
190
|
+
sql = f"INSERT INTO {full_table} ({col_list}) VALUES ({placeholders})"
|
|
191
|
+
|
|
192
|
+
conn = self._get_connection()
|
|
193
|
+
with conn.cursor() as cur:
|
|
194
|
+
cur.execute(sql, vals)
|
|
195
|
+
conn.commit()
|
|
196
|
+
return WriteResponse.ok()
|
|
197
|
+
except DataStoreException:
|
|
198
|
+
raise
|
|
199
|
+
except Exception as e:
|
|
200
|
+
return WriteResponse.failure(str(e))
|
|
201
|
+
|
|
202
|
+
def delete(self, options: Optional[Dict[str, Any]] = None) -> WriteResponse:
|
|
203
|
+
where_clause = (options or {}).get(ReadRequest.OPT_WHERE_CLAUSE)
|
|
204
|
+
if not where_clause or not str(where_clause).strip():
|
|
205
|
+
raise DataStoreException("PostgreSQL delete requires whereClause in options")
|
|
206
|
+
try:
|
|
207
|
+
full_table = f"{_quote_identifier(self._schema)}.{_quote_identifier(self._table)}"
|
|
208
|
+
sql = f"DELETE FROM {full_table} WHERE {where_clause}"
|
|
209
|
+
conn = self._get_connection()
|
|
210
|
+
with conn.cursor() as cur:
|
|
211
|
+
cur.execute(sql)
|
|
212
|
+
conn.commit()
|
|
213
|
+
return WriteResponse.ok()
|
|
214
|
+
except DataStoreException:
|
|
215
|
+
raise
|
|
216
|
+
except Exception as e:
|
|
217
|
+
return WriteResponse.failure(str(e))
|
|
218
|
+
|
|
219
|
+
def close(self) -> None:
|
|
220
|
+
with self._conn_lock:
|
|
221
|
+
if self._connection and not self._connection.closed:
|
|
222
|
+
try:
|
|
223
|
+
self._connection.close()
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
self._connection = None
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""PostgreSQL 存储驱动"""
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from datastore_client.model.resource import ResourceCategory
|
|
6
|
+
from datastore_client.spi.config import ConnectionConfig
|
|
7
|
+
from datastore_client.spi.connection import DataStoreConnection
|
|
8
|
+
from datastore_client.spi.driver import DataStoreDriver
|
|
9
|
+
|
|
10
|
+
from .connection import PgDataStoreConnection
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PgDataStoreDriver(DataStoreDriver):
|
|
14
|
+
"""PostgreSQL 存储驱动
|
|
15
|
+
URL 格式: pg://host:port/database 或 postgresql://host:port/database 或 pg://host:port/database/schema/table
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def accepts(self, scheme: str) -> bool:
|
|
19
|
+
s = (scheme or "").lower()
|
|
20
|
+
return s in ("pg", "postgresql", "pgsql")
|
|
21
|
+
|
|
22
|
+
def connect(
|
|
23
|
+
self,
|
|
24
|
+
config: ConnectionConfig,
|
|
25
|
+
data_schema: Optional[str],
|
|
26
|
+
) -> DataStoreConnection:
|
|
27
|
+
return PgDataStoreConnection(config, data_schema)
|
|
28
|
+
|
|
29
|
+
def get_category(self) -> ResourceCategory:
|
|
30
|
+
return ResourceCategory.DATABASE
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""PostgreSQL 读取请求的强类型封装(对齐 Java PgReadRequest)。"""
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from datastore_client.model.request import ReadRequest, ReadRequestBuilder
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PgReadRequest(ReadRequest):
|
|
9
|
+
def get_where_clause(self) -> Optional[str]:
|
|
10
|
+
return self.get_option(ReadRequest.OPT_WHERE_CLAUSE)
|
|
11
|
+
|
|
12
|
+
def get_sql(self) -> Optional[str]:
|
|
13
|
+
return self.get_option(ReadRequest.OPT_SQL)
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def builder(cls) -> "PgReadRequestBuilder":
|
|
17
|
+
return PgReadRequestBuilder()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PgReadRequestBuilder(ReadRequestBuilder):
|
|
21
|
+
def _self(self) -> "PgReadRequestBuilder":
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def where_clause(self, where_clause: str) -> "PgReadRequestBuilder":
|
|
25
|
+
return self.option(ReadRequest.OPT_WHERE_CLAUSE, where_clause)
|
|
26
|
+
|
|
27
|
+
def sql(self, sql: str) -> "PgReadRequestBuilder":
|
|
28
|
+
return self.option(ReadRequest.OPT_SQL, sql)
|
|
29
|
+
|
|
30
|
+
def build(self) -> PgReadRequest:
|
|
31
|
+
return PgReadRequest(self._timeout_ms, dict(self._options))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
datastore_postgresql/__init__.py,sha256=vhHPFxter_zDjwSe3yVCNYYtaLaK__WX18TUcOkTyXo,321
|
|
2
|
+
datastore_postgresql/connection.py,sha256=X-BtLTYDiYr2mKGLJ8eQbxue8tfkObzeiJKiB_NifME,8685
|
|
3
|
+
datastore_postgresql/driver.py,sha256=yDCjW2Y4UwNhe_j3K8AV5HLc-Ko1VNVvmtRL3JhvNgk,1008
|
|
4
|
+
datastore_postgresql/pg_read_request.py,sha256=nMnfhCwTE4omNnCoSLb9qB7R1TA6ptWSEKZCzL_1CCo,1062
|
|
5
|
+
ronds_datastore_postgresql-0.0.1.dist-info/METADATA,sha256=_1Y8mgDkjLH9D7HfjfT7Z3uAXVWfUuqIfqDE5_BCHG0,216
|
|
6
|
+
ronds_datastore_postgresql-0.0.1.dist-info/WHEEL,sha256=BNRMDyzLkkcmlv0J8ppDQkk2VED33SesJDynr9ED1gc,91
|
|
7
|
+
ronds_datastore_postgresql-0.0.1.dist-info/entry_points.txt,sha256=S57eyvXNiboW9ocE5G5q1XCAWnSo98PIcDgLAVL0RIQ,184
|
|
8
|
+
ronds_datastore_postgresql-0.0.1.dist-info/top_level.txt,sha256=LdSbSYi9J95RU07dFhShT8OMJWzLu_ZJP_kJJJhkukE,21
|
|
9
|
+
ronds_datastore_postgresql-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
datastore_postgresql
|