harlequin-postgres 0.2.2__tar.gz → 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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: harlequin-postgres
3
- Version: 0.2.2
3
+ Version: 0.4.0
4
4
  Summary: A Harlequin adapter for Postgres.
5
5
  License: MIT
6
6
  Author: Ted Conbeer
@@ -11,8 +11,8 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.9
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
- Requires-Dist: harlequin (>=1.7,<2.0)
15
- Requires-Dist: psycopg2-binary (>=2.9.9,<3.0.0)
14
+ Requires-Dist: harlequin (>=1.20,<2.0)
15
+ Requires-Dist: psycopg[binary,pool] (>=3.2,<4.0)
16
16
  Description-Content-Type: text/markdown
17
17
 
18
18
  # harlequin-postgres
@@ -85,4 +85,10 @@ Many more options are available; to see the full list, run:
85
85
  harlequin --help
86
86
  ```
87
87
 
88
+ ## Manual Transactions
89
+
90
+ To use Manual transaction mode, click on the label in the Run Query Bar to toggle the transaction mode from Auto to Manual.
91
+
92
+ ## Further Documentation
93
+
88
94
  For more information, see the [Harlequin Docs](https://harlequin.sh/docs/postgres/index).
@@ -68,4 +68,10 @@ Many more options are available; to see the full list, run:
68
68
  harlequin --help
69
69
  ```
70
70
 
