1
0
Fork 0

Adding 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:15 +01:00
parent 9442fe97a5
commit e1417b2235
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) * Chris Rose (offbyone/offby1)
* Mathieu Dupuy (deronnax) * Mathieu Dupuy (deronnax)
* Chris Novakovic * Chris Novakovic
* Max Smolin (maximsmol)
* Josh Lynch (josh-lynch) * Josh Lynch (josh-lynch)
* Fabio (3ximus)
* Doug Harris (dougharris)
Creator: 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) 4.2.0 (2025-03-06)
================== ==================
@ -5,6 +12,8 @@ Features
-------- --------
* Add a `--ping` command line option; allows pgcli to replace `pg_isready` * Add a `--ping` command line option; allows pgcli to replace `pg_isready`
* Changed the packaging metadata from setup.py to pyproject.toml * 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: Bug fixes:
---------- ----------

View file

@ -14,6 +14,17 @@ _pg_users()
[[ ${#COMPREPLY[@]} -eq 0 ]] && COMPREPLY=( $( compgen -u -- "$cur" ) ) [[ ${#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() _pgcli()
{ {
local cur prev words cword local cur prev words cword
@ -39,6 +50,10 @@ _pgcli()
esac esac
case "$cur" in case "$cur" in
service=*)
_pg_services
return 0
;;
--*) --*)
# return list of available options # return list of available options
COMPREPLY=( $( compgen -W '--host --port --user --password --no-password COMPREPLY=( $( compgen -W '--host --port --user --password --no-password

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 configobj import ConfigObj, ParseError
from pgspecial.namedqueries import NamedQueries from pgspecial.namedqueries import NamedQueries
from .config import skip_initial_comment from .config import skip_initial_comment
@ -19,10 +20,15 @@ from time import time, sleep
from typing import Optional from typing import Optional
from cli_helpers.tabular_output import TabularOutputFormatter 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 cli_helpers.utils import strip_ansi
from .explain_output_formatter import ExplainOutputFormatter from .explain_output_formatter import ExplainOutputFormatter
import click import click
import tzlocal
try: try:
import setproctitle import setproctitle
@ -111,12 +117,13 @@ MetaQuery.__new__.__defaults__ = ("", False, 0, 0, False, False, False, False)
OutputSettings = namedtuple( OutputSettings = namedtuple(
"OutputSettings", "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__ = ( OutputSettings.__new__.__defaults__ = (
None, None,
None, None,
None, None,
None,
"<null>", "<null>",
False, False,
None, None,
@ -264,6 +271,7 @@ class PGCli:
self.on_error = c["main"]["on_error"].upper() self.on_error = c["main"]["on_error"].upper()
self.decimal_format = c["data_formats"]["decimal"] self.decimal_format = c["data_formats"]["decimal"]
self.float_format = c["data_formats"]["float"] 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) auth.keyring_initialize(c["main"].as_bool("keyring"), logger=self.logger)
self.show_bottom_toolbar = c["main"].as_bool("show_bottom_toolbar") self.show_bottom_toolbar = c["main"].as_bool("show_bottom_toolbar")
@ -1179,6 +1187,7 @@ class PGCli:
table_format=self.table_format, table_format=self.table_format,
dcmlfmt=self.decimal_format, dcmlfmt=self.decimal_format,
floatfmt=self.float_format, floatfmt=self.float_format,
column_date_formats=self.column_date_formats,
missingval=self.null_string, missingval=self.null_string,
expanded=expanded, expanded=expanded,
max_width=max_width, max_width=max_width,
@ -1593,9 +1602,9 @@ def cli(
if list_databases or ping_database: if list_databases or ping_database:
database = "postgres" database = "postgres"
cfg = load_config(pgclirc, config_full_path)
if dsn != "": if dsn != "":
try: try:
cfg = load_config(pgclirc, config_full_path)
dsn_config = cfg["alias_dsn"][dsn] dsn_config = cfg["alias_dsn"][dsn]
except KeyError: except KeyError:
click.secho( click.secho(
@ -1624,6 +1633,55 @@ def cli(
else: else:
pgcli.connect(database, host, user, port) 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: if list_databases:
cur, headers, status = pgcli.pgexecute.full_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, "missing_value": settings.missingval,
"integer_format": settings.dcmlfmt, "integer_format": settings.dcmlfmt,
"float_format": settings.floatfmt, "float_format": settings.floatfmt,
"column_date_formats": settings.column_date_formats,
"preprocessors": (format_numbers, format_arrays), "preprocessors": (format_numbers, format_arrays),
"disable_numparse": True, "disable_numparse": True,
"preserve_whitespace": True, "preserve_whitespace": True,
@ -1839,6 +1898,9 @@ def format_output(title, cur, headers, status, settings, explain_mode=False):
if not settings.floatfmt: if not settings.floatfmt:
output_kwargs["preprocessors"] = (align_decimals,) output_kwargs["preprocessors"] = (align_decimals,)
if settings.column_date_formats:
output_kwargs["preprocessors"] += (format_timestamps,)
if table_format == "csv": if table_format == "csv":
# The default CSV dialect is "excel" which is not handling newline values correctly # 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' # 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 # Use keyring to automatically save and load password in a secure manner
keyring = True 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. # Custom colors for the completion menu, toolbar, etc.
[colors] [colors]
completion-menu.completion.current = 'bg:#ffffff #000000' completion-menu.completion.current = 'bg:#ffffff #000000'
@ -240,3 +244,8 @@ output.null = "#808080"
[data_formats] [data_formats]
decimal = "" decimal = ""
float = "" 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): def explain_prefix(self):
return "EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) " 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'", "psycopg-binary >= 3.0.14; sys_platform == 'win32'",
"sqlparse >=0.3.0,<0.6", "sqlparse >=0.3.0,<0.6",
"configobj >= 5.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. # 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 # 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, # task manager. Also setproctitle is a hard dependency to install in Windows,
# so we'll only install it if we're not 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", "setproctitle >= 1.1.9; sys_platform != 'win32' and 'CYGWIN' not in sys_platform",
"tzlocal >= 5.2",
] ]
dynamic = ["version"] dynamic = ["version"]

View file

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

View file

@ -76,6 +76,57 @@ def test_format_output():
assert list(results) == expected 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(): def test_format_output_truncate_on():
settings = OutputSettings( settings = OutputSettings(
table_format="psql", dcmlfmt="d", floatfmt="g", max_field_width=10 table_format="psql", dcmlfmt="d", floatfmt="g", max_field_width=10