Merging upstream version 4.3.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
cc0be9576d
commit
7736adc9ff
10 changed files with 197 additions and 33 deletions
3
AUTHORS
3
AUTHORS
|
@ -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:
|
||||
--------
|
||||
|
|
|
@ -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:
|
||||
----------
|
||||
|
|
|
@ -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,6 +50,10 @@ _pgcli()
|
|||
esac
|
||||
|
||||
case "$cur" in
|
||||
service=*)
|
||||
_pg_services
|
||||
return 0
|
||||
;;
|
||||
--*)
|
||||
# return list of available options
|
||||
COMPREPLY=( $( compgen -W '--host --port --user --password --no-password
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = "4.2.0"
|
||||
__version__ = "4.3.0"
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
15
release.py
15
release.py
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue