pgcli 4.0.0__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.
Files changed (99) hide show
  1. {pgcli-4.0.0 → pgcli-4.1.0}/AUTHORS +4 -0
  2. {pgcli-4.0.0/pgcli.egg-info → pgcli-4.1.0}/PKG-INFO +4 -4
  3. {pgcli-4.0.0 → pgcli-4.1.0}/changelog.rst +30 -1
  4. pgcli-4.1.0/pgcli/__init__.py +1 -0
  5. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/main.py +213 -11
  6. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/pgclirc +10 -4
  7. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/pgexecute.py +16 -1
  8. {pgcli-4.0.0 → pgcli-4.1.0/pgcli.egg-info}/PKG-INFO +4 -4
  9. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli.egg-info/SOURCES.txt +1 -0
  10. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli.egg-info/requires.txt +7 -3
  11. {pgcli-4.0.0 → pgcli-4.1.0}/setup.py +3 -8
  12. {pgcli-4.0.0 → pgcli-4.1.0}/tests/conftest.py +2 -0
  13. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/steps/crud_database.py +1 -0
  14. pgcli-4.1.0/tests/test_application_name.py +17 -0
  15. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_main.py +98 -1
  16. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_pgexecute.py +64 -2
  17. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_ssh_tunnel.py +3 -1
  18. pgcli-4.0.0/pgcli/__init__.py +0 -1
  19. {pgcli-4.0.0 → pgcli-4.1.0}/LICENSE.txt +0 -0
  20. {pgcli-4.0.0 → pgcli-4.1.0}/MANIFEST.in +0 -0
  21. {pgcli-4.0.0 → pgcli-4.1.0}/README.rst +0 -0
  22. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/__main__.py +0 -0
  23. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/auth.py +0 -0
  24. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/completion_refresher.py +0 -0
  25. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/config.py +0 -0
  26. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/explain_output_formatter.py +0 -0
  27. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/key_bindings.py +0 -0
  28. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/magic.py +0 -0
  29. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/__init__.py +0 -0
  30. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/formatter/__init__.py +0 -0
  31. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/formatter/sqlformatter.py +0 -0
  32. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/parseutils/__init__.py +0 -0
  33. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/parseutils/ctes.py +0 -0
  34. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/parseutils/meta.py +0 -0
  35. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/parseutils/tables.py +0 -0
  36. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/parseutils/utils.py +0 -0
  37. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/pgliterals/__init__.py +0 -0
  38. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/pgliterals/main.py +0 -0
  39. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/pgliterals/pgliterals.json +0 -0
  40. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/prioritization.py +0 -0
  41. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/prompt_utils.py +0 -0
  42. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/packages/sqlcompletion.py +0 -0
  43. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/pgbuffer.py +0 -0
  44. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/pgcompleter.py +0 -0
  45. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/pgstyle.py +0 -0
  46. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/pgtoolbar.py +0 -0
  47. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli/pyev.py +0 -0
  48. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli.egg-info/dependency_links.txt +0 -0
  49. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli.egg-info/entry_points.txt +0 -0
  50. {pgcli-4.0.0 → pgcli-4.1.0}/pgcli.egg-info/top_level.txt +0 -0
  51. {pgcli-4.0.0 → pgcli-4.1.0}/pyproject.toml +0 -0
  52. {pgcli-4.0.0 → pgcli-4.1.0}/setup.cfg +0 -0
  53. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/__init__.py +0 -0
  54. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/auto_vertical.feature +0 -0
  55. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/basic_commands.feature +0 -0
  56. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/crud_database.feature +0 -0
  57. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/crud_table.feature +0 -0
  58. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/db_utils.py +0 -0
  59. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/environment.py +0 -0
  60. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/expanded.feature +0 -0
  61. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/fixture_data/help.txt +0 -0
  62. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/fixture_data/help_commands.txt +0 -0
  63. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/fixture_utils.py +0 -0
  64. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/iocommands.feature +0 -0
  65. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/named_queries.feature +0 -0
  66. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/pgbouncer.feature +0 -0
  67. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/specials.feature +0 -0
  68. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/steps/__init__.py +0 -0
  69. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/steps/auto_vertical.py +0 -0
  70. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/steps/basic_commands.py +0 -0
  71. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/steps/crud_table.py +0 -0
  72. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/steps/expanded.py +0 -0
  73. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/steps/iocommands.py +0 -0
  74. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/steps/named_queries.py +0 -0
  75. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/steps/pgbouncer.py +0 -0
  76. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/steps/specials.py +0 -0
  77. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/steps/wrappers.py +0 -0
  78. {pgcli-4.0.0 → pgcli-4.1.0}/tests/features/wrappager.py +0 -0
  79. {pgcli-4.0.0 → pgcli-4.1.0}/tests/formatter/__init__.py +0 -0
  80. {pgcli-4.0.0 → pgcli-4.1.0}/tests/formatter/test_sqlformatter.py +0 -0
  81. {pgcli-4.0.0 → pgcli-4.1.0}/tests/metadata.py +0 -0
  82. {pgcli-4.0.0 → pgcli-4.1.0}/tests/parseutils/test_ctes.py +0 -0
  83. {pgcli-4.0.0 → pgcli-4.1.0}/tests/parseutils/test_function_metadata.py +0 -0
  84. {pgcli-4.0.0 → pgcli-4.1.0}/tests/parseutils/test_parseutils.py +0 -0
  85. {pgcli-4.0.0 → pgcli-4.1.0}/tests/pytest.ini +0 -0
  86. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_auth.py +0 -0
  87. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_completion_refresher.py +0 -0
  88. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_config.py +0 -0
  89. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_fuzzy_completion.py +0 -0
  90. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_naive_completion.py +0 -0
  91. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_pgcompleter.py +0 -0
  92. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_pgspecial.py +0 -0
  93. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_prioritization.py +0 -0
  94. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_prompt_utils.py +0 -0
  95. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_rowlimit.py +0 -0
  96. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_smart_completion_multiple_schemata.py +0 -0
  97. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_smart_completion_public_schema_only.py +0 -0
  98. {pgcli-4.0.0 → pgcli-4.1.0}/tests/test_sqlcompletion.py +0 -0
  99. {pgcli-4.0.0 → pgcli-4.1.0}/tests/utils.py +0 -0