71
+ ## Manual Transactions
72
+
73
+ To use Manual transaction mode, click on the label in the Run Query Bar to toggle the transaction mode from Auto to Manual.
74
+
75
+ ## Further Documentation
76
+
71
77
  For more information, see the [Harlequin Docs](https://harlequin.sh/docs/postgres/index).
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "harlequin-postgres"
3
- version = "0.2.2"
3
+ version = "0.4.0"
4
4
  description = "A Harlequin adapter for Postgres."
5
5
  authors = ["Ted Conbeer <tconbeer@users.noreply.github.com>"]
6
6
  license = "MIT"
@@ -14,16 +14,15 @@ postgres = "harlequin_postgres:HarlequinPostgresAdapter"
14
14
 
15
15
  [tool.poetry.dependencies]
16
16
  python = ">=3.8.1,<4.0"
17
- harlequin = "^1.7"
18
- psycopg2-binary = "^2.9.9"
17
+ harlequin = "^1.20"
18
+ psycopg = { version = "^3.2", extras = ["binary", "pool"]}
19
19
 
20
20
  [tool.poetry.group.dev.dependencies]
21
- ruff = "^0.1.6"
21
+ ruff = "^0.5"
22
22
  pytest = "^7.4.3"
23
- mypy = "^1.7.0"
23
+ mypy = "^1.10.0"
24
24
  pre-commit = "^3.5.0"
25
25
  importlib_metadata = { version = ">=4.6.0", python = "<3.10.0" }
26
- types-psycopg2 = "^2.9.21.16"
27
26
  pyinstrument = "^4.6.1"
28
27
 
29
28
  [build-system]
@@ -32,9 +31,11 @@ build-backend = "poetry.core.masonry.api"
32
31
 
33
32
 
34
33
  [tool.ruff]
35
- select = ["A", "B", "E", "F", "I"]
36
34
  target-version = "py38"
37
35
 
36
+ [tool.ruff.lint]
37
+ select = ["A", "B", "E", "F", "I"]
38
+
38
39
  [tool.mypy]
39
40
  python_version = "3.8"
40
41
  files = [
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from itertools import cycle
3
4
  from typing import Any, Sequence
4
5
 
5
6
  from harlequin import (
@@ -7,11 +8,14 @@ from harlequin import (
7
8
  HarlequinCompletion,
8
9
  HarlequinConnection,
9
10
  HarlequinCursor,
11
+ HarlequinTransactionMode,
10
12
  )
11
13
  from harlequin.catalog import Catalog, CatalogItem
12
14
  from harlequin.exception import HarlequinConnectionError, HarlequinQueryError
13
- from psycopg2.extensions import connection, cursor
14
- from psycopg2.pool import ThreadedConnectionPool
15
+ from psycopg import Connection, Cursor, conninfo
16
+ from psycopg.errors import QueryCanceled
17
+ from psycopg.pq import TransactionStatus
18
+ from psycopg_pool import ConnectionPool
15
19
  from textual_fastdatatable.backend import AutoBackendType
16
20
 
17
21
  from harlequin_postgres.cli_options import POSTGRES_OPTIONS
@@ -19,16 +23,19 @@ from harlequin_postgres.completions import _get_completions
19
23
 
20
24
 
21
25
  class HarlequinPostgresCursor(HarlequinCursor):
22
- def __init__(self, conn: HarlequinPostgresConnection, cur: cursor) -> None:
26
+ def __init__(self, conn: HarlequinPostgresConnection, cur: Cursor) -> None:
23
27
  self.conn = conn
24
28
  self.cur = cur
29
+ # we need to copy the description from the cursor in case the results are
30
+ # fetched and the cursor is closed before columns() is called.
31
+ assert cur.description is not None
32
+ self.description = cur.description.copy()
25
33
  self._limit: int | None = None
26
34
 
27
35
  def columns(self) -> list[tuple[str, str]]:
28
- assert self.cur.description is not None
29
36
  return [
30
37
  (col.name, self.conn._get_short_type_from_oid(col.type_code))
31
- for col in self.cur.description
38
+ for col in self.description
32
39
  ]
33
40
 
34
41
  def set_limit(self, limit: int) -> HarlequinPostgresCursor:
@@ -41,6 +48,8 @@ class HarlequinPostgresCursor(HarlequinCursor):
41
48
  return self.cur.fetchall()
42
49
  else:
43
50
  return self.cur.fetchmany(self._limit)
51
+ except QueryCanceled:
52
+ return []
44
53
  except Exception as e:
45
54
  raise HarlequinQueryError(
46
55
  msg=str(e),
@@ -60,25 +69,73 @@ class HarlequinPostgresConnection(HarlequinConnection):
60
69
  ) -> None:
61
70
  self.init_message = init_message
62
71
  try:
63
- if conn_str and conn_str[0]:
64
- self.pool: ThreadedConnectionPool = ThreadedConnectionPool(
65
- 1, 5, dsn=conn_str[0], **options
66
- )
67
- else:
68
- self.pool = ThreadedConnectionPool(1, 5, **options)
72
+ self.conn_info = conninfo.conninfo_to_dict(
73
+ conninfo=conn_str[0] if conn_str else "", **options
74
+ )
75
+ except Exception as e:
76
+ raise HarlequinConnectionError(
77
+ msg=str(e),
78
+ title=(
79
+ "Harlequin could not connect to Postgres. "
80
+ "Invalid connection string."
81
+ ),
82
+ ) from e
83
+ try:
84
+ raw_timeout = self.conn_info.get("connect_timeout")
85
+ timeout = float(raw_timeout) if raw_timeout is not None else 30.0
86
+ except (TypeError, ValueError) as e:
87
+ raise HarlequinConnectionError(
88
+ msg=str(e),
89
+ title=(
90
+ "Harlequin could not connect to Postgres. "
91
+ "Invalid value for connection_timeout."
92
+ ),
93
+ ) from e
94
+ try:
95
+ self.pool: ConnectionPool = ConnectionPool(
96
+ conninfo=conn_str[0] if conn_str and conn_str[0] else "",
97
+ min_size=2,
98
+ max_size=5,
99
+ kwargs=options,
100
+ open=True,
101
+ timeout=timeout,
102
+ )
103
+ self._main_conn: Connection = self.pool.getconn()
69
104
  except Exception as e:
70
105
  raise HarlequinConnectionError(
71
106
  msg=str(e), title="Harlequin could not connect to Postgres."
72
107
  ) from e
73
108
 
109
+ self._transaction_modes = cycle(
110
+ [
111
+ HarlequinTransactionMode(label="Auto"),
112
+ HarlequinTransactionMode(
113
+ label="Manual",
114
+ commit=self.commit,
115
+ rollback=self.rollback,
116
+ ),
117
+ ]
118
+ )
119
+ self.toggle_transaction_mode()
120
+
74
121
  def execute(self, query: str) -> HarlequinCursor | None:
122
+ if (
123
+ self.transaction_mode.label != "Auto"
124
+ and self._main_conn.info.transaction_status == TransactionStatus.IDLE
125
+ ):
126
+ cur = self._main_conn.cursor()
127
+ cur.execute(query="begin;")
128
+ cur.close()
129
+
75
130
  try:
76
- conn: connection = self.pool.getconn(key="main")
77
- with conn: # autocommit transaction
78
- cur = conn.cursor()
79
- cur.execute(query=query)
131
+ cur = self._main_conn.cursor()
132
+ cur.execute(query=query)
133
+ except QueryCanceled:
134
+ cur.close()
135
+ return None
80
136
  except Exception as e:
81
137
  cur.close()
138
+ self.rollback()
82
139
  raise HarlequinQueryError(
83
140
  msg=str(e),
84
141
  title="Harlequin encountered an error while executing your query.",
@@ -89,8 +146,15 @@ class HarlequinPostgresConnection(HarlequinConnection):
89
146
  else:
90
147
  cur.close()
91
148
  return None
92
- finally:
93
- self.pool.putconn(conn, key="main")
149
+
150
+ def cancel(self) -> None:
151
+ self._main_conn.cancel_safe()
152
+
153
+ def commit(self) -> None:
154
+ self._main_conn.commit()
155
+
156
+ def rollback(self) -> None:
157
+ self._main_conn.rollback()
94
158
 
95
159
  def get_catalog(self) -> Catalog:
96
160
  databases = self._get_databases()
@@ -142,14 +206,37 @@ class HarlequinPostgresConnection(HarlequinConnection):
142
206
  return Catalog(items=db_items)
143
207
 
144
208
  def get_completions(self) -> list[HarlequinCompletion]:
145
- conn: connection = self.pool.getconn()
209
+ conn: Connection = self.pool.getconn()
146
210
  completions = _get_completions(conn)
147
211
  self.pool.putconn(conn)
148
212
  return completions
149
- ...
213
+
214
+ def close(self) -> None:
215
+ self.pool.putconn(self._main_conn)
216
+ self.pool.close()
217
+
218
+ @property
219
+ def transaction_mode(self) -> HarlequinTransactionMode:
220
+ return self._transaction_mode
221
+
222
+ def toggle_transaction_mode(self) -> HarlequinTransactionMode:
223
+ self._transaction_mode = next(self._transaction_modes)
224
+ self._sync_transaction_mode()
225
+ return self._transaction_mode
226
+
227
+ def _sync_transaction_mode(self) -> None:
228
+ """
229
+ Sync this class's transaction mode with the main connection
230
+ """
231
+ conn = self._main_conn
232
+ if self.transaction_mode.label == "Auto":
233
+ conn.autocommit = True
234
+ conn.commit()
235
+ else:
236
+ conn.autocommit = False
150
237
 
151
238
  def _get_databases(self) -> list[tuple[str]]:
152
- conn: connection = self.pool.getconn()
239
+ conn: Connection = self.pool.getconn()
153
240
  with conn.cursor() as cur:
154
241
  cur.execute(
155
242
  """
@@ -166,7 +253,7 @@ class HarlequinPostgresConnection(HarlequinConnection):
166
253
  return results
167
254
 
168
255
  def _get_schemas(self, dbname: str) -> list[tuple[str]]:
169
- conn: connection = self.pool.getconn()
256
+ conn: Connection = self.pool.getconn()
170
257
  with conn.cursor() as cur:
171
258
  cur.execute(
172
259
  f"""
@@ -184,7 +271,7 @@ class HarlequinPostgresConnection(HarlequinConnection):
184
271
  return results
185
272
 
186
273
  def _get_relations(self, dbname: str, schema: str) -> list[tuple[str, str]]:
187
- conn: connection = self.pool.getconn()
274
+ conn: Connection = self.pool.getconn()
188
275
  with conn.cursor() as cur:
189
276
  cur.execute(
190
277
  f"""
@@ -203,7 +290,7 @@ class HarlequinPostgresConnection(HarlequinConnection):
203
290
  def _get_columns(
204
291
  self, dbname: str, schema: str, relation: str
205
292
  ) -> list[tuple[str, str]]:
206
- conn: connection = self.pool.getconn()
293
+ conn: Connection = self.pool.getconn()
207
294
  with conn.cursor() as cur:
208
295
  cur.execute(
209
296
  f"""
@@ -339,6 +426,7 @@ class HarlequinPostgresConnection(HarlequinConnection):
339
426
 
340
427
  class HarlequinPostgresAdapter(HarlequinAdapter):
341
428
  ADAPTER_OPTIONS = POSTGRES_OPTIONS
429
+ IMPLEMENTS_CANCEL = True
342
430
 
343
431
  def __init__(
344
432
  self,
@@ -351,14 +439,14 @@ class HarlequinPostgresAdapter(HarlequinAdapter):
351
439
  passfile: str | None = None,
352
440
  require_auth: str | None = None,
353
441
  channel_binding: str | None = None,
354
- connect_timeout: int | None = None,
442
+ connect_timeout: int | float | None = None,
355
443
  sslmode: str | None = None,
356
444
  sslcert: str | None = None,
357
445
  sslkey: str | None = None,
358
446
  **_: Any,
359
447
  ) -> None:
360
448
  self.conn_str = conn_str
361
- self.options = {
449
+ self.options: dict[str, str | int | None] = {
362
450
  "host": host,
363
451
  "port": port,
364
452
  "dbname": dbname,
@@ -367,12 +455,30 @@ class HarlequinPostgresAdapter(HarlequinAdapter):
367
455
  "passfile": passfile,
368
456
  "require_auth": require_auth,
369
457
  "channel_binding": channel_binding,
370
- "connect_timeout": connect_timeout,
458
+ "connect_timeout": connect_timeout, # type: ignore[dict-item]
371
459
  "sslmode": sslmode,
372
460
  "sslcert": sslcert,
373
461
  "sslkey": sslkey,
374
462
  }
375
463
 
464
+ @property
465
+ def connection_id(self) -> str | None:
466
+ """
467
+ Use a simplified connection string, with only the host, port, and database
468
+ """
469
+ try:
470
+ conn_info = conninfo.conninfo_to_dict(
471
+ conninfo=self.conn_str[0] if self.conn_str else "",
472
+ **self.options,
473
+ )
474
+ except Exception:
475
+ return None
476
+
477
+ host = conn_info.get("host", "localhost")
478
+ port = conn_info.get("port", "5432")
479
+ dbname = conn_info.get("dbname", "postgres")
480
+ return f"{host}:{port}/{dbname}"
481
+
376
482
  def connect(self) -> HarlequinPostgresConnection:
377
483
  if len(self.conn_str) > 1:
378
484
  raise HarlequinConnectionError(
@@ -33,10 +33,7 @@ port = TextOption(
33
33
 
34
34
  dbname = TextOption(
35
35
  name="dbname",
36
- description=(
37
- "Port number to connect to at the server host, or socket file name extension "
38
- "for Unix-domain connections."
39
- ),
36
+ description=("The database name to use when connecting with the Postgres server."),
40
37
  short_decls=["-d"],
41
38
  default="postgres",
42
39
  )
@@ -4,10 +4,10 @@ import csv
4
4
  from pathlib import Path
5
5
 
6
6
  from harlequin import HarlequinCompletion
7
- from psycopg2.extensions import connection
7
+ from psycopg import Connection
8
8
 
9
9
 
10
- def _get_completions(conn: connection) -> list[HarlequinCompletion]:
10
+ def _get_completions(conn: Connection) -> list[HarlequinCompletion]:
11
11
  completions: list[HarlequinCompletion] = []
12
12
 
13
13
  # source: https://www.postgresql.org/docs/current/sql-keywords-appendix.html