1
0
Fork 0

Merging upstream version 4.3.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-03-24 15:47:19 +01:00
parent cc0be9576d
commit 7736adc9ff
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
10 changed files with 197 additions and 33 deletions

View file

@ -137,7 +137,10 @@ Contributors:
* Chris Rose (offbyone/offby1)
* Mathieu Dupuy (deronnax)
* Chris Novakovic
* Max Smolin (maximsmol)
* Josh Lynch (josh-lynch)
* Fabio (3ximus)
* Doug Harris (dougharris)
Creator:
--------

View file

@ -1,3 +1,10 @@
4.3.0 (2025-03-22)
==================
Features
--------
* The session time zone setting is set to the system time zone by default
4.2.0 (2025-03-06)
==================
@ -5,6 +12,8 @@ Features
--------
* Add a `--ping` command line option; allows pgcli to replace `pg_isready`
* Changed the packaging metadata from setup.py to pyproject.toml
* Add bash completion for services defined in the service file `~/.pg_service.conf`
* Added support for per-column date/time formatting using `column_date_formats` in config
Bug fixes:
----------

View file

@ -3,7 +3,7 @@ _pg_databases()
# -w was introduced in 8.4, https://launchpad.net/bugs/164772
# "Access privileges" in output may contain linefeeds, hence the NF > 1
COMPREPLY=( $( compgen -W "$( psql -AtqwlF $'\t' 2>/dev/null | \
awk 'NF > 1 { print $1 }' )" -- "$cur" ) )
awk 'NF > 1 { print $1 }' )" -- "$cur" ) )
}
_pg_users()
@ -14,6 +14,17 @@ _pg_users()
[[ ${#COMPREPLY[@]} -eq 0 ]] && COMPREPLY=( $( compgen -u -- "$cur" ) )
}
_pg_services()
{
# return list of available services
local services
if [[ -f "$HOME/.pg_service.conf" ]]; then
services=$(grep -oP '(?<=^\[).*?(?=\])' "$HOME/.pg_service.conf")
fi
local suffix="${cur#*=}"
COMPREPLY=( $(compgen -W "$services" -- "$suffix") )
}
_pgcli()
{
local cur prev words cword
@ -39,23 +50,27 @@ _pgcli()
esac
case "$cur" in
--*)
# return list of available options
COMPREPLY=( $( compgen -W '--host --port --user --password --no-password
--single-connection --version --dbname --pgclirc --dsn
--row-limit --help' -- "$cur" ) )
[[ $COMPREPLY == *= ]] && compopt -o nospace
return 0
;;
-)
# only complete long options
compopt -o nospace
COMPREPLY=( -- )
return 0
;;
*)
service=*)
_pg_services
return 0
;;
--*)
# return list of available options
COMPREPLY=( $( compgen -W '--host --port --user --password --no-password
--single-connection --version --dbname --pgclirc --dsn
--row-limit --help' -- "$cur" ) )
[[ $COMPREPLY == *= ]] && compopt -o nospace
return 0
;;
-)
# only complete long options
compopt -o nospace
COMPREPLY=( -- )
return 0
;;
*)
# return list of available databases
_pg_databases
_pg_databases
esac
} &&
complete -F _pgcli pgcli

View file

@ -1 +1 @@
__version__ = "4.2.0"
__version__ = "4.3.0"

View file