@@ -130,6 +130,10 @@ 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
133
137
 
134
138
  Creator:
135
139
  --------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pgcli
3
- Version: 4.0.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: sqlparse<0.5,>=0.3.0
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>=3.0.0b1
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,34 @@
1
+ 4.1.0 (2024-03-09)
1
2
  ==================
2
- 4.0.0 (2023-11-27)
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)
24
+ ==================
25
+
26
+ Internal:
27
+ ---------
28
+ * Allow stable version of pendulum.
29
+
30
+ ==================
31
+ 4.0.0 (2023-10-27)
3
32
  ==================
4
33
 
5
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", "pgcli")
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(database, user, passwd, host, port, dsn, **kwargs)
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, user, passwd, host, port, dsn, **kwargs
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
- pendulum.Duration(seconds=query.total_time).in_words(),
901
+ duration_in_words(query.total_time),
801
902
  query.execution_time,
802
- pendulum.Duration(seconds=query.execution_time).in_words(),
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 exception_formatter(e):
1587
- return click.style(str(e), fg="red")
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
- # Destructive warning can restart the connection if this is enabled and the
37
- # user declines. This means that any current uncommitted transaction can be
38
- # aborted if the user doesn't want to proceed with a destructive_warning
39
- # statement.
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"{n.message_primary}\n{n.message_detail}\n{title}"
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.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: sqlparse<0.5,>=0.3.0
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>=3.0.0b1
35
35
  Requires-Dist: cli_helpers[styles]>=2.2.1
36
36
  Requires-Dist: setproctitle>=1.1.9
37
37
  Provides-Extra: keyring
@@ -44,6 +44,7 @@ pgcli/packages/pgliterals/pgliterals.json
44
44
  tests/conftest.py
45
45
  tests/metadata.py
46
46
  tests/pytest.ini
47
+ tests/test_application_name.py
47
48
  tests/test_auth.py
48
49
  tests/test_completion_refresher.py
49
50
  tests/test_config.py
@@ -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
- psycopg>=3.0.14
6
- sqlparse<0.5,>=0.3.0
5
+ sqlparse<0.6,>=0.3.0
7
6
  configobj>=5.0.6
8
- pendulum>=3.0.0b1
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
- "sqlparse >=0.3.0,<0.5",
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>=3.0.0b1",
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
 
@@ -3,6 +3,7 @@ Steps for behavioral style tests are defined in this module.
3
3
  Each step is defined by the string decorating it.
4
4
  This string is used to call the step in "*.feature" file.
5
5
  """
6
+
6
7
  import pexpect
7
8
 
8
9
  from behave import when, then
@@ -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(executor, "invalid syntax!", exception_formatter=exception_formatter)
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()
@@ -1 +0,0 @@
1
- __version__ = "4.0.0"
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