pgcli 4.0.1__tar.gz → 4.2.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.
- {pgcli-4.0.1 → pgcli-4.2.0}/AUTHORS +8 -0
- {pgcli-4.0.1/pgcli.egg-info → pgcli-4.2.0}/PKG-INFO +17 -14
- {pgcli-4.0.1 → pgcli-4.2.0}/README.rst +6 -3
- {pgcli-4.0.1 → pgcli-4.2.0}/changelog.rst +41 -2
- pgcli-4.2.0/pgcli/__init__.py +1 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/main.py +244 -20
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/pgclirc +10 -4
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/pgcompleter.py +15 -8
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/pgexecute.py +16 -1
- {pgcli-4.0.1 → pgcli-4.2.0/pgcli.egg-info}/PKG-INFO +17 -14
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli.egg-info/SOURCES.txt +1 -1
- pgcli-4.2.0/pgcli.egg-info/dependency_links.txt +1 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli.egg-info/requires.txt +9 -3
- pgcli-4.2.0/pyproject.toml +93 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/conftest.py +2 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/basic_commands.feature +4 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/steps/basic_commands.py +13 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/steps/crud_database.py +1 -0
- pgcli-4.2.0/tests/test_application_name.py +17 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_main.py +98 -1
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_pgcompleter.py +24 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_pgexecute.py +64 -2
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_ssh_tunnel.py +3 -1
- pgcli-4.0.1/pgcli/__init__.py +0 -1
- pgcli-4.0.1/pgcli.egg-info/dependency_links.txt +0 -1
- pgcli-4.0.1/pyproject.toml +0 -21
- pgcli-4.0.1/setup.py +0 -76
- {pgcli-4.0.1 → pgcli-4.2.0}/LICENSE.txt +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/MANIFEST.in +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/__main__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/auth.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/completion_refresher.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/config.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/explain_output_formatter.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/key_bindings.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/magic.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/formatter/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/formatter/sqlformatter.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/parseutils/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/parseutils/ctes.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/parseutils/meta.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/parseutils/tables.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/parseutils/utils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/pgliterals/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/pgliterals/main.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/pgliterals/pgliterals.json +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/prioritization.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/prompt_utils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/packages/sqlcompletion.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/pgbuffer.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/pgstyle.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/pgtoolbar.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli/pyev.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli.egg-info/entry_points.txt +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/pgcli.egg-info/top_level.txt +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/setup.cfg +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/auto_vertical.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/crud_database.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/crud_table.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/db_utils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/environment.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/expanded.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/fixture_data/help.txt +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/fixture_data/help_commands.txt +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/fixture_utils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/iocommands.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/named_queries.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/pgbouncer.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/specials.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/steps/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/steps/auto_vertical.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/steps/crud_table.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/steps/expanded.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/steps/iocommands.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/steps/named_queries.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/steps/pgbouncer.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/steps/specials.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/steps/wrappers.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/features/wrappager.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/formatter/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/formatter/test_sqlformatter.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/metadata.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/parseutils/test_ctes.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/parseutils/test_function_metadata.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/parseutils/test_parseutils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/pytest.ini +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_auth.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_completion_refresher.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_config.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_fuzzy_completion.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_naive_completion.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_pgspecial.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_prioritization.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_prompt_utils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_rowlimit.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_smart_completion_multiple_schemata.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_smart_completion_public_schema_only.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/test_sqlcompletion.py +0 -0
- {pgcli-4.0.1 → pgcli-4.2.0}/tests/utils.py +0 -0
|
@@ -130,6 +130,14 @@ Contributors:
|
|
|
130
130
|
* blag
|
|
131
131
|
* Rob Berry (rob-b)
|
|
132
132
|
* Sharon Yogev (sharonyogev)
|
|
133
|
+
* Hollis Wu (holi0317)
|
|
134
|
+
* Antonio Aguilar (crazybolillo)
|
|
135
|
+
* Andrew M. MacFie (amacfie)
|
|
136
|
+
* saucoide
|
|
137
|
+
* Chris Rose (offbyone/offby1)
|
|
138
|
+
* Mathieu Dupuy (deronnax)
|
|
139
|
+
* Chris Novakovic
|
|
140
|
+
* Josh Lynch (josh-lynch)
|
|
133
141
|
|
|
134
142
|
Creator:
|
|
135
143
|
--------
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: pgcli
|
|
3
|
-
Version: 4.0
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Summary: CLI for Postgres Database. With auto-completion and syntax highlighting.
|
|
5
|
-
|
|
6
|
-
Author: Pgcli Core Team
|
|
7
|
-
Author-email: pgcli-dev@googlegroups.com
|
|
5
|
+
Author-email: Pgcli Core Team <pgcli-dev@googlegroups.com>
|
|
8
6
|
License: BSD
|
|
7
|
+
Project-URL: Homepage, https://pgcli.com
|
|
9
8
|
Classifier: Intended Audience :: Developers
|
|
10
9
|
Classifier: License :: OSI Approved :: BSD License
|
|
11
10
|
Classifier: Operating System :: Unix
|
|
12
11
|
Classifier: Programming Language :: Python
|
|
13
12
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
15
13
|
Classifier: Programming Language :: Python :: 3.9
|
|
16
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
18
|
Classifier: Programming Language :: SQL
|
|
20
19
|
Classifier: Topic :: Database
|
|
21
20
|
Classifier: Topic :: Database :: Front-Ends
|
|
22
21
|
Classifier: Topic :: Software Development
|
|
23
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/x-rst
|
|
25
25
|
License-File: LICENSE.txt
|
|
26
26
|
License-File: AUTHORS
|
|
27
27
|
Requires-Dist: pgspecial>=2.0.0
|
|
28
28
|
Requires-Dist: click>=4.1
|
|
29
29
|
Requires-Dist: Pygments>=2.0
|
|
30
30
|
Requires-Dist: prompt_toolkit<4.0.0,>=2.0.6
|
|
31
|
-
Requires-Dist: psycopg>=3.0.14
|
|
32
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: psycopg>=3.0.14; sys_platform != "win32"
|
|
32
|
+
Requires-Dist: psycopg-binary>=3.0.14; sys_platform == "win32"
|
|
33
|
+
Requires-Dist: sqlparse<0.6,>=0.3.0
|
|
33
34
|
Requires-Dist: configobj>=5.0.6
|
|
34
|
-
Requires-Dist: pendulum>=2.1.0
|
|
35
35
|
Requires-Dist: cli_helpers[styles]>=2.2.1
|
|
36
|
-
Requires-Dist: setproctitle>=1.1.9
|
|
36
|
+
Requires-Dist: setproctitle>=1.1.9; sys_platform != "win32" and "CYGWIN" not in sys_platform
|
|
37
37
|
Provides-Extra: keyring
|
|
38
38
|
Requires-Dist: keyring>=12.2.0; extra == "keyring"
|
|
39
39
|
Provides-Extra: sshtunnel
|
|
@@ -220,7 +220,7 @@ Alternatively, you can install ``pgcli`` as a python package using a package
|
|
|
220
220
|
manager called called ``pip``. You will need postgres installed on your system
|
|
221
221
|
for this to work.
|
|
222
222
|
|
|
223
|
-
In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/
|
|
223
|
+
In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/installation/
|
|
224
224
|
|
|
225
225
|
::
|
|
226
226
|
|
|
@@ -250,7 +250,7 @@ If pip is not installed check if easy_install is available on the system.
|
|
|
250
250
|
Linux:
|
|
251
251
|
======
|
|
252
252
|
|
|
253
|
-
In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/
|
|
253
|
+
In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/installation/
|
|
254
254
|
|
|
255
255
|
Check if pip is already available in your system.
|
|
256
256
|
|
|
@@ -393,7 +393,10 @@ choice:
|
|
|
393
393
|
|
|
394
394
|
In [3]: my_result = _
|
|
395
395
|
|
|
396
|
-
Pgcli dropped support for
|
|
396
|
+
Pgcli dropped support for:
|
|
397
|
+
|
|
398
|
+
* Python<3.8 as of 4.0.0.
|
|
399
|
+
* Python<3.9 as of 4.2.0.
|
|
397
400
|
|
|
398
401
|
Thanks:
|
|
399
402
|
-------
|
|
@@ -179,7 +179,7 @@ Alternatively, you can install ``pgcli`` as a python package using a package
|
|
|
179
179
|
manager called called ``pip``. You will need postgres installed on your system
|
|
180
180
|
for this to work.
|
|
181
181
|
|
|
182
|
-
In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/
|
|
182
|
+
In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/installation/
|
|
183
183
|
|
|
184
184
|
::
|
|
185
185
|
|
|
@@ -209,7 +209,7 @@ If pip is not installed check if easy_install is available on the system.
|
|
|
209
209
|
Linux:
|
|
210
210
|
======
|
|
211
211
|
|
|
212
|
-
In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/
|
|
212
|
+
In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/installation/
|
|
213
213
|
|
|
214
214
|
Check if pip is already available in your system.
|
|
215
215
|
|
|
@@ -352,7 +352,10 @@ choice:
|
|
|
352
352
|
|
|
353
353
|
In [3]: my_result = _
|
|
354
354
|
|
|
355
|
-
Pgcli dropped support for
|
|
355
|
+
Pgcli dropped support for:
|
|
356
|
+
|
|
357
|
+
* Python<3.8 as of 4.0.0.
|
|
358
|
+
* Python<3.9 as of 4.2.0.
|
|
356
359
|
|
|
357
360
|
Thanks:
|
|
358
361
|
-------
|
|
@@ -1,5 +1,44 @@
|
|
|
1
|
+
4.2.0 (2025-03-06)
|
|
1
2
|
==================
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
Features
|
|
5
|
+
--------
|
|
6
|
+
* Add a `--ping` command line option; allows pgcli to replace `pg_isready`
|
|
7
|
+
* Changed the packaging metadata from setup.py to pyproject.toml
|
|
8
|
+
|
|
9
|
+
Bug fixes:
|
|
10
|
+
----------
|
|
11
|
+
* Avoid raising `NameError` when exiting unsuccessfully in some cases
|
|
12
|
+
* Use configured `alias_map_file` to generate table aliases if available.
|
|
13
|
+
|
|
14
|
+
Internal:
|
|
15
|
+
---------
|
|
16
|
+
|
|
17
|
+
* Drop support for Python 3.8 and add 3.13.
|
|
18
|
+
|
|
19
|
+
4.1.0 (2024-03-09)
|
|
20
|
+
==================
|
|
21
|
+
|
|
22
|
+
Features:
|
|
23
|
+
---------
|
|
24
|
+
* Support `PGAPPNAME` as an environment variable and `--application-name` as a command line argument.
|
|
25
|
+
* Add `verbose_errors` config and `\v` special command which enable the
|
|
26
|
+
displaying of all Postgres error fields received.
|
|
27
|
+
* Show Postgres notifications.
|
|
28
|
+
* Support sqlparse 0.5.x
|
|
29
|
+
* Add `--log-file [filename]` cli argument and `\log-file [filename]` special commands to
|
|
30
|
+
log to an external file in addition to the normal output
|
|
31
|
+
|
|
32
|
+
Bug fixes:
|
|
33
|
+
----------
|
|
34
|
+
|
|
35
|
+
* Fix display of "short host" in prompt (with `\h`) for IPv4 addresses ([issue 964](https://github.com/dbcli/pgcli/issues/964)).
|
|
36
|
+
* Fix backwards display of NOTICEs from a Function ([issue 1443](https://github.com/dbcli/pgcli/issues/1443))
|
|
37
|
+
* Fix psycopg errors when installing on Windows. ([issue 1413](https://https://github.com/dbcli/pgcli/issues/1413))
|
|
38
|
+
* Use a home-made function to display query duration instead of relying on a third-party library (the general behaviour does not change), which fixes the installation of `pgcli` on 32-bit architectures ([issue 1451](https://github.com/dbcli/pgcli/issues/1451))
|
|
39
|
+
|
|
40
|
+
==================
|
|
41
|
+
4.0.1 (2023-10-30)
|
|
3
42
|
==================
|
|
4
43
|
|
|
5
44
|
Internal:
|
|
@@ -7,7 +46,7 @@ Internal:
|
|
|
7
46
|
* Allow stable version of pendulum.
|
|
8
47
|
|
|
9
48
|
==================
|
|
10
|
-
4.0.0 (2023-
|
|
49
|
+
4.0.0 (2023-10-27)
|
|
11
50
|
==================
|
|
12
51
|
|
|
13
52
|
Features:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.2.0"
|
|
@@ -11,9 +11,9 @@ import logging
|
|
|
11
11
|
import threading
|
|
12
12
|
import shutil
|
|
13
13
|
import functools
|
|
14
|
-
import pendulum
|
|
15
14
|
import datetime as dt
|
|
16
15
|
import itertools
|
|
16
|
+
import pathlib
|
|
17
17
|
import platform
|
|
18
18
|
from time import time, sleep
|
|
19
19
|
from typing import Optional
|
|
@@ -74,8 +74,9 @@ from urllib.parse import urlparse
|
|
|
74
74
|
|
|
75
75
|
from getpass import getuser
|
|
76
76
|
|
|
77
|
-
from psycopg import OperationalError, InterfaceError
|
|
77
|
+
from psycopg import OperationalError, InterfaceError, Notify
|
|
78
78
|
from psycopg.conninfo import make_conninfo, conninfo_to_dict
|
|
79
|
+
from psycopg.errors import Diagnostic
|
|
79
80
|
|
|
80
81
|
from collections import namedtuple
|
|
81
82
|
|
|
@@ -129,6 +130,15 @@ class PgCliQuitError(Exception):
|
|
|
129
130
|
pass
|
|
130
131
|
|
|
131
132
|
|
|
133
|
+
def notify_callback(notify: Notify):
|
|
134
|
+
click.secho(
|
|
135
|
+
'Notification received on channel "{}" (PID {}):\n{}'.format(
|
|
136
|
+
notify.channel, notify.pid, notify.payload
|
|
137
|
+
),
|
|
138
|
+
fg="green",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
132
142
|
class PGCli:
|
|
133
143
|
default_prompt = "\\u@\\h:\\d> "
|
|
134
144
|
max_len_prompt = 30
|
|
@@ -165,6 +175,7 @@ class PGCli:
|
|
|
165
175
|
pgexecute=None,
|
|
166
176
|
pgclirc_file=None,
|
|
167
177
|
row_limit=None,
|
|
178
|
+
application_name="pgcli",
|
|
168
179
|
single_connection=False,
|
|
169
180
|
less_chatty=None,
|
|
170
181
|
prompt=None,
|
|
@@ -172,6 +183,7 @@ class PGCli:
|
|
|
172
183
|
auto_vertical_output=False,
|
|
173
184
|
warn=None,
|
|
174
185
|
ssh_tunnel_url: Optional[str] = None,
|
|
186
|
+
log_file: Optional[str] = None,
|
|
175
187
|
):
|
|
176
188
|
self.force_passwd_prompt = force_passwd_prompt
|
|
177
189
|
self.never_passwd_prompt = never_passwd_prompt
|
|
@@ -210,6 +222,8 @@ class PGCli:
|
|
|
210
222
|
else:
|
|
211
223
|
self.row_limit = c["main"].as_int("row_limit")
|
|
212
224
|
|
|
225
|
+
self.application_name = application_name
|
|
226
|
+
|
|
213
227
|
# if not specified, set to DEFAULT_MAX_FIELD_WIDTH
|
|
214
228
|
# if specified but empty, set to None to disable truncation
|
|
215
229
|
# ellipsis will take at least 3 symbols, so this can't be less than 3 if specified and > 0
|
|
@@ -237,6 +251,9 @@ class PGCli:
|
|
|
237
251
|
)
|
|
238
252
|
|
|
239
253
|
self.less_chatty = bool(less_chatty) or c["main"].as_bool("less_chatty")
|
|
254
|
+
self.verbose_errors = "verbose_errors" in c["main"] and c["main"].as_bool(
|
|
255
|
+
"verbose_errors"
|
|
256
|
+
)
|
|
240
257
|
self.null_string = c["main"].get("null_string", "<null>")
|
|
241
258
|
self.prompt_format = (
|
|
242
259
|
prompt
|
|
@@ -295,6 +312,11 @@ class PGCli:
|
|
|
295
312
|
self.ssh_tunnel_url = ssh_tunnel_url
|
|
296
313
|
self.ssh_tunnel = None
|
|
297
314
|
|
|
315
|
+
if log_file:
|
|
316
|
+
with open(log_file, "a+"):
|
|
317
|
+
pass # ensure writeable
|
|
318
|
+
self.log_file = log_file
|
|
319
|
+
|
|
298
320
|
# formatter setup
|
|
299
321
|
self.formatter = TabularOutputFormatter(format_name=c["main"]["table_format"])
|
|
300
322
|
register_new_formatter(self.formatter)
|
|
@@ -354,6 +376,12 @@ class PGCli:
|
|
|
354
376
|
"\\o [filename]",
|
|
355
377
|
"Send all query results to file.",
|
|
356
378
|
)
|
|
379
|
+
self.pgspecial.register(
|
|
380
|
+
self.write_to_logfile,
|
|
381
|
+
"\\log-file",
|
|
382
|
+
"\\log-file [filename]",
|
|
383
|
+
"Log all query results to a logfile, in addition to the normal output destination.",
|
|
384
|
+
)
|
|
357
385
|
self.pgspecial.register(
|
|
358
386
|
self.info_connection, "\\conninfo", "\\conninfo", "Get connection details"
|
|
359
387
|
)
|
|
@@ -378,6 +406,26 @@ class PGCli:
|
|
|
378
406
|
"Echo a string to the query output channel.",
|
|
379
407
|
)
|
|
380
408
|
|
|
409
|
+
self.pgspecial.register(
|
|
410
|
+
self.toggle_verbose_errors,
|
|
411
|
+
"\\v",
|
|
412
|
+
"\\v [on|off]",
|
|
413
|
+
"Toggle verbose errors.",
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def toggle_verbose_errors(self, pattern, **_):
|
|
417
|
+
flag = pattern.strip()
|
|
418
|
+
|
|
419
|
+
if flag == "on":
|
|
420
|
+
self.verbose_errors = True
|
|
421
|
+
elif flag == "off":
|
|
422
|
+
self.verbose_errors = False
|
|
423
|
+
else:
|
|
424
|
+
self.verbose_errors = not self.verbose_errors
|
|
425
|
+
|
|
426
|
+
message = "Verbose errors " + "on." if self.verbose_errors else "off."
|
|
427
|
+
return [(None, None, None, message)]
|
|
428
|
+
|
|
381
429
|
def echo(self, pattern, **_):
|
|
382
430
|
return [(None, None, None, pattern)]
|
|
383
431
|
|
|
@@ -473,6 +521,26 @@ class PGCli:
|
|
|
473
521
|
explain_mode=self.explain_mode,
|
|
474
522
|
)
|
|
475
523
|
|
|
524
|
+
def write_to_logfile(self, pattern, **_):
|
|
525
|
+
if not pattern:
|
|
526
|
+
self.log_file = None
|
|
527
|
+
message = "Logfile capture disabled"
|
|
528
|
+
return [(None, None, None, message, "", True, True)]
|
|
529
|
+
|
|
530
|
+
log_file = pathlib.Path(pattern).expanduser().absolute()
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
with open(log_file, "a+"):
|
|
534
|
+
pass # ensure writeable
|
|
535
|
+
except OSError as e:
|
|
536
|
+
self.log_file = None
|
|
537
|
+
message = str(e) + "\nLogfile capture disabled"
|
|
538
|
+
return [(None, None, None, message, "", False, True)]
|
|
539
|
+
|
|
540
|
+
self.log_file = str(log_file)
|
|
541
|
+
message = 'Writing to file "%s"' % self.log_file
|
|
542
|
+
return [(None, None, None, message, "", True, True)]
|
|
543
|
+
|
|
476
544
|
def write_to_file(self, pattern, **_):
|
|
477
545
|
if not pattern:
|
|
478
546
|
self.output_file = None
|
|
@@ -542,7 +610,7 @@ class PGCli:
|
|
|
542
610
|
click.secho(
|
|
543
611
|
f"service '{service}' was not found in {file}", err=True, fg="red"
|
|
544
612
|
)
|
|
545
|
-
exit(1)
|
|
613
|
+
sys.exit(1)
|
|
546
614
|
self.connect(
|
|
547
615
|
database=service_config.get("dbname"),
|
|
548
616
|
host=service_config.get("host"),
|
|
@@ -568,7 +636,7 @@ class PGCli:
|
|
|
568
636
|
if not database:
|
|
569
637
|
database = user
|
|
570
638
|
|
|
571
|
-
kwargs.setdefault("application_name",
|
|
639
|
+
kwargs.setdefault("application_name", self.application_name)
|
|
572
640
|
|
|
573
641
|
# If password prompt is not forced but no password is provided, try
|
|
574
642
|
# getting it from environment variable.
|
|
@@ -642,7 +710,7 @@ class PGCli:
|
|
|
642
710
|
self.logger.handlers = logger_handlers
|
|
643
711
|
self.logger.error("traceback: %r", traceback.format_exc())
|
|
644
712
|
click.secho(str(e), err=True, fg="red")
|
|
645
|
-
exit(1)
|
|
713
|
+
sys.exit(1)
|
|
646
714
|
self.logger.handlers = logger_handlers
|
|
647
715
|
|
|
648
716
|
atexit.register(self.ssh_tunnel.stop)
|
|
@@ -658,7 +726,16 @@ class PGCli:
|
|
|
658
726
|
# prompt for a password (no -w flag), prompt for a passwd and try again.
|
|
659
727
|
try:
|
|
660
728
|
try:
|
|
661
|
-
pgexecute = PGExecute(
|
|
729
|
+
pgexecute = PGExecute(
|
|
730
|
+
database,
|
|
731
|
+
user,
|
|
732
|
+
passwd,
|
|
733
|
+
host,
|
|
734
|
+
port,
|
|
735
|
+
dsn,
|
|
736
|
+
notify_callback,
|
|
737
|
+
**kwargs,
|
|
738
|
+
)
|
|
662
739
|
except (OperationalError, InterfaceError) as e:
|
|
663
740
|
if should_ask_for_password(e):
|
|
664
741
|
passwd = click.prompt(
|
|
@@ -668,7 +745,14 @@ class PGCli:
|
|
|
668
745
|
type=str,
|
|
669
746
|
)
|
|
670
747
|
pgexecute = PGExecute(
|
|
671
|
-
database,
|
|
748
|
+
database,
|
|
749
|
+
user,
|
|
750
|
+
passwd,
|
|
751
|
+
host,
|
|
752
|
+
port,
|
|
753
|
+
dsn,
|
|
754
|
+
notify_callback,
|
|
755
|
+
**kwargs,
|
|
672
756
|
)
|
|
673
757
|
else:
|
|
674
758
|
raise e
|
|
@@ -679,7 +763,7 @@ class PGCli:
|
|
|
679
763
|
self.logger.debug("Database connection failed: %r.", e)
|
|
680
764
|
self.logger.error("traceback: %r", traceback.format_exc())
|
|
681
765
|
click.secho(str(e), err=True, fg="red")
|
|
682
|
-
exit(1)
|
|
766
|
+
sys.exit(1)
|
|
683
767
|
|
|
684
768
|
self.pgexecute = pgexecute
|
|
685
769
|
|
|
@@ -775,7 +859,7 @@ class PGCli:
|
|
|
775
859
|
else:
|
|
776
860
|
try:
|
|
777
861
|
if self.output_file and not text.startswith(
|
|
778
|
-
("\\o ", "\\? ", "\\echo ")
|
|
862
|
+
("\\o ", "\\log-file", "\\? ", "\\echo ")
|
|
779
863
|
):
|
|
780
864
|
try:
|
|
781
865
|
with open(self.output_file, "a", encoding="utf-8") as f:
|
|
@@ -787,6 +871,23 @@ class PGCli:
|
|
|
787
871
|
else:
|
|
788
872
|
if output:
|
|
789
873
|
self.echo_via_pager("\n".join(output))
|
|
874
|
+
|
|
875
|
+
# Log to file in addition to normal output
|
|
876
|
+
if (
|
|
877
|
+
self.log_file
|
|
878
|
+
and not text.startswith(("\\o ", "\\log-file", "\\? ", "\\echo "))
|
|
879
|
+
and not text.strip() == ""
|
|
880
|
+
):
|
|
881
|
+
try:
|
|
882
|
+
with open(self.log_file, "a", encoding="utf-8") as f:
|
|
883
|
+
click.echo(
|
|
884
|
+
dt.datetime.now().isoformat(), file=f
|
|
885
|
+
) # timestamp log
|
|
886
|
+
click.echo(text, file=f)
|
|
887
|
+
click.echo("\n".join(output), file=f)
|
|
888
|
+
click.echo("", file=f) # extra newline
|
|
889
|
+
except OSError as e:
|
|
890
|
+
click.secho(str(e), err=True, fg="red")
|
|
790
891
|
except KeyboardInterrupt:
|
|
791
892
|
pass
|
|
792
893
|
|
|
@@ -797,9 +898,9 @@ class PGCli:
|
|
|
797
898
|
"Time: %0.03fs (%s), executed in: %0.03fs (%s)"
|
|
798
899
|
% (
|
|
799
900
|
query.total_time,
|
|
800
|
-
|
|
901
|
+
duration_in_words(query.total_time),
|
|
801
902
|
query.execution_time,
|
|
802
|
-
|
|
903
|
+
duration_in_words(query.execution_time),
|
|
803
904
|
)
|
|
804
905
|
)
|
|
805
906
|
else:
|
|
@@ -1053,7 +1154,7 @@ class PGCli:
|
|
|
1053
1154
|
res = self.pgexecute.run(
|
|
1054
1155
|
text,
|
|
1055
1156
|
self.pgspecial,
|
|
1056
|
-
exception_formatter,
|
|
1157
|
+
lambda x: exception_formatter(x, self.verbose_errors),
|
|
1057
1158
|
on_error_resume,
|
|
1058
1159
|
explain_mode=self.explain_mode,
|
|
1059
1160
|
)
|
|
@@ -1337,6 +1438,12 @@ class PGCli:
|
|
|
1337
1438
|
type=click.INT,
|
|
1338
1439
|
help="Set threshold for row limit prompt. Use 0 to disable prompt.",
|
|
1339
1440
|
)
|
|
1441
|
+
@click.option(
|
|
1442
|
+
"--application-name",
|
|
1443
|
+
default="pgcli",
|
|
1444
|
+
envvar="PGAPPNAME",
|
|
1445
|
+
help="Application name for the connection.",
|
|
1446
|
+
)
|
|
1340
1447
|
@click.option(
|
|
1341
1448
|
"--less-chatty",
|
|
1342
1449
|
"less_chatty",
|
|
@@ -1356,6 +1463,13 @@ class PGCli:
|
|
|
1356
1463
|
is_flag=True,
|
|
1357
1464
|
help="list available databases, then exit.",
|
|
1358
1465
|
)
|
|
1466
|
+
@click.option(
|
|
1467
|
+
"--ping",
|
|
1468
|
+
"ping_database",
|
|
1469
|
+
is_flag=True,
|
|
1470
|
+
default=False,
|
|
1471
|
+
help="Check database connectivity, then exit.",
|
|
1472
|
+
)
|
|
1359
1473
|
@click.option(
|
|
1360
1474
|
"--auto-vertical-output",
|
|
1361
1475
|
is_flag=True,
|
|
@@ -1371,6 +1485,11 @@ class PGCli:
|
|
|
1371
1485
|
default=None,
|
|
1372
1486
|
help="Open an SSH tunnel to the given address and connect to the database from it.",
|
|
1373
1487
|
)
|
|
1488
|
+
@click.option(
|
|
1489
|
+
"--log-file",
|
|
1490
|
+
default=None,
|
|
1491
|
+
help="Write all queries & output into a file, in addition to the normal output destination.",
|
|
1492
|
+
)
|
|
1374
1493
|
@click.argument("dbname", default=lambda: None, envvar="PGDATABASE", nargs=1)
|
|
1375
1494
|
@click.argument("username", default=lambda: None, envvar="PGUSER", nargs=1)
|
|
1376
1495
|
def cli(
|
|
@@ -1387,14 +1506,17 @@ def cli(
|
|
|
1387
1506
|
pgclirc,
|
|
1388
1507
|
dsn,
|
|
1389
1508
|
row_limit,
|
|
1509
|
+
application_name,
|
|
1390
1510
|
less_chatty,
|
|
1391
1511
|
prompt,
|
|
1392
1512
|
prompt_dsn,
|
|
1393
1513
|
list_databases,
|
|
1514
|
+
ping_database,
|
|
1394
1515
|
auto_vertical_output,
|
|
1395
1516
|
list_dsn,
|
|
1396
1517
|
warn,
|
|
1397
1518
|
ssh_tunnel: str,
|
|
1519
|
+
log_file: str,
|
|
1398
1520
|
):
|
|
1399
1521
|
if version:
|
|
1400
1522
|
print("Version:", __version__)
|
|
@@ -1429,7 +1551,7 @@ def cli(
|
|
|
1429
1551
|
err=True,
|
|
1430
1552
|
fg="red",
|
|
1431
1553
|
)
|
|
1432
|
-
exit(1)
|
|
1554
|
+
sys.exit(1)
|
|
1433
1555
|
|
|
1434
1556
|
if ssh_tunnel and not SSH_TUNNEL_SUPPORT:
|
|
1435
1557
|
click.secho(
|
|
@@ -1438,13 +1560,14 @@ def cli(
|
|
|
1438
1560
|
err=True,
|
|
1439
1561
|
fg="red",
|
|
1440
1562
|
)
|
|
1441
|
-
exit(1)
|
|
1563
|
+
sys.exit(1)
|
|
1442
1564
|
|
|
1443
1565
|
pgcli = PGCli(
|
|
1444
1566
|
prompt_passwd,
|
|
1445
1567
|
never_prompt,
|
|
1446
1568
|
pgclirc_file=pgclirc,
|
|
1447
1569
|
row_limit=row_limit,
|
|
1570
|
+
application_name=application_name,
|
|
1448
1571
|
single_connection=single_connection,
|
|
1449
1572
|
less_chatty=less_chatty,
|
|
1450
1573
|
prompt=prompt,
|
|
@@ -1452,6 +1575,7 @@ def cli(
|
|
|
1452
1575
|
auto_vertical_output=auto_vertical_output,
|
|
1453
1576
|
warn=warn,
|
|
1454
1577
|
ssh_tunnel_url=ssh_tunnel,
|
|
1578
|
+
log_file=log_file,
|
|
1455
1579
|
)
|
|
1456
1580
|
|
|
1457
1581
|
# Choose which ever one has a valid value.
|
|
@@ -1465,8 +1589,8 @@ def cli(
|
|
|
1465
1589
|
service = database[8:]
|
|
1466
1590
|
elif os.getenv("PGSERVICE") is not None:
|
|
1467
1591
|
service = os.getenv("PGSERVICE")
|
|
1468
|
-
# because option --list or -l are not supposed to have a db name
|
|
1469
|
-
if list_databases:
|
|
1592
|
+
# because option --ping, --list or -l are not supposed to have a db name
|
|
1593
|
+
if list_databases or ping_database:
|
|
1470
1594
|
database = "postgres"
|
|
1471
1595
|
|
|
1472
1596
|
if dsn != "":
|
|
@@ -1480,7 +1604,7 @@ def cli(
|
|
|
1480
1604
|
err=True,
|
|
1481
1605
|
fg="red",
|
|
1482
1606
|
)
|
|
1483
|
-
exit(1)
|
|
1607
|
+
sys.exit(1)
|
|
1484
1608
|
except Exception:
|
|
1485
1609
|
click.secho(
|
|
1486
1610
|
"Invalid DSNs found in the config file. "
|
|
@@ -1488,7 +1612,7 @@ def cli(
|
|
|
1488
1612
|
err=True,
|
|
1489
1613
|
fg="red",
|
|
1490
1614
|
)
|
|
1491
|
-
exit(1)
|
|
1615
|
+
sys.exit(1)
|
|
1492
1616
|
pgcli.connect_uri(dsn_config)
|
|
1493
1617
|
pgcli.dsn_alias = dsn
|
|
1494
1618
|
elif "://" in database:
|
|
@@ -1510,6 +1634,20 @@ def cli(
|
|
|
1510
1634
|
|
|
1511
1635
|
sys.exit(0)
|
|
1512
1636
|
|
|
1637
|
+
if ping_database:
|
|
1638
|
+
try:
|
|
1639
|
+
list(pgcli.pgexecute.run("SELECT 1"))
|
|
1640
|
+
except Exception:
|
|
1641
|
+
click.secho(
|
|
1642
|
+
"Could not connect to the database. Please check that the database is running.",
|
|
1643
|
+
err=True,
|
|
1644
|
+
fg="red",
|
|
1645
|
+
)
|
|
1646
|
+
sys.exit(1)
|
|
1647
|
+
else:
|
|
1648
|
+
click.echo("PONG")
|
|
1649
|
+
sys.exit(0)
|
|
1650
|
+
|
|
1513
1651
|
pgcli.logger.debug(
|
|
1514
1652
|
"Launch Params: \n" "\tdatabase: %r" "\tuser: %r" "\thost: %r" "\tport: %r",
|
|
1515
1653
|
database,
|
|
@@ -1583,8 +1721,71 @@ def is_select(status):
|
|
|
1583
1721
|
return status.split(None, 1)[0].lower() == "select"
|
|
1584
1722
|
|
|
1585
1723
|
|
|
1586
|
-
def
|
|
1587
|
-
|
|
1724
|
+
def diagnostic_output(diagnostic: Diagnostic) -> str:
|
|
1725
|
+
fields = []
|
|
1726
|
+
|
|
1727
|
+
if diagnostic.severity is not None:
|
|
1728
|
+
fields.append("Severity: " + diagnostic.severity)
|
|
1729
|
+
|
|
1730
|
+
if diagnostic.severity_nonlocalized is not None:
|
|
1731
|
+
fields.append("Severity (non-localized): " + diagnostic.severity_nonlocalized)
|
|
1732
|
+
|
|
1733
|
+
if diagnostic.sqlstate is not None:
|
|
1734
|
+
fields.append("SQLSTATE code: " + diagnostic.sqlstate)
|
|
1735
|
+
|
|
1736
|
+
if diagnostic.message_primary is not None:
|
|
1737
|
+
fields.append("Message: " + diagnostic.message_primary)
|
|
1738
|
+
|
|
1739
|
+
if diagnostic.message_detail is not None:
|
|
1740
|
+
fields.append("Detail: " + diagnostic.message_detail)
|
|
1741
|
+
|
|
1742
|
+
if diagnostic.message_hint is not None:
|
|
1743
|
+
fields.append("Hint: " + diagnostic.message_hint)
|
|
1744
|
+
|
|
1745
|
+
if diagnostic.statement_position is not None:
|
|
1746
|
+
fields.append("Position: " + diagnostic.statement_position)
|
|
1747
|
+
|
|
1748
|
+
if diagnostic.internal_position is not None:
|
|
1749
|
+
fields.append("Internal position: " + diagnostic.internal_position)
|
|
1750
|
+
|
|
1751
|
+
if diagnostic.internal_query is not None:
|
|
1752
|
+
fields.append("Internal query: " + diagnostic.internal_query)
|
|
1753
|
+
|
|
1754
|
+
if diagnostic.context is not None:
|
|
1755
|
+
fields.append("Where: " + diagnostic.context)
|
|
1756
|
+
|
|
1757
|
+
if diagnostic.schema_name is not None:
|
|
1758
|
+
fields.append("Schema name: " + diagnostic.schema_name)
|
|
1759
|
+
|
|
1760
|
+
if diagnostic.table_name is not None:
|
|
1761
|
+
fields.append("Table name: " + diagnostic.table_name)
|
|
1762
|
+
|
|
1763
|
+
if diagnostic.column_name is not None:
|
|
1764
|
+
fields.append("Column name: " + diagnostic.column_name)
|
|
1765
|
+
|
|
1766
|
+
if diagnostic.datatype_name is not None:
|
|
1767
|
+
fields.append("Data type name: " + diagnostic.datatype_name)
|
|
1768
|
+
|
|
1769
|
+
if diagnostic.constraint_name is not None:
|
|
1770
|
+
fields.append("Constraint name: " + diagnostic.constraint_name)
|
|
1771
|
+
|
|
1772
|
+
if diagnostic.source_file is not None:
|
|
1773
|
+
fields.append("File: " + diagnostic.source_file)
|
|
1774
|
+
|
|
1775
|
+
if diagnostic.source_line is not None:
|
|
1776
|
+
fields.append("Line: " + diagnostic.source_line)
|
|
1777
|
+
|
|
1778
|
+
if diagnostic.source_function is not None:
|
|
1779
|
+
fields.append("Routine: " + diagnostic.source_function)
|
|
1780
|
+
|
|
1781
|
+
return "\n".join(fields)
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
def exception_formatter(e, verbose_errors: bool = False):
|
|
1785
|
+
s = str(e)
|
|
1786
|
+
if verbose_errors:
|
|
1787
|
+
s += "\n" + diagnostic_output(e.diag)
|
|
1788
|
+
return click.style(s, fg="red")
|
|
1588
1789
|
|
|
1589
1790
|
|
|
1590
1791
|
def format_output(title, cur, headers, status, settings, explain_mode=False):
|
|
@@ -1724,5 +1925,28 @@ def parse_service_info(service):
|
|
|
1724
1925
|
return service_conf, service_file
|
|
1725
1926
|
|
|
1726
1927
|
|
|
1928
|
+
def duration_in_words(duration_in_seconds: float) -> str:
|
|
1929
|
+
if not duration_in_seconds:
|
|
1930
|
+
return "0 seconds"
|
|
1931
|
+
components = []
|
|
1932
|
+
hours, remainder = divmod(duration_in_seconds, 3600)
|
|
1933
|
+
if hours > 1:
|
|
1934
|
+
components.append(f"{hours} hours")
|
|
1935
|
+
elif hours == 1:
|
|
1936
|
+
components.append("1 hour")
|
|
1937
|
+
minutes, seconds = divmod(remainder, 60)
|
|
1938
|
+
if minutes > 1:
|
|
1939
|
+
components.append(f"{minutes} minutes")
|
|
1940
|
+
elif minutes == 1:
|
|
1941
|
+
components.append("1 minute")
|
|
1942
|
+
if seconds >= 2:
|
|
1943
|
+
components.append(f"{int(seconds)} seconds")
|
|
1944
|
+
elif seconds >= 1:
|
|
1945
|
+
components.append("1 second")
|
|
1946
|
+
elif seconds:
|
|
1947
|
+
components.append(f"{round(seconds, 3)} second")
|
|
1948
|
+
return " ".join(components)
|
|
1949
|
+
|
|
1950
|
+
|
|
1727
1951
|
if __name__ == "__main__":
|
|
1728
1952
|
cli()
|