pgcli 4.0.1__tar.gz → 4.1.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.1.0}/AUTHORS +4 -0
- {pgcli-4.0.1/pgcli.egg-info → pgcli-4.1.0}/PKG-INFO +4 -4
- {pgcli-4.0.1 → pgcli-4.1.0}/changelog.rst +23 -2
- pgcli-4.1.0/pgcli/__init__.py +1 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/main.py +213 -11
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/pgclirc +10 -4
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/pgexecute.py +16 -1
- {pgcli-4.0.1 → pgcli-4.1.0/pgcli.egg-info}/PKG-INFO +4 -4
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli.egg-info/SOURCES.txt +1 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli.egg-info/requires.txt +7 -3
- {pgcli-4.0.1 → pgcli-4.1.0}/setup.py +3 -8
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/conftest.py +2 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/steps/crud_database.py +1 -0
- pgcli-4.1.0/tests/test_application_name.py +17 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_main.py +98 -1
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_pgexecute.py +64 -2
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_ssh_tunnel.py +3 -1
- pgcli-4.0.1/pgcli/__init__.py +0 -1
- {pgcli-4.0.1 → pgcli-4.1.0}/LICENSE.txt +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/MANIFEST.in +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/README.rst +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/__main__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/auth.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/completion_refresher.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/config.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/explain_output_formatter.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/key_bindings.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/magic.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/formatter/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/formatter/sqlformatter.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/parseutils/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/parseutils/ctes.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/parseutils/meta.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/parseutils/tables.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/parseutils/utils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/pgliterals/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/pgliterals/main.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/pgliterals/pgliterals.json +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/prioritization.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/prompt_utils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/packages/sqlcompletion.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/pgbuffer.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/pgcompleter.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/pgstyle.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/pgtoolbar.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli/pyev.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli.egg-info/dependency_links.txt +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli.egg-info/entry_points.txt +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pgcli.egg-info/top_level.txt +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/pyproject.toml +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/setup.cfg +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/auto_vertical.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/basic_commands.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/crud_database.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/crud_table.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/db_utils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/environment.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/expanded.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/fixture_data/help.txt +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/fixture_data/help_commands.txt +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/fixture_utils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/iocommands.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/named_queries.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/pgbouncer.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/specials.feature +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/steps/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/steps/auto_vertical.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/steps/basic_commands.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/steps/crud_table.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/steps/expanded.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/steps/iocommands.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/steps/named_queries.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/steps/pgbouncer.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/steps/specials.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/steps/wrappers.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/features/wrappager.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/formatter/__init__.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/formatter/test_sqlformatter.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/metadata.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/parseutils/test_ctes.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/parseutils/test_function_metadata.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/parseutils/test_parseutils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/pytest.ini +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_auth.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_completion_refresher.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_config.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_fuzzy_completion.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_naive_completion.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_pgcompleter.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_pgspecial.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_prioritization.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_prompt_utils.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_rowlimit.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_smart_completion_multiple_schemata.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_smart_completion_public_schema_only.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/test_sqlcompletion.py +0 -0
- {pgcli-4.0.1 → pgcli-4.1.0}/tests/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pgcli
|
|
3
|
-
Version: 4.0
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: CLI for Postgres Database. With auto-completion and syntax highlighting.
|
|
5
5
|
Home-page: http://pgcli.com
|
|
6
6
|
Author: Pgcli Core Team
|
|
@@ -28,10 +28,10 @@ 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
36
|
Requires-Dist: setproctitle>=1.1.9
|
|
37
37
|
Provides-Extra: keyring
|
|
@@ -1,5 +1,26 @@
|
|
|
1
|
+
4.1.0 (2024-03-09)
|
|
1
2
|
==================
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
---------
|
|
6
|
+
* Support `PGAPPNAME` as an environment variable and `--application-name` as a command line argument.
|
|
7
|
+
* Add `verbose_errors` config and `\v` special command which enable the
|
|
8
|
+
displaying of all Postgres error fields received.
|
|
9
|
+
* Show Postgres notifications.
|
|
10
|
+
* Support sqlparse 0.5.x
|
|
11
|
+
* Add `--log-file [filename]` cli argument and `\log-file [filename]` special commands to
|
|
12
|
+
log to an external file in addition to the normal output
|
|
13
|
+
|
|
14
|
+
Bug fixes:
|
|
15
|
+
----------
|
|
16
|
+
|
|
17
|
+
* Fix display of "short host" in prompt (with `\h`) for IPv4 addresses ([issue 964](https://github.com/dbcli/pgcli/issues/964)).
|
|
18
|
+
* Fix backwards display of NOTICEs from a Function ([issue 1443](https://github.com/dbcli/pgcli/issues/1443))
|
|
19
|
+
* Fix psycopg errors when installing on Windows. ([issue 1413](https://https://github.com/dbcli/pgcli/issues/1413))
|
|
20
|
+
* 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))
|
|
21
|
+
|
|
22
|
+
==================
|
|
23
|
+
4.0.1 (2023-10-30)
|
|
3
24
|
==================
|
|
4
25
|
|
|
5
26
|
Internal:
|
|
@@ -7,7 +28,7 @@ Internal:
|
|
|
7
28
|
* Allow stable version of pendulum.
|
|
8
29
|
|
|
9
30
|
==================
|
|
10
|
-
4.0.0 (2023-
|
|
31
|
+
4.0.0 (2023-10-27)
|
|
11
32
|
==================
|
|
12
33
|
|
|
13
34
|
Features:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.1.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
|
|
@@ -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.
|
|
@@ -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
|
|
@@ -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",
|
|
@@ -1371,6 +1478,11 @@ class PGCli:
|
|
|
1371
1478
|
default=None,
|
|
1372
1479
|
help="Open an SSH tunnel to the given address and connect to the database from it.",
|
|
1373
1480
|
)
|
|
1481
|
+
@click.option(
|
|
1482
|
+
"--log-file",
|
|
1483
|
+
default=None,
|
|
1484
|
+
help="Write all queries & output into a file, in addition to the normal output destination.",
|
|
1485
|
+
)
|
|
1374
1486
|
@click.argument("dbname", default=lambda: None, envvar="PGDATABASE", nargs=1)
|
|
1375
1487
|
@click.argument("username", default=lambda: None, envvar="PGUSER", nargs=1)
|
|
1376
1488
|
def cli(
|
|
@@ -1387,6 +1499,7 @@ def cli(
|
|
|
1387
1499
|
pgclirc,
|
|
1388
1500
|
dsn,
|
|
1389
1501
|
row_limit,
|
|
1502
|
+
application_name,
|
|
1390
1503
|
less_chatty,
|
|
1391
1504
|
prompt,
|
|
1392
1505
|
prompt_dsn,
|
|
@@ -1395,6 +1508,7 @@ def cli(
|
|
|
1395
1508
|
list_dsn,
|
|
1396
1509
|
warn,
|
|
1397
1510
|
ssh_tunnel: str,
|
|
1511
|
+
log_file: str,
|
|
1398
1512
|
):
|
|
1399
1513
|
if version:
|
|
1400
1514
|
print("Version:", __version__)
|
|
@@ -1445,6 +1559,7 @@ def cli(
|
|
|
1445
1559
|
never_prompt,
|
|
1446
1560
|
pgclirc_file=pgclirc,
|
|
1447
1561
|
row_limit=row_limit,
|
|
1562
|
+
application_name=application_name,
|
|
1448
1563
|
single_connection=single_connection,
|
|
1449
1564
|
less_chatty=less_chatty,
|
|
1450
1565
|
prompt=prompt,
|
|
@@ -1452,6 +1567,7 @@ def cli(
|
|
|
1452
1567
|
auto_vertical_output=auto_vertical_output,
|
|
1453
1568
|
warn=warn,
|
|
1454
1569
|
ssh_tunnel_url=ssh_tunnel,
|
|
1570
|
+
log_file=log_file,
|
|
1455
1571
|
)
|
|
1456
1572
|
|
|
1457
1573
|
# Choose which ever one has a valid value.
|
|
@@ -1583,8 +1699,71 @@ def is_select(status):
|
|
|
1583
1699
|
return status.split(None, 1)[0].lower() == "select"
|
|
1584
1700
|
|
|
1585
1701
|
|
|
1586
|
-
def
|
|
1587
|
-
|
|
1702
|
+
def diagnostic_output(diagnostic: Diagnostic) -> str:
|
|
1703
|
+
fields = []
|
|
1704
|
+
|
|
1705
|
+
if diagnostic.severity is not None:
|
|
1706
|
+
fields.append("Severity: " + diagnostic.severity)
|
|
1707
|
+
|
|
1708
|
+
if diagnostic.severity_nonlocalized is not None:
|
|
1709
|
+
fields.append("Severity (non-localized): " + diagnostic.severity_nonlocalized)
|
|
1710
|
+
|
|
1711
|
+
if diagnostic.sqlstate is not None:
|
|
1712
|
+
fields.append("SQLSTATE code: " + diagnostic.sqlstate)
|
|
1713
|
+
|
|
1714
|
+
if diagnostic.message_primary is not None:
|
|
1715
|
+
fields.append("Message: " + diagnostic.message_primary)
|
|
1716
|
+
|
|
1717
|
+
if diagnostic.message_detail is not None:
|
|
1718
|
+
fields.append("Detail: " + diagnostic.message_detail)
|
|
1719
|
+
|
|
1720
|
+
if diagnostic.message_hint is not None:
|
|
1721
|
+
fields.append("Hint: " + diagnostic.message_hint)
|
|
1722
|
+
|
|
1723
|
+
if diagnostic.statement_position is not None:
|
|
1724
|
+
fields.append("Position: " + diagnostic.statement_position)
|
|
1725
|
+
|
|
1726
|
+
if diagnostic.internal_position is not None:
|
|
1727
|
+
fields.append("Internal position: " + diagnostic.internal_position)
|
|
1728
|
+
|
|
1729
|
+
if diagnostic.internal_query is not None:
|
|
1730
|
+
fields.append("Internal query: " + diagnostic.internal_query)
|
|
1731
|
+
|
|
1732
|
+
if diagnostic.context is not None:
|
|
1733
|
+
fields.append("Where: " + diagnostic.context)
|
|
1734
|
+
|
|
1735
|
+
if diagnostic.schema_name is not None:
|
|
1736
|
+
fields.append("Schema name: " + diagnostic.schema_name)
|
|
1737
|
+
|
|
1738
|
+
if diagnostic.table_name is not None:
|
|
1739
|
+
fields.append("Table name: " + diagnostic.table_name)
|
|
1740
|
+
|
|
1741
|
+
if diagnostic.column_name is not None:
|
|
1742
|
+
fields.append("Column name: " + diagnostic.column_name)
|
|
1743
|
+
|
|
1744
|
+
if diagnostic.datatype_name is not None:
|
|
1745
|
+
fields.append("Data type name: " + diagnostic.datatype_name)
|
|
1746
|
+
|
|
1747
|
+
if diagnostic.constraint_name is not None:
|
|
1748
|
+
fields.append("Constraint name: " + diagnostic.constraint_name)
|
|
1749
|
+
|
|
1750
|
+
if diagnostic.source_file is not None:
|
|
1751
|
+
fields.append("File: " + diagnostic.source_file)
|
|
1752
|
+
|
|
1753
|
+
if diagnostic.source_line is not None:
|
|
1754
|
+
fields.append("Line: " + diagnostic.source_line)
|
|
1755
|
+
|
|
1756
|
+
if diagnostic.source_function is not None:
|
|
1757
|
+
fields.append("Routine: " + diagnostic.source_function)
|
|
1758
|
+
|
|
1759
|
+
return "\n".join(fields)
|
|
1760
|
+
|
|
1761
|
+
|
|
1762
|
+
def exception_formatter(e, verbose_errors: bool = False):
|
|
1763
|
+
s = str(e)
|
|
1764
|
+
if verbose_errors:
|
|
1765
|
+
s += "\n" + diagnostic_output(e.diag)
|
|
1766
|
+
return click.style(s, fg="red")
|
|
1588
1767
|
|
|
1589
1768
|
|
|
1590
1769
|
def format_output(title, cur, headers, status, settings, explain_mode=False):
|
|
@@ -1724,5 +1903,28 @@ def parse_service_info(service):
|
|
|
1724
1903
|
return service_conf, service_file
|
|
1725
1904
|
|
|
1726
1905
|
|
|
1906
|
+
def duration_in_words(duration_in_seconds: float) -> str:
|
|
1907
|
+
if not duration_in_seconds:
|
|
1908
|
+
return "0 seconds"
|
|
1909
|
+
components = []
|
|
1910
|
+
hours, remainder = divmod(duration_in_seconds, 3600)
|
|
1911
|
+
if hours > 1:
|
|
1912
|
+
components.append(f"{hours} hours")
|
|
1913
|
+
elif hours == 1:
|
|
1914
|
+
components.append("1 hour")
|
|
1915
|
+
minutes, seconds = divmod(remainder, 60)
|
|
1916
|
+
if minutes > 1:
|
|
1917
|
+
components.append(f"{minutes} minutes")
|
|
1918
|
+
elif minutes == 1:
|
|
1919
|
+
components.append("1 minute")
|
|
1920
|
+
if seconds >= 2:
|
|
1921
|
+
components.append(f"{int(seconds)} seconds")
|
|
1922
|
+
elif seconds >= 1:
|
|
1923
|
+
components.append("1 second")
|
|
1924
|
+
elif seconds:
|
|
1925
|
+
components.append(f"{round(seconds, 3)} second")
|
|
1926
|
+
return " ".join(components)
|
|
1927
|
+
|
|
1928
|
+
|
|
1727
1929
|
if __name__ == "__main__":
|
|
1728
1930
|
cli()
|
|
@@ -33,10 +33,11 @@ multi_line_mode = psql
|
|
|
33
33
|
# "unconditional_update" will warn you of update statements that don't have a where clause
|
|
34
34
|
destructive_warning = drop, shutdown, delete, truncate, alter, update, unconditional_update
|
|
35
35
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
36
|
+
# When `destructive_warning` is on and the user declines to proceed with a
|
|
37
|
+
# destructive statement, the current transaction (if any) is left untouched,
|
|
38
|
+
# by default. When setting `destructive_warning_restarts_connection` to
|
|
39
|
+
# "True", the connection to the server is restarted. In that case, the
|
|
40
|
+
# transaction (if any) is rolled back.
|
|
40
41
|
destructive_warning_restarts_connection = False
|
|
41
42
|
|
|
42
43
|
# When this option is on (and if `destructive_warning` is not empty),
|
|
@@ -155,6 +156,11 @@ max_field_width = 500
|
|
|
155
156
|
# Skip intro on startup and goodbye on exit
|
|
156
157
|
less_chatty = False
|
|
157
158
|
|
|
159
|
+
# Show all Postgres error fields (as listed in
|
|
160
|
+
# https://www.postgresql.org/docs/current/protocol-error-fields.html).
|
|
161
|
+
# Can be toggled with \v.
|
|
162
|
+
verbose_errors = False
|
|
163
|
+
|
|
158
164
|
# Postgres prompt
|
|
159
165
|
# \t - Current date and time
|
|
160
166
|
# \u - Username
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ipaddress
|
|
1
2
|
import logging
|
|
2
3
|
import traceback
|
|
3
4
|
from collections import namedtuple
|
|
@@ -166,6 +167,7 @@ class PGExecute:
|
|
|
166
167
|
host=None,
|
|
167
168
|
port=None,
|
|
168
169
|
dsn=None,
|
|
170
|
+
notify_callback=None,
|
|
169
171
|
**kwargs,
|
|
170
172
|
):
|
|
171
173
|
self._conn_params = {}
|
|
@@ -178,6 +180,7 @@ class PGExecute:
|
|
|
178
180
|
self.port = None
|
|
179
181
|
self.server_version = None
|
|
180
182
|
self.extra_args = None
|
|
183
|
+
self.notify_callback = notify_callback
|
|
181
184
|
self.connect(database, user, password, host, port, dsn, **kwargs)
|
|
182
185
|
self.reset_expanded = None
|
|
183
186
|
|
|
@@ -236,6 +239,9 @@ class PGExecute:
|
|
|
236
239
|
self.conn = conn
|
|
237
240
|
self.conn.autocommit = True
|
|
238
241
|
|
|
242
|
+
if self.notify_callback is not None:
|
|
243
|
+
self.conn.add_notify_handler(self.notify_callback)
|
|
244
|
+
|
|
239
245
|
# When we connect using a DSN, we don't really know what db,
|
|
240
246
|
# user, etc. we connected to. Let's read it.
|
|
241
247
|
# Note: moved this after setting autocommit because of #664.
|
|
@@ -273,6 +279,11 @@ class PGExecute:
|
|
|
273
279
|
|
|
274
280
|
@property
|
|
275
281
|
def short_host(self):
|
|
282
|
+
try:
|
|
283
|
+
ipaddress.ip_address(self.host)
|
|
284
|
+
return self.host
|
|
285
|
+
except ValueError:
|
|
286
|
+
pass
|
|
276
287
|
if "," in self.host:
|
|
277
288
|
host, _, _ = self.host.partition(",")
|
|
278
289
|
else:
|
|
@@ -431,7 +442,11 @@ class PGExecute:
|
|
|
431
442
|
|
|
432
443
|
def handle_notices(n):
|
|
433
444
|
nonlocal title
|
|
434
|
-
title = f"{
|
|
445
|
+
title = f"{title}"
|
|
446
|
+
if n.message_primary is not None:
|
|
447
|
+
title = f"{title}\n{n.message_primary}"
|
|
448
|
+
if n.message_detail is not None:
|
|
449
|
+
title = f"{title}\n{n.message_detail}"
|
|
435
450
|
|
|
436
451
|
self.conn.add_notice_handler(handle_notices)
|
|
437
452
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pgcli
|
|
3
|
-
Version: 4.0
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: CLI for Postgres Database. With auto-completion and syntax highlighting.
|
|
5
5
|
Home-page: http://pgcli.com
|
|
6
6
|
Author: Pgcli Core Team
|
|
@@ -28,10 +28,10 @@ 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
36
|
Requires-Dist: setproctitle>=1.1.9
|
|
37
37
|
Provides-Extra: keyring
|
|
@@ -2,13 +2,17 @@ pgspecial>=2.0.0
|
|
|
2
2
|
click>=4.1
|
|
3
3
|
Pygments>=2.0
|
|
4
4
|
prompt_toolkit<4.0.0,>=2.0.6
|
|
5
|
-
|
|
6
|
-
sqlparse<0.5,>=0.3.0
|
|
5
|
+
sqlparse<0.6,>=0.3.0
|
|
7
6
|
configobj>=5.0.6
|
|
8
|
-
pendulum>=2.1.0
|
|
9
7
|
cli_helpers[styles]>=2.2.1
|
|
10
8
|
setproctitle>=1.1.9
|
|
11
9
|
|
|
10
|
+
[:sys_platform != "win32"]
|
|
11
|
+
psycopg>=3.0.14
|
|
12
|
+
|
|
13
|
+
[:sys_platform == "win32"]
|
|
14
|
+
psycopg-binary>=3.0.14
|
|
15
|
+
|
|
12
16
|
[keyring]
|
|
13
17
|
keyring>=12.2.0
|
|
14
18
|
|
|
@@ -12,10 +12,10 @@ install_requirements = [
|
|
|
12
12
|
# We still need to use pt-2 unless pt-3 released on Fedora32
|
|
13
13
|
# see: https://github.com/dbcli/pgcli/pull/1197
|
|
14
14
|
"prompt_toolkit>=2.0.6,<4.0.0",
|
|
15
|
-
"psycopg >= 3.0.14",
|
|
16
|
-
"
|
|
15
|
+
"psycopg >= 3.0.14; sys_platform != 'win32'",
|
|
16
|
+
"psycopg-binary >= 3.0.14; sys_platform == 'win32'",
|
|
17
|
+
"sqlparse >=0.3.0,<0.6",
|
|
17
18
|
"configobj >= 5.0.6",
|
|
18
|
-
"pendulum>=2.1.0",
|
|
19
19
|
"cli_helpers[styles] >= 2.2.1",
|
|
20
20
|
]
|
|
21
21
|
|
|
@@ -27,11 +27,6 @@ install_requirements = [
|
|
|
27
27
|
if platform.system() != "Windows" and not platform.system().startswith("CYGWIN"):
|
|
28
28
|
install_requirements.append("setproctitle >= 1.1.9")
|
|
29
29
|
|
|
30
|
-
# Windows will require the binary psycopg to run pgcli
|
|
31
|
-
if platform.system() == "Windows":
|
|
32
|
-
install_requirements.append("psycopg-binary >= 3.0.14")
|
|
33
|
-
|
|
34
|
-
|
|
35
30
|
setup(
|
|
36
31
|
name="pgcli",
|
|
37
32
|
author="Pgcli Core Team",
|
|
@@ -9,6 +9,7 @@ from utils import (
|
|
|
9
9
|
db_connection,
|
|
10
10
|
drop_tables,
|
|
11
11
|
)
|
|
12
|
+
import pgcli.main
|
|
12
13
|
import pgcli.pgexecute
|
|
13
14
|
|
|
14
15
|
|
|
@@ -37,6 +38,7 @@ def executor(connection):
|
|
|
37
38
|
password=POSTGRES_PASSWORD,
|
|
38
39
|
port=POSTGRES_PORT,
|
|
39
40
|
dsn=None,
|
|
41
|
+
notify_callback=pgcli.main.notify_callback,
|
|
40
42
|
)
|
|
41
43
|
|
|
42
44
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
from click.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
from pgcli.main import cli
|
|
6
|
+
from pgcli.pgexecute import PGExecute
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_application_name_in_env():
|
|
10
|
+
runner = CliRunner()
|
|
11
|
+
app_name = "wonderful_app"
|
|
12
|
+
with patch.object(PGExecute, "__init__") as mock_pgxecute:
|
|
13
|
+
runner.invoke(
|
|
14
|
+
cli, ["127.0.0.1:5432/hello", "user"], env={"PGAPPNAME": app_name}
|
|
15
|
+
)
|
|
16
|
+
kwargs = mock_pgxecute.call_args.kwargs
|
|
17
|
+
assert kwargs.get("application_name") == app_name
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import platform
|
|
3
|
+
import re
|
|
4
|
+
import tempfile
|
|
5
|
+
import datetime
|
|
3
6
|
from unittest import mock
|
|
4
7
|
|
|
5
8
|
import pytest
|
|
@@ -11,7 +14,9 @@ except ImportError:
|
|
|
11
14
|
|
|
12
15
|
from pgcli.main import (
|
|
13
16
|
obfuscate_process_password,
|
|
17
|
+
duration_in_words,
|
|
14
18
|
format_output,
|
|
19
|
+
notify_callback,
|
|
15
20
|
PGCli,
|
|
16
21
|
OutputSettings,
|
|
17
22
|
COLOR_CODE_REGEX,
|
|
@@ -296,6 +301,24 @@ def test_i_works(tmpdir, executor):
|
|
|
296
301
|
run(executor, statement, pgspecial=cli.pgspecial)
|
|
297
302
|
|
|
298
303
|
|
|
304
|
+
@dbtest
|
|
305
|
+
def test_toggle_verbose_errors(executor):
|
|
306
|
+
cli = PGCli(pgexecute=executor)
|
|
307
|
+
|
|
308
|
+
cli._evaluate_command("\\v on")
|
|
309
|
+
assert cli.verbose_errors
|
|
310
|
+
output, _ = cli._evaluate_command("SELECT 1/0")
|
|
311
|
+
assert "SQLSTATE" in output[0]
|
|
312
|
+
|
|
313
|
+
cli._evaluate_command("\\v off")
|
|
314
|
+
assert not cli.verbose_errors
|
|
315
|
+
output, _ = cli._evaluate_command("SELECT 1/0")
|
|
316
|
+
assert "SQLSTATE" not in output[0]
|
|
317
|
+
|
|
318
|
+
cli._evaluate_command("\\v")
|
|
319
|
+
assert cli.verbose_errors
|
|
320
|
+
|
|
321
|
+
|
|
299
322
|
@dbtest
|
|
300
323
|
def test_echo_works(executor):
|
|
301
324
|
cli = PGCli(pgexecute=executor)
|
|
@@ -312,6 +335,34 @@ def test_qecho_works(executor):
|
|
|
312
335
|
assert result == ["asdf"]
|
|
313
336
|
|
|
314
337
|
|
|
338
|
+
@dbtest
|
|
339
|
+
def test_logfile_works(executor):
|
|
340
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
341
|
+
log_file = f"{tmpdir}/tempfile.log"
|
|
342
|
+
cli = PGCli(pgexecute=executor, log_file=log_file)
|
|
343
|
+
statement = r"\qecho hello!"
|
|
344
|
+
cli.execute_command(statement)
|
|
345
|
+
with open(log_file, "r") as f:
|
|
346
|
+
log_contents = f.readlines()
|
|
347
|
+
assert datetime.datetime.fromisoformat(log_contents[0].strip())
|
|
348
|
+
assert log_contents[1].strip() == r"\qecho hello!"
|
|
349
|
+
assert log_contents[2].strip() == "hello!"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@dbtest
|
|
353
|
+
def test_logfile_unwriteable_file(executor):
|
|
354
|
+
cli = PGCli(pgexecute=executor)
|
|
355
|
+
statement = r"\log-file forbidden.log"
|
|
356
|
+
with mock.patch("builtins.open") as mock_open:
|
|
357
|
+
mock_open.side_effect = PermissionError(
|
|
358
|
+
"[Errno 13] Permission denied: 'forbidden.log'"
|
|
359
|
+
)
|
|
360
|
+
result = run(executor, statement, pgspecial=cli.pgspecial)
|
|
361
|
+
assert result == [
|
|
362
|
+
"[Errno 13] Permission denied: 'forbidden.log'\nLogfile capture disabled"
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
|
|
315
366
|
@dbtest
|
|
316
367
|
def test_watch_works(executor):
|
|
317
368
|
cli = PGCli(pgexecute=executor)
|
|
@@ -431,6 +482,7 @@ def test_pg_service_file(tmpdir):
|
|
|
431
482
|
"b_host",
|
|
432
483
|
"5435",
|
|
433
484
|
"",
|
|
485
|
+
notify_callback,
|
|
434
486
|
application_name="pgcli",
|
|
435
487
|
)
|
|
436
488
|
del os.environ["PGPASSWORD"]
|
|
@@ -486,5 +538,50 @@ def test_application_name_db_uri(tmpdir):
|
|
|
486
538
|
cli = PGCli(pgclirc_file=str(tmpdir.join("rcfile")))
|
|
487
539
|
cli.connect_uri("postgres://bar@baz.com/?application_name=cow")
|
|
488
540
|
mock_pgexecute.assert_called_with(
|
|
489
|
-
"bar", "bar", "", "baz.com", "", "", application_name="cow"
|
|
541
|
+
"bar", "bar", "", "baz.com", "", "", notify_callback, application_name="cow"
|
|
490
542
|
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
@pytest.mark.parametrize(
|
|
546
|
+
"duration_in_seconds,words",
|
|
547
|
+
[
|
|
548
|
+
(0, "0 seconds"),
|
|
549
|
+
(0.0009, "0.001 second"),
|
|
550
|
+
(0.0005, "0.001 second"),
|
|
551
|
+
(0.0004, "0.0 second"), # not perfect, but will do
|
|
552
|
+
(0.2, "0.2 second"),
|
|
553
|
+
(1, "1 second"),
|
|
554
|
+
(1.4, "1 second"),
|
|
555
|
+
(2, "2 seconds"),
|
|
556
|
+
(3.4, "3 seconds"),
|
|
557
|
+
(60, "1 minute"),
|
|
558
|
+
(61, "1 minute 1 second"),
|
|
559
|
+
(123, "2 minutes 3 seconds"),
|
|
560
|
+
(3600, "1 hour"),
|
|
561
|
+
(7235, "2 hours 35 seconds"),
|
|
562
|
+
(9005, "2 hours 30 minutes 5 seconds"),
|
|
563
|
+
(86401, "24 hours 1 second"),
|
|
564
|
+
],
|
|
565
|
+
)
|
|
566
|
+
def test_duration_in_words(duration_in_seconds, words):
|
|
567
|
+
assert duration_in_words(duration_in_seconds) == words
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@dbtest
|
|
571
|
+
def test_notifications(executor):
|
|
572
|
+
run(executor, "listen chan1")
|
|
573
|
+
|
|
574
|
+
with mock.patch("pgcli.main.click.secho") as mock_secho:
|
|
575
|
+
run(executor, "notify chan1, 'testing1'")
|
|
576
|
+
mock_secho.assert_called()
|
|
577
|
+
arg = mock_secho.call_args_list[0].args[0]
|
|
578
|
+
assert re.match(
|
|
579
|
+
r'Notification received on channel "chan1" \(PID \d+\):\ntesting1',
|
|
580
|
+
arg,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
run(executor, "unlisten chan1")
|
|
584
|
+
|
|
585
|
+
with mock.patch("pgcli.main.click.secho") as mock_secho:
|
|
586
|
+
run(executor, "notify chan1, 'testing2'")
|
|
587
|
+
mock_secho.assert_not_called()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from textwrap import dedent
|
|
2
3
|
|
|
3
4
|
import psycopg
|
|
@@ -6,7 +7,7 @@ from unittest.mock import patch, MagicMock
|
|
|
6
7
|
from pgspecial.main import PGSpecial, NO_QUERY
|
|
7
8
|
from utils import run, dbtest, requires_json, requires_jsonb
|
|
8
9
|
|
|
9
|
-
from pgcli.main import PGCli
|
|
10
|
+
from pgcli.main import PGCli, exception_formatter as main_exception_formatter
|
|
10
11
|
from pgcli.packages.parseutils.meta import FunctionMetadata
|
|
11
12
|
|
|
12
13
|
|
|
@@ -219,8 +220,33 @@ def test_database_list(executor):
|
|
|
219
220
|
|
|
220
221
|
@dbtest
|
|
221
222
|
def test_invalid_syntax(executor, exception_formatter):
|
|
222
|
-
result = run(
|
|
223
|
+
result = run(
|
|
224
|
+
executor,
|
|
225
|
+
"invalid syntax!",
|
|
226
|
+
exception_formatter=lambda x: main_exception_formatter(x, verbose_errors=False),
|
|
227
|
+
)
|
|
223
228
|
assert 'syntax error at or near "invalid"' in result[0]
|
|
229
|
+
assert "SQLSTATE" not in result[0]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dbtest
|
|
233
|
+
def test_invalid_syntax_verbose(executor):
|
|
234
|
+
result = run(
|
|
235
|
+
executor,
|
|
236
|
+
"invalid syntax!",
|
|
237
|
+
exception_formatter=lambda x: main_exception_formatter(x, verbose_errors=True),
|
|
238
|
+
)
|
|
239
|
+
fields = r"""
|
|
240
|
+
Severity: ERROR
|
|
241
|
+
Severity \(non-localized\): ERROR
|
|
242
|
+
SQLSTATE code: 42601
|
|
243
|
+
Message: syntax error at or near "invalid"
|
|
244
|
+
Position: 1
|
|
245
|
+
File: scan\.l
|
|
246
|
+
Line: \d+
|
|
247
|
+
Routine: scanner_yyerror
|
|
248
|
+
""".strip()
|
|
249
|
+
assert re.search(fields, result[0])
|
|
224
250
|
|
|
225
251
|
|
|
226
252
|
@dbtest
|
|
@@ -690,6 +716,38 @@ def test_function_definition(executor):
|
|
|
690
716
|
result = executor.function_definition("the_number_three")
|
|
691
717
|
|
|
692
718
|
|
|
719
|
+
@dbtest
|
|
720
|
+
def test_function_notice_order(executor):
|
|
721
|
+
run(
|
|
722
|
+
executor,
|
|
723
|
+
"""
|
|
724
|
+
CREATE OR REPLACE FUNCTION demo_order() RETURNS VOID AS
|
|
725
|
+
$$
|
|
726
|
+
BEGIN
|
|
727
|
+
RAISE NOTICE 'first';
|
|
728
|
+
RAISE NOTICE 'second';
|
|
729
|
+
RAISE NOTICE 'third';
|
|
730
|
+
RAISE NOTICE 'fourth';
|
|
731
|
+
RAISE NOTICE 'fifth';
|
|
732
|
+
RAISE NOTICE 'sixth';
|
|
733
|
+
END;
|
|
734
|
+
$$
|
|
735
|
+
LANGUAGE plpgsql;
|
|
736
|
+
""",
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
executor.function_definition("demo_order")
|
|
740
|
+
|
|
741
|
+
result = run(executor, "select demo_order()")
|
|
742
|
+
assert "first\nsecond\nthird\nfourth\nfifth\nsixth" in result[0]
|
|
743
|
+
assert "+------------+" in result[1]
|
|
744
|
+
assert "| demo_order |" in result[2]
|
|
745
|
+
assert "|------------|" in result[3]
|
|
746
|
+
assert "| |" in result[4]
|
|
747
|
+
assert "+------------+" in result[5]
|
|
748
|
+
assert "SELECT 1" in result[6]
|
|
749
|
+
|
|
750
|
+
|
|
693
751
|
@dbtest
|
|
694
752
|
def test_view_definition(executor):
|
|
695
753
|
run(executor, "create table tbl1 (a text, b numeric)")
|
|
@@ -721,6 +779,10 @@ def test_short_host(executor):
|
|
|
721
779
|
executor, "host", "localhost1.example.org,localhost2.example.org"
|
|
722
780
|
):
|
|
723
781
|
assert executor.short_host == "localhost1"
|
|
782
|
+
with patch.object(executor, "host", "ec2-11-222-333-444.compute-1.amazonaws.com"):
|
|
783
|
+
assert executor.short_host == "ec2-11-222-333-444"
|
|
784
|
+
with patch.object(executor, "host", "1.2.3.4"):
|
|
785
|
+
assert executor.short_host == "1.2.3.4"
|
|
724
786
|
|
|
725
787
|
|
|
726
788
|
class VirtualCursor:
|
|
@@ -6,7 +6,7 @@ from configobj import ConfigObj
|
|
|
6
6
|
from click.testing import CliRunner
|
|
7
7
|
from sshtunnel import SSHTunnelForwarder
|
|
8
8
|
|
|
9
|
-
from pgcli.main import cli, PGCli
|
|
9
|
+
from pgcli.main import cli, notify_callback, PGCli
|
|
10
10
|
from pgcli.pgexecute import PGExecute
|
|
11
11
|
|
|
12
12
|
|
|
@@ -61,6 +61,7 @@ def test_ssh_tunnel(
|
|
|
61
61
|
"127.0.0.1",
|
|
62
62
|
pgcli.ssh_tunnel.local_bind_ports[0],
|
|
63
63
|
"",
|
|
64
|
+
notify_callback,
|
|
64
65
|
)
|
|
65
66
|
mock_ssh_tunnel_forwarder.reset_mock()
|
|
66
67
|
mock_pgexecute.reset_mock()
|
|
@@ -96,6 +97,7 @@ def test_ssh_tunnel(
|
|
|
96
97
|
"127.0.0.1",
|
|
97
98
|
pgcli.ssh_tunnel.local_bind_ports[0],
|
|
98
99
|
"",
|
|
100
|
+
notify_callback,
|
|
99
101
|
)
|
|
100
102
|
mock_ssh_tunnel_forwarder.reset_mock()
|
|
101
103
|
mock_pgexecute.reset_mock()
|
pgcli-4.0.1/pgcli/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "4.0.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|