datus-postgresql 0.1.0__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,16 @@
1
+ # Copyright 2025-present DatusAI, Inc.
2
+ # Licensed under the Apache License, Version 2.0.
3
+ # See http://www.apache.org/licenses/LICENSE-2.0 for details.
4
+
5
+ from .config import PostgreSQLConfig
6
+ from .connector import PostgreSQLConnector
7
+
8
+ __version__ = "0.1.0"
9
+ __all__ = ["PostgreSQLConnector", "PostgreSQLConfig", "register"]
10
+
11
+
12
+ def register():
13
+ """Register PostgreSQL connector with Datus registry."""
14
+ from datus.tools.db_tools import connector_registry
15
+
16
+ connector_registry.register("postgresql", PostgreSQLConnector, config_class=PostgreSQLConfig)
@@ -0,0 +1,24 @@
1
+ # Copyright 2025-present DatusAI, Inc.
2
+ # Licensed under the Apache License, Version 2.0.
3
+ # See http://www.apache.org/licenses/LICENSE-2.0 for details.
4
+
5
+ from typing import Optional
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+
10
+ class PostgreSQLConfig(BaseModel):
11
+ """PostgreSQL-specific configuration."""
12
+
13
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
14
+
15
+ host: str = Field(default="127.0.0.1", description="PostgreSQL server host")
16
+ port: int = Field(default=5432, description="PostgreSQL server port")
17
+ username: str = Field(..., description="PostgreSQL username")
18
+ password: str = Field(default="", description="PostgreSQL password", json_schema_extra={"input_type": "password"})
19
+ database: Optional[str] = Field(default=None, description="Default database name")
20
+ schema_name: Optional[str] = Field(default="public", alias="schema", description="Default schema name")
21
+ sslmode: str = Field(
22
+ default="prefer", description="SSL mode (disable, allow, prefer, require, verify-ca, verify-full)"
23
+ )
24
+ timeout_seconds: int = Field(default=30, description="Connection timeout in seconds")
@@ -0,0 +1,530 @@
1
+ # Copyright 2025-present DatusAI, Inc.
2
+ # Licensed under the Apache License, Version 2.0.
3
+ # See http://www.apache.org/licenses/LICENSE-2.0 for details.
4
+
5
+ from typing import Any, Dict, List, Optional, Set, Union, override
6
+ from urllib.parse import quote_plus
7
+
8
+ from datus.schemas.base import TABLE_TYPE
9
+ from datus.tools.db_tools.base import list_to_in_str
10
+ from datus.utils.constants import DBType
11
+ from datus.utils.exceptions import DatusException, ErrorCode
12
+ from datus.utils.loggings import get_logger
13
+ from datus_sqlalchemy import SQLAlchemyConnector
14
+ from pydantic import BaseModel, Field
15
+
16
+ from .config import PostgreSQLConfig
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class TableMetadataNames(BaseModel):
22
+ """Metadata configuration for different PostgreSQL object types."""
23
+
24
+ info_table: str = Field(..., description="INFORMATION_SCHEMA table name or pg_catalog view")
25
+ table_types: Optional[List[str]] = Field(default=None, description="TABLE_TYPE values in INFORMATION_SCHEMA")
26
+
27
+
28
+ # Metadata configuration for PostgreSQL objects
29
+ METADATA_DICT: Dict[TABLE_TYPE, TableMetadataNames] = {
30
+ "table": TableMetadataNames(
31
+ info_table="tables",
32
+ table_types=["BASE TABLE"],
33
+ ),
34
+ "view": TableMetadataNames(
35
+ info_table="views",
36
+ ),
37
+ "mv": TableMetadataNames(
38
+ info_table="pg_matviews",
39
+ ),
40
+ }
41
+
42
+
43
+ def _get_metadata_config(table_type: TABLE_TYPE) -> TableMetadataNames:
44
+ """Get metadata configuration for given table type."""
45
+ if table_type not in METADATA_DICT:
46
+ raise DatusException(ErrorCode.COMMON_FIELD_INVALID, f"Invalid table type '{table_type}'")
47
+ return METADATA_DICT[table_type]
48
+
49
+
50
+ class PostgreSQLConnector(SQLAlchemyConnector):
51
+ """PostgreSQL database connector."""
52
+
53
+ def __init__(self, config: Union[PostgreSQLConfig, dict]):
54
+ """
55
+ Initialize PostgreSQL connector.
56
+
57
+ Args:
58
+ config: PostgreSQLConfig object or dict with configuration
59
+ """
60
+ # Handle config object or dict
61
+ if isinstance(config, dict):
62
+ config = PostgreSQLConfig(**config)
63
+ elif not isinstance(config, PostgreSQLConfig):
64
+ raise TypeError(f"config must be PostgreSQLConfig or dict, got {type(config)}")
65
+
66
+ self.config = config
67
+ self.host = config.host
68
+ self.port = config.port
69
+ self.username = config.username
70
+ self.password = config.password
71
+ database = config.database or "postgres"
72
+
73
+ # URL encode username and password to handle special characters
74
+ encoded_username = quote_plus(self.username) if self.username else ""
75
+ encoded_password = quote_plus(self.password) if self.password else ""
76
+
77
+ # Build connection string
78
+ connection_string = (
79
+ f"postgresql+psycopg2://{encoded_username}:{encoded_password}@{self.host}:{self.port}/"
80
+ f"{database}?sslmode={config.sslmode}"
81
+ )
82
+
83
+ super().__init__(connection_string, dialect=DBType.POSTGRESQL, timeout_seconds=config.timeout_seconds)
84
+ self.database_name = database
85
+ self.schema_name = config.schema_name or "public"
86
+
87
+ # ==================== System Resources ====================
88
+
89
+ @override
90
+ def _sys_databases(self) -> Set[str]:
91
+ """System databases to filter out."""
92
+ return {"template0", "template1"}
93
+
94
+ @override
95
+ def _sys_schemas(self) -> Set[str]:
96
+ """System schemas to filter out."""
97
+ return {"pg_catalog", "information_schema", "pg_toast", "pg_temp_1", "pg_toast_temp_1"}
98
+
99
+ # ==================== Utility Methods ====================
100
+
101
+ @staticmethod
102
+ def _quote_identifier(identifier: str) -> str:
103
+ """Safely wrap identifiers with double quotes for PostgreSQL."""
104
+ escaped = identifier.replace('"', '""')
105
+ return f'"{escaped}"'
106
+
107
+ # ==================== Metadata Retrieval ====================
108
+
109
+ def _get_metadata(
110
+ self,
111
+ table_type: TABLE_TYPE = "table",
112
+ catalog_name: str = "",
113
+ database_name: str = "",
114
+ schema_name: str = "",
115
+ ) -> List[Dict[str, str]]:
116
+ """
117
+ Get metadata for tables/views from INFORMATION_SCHEMA or pg_catalog.
118
+
119
+ Args:
120
+ table_type: Type of object (table, view, mv)
121
+ catalog_name: Catalog name (unused in PostgreSQL)
122
+ database_name: Database name (unused, uses current database)
123
+ schema_name: Schema name to query
124
+
125
+ Returns:
126
+ List of metadata dictionaries
127
+ """
128
+ self.connect()
129
+ schema_name = schema_name or self.schema_name
130
+
131
+ # Get metadata configuration
132
+ metadata_config = _get_metadata_config(table_type)
133
+
134
+ if table_type == "mv":
135
+ # Materialized views use pg_matviews
136
+ if schema_name:
137
+ where = f"schemaname = '{schema_name}'"
138
+ else:
139
+ where = f"{list_to_in_str('schemaname not in', list(self._sys_schemas()))}"
140
+
141
+ query = f"""
142
+ SELECT schemaname as table_schema, matviewname as table_name
143
+ FROM pg_matviews
144
+ WHERE {where}
145
+ """
146
+ else:
147
+ # Tables and views use information_schema
148
+ if schema_name:
149
+ where = f"table_schema = '{schema_name}'"
150
+ else:
151
+ where = f"{list_to_in_str('table_schema not in', list(self._sys_schemas()))}"
152
+
153
+ if table_type == "table":
154
+ type_filter = list_to_in_str("and table_type in", metadata_config.table_types)
155
+ else:
156
+ type_filter = ""
157
+
158
+ query = f"""
159
+ SELECT table_schema, table_name
160
+ FROM information_schema.{metadata_config.info_table}
161
+ WHERE {where} {type_filter}
162
+ """
163
+
164
+ query_result = self._execute_pandas(query)
165
+
166
+ # Format results
167
+ result = []
168
+ for i in range(len(query_result)):
169
+ schema = query_result["table_schema"][i]
170
+ tb_name = query_result["table_name"][i]
171
+ result.append(
172
+ {
173
+ "identifier": self.identifier(schema_name=schema, table_name=tb_name),
174
+ "catalog_name": "",
175
+ "database_name": self.database_name,
176
+ "schema_name": schema,
177
+ "table_name": tb_name,
178
+ "table_type": table_type,
179
+ }
180
+ )
181
+ return result
182
+
183
+ def _get_ddl(self, schema_name: str, table_name: str, object_type: str = "TABLE") -> str:
184
+ """
185
+ Get DDL for a table/view using pg_get_tabledef or reconstructing from metadata.
186
+
187
+ Args:
188
+ schema_name: Schema name
189
+ table_name: Table name
190
+ object_type: Object type (TABLE, VIEW, MATERIALIZED VIEW)
191
+
192
+ Returns:
193
+ DDL statement as string
194
+ """
195
+ full_name = self.full_name(schema_name=schema_name, table_name=table_name)
196
+
197
+ if object_type == "VIEW":
198
+ # Get view definition
199
+ sql = f"""
200
+ SELECT pg_get_viewdef('{schema_name}.{table_name}'::regclass, true) as definition
201
+ """
202
+ result = self._execute_pandas(sql)
203
+ if not result.empty and result["definition"][0]:
204
+ return f"CREATE VIEW {full_name} AS\n{result['definition'][0]}"
205
+ return f"-- DDL not available for {full_name}"
206
+
207
+ elif object_type == "MATERIALIZED VIEW":
208
+ # Get materialized view definition
209
+ sql = f"""
210
+ SELECT definition
211
+ FROM pg_matviews
212
+ WHERE schemaname = '{schema_name}' AND matviewname = '{table_name}'
213
+ """
214
+ result = self._execute_pandas(sql)
215
+ if not result.empty and result["definition"][0]:
216
+ return f"CREATE MATERIALIZED VIEW {full_name} AS\n{result['definition'][0]}"
217
+ return f"-- DDL not available for {full_name}"
218
+
219
+ else:
220
+ # For tables, reconstruct DDL from column info
221
+ columns = self.get_schema(schema_name=schema_name, table_name=table_name)
222
+ if not columns:
223
+ return f"-- DDL not available for {full_name}"
224
+
225
+ col_defs = []
226
+ pk_cols = []
227
+ for col in columns:
228
+ col_def = f" {self._quote_identifier(col['name'])} {col['type']}"
229
+ if not col.get("nullable", True):
230
+ col_def += " NOT NULL"
231
+ if col.get("default_value"):
232
+ col_def += f" DEFAULT {col['default_value']}"
233
+ col_defs.append(col_def)
234
+ if col.get("pk"):
235
+ pk_cols.append(col["name"])
236
+
237
+ ddl = f"CREATE TABLE {full_name} (\n"
238
+ ddl += ",\n".join(col_defs)
239
+ if pk_cols:
240
+ pk_names = ", ".join(self._quote_identifier(c) for c in pk_cols)
241
+ ddl += f",\n PRIMARY KEY ({pk_names})"
242
+ ddl += "\n);"
243
+ return ddl
244
+
245
+ def _get_objects_with_ddl(
246
+ self,
247
+ table_type: TABLE_TYPE = "table",
248
+ tables: Optional[List[str]] = None,
249
+ catalog_name: str = "",
250
+ database_name: str = "",
251
+ schema_name: str = "",
252
+ ) -> List[Dict[str, str]]:
253
+ """
254
+ Get metadata with DDL statements.
255
+
256
+ Args:
257
+ table_type: Type of object
258
+ tables: Optional list of specific tables to retrieve
259
+ catalog_name: Catalog name (unused)
260
+ database_name: Database name (unused)
261
+ schema_name: Schema name
262
+
263
+ Returns:
264
+ List of metadata dictionaries with DDL
265
+ """
266
+ result = []
267
+ filter_tables = self._reset_filter_tables(tables, catalog_name, database_name, schema_name)
268
+
269
+ object_type_map = {
270
+ "table": "TABLE",
271
+ "view": "VIEW",
272
+ "mv": "MATERIALIZED VIEW",
273
+ }
274
+ object_type = object_type_map.get(table_type, "TABLE")
275
+
276
+ for meta in self._get_metadata(table_type, catalog_name, database_name, schema_name):
277
+ full_name = self.full_name(schema_name=meta["schema_name"], table_name=meta["table_name"])
278
+
279
+ # Skip if not in filter list
280
+ if filter_tables and full_name not in filter_tables:
281
+ continue
282
+
283
+ # Get DDL
284
+ try:
285
+ ddl = self._get_ddl(meta["schema_name"], meta["table_name"], object_type)
286
+ except Exception as e:
287
+ logger.warning(f"Could not get DDL for {full_name}: {e}")
288
+ ddl = f"-- DDL not available for {meta['table_name']}"
289
+
290
+ meta["definition"] = ddl
291
+ result.append(meta)
292
+
293
+ return result
294
+
295
+ @override
296
+ def get_tables(self, catalog_name: str = "", database_name: str = "", schema_name: str = "") -> List[str]:
297
+ """Get list of table names."""
298
+ return [meta["table_name"] for meta in self._get_metadata("table", catalog_name, database_name, schema_name)]
299
+
300
+ @override
301
+ def get_views(self, catalog_name: str = "", database_name: str = "", schema_name: str = "") -> List[str]:
302
+ """Get list of view names."""
303
+ return [meta["table_name"] for meta in self._get_metadata("view", catalog_name, database_name, schema_name)]
304
+
305
+ @override
306
+ def get_materialized_views(
307
+ self, catalog_name: str = "", database_name: str = "", schema_name: str = ""
308
+ ) -> List[str]:
309
+ """Get list of materialized view names."""
310
+ return [meta["table_name"] for meta in self._get_metadata("mv", catalog_name, database_name, schema_name)]
311
+
312
+ @override
313
+ def get_tables_with_ddl(
314
+ self, catalog_name: str = "", database_name: str = "", schema_name: str = "", tables: Optional[List[str]] = None
315
+ ) -> List[Dict[str, str]]:
316
+ """Get tables with DDL statements."""
317
+ return self._get_objects_with_ddl("table", tables, catalog_name, database_name, schema_name)
318
+
319
+ @override
320
+ def get_views_with_ddl(
321
+ self, catalog_name: str = "", database_name: str = "", schema_name: str = ""
322
+ ) -> List[Dict[str, str]]:
323
+ """Get views with DDL statements."""
324
+ return self._get_objects_with_ddl("view", None, catalog_name, database_name, schema_name)
325
+
326
+ @override
327
+ def get_schema(
328
+ self, catalog_name: str = "", database_name: str = "", schema_name: str = "", table_name: str = ""
329
+ ) -> List[Dict[str, Any]]:
330
+ """
331
+ Get table schema using INFORMATION_SCHEMA.
332
+
333
+ Args:
334
+ catalog_name: Catalog name (unused)
335
+ database_name: Database name (unused)
336
+ schema_name: Schema name
337
+ table_name: Table name
338
+
339
+ Returns:
340
+ List of column information dictionaries
341
+ """
342
+ if not table_name:
343
+ return []
344
+
345
+ schema_name = schema_name or self.schema_name
346
+
347
+ # Use INFORMATION_SCHEMA to get schema with comments
348
+ sql = f"""
349
+ SELECT
350
+ c.column_name as field,
351
+ c.data_type as type,
352
+ c.is_nullable as nullable,
353
+ c.column_default as default_value,
354
+ CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_pk,
355
+ pgd.description as comment
356
+ FROM information_schema.columns c
357
+ LEFT JOIN (
358
+ SELECT kcu.column_name
359
+ FROM information_schema.table_constraints tc
360
+ JOIN information_schema.key_column_usage kcu
361
+ ON tc.constraint_name = kcu.constraint_name
362
+ AND tc.table_schema = kcu.table_schema
363
+ WHERE tc.constraint_type = 'PRIMARY KEY'
364
+ AND tc.table_schema = '{schema_name}'
365
+ AND tc.table_name = '{table_name}'
366
+ ) pk ON c.column_name = pk.column_name
367
+ LEFT JOIN pg_catalog.pg_statio_all_tables st
368
+ ON st.schemaname = c.table_schema AND st.relname = c.table_name
369
+ LEFT JOIN pg_catalog.pg_description pgd
370
+ ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
371
+ WHERE c.table_schema = '{schema_name}'
372
+ AND c.table_name = '{table_name}'
373
+ ORDER BY c.ordinal_position
374
+ """
375
+ query_result = self._execute_pandas(sql)
376
+
377
+ result = []
378
+ for i in range(len(query_result)):
379
+ result.append(
380
+ {
381
+ "cid": i,
382
+ "name": query_result["field"][i],
383
+ "type": query_result["type"][i],
384
+ "nullable": query_result["nullable"][i] == "YES",
385
+ "default_value": query_result["default_value"][i],
386
+ "pk": bool(query_result["is_pk"][i]),
387
+ "comment": query_result["comment"][i] if query_result["comment"][i] else None,
388
+ }
389
+ )
390
+ return result
391
+
392
+ # ==================== Database/Schema Management ====================
393
+
394
+ @override
395
+ def get_databases(self, catalog_name: str = "", include_sys: bool = False) -> List[str]:
396
+ """Get list of databases."""
397
+ sql = "SELECT datname FROM pg_database WHERE datistemplate = false"
398
+ result = self._execute_pandas(sql)
399
+ databases = result["datname"].tolist()
400
+
401
+ if not include_sys:
402
+ sys_dbs = self._sys_databases()
403
+ databases = [db for db in databases if db not in sys_dbs]
404
+
405
+ return databases
406
+
407
+ @override
408
+ def get_schemas(self, catalog_name: str = "", database_name: str = "", include_sys: bool = False) -> List[str]:
409
+ """Get list of schemas in the current database."""
410
+ sql = "SELECT schema_name FROM information_schema.schemata"
411
+ result = self._execute_pandas(sql)
412
+ schemas = result["schema_name"].tolist()
413
+
414
+ if not include_sys:
415
+ sys_schemas = self._sys_schemas()
416
+ schemas = [s for s in schemas if s not in sys_schemas]
417
+
418
+ return schemas
419
+
420
+ @override
421
+ def _sqlalchemy_schema(
422
+ self, catalog_name: str = "", database_name: str = "", schema_name: str = ""
423
+ ) -> Optional[str]:
424
+ """Get schema name for SQLAlchemy Inspector."""
425
+ return schema_name or self.schema_name
426
+
427
+ @override
428
+ def do_switch_context(self, catalog_name: str = "", database_name: str = "", schema_name: str = ""):
429
+ """Switch schema context by updating self.schema_name.
430
+
431
+ Note: All queries use explicit schema qualification via full_name(),
432
+ so we only need to update self.schema_name here.
433
+ """
434
+ if schema_name:
435
+ self.schema_name = schema_name
436
+
437
+ # ==================== Sample Data ====================
438
+
439
+ def get_sample_rows(
440
+ self,
441
+ tables: Optional[List[str]] = None,
442
+ top_n: int = 5,
443
+ catalog_name: str = "",
444
+ database_name: str = "",
445
+ schema_name: str = "",
446
+ table_type: TABLE_TYPE = "table",
447
+ ) -> List[Dict[str, str]]:
448
+ """Get sample rows from tables."""
449
+ # Delegate to base class for unsupported table types (e.g., "full")
450
+ if table_type == "full" or table_type not in METADATA_DICT:
451
+ return super().get_sample_rows(
452
+ tables=tables,
453
+ top_n=top_n,
454
+ catalog_name=catalog_name,
455
+ database_name=database_name,
456
+ schema_name=schema_name,
457
+ table_type=table_type,
458
+ )
459
+
460
+ self.connect()
461
+ schema_name = schema_name or self.schema_name
462
+ result = []
463
+
464
+ # If specific tables provided, query those
465
+ if tables:
466
+ for table_name in tables:
467
+ full_name = self.full_name(schema_name=schema_name, table_name=table_name)
468
+ sql = f"SELECT * FROM {full_name} LIMIT {top_n}"
469
+ df = self._execute_pandas(sql)
470
+ if not df.empty:
471
+ result.append(
472
+ {
473
+ "identifier": self.identifier(schema_name=schema_name, table_name=table_name),
474
+ "catalog_name": "",
475
+ "database_name": self.database_name,
476
+ "schema_name": schema_name,
477
+ "table_name": table_name,
478
+ "sample_rows": df.to_csv(index=False),
479
+ }
480
+ )
481
+ return result
482
+
483
+ # Otherwise get metadata and query all tables
484
+ metadata = self._get_metadata(table_type, "", "", schema_name)
485
+ for meta in metadata:
486
+ full_name = self.full_name(schema_name=meta["schema_name"], table_name=meta["table_name"])
487
+ sql = f"SELECT * FROM {full_name} LIMIT {top_n}"
488
+ df = self._execute_pandas(sql)
489
+ if not df.empty:
490
+ result.append(
491
+ {
492
+ "identifier": meta["identifier"],
493
+ "catalog_name": "",
494
+ "database_name": self.database_name,
495
+ "schema_name": meta["schema_name"],
496
+ "table_name": meta["table_name"],
497
+ "sample_rows": df.to_csv(index=False),
498
+ }
499
+ )
500
+ return result
501
+
502
+ # ==================== Utility Methods ====================
503
+
504
+ @override
505
+ def identifier(
506
+ self, catalog_name: str = "", database_name: str = "", schema_name: str = "", table_name: str = ""
507
+ ) -> str:
508
+ """Generate a unique identifier for a table."""
509
+ schema_name = schema_name or self.schema_name
510
+ if schema_name:
511
+ return f"{schema_name}.{table_name}"
512
+ return table_name
513
+
514
+ @override
515
+ def full_name(
516
+ self, catalog_name: str = "", database_name: str = "", schema_name: str = "", table_name: str = ""
517
+ ) -> str:
518
+ """Build fully-qualified table name."""
519
+ schema_name = schema_name or self.schema_name
520
+ if schema_name:
521
+ return f"{self._quote_identifier(schema_name)}.{self._quote_identifier(table_name)}"
522
+ return self._quote_identifier(table_name)
523
+
524
+ @override
525
+ def _reset_filter_tables(
526
+ self, tables: Optional[List[str]] = None, catalog_name: str = "", database_name: str = "", schema_name: str = ""
527
+ ) -> List[str]:
528
+ """Reset filter tables with full names."""
529
+ schema_name = schema_name or self.schema_name
530
+ return super()._reset_filter_tables(tables, "", "", schema_name)
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: datus-postgresql
3
+ Version: 0.1.0
4
+ Summary: PostgreSQL database adapter for Datus
5
+ Project-URL: Homepage, https://github.com/Datus-ai/datus-db-adapters
6
+ Project-URL: Repository, https://github.com/Datus-ai/datus-db-adapters
7
+ Project-URL: Issues, https://github.com/Datus-ai/datus-db-adapters/issues
8
+ Author-email: DatusAI <support@datus.ai>
9
+ License: Apache-2.0
10
+ Keywords: adapter,database,datus,postgresql
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: datus-agent>0.2.1
18
+ Requires-Dist: datus-sqlalchemy>=0.1.2
19
+ Requires-Dist: psycopg2-binary>=2.9.11
20
+ Requires-Dist: pydantic>=2.0.0
21
+ Description-Content-Type: text/markdown
22
+
23
+ # datus-postgresql
24
+
25
+ PostgreSQL database adapter for Datus.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install datus-postgresql
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+ from datus_postgresql import PostgreSQLConnector, PostgreSQLConfig
37
+
38
+ # Using config object
39
+ config = PostgreSQLConfig(
40
+ host="localhost",
41
+ port=5432,
42
+ username="postgres",
43
+ password="password",
44
+ database="mydb",
45
+ schema_name="public",
46
+ )
47
+
48
+ connector = PostgreSQLConnector(config)
49
+
50
+ # Or using dict
51
+ connector = PostgreSQLConnector({
52
+ "host": "localhost",
53
+ "port": 5432,
54
+ "username": "postgres",
55
+ "password": "password",
56
+ "database": "mydb",
57
+ })
58
+
59
+ # Test connection
60
+ connector.test_connection()
61
+
62
+ # Execute queries
63
+ result = connector.execute({"sql_query": "SELECT * FROM users"})
64
+ ```
65
+
66
+ ## Configuration Options
67
+
68
+ | Option | Type | Default | Description |
69
+ |--------|------|---------|-------------|
70
+ | host | str | "127.0.0.1" | PostgreSQL server host |
71
+ | port | int | 5432 | PostgreSQL server port |
72
+ | username | str | required | PostgreSQL username |
73
+ | password | str | "" | PostgreSQL password |
74
+ | database | str | None | Default database name |
75
+ | schema | str | "public" | Default schema name |
76
+ | sslmode | str | "prefer" | SSL mode |
77
+ | timeout_seconds | int | 30 | Connection timeout |
78
+
79
+ ## License
80
+
81
+ Apache-2.0
@@ -0,0 +1,7 @@
1
+ datus_postgresql/__init__.py,sha256=BnQyHwfxdDimY9U_MEItMDKGqqd0oVjXx-naFKKQaqE,555
2
+ datus_postgresql/config.py,sha256=jvl2XC_A-B1Bb_fFEUi5QxsOBqWgLC-stYLxGAQOmPU,1148
3
+ datus_postgresql/connector.py,sha256=gB5dEeozeGXEc5PVAmRFQWT5I-rbCt6FtxiTVN-btrs,20440
4
+ datus_postgresql-0.1.0.dist-info/METADATA,sha256=rHXdndcV6dBpiBdl0CxJFVmyB3EQdsiNj4w2rTVsnAM,2153
5
+ datus_postgresql-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ datus_postgresql-0.1.0.dist-info/entry_points.txt,sha256=GHceNS-xqgYKFLZEm8Jhw7iznrZ86Ty-SHadUzT7Fao,56
7
+ datus_postgresql-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [datus.adapters]
2
+ postgresql = datus_postgresql:register