@ -1,3 +1,4 @@
from zoneinfo import ZoneInfoNotFoundError
from configobj import ConfigObj, ParseError
from pgspecial.namedqueries import NamedQueries
from .config import skip_initial_comment
@ -19,10 +20,15 @@ from time import time, sleep
from typing import Optional
from cli_helpers.tabular_output import TabularOutputFormatter
from cli_helpers.tabular_output.preprocessors import align_decimals, format_numbers
from cli_helpers.tabular_output.preprocessors import (
align_decimals,
format_numbers,
format_timestamps,
)
from cli_helpers.utils import strip_ansi
from .explain_output_formatter import ExplainOutputFormatter
import click
import tzlocal
try:
import setproctitle
@ -111,12 +117,13 @@ MetaQuery.__new__.__defaults__ = ("", False, 0, 0, False, False, False, False)
OutputSettings = namedtuple(
"OutputSettings",
"table_format dcmlfmt floatfmt missingval expanded max_width case_function style_output max_field_width",
"table_format dcmlfmt floatfmt column_date_formats missingval expanded max_width case_function style_output max_field_width",
)
OutputSettings.__new__.__defaults__ = (
None,
None,
None,
None,
"<null>",
False,
None,
@ -264,6 +271,7 @@ class PGCli:
self.on_error = c["main"]["on_error"].upper()
self.decimal_format = c["data_formats"]["decimal"]
self.float_format = c["data_formats"]["float"]
self.column_date_formats = c["column_date_formats"]
auth.keyring_initialize(c["main"].as_bool("keyring"), logger=self.logger)
self.show_bottom_toolbar = c["main"].as_bool("show_bottom_toolbar")
@ -1179,6 +1187,7 @@ class PGCli:
table_format=self.table_format,
dcmlfmt=self.decimal_format,
floatfmt=self.float_format,
column_date_formats=self.column_date_formats,
missingval=self.null_string,
expanded=expanded,
max_width=max_width,
@ -1593,9 +1602,9 @@ def cli(
if list_databases or ping_database:
database = "postgres"
cfg = load_config(pgclirc, config_full_path)
if dsn != "":
try:
cfg = load_config(pgclirc, config_full_path)
dsn_config = cfg["alias_dsn"][dsn]
except KeyError:
click.secho(
@ -1624,6 +1633,55 @@ def cli(
else:
pgcli.connect(database, host, user, port)
if "use_local_timezone" not in cfg["main"] or cfg["main"].as_bool(
"use_local_timezone"
):
server_tz = pgcli.pgexecute.get_timezone()
def echo_error(msg: str):
click.secho(
"Failed to determine the local time zone",
err=True,
fg="yellow",
)
click.secho(
msg,
err=True,
fg="yellow",
)
click.secho(
f"Continuing with the default time zone as preset by the server ({server_tz})",
err=True,
fg="yellow",
)
click.secho(
"Set `use_local_timezone = False` in the config to avoid trying to override the server time zone\n",
err=True,
dim=True,
)
local_tz = None
try:
local_tz = tzlocal.get_localzone_name()
if local_tz is None:
echo_error("No local time zone configuration found\n")
else:
click.secho(
f"Using local time zone {local_tz} (server uses {server_tz})",
fg="green",
)
click.secho(
"Use `set time zone <TZ>` to override, or set `use_local_timezone = False` in the config",
dim=True,
)
pgcli.pgexecute.set_timezone(local_tz)
except ZoneInfoNotFoundError as e:
# e.args[0] is the pre-formatted message which includes a list
# of conflicting sources
echo_error(e.args[0])
if list_databases:
cur, headers, status = pgcli.pgexecute.full_databases()
@ -1830,6 +1888,7 @@ def format_output(title, cur, headers, status, settings, explain_mode=False):
"missing_value": settings.missingval,
"integer_format": settings.dcmlfmt,
"float_format": settings.floatfmt,
"column_date_formats": settings.column_date_formats,
"preprocessors": (format_numbers, format_arrays),
"disable_numparse": True,
"preserve_whitespace": True,
@ -1839,6 +1898,9 @@ def format_output(title, cur, headers, status, settings, explain_mode=False):
if not settings.floatfmt:
output_kwargs["preprocessors"] = (align_decimals,)
if settings.column_date_formats:
output_kwargs["preprocessors"] += (format_timestamps,)
if table_format == "csv":
# The default CSV dialect is "excel" which is not handling newline values correctly
# Nevertheless, we want to keep on using "excel" on Windows since it uses '\r\n'

View file

@ -191,6 +191,10 @@ enable_pager = True
# Use keyring to automatically save and load password in a secure manner
keyring = True
# Automatically set the session time zone to the local time zone
# If unset, uses the server's time zone, which is the Postgres default
use_local_timezone = True
# Custom colors for the completion menu, toolbar, etc.
[colors]
completion-menu.completion.current = 'bg:#ffffff #000000'
@ -240,3 +244,8 @@ output.null = "#808080"
[data_formats]
decimal = ""
float = ""
# Per column formats for date/timestamp columns
[column_date_formats]
# use strftime format, e.g.
# created = "%Y-%m-%d"

View file

@ -881,3 +881,16 @@ class PGExecute:
def explain_prefix(self):
return "EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) "
def get_timezone(self) -> str:
query = psycopg.sql.SQL("show time zone")
with self.conn.cursor() as cur:
cur.execute(query)
return cur.fetchone()[0]
def set_timezone(self, timezone: str):
query = psycopg.sql.SQL("set time zone {}").format(
psycopg.sql.Identifier(timezone)
)
with self.conn.cursor() as cur:
cur.execute(query)

View file

@ -34,12 +34,13 @@ dependencies = [
"psycopg-binary >= 3.0.14; sys_platform == 'win32'",
"sqlparse >=0.3.0,<0.6",
"configobj >= 5.0.6",
"cli_helpers[styles] >= 2.2.1",
"cli_helpers[styles] >= 2.4.0",
# setproctitle is used to mask the password when running `ps` in command line.
# But this is not necessary in Windows since the password is never shown in the
# task manager. Also setproctitle is a hard dependency to install in Windows,
# so we'll only install it if we're not in Windows.
"setproctitle >= 1.1.9; sys_platform != 'win32' and 'CYGWIN' not in sys_platform",
"tzlocal >= 5.2",
]
dynamic = ["version"]

View file

@ -2,10 +2,10 @@
"""A script to publish a release of pgcli to PyPI."""
import io
from optparse import OptionParser
import re
import subprocess
import sys
from optparse import OptionParser
import click
@ -66,7 +66,8 @@ def create_git_tag(tag_name):
def create_distribution_files():
run_step("python", "setup.py", "clean", "--all", "sdist", "bdist_wheel")
run_step("rm", "-rf", "dist/")
run_step("python", "-m", "build")
def upload_distribution_files():
@ -91,11 +92,11 @@ if __name__ == "__main__":
if DEBUG:
subprocess.check_output = lambda x: x
checks = [
"Have you updated the AUTHORS file?",
"Have you updated the `Usage` section of the README?",
]
checklist(checks)
# checks = [
# "Have you updated the AUTHORS file?",
# "Have you updated the `Usage` section of the README?",
# ]
# checklist(checks)
ver = version("pgcli/__init__.py")
print("Releasing Version:", ver)

View file

@ -76,6 +76,57 @@ def test_format_output():
assert list(results) == expected
def test_column_date_formats():
settings = OutputSettings(
table_format="psql",
column_date_formats={
"date_col": "%Y-%m-%d",
"datetime_col": "%I:%M:%S %m/%d/%y",
},
)
data = [
("name1", "2024-12-13T18:32:22", "2024-12-13T19:32:22", "2024-12-13T20:32:22"),
("name2", "2025-02-13T02:32:22", "2025-02-13T02:32:22", "2025-02-13T02:32:22"),
]
headers = ["name", "date_col", "datetime_col", "unchanged_col"]
results = format_output("Title", data, headers, "test status", settings)
expected = [
"Title",
"+-------+------------+-------------------+---------------------+",
"| name | date_col | datetime_col | unchanged_col |",
"|-------+------------+-------------------+---------------------|",
"| name1 | 2024-12-13 | 07:32:22 12/13/24 | 2024-12-13T20:32:22 |",
"| name2 | 2025-02-13 | 02:32:22 02/13/25 | 2025-02-13T02:32:22 |",
"+-------+------------+-------------------+---------------------+",
"test status",
]
assert list(results) == expected
def test_no_column_date_formats():
"""Test that not setting any column date formats returns unaltered datetime columns"""
settings = OutputSettings(table_format="psql")
data = [
("name1", "2024-12-13T18:32:22", "2024-12-13T19:32:22", "2024-12-13T20:32:22"),
("name2", "2025-02-13T02:32:22", "2025-02-13T02:32:22", "2025-02-13T02:32:22"),
]
headers = ["name", "date_col", "datetime_col", "unchanged_col"]
results = format_output("Title", data, headers, "test status", settings)
expected = [
"Title",
"+-------+---------------------+---------------------+---------------------+",
"| name | date_col | datetime_col | unchanged_col |",
"|-------+---------------------+---------------------+---------------------|",
"| name1 | 2024-12-13T18:32:22 | 2024-12-13T19:32:22 | 2024-12-13T20:32:22 |",
"| name2 | 2025-02-13T02:32:22 | 2025-02-13T02:32:22 | 2025-02-13T02:32:22 |",
"+-------+---------------------+---------------------+---------------------+",
"test status",
]
assert list(results) == expected
def test_format_output_truncate_on():
settings = OutputSettings(
table_format="psql", dcmlfmt="d", floatfmt="g", max_field_width=10