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.
@@ -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,8 @@
1
+ Metadata-Version: 2.1
2
+ Name: ronds-datastore-postgresql
3
+ Version: 0.0.1
4
+ Summary: PostgreSQL driver for DataStore SDK
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: ronds-datastore-client
7
+ Requires-Dist: psycopg2-binary
8
+
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.3.4)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [datastore.drivers]
2
+ pg = datastore_postgresql.driver:PgDataStoreDriver
3
+ pgsql = datastore_postgresql.driver:PgDataStoreDriver
4
+ postgresql = datastore_postgresql.driver:PgDataStoreDriver
@@ -0,0 +1 @@
1
+ datastore_postgresql