2021-05-19 18:07:42 +02:00
|
|
|
""" PostgreSQL client """
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
import logging
|
|
|
|
import sys
|
|
|
|
|
|
|
|
import psycopg2
|
2023-12-15 12:12:48 +01:00
|
|
|
from psycopg2.extras import RealDictCursor
|
2021-05-19 18:07:42 +02:00
|
|
|
|
2023-01-07 02:19:18 +01:00
|
|
|
from mylib.db import DB, DBFailToConnect
|
2021-10-06 21:33:25 +02:00
|
|
|
|
2023-01-07 02:19:18 +01:00
|
|
|
log = logging.getLogger(__name__)
|
2021-11-18 12:25:17 +01:00
|
|
|
|
|
|
|
|
2023-01-07 02:19:18 +01:00
|
|
|
class PgDB(DB):
|
2023-01-16 12:56:12 +01:00
|
|
|
"""PostgreSQL client"""
|
2021-05-19 18:07:42 +02:00
|
|
|
|
2023-01-07 02:19:18 +01:00
|
|
|
_host = None
|
|
|
|
_user = None
|
|
|
|
_pwd = None
|
|
|
|
_db = None
|
|
|
|
|
2023-01-16 12:56:12 +01:00
|
|
|
date_format = "%Y-%m-%d"
|
|
|
|
datetime_format = "%Y-%m-%d %H:%M:%S"
|
2021-05-19 18:07:42 +02:00
|
|
|
|
2023-01-07 02:19:18 +01:00
|
|
|
def __init__(self, host, user, pwd, db, **kwargs):
|
2021-11-07 21:37:18 +01:00
|
|
|
self._host = host
|
|
|
|
self._user = user
|
|
|
|
self._pwd = pwd
|
|
|
|
self._db = db
|
2023-01-07 02:19:18 +01:00
|
|
|
super().__init__(**kwargs)
|
2021-05-19 18:07:42 +02:00
|
|
|
|
2021-11-23 13:08:24 +01:00
|
|
|
def connect(self, exit_on_error=True):
|
2023-01-16 12:56:12 +01:00
|
|
|
"""Connect to PostgreSQL server"""
|
2021-11-07 21:37:18 +01:00
|
|
|
if self._conn is None:
|
2021-05-19 18:07:42 +02:00
|
|
|
try:
|
2021-11-18 19:56:18 +01:00
|
|
|
log.info(
|
2023-01-16 12:56:12 +01:00
|
|
|
"Connect on PostgreSQL server %s as %s on database %s",
|
|
|
|
self._host,
|
|
|
|
self._user,
|
|
|
|
self._db,
|
|
|
|
)
|
2021-11-07 21:37:18 +01:00
|
|
|
self._conn = psycopg2.connect(
|
2023-01-16 12:56:12 +01:00
|
|
|
dbname=self._db, user=self._user, host=self._host, password=self._pwd
|
2021-05-19 19:19:57 +02:00
|
|
|
)
|
2023-01-06 19:36:14 +01:00
|
|
|
except psycopg2.Error as err:
|
2021-11-07 21:37:18 +01:00
|
|
|
log.fatal(
|
2023-01-16 12:56:12 +01:00
|
|
|
"An error occured during Postgresql database connection (%s@%s, database=%s).",
|
|
|
|
self._user,
|
|
|
|
self._host,
|
|
|
|
self._db,
|
|
|
|
exc_info=1,
|
2021-07-12 12:18:29 +02:00
|
|
|
)
|
2021-11-23 13:08:24 +01:00
|
|
|
if exit_on_error:
|
|
|
|
sys.exit(1)
|
|
|
|
else:
|
2023-01-16 12:56:12 +01:00
|
|
|
raise DBFailToConnect(f"{self._user}@{self._host}:{self._db}") from err
|
2021-07-12 12:18:29 +02:00
|
|
|
return True
|
2021-05-19 18:07:42 +02:00
|
|
|
|
|
|
|
def close(self):
|
2023-01-16 12:56:12 +01:00
|
|
|
"""Close connection with PostgreSQL server (if opened)"""
|
2021-11-07 21:37:18 +01:00
|
|
|
if self._conn:
|
|
|
|
self._conn.close()
|
|
|
|
self._conn = None
|
2021-05-19 18:07:42 +02:00
|
|
|
|
|
|
|
def setEncoding(self, enc):
|
2023-01-16 12:56:12 +01:00
|
|
|
"""Set connection encoding"""
|
2021-11-07 21:37:18 +01:00
|
|
|
if self._conn:
|
2021-05-19 18:07:42 +02:00
|
|
|
try:
|
2021-11-07 21:37:18 +01:00
|
|
|
self._conn.set_client_encoding(enc)
|
2021-05-19 18:07:42 +02:00
|
|
|
return True
|
2023-01-06 19:36:14 +01:00
|
|
|
except psycopg2.Error:
|
2021-10-06 21:33:25 +02:00
|
|
|
log.error(
|
|
|
|
'An error occured setting Postgresql database connection encoding to "%s"',
|
2023-01-16 12:56:12 +01:00
|
|
|
enc,
|
|
|
|
exc_info=1,
|
2021-10-06 21:33:25 +02:00
|
|
|
)
|
2021-05-19 18:07:42 +02:00
|
|
|
return False
|
|
|
|
|
|
|
|
def doSQL(self, sql, params=None):
|
2021-10-06 21:33:25 +02:00
|
|
|
"""
|
|
|
|
Run SQL query and commit changes (rollback on error)
|
|
|
|
|
|
|
|
:param sql: The SQL query
|
|
|
|
:param params: The SQL query's parameters as dict (optional)
|
|
|
|
|
|
|
|
:return: True on success, False otherwise
|
|
|
|
:rtype: bool
|
|
|
|
"""
|
2021-05-19 18:07:42 +02:00
|
|
|
if self.just_try:
|
2021-10-06 21:33:25 +02:00
|
|
|
log.debug("Just-try mode : do not really execute SQL query '%s'", sql)
|
2021-05-19 18:07:42 +02:00
|
|
|
return True
|
|
|
|
|
2021-11-07 21:37:18 +01:00
|
|
|
cursor = self._conn.cursor()
|
2021-05-19 18:07:42 +02:00
|
|
|
try:
|
2023-01-06 19:36:14 +01:00
|
|
|
self._log_query(sql, params)
|
2021-05-19 18:07:42 +02:00
|
|
|
if params is None:
|
|
|
|
cursor.execute(sql)
|
|
|
|
else:
|
|
|
|
cursor.execute(sql, params)
|
2021-11-07 21:37:18 +01:00
|
|
|
self._conn.commit()
|
2021-05-19 18:07:42 +02:00
|
|
|
return True
|
2023-01-06 19:36:14 +01:00
|
|
|
except psycopg2.Error:
|
|
|
|
self._log_query_exception(sql, params)
|
2021-11-07 21:37:18 +01:00
|
|
|
self._conn.rollback()
|
2021-05-19 18:07:42 +02:00
|
|
|
return False
|
|
|
|
|
2021-10-06 21:33:25 +02:00
|
|
|
def doSelect(self, sql, params=None):
|
|
|
|
"""
|
|
|
|
Run SELECT SQL query and return list of selected rows as dict
|
|
|
|
|
|
|
|
:param sql: The SQL query
|
|
|
|
:param params: The SQL query's parameters as dict (optional)
|
|
|
|
|
|
|
|
:return: List of selected rows as dict on success, False otherwise
|
|
|
|
:rtype: list, bool
|
|
|
|
"""
|
2023-12-15 12:12:48 +01:00
|
|
|
cursor = self._conn.cursor(cursor_factory=RealDictCursor)
|
2021-05-19 18:07:42 +02:00
|
|
|
try:
|
2023-01-06 19:36:14 +01:00
|
|
|
self._log_query(sql, params)
|
2021-07-12 12:18:38 +02:00
|
|
|
cursor.execute(sql, params)
|
2021-05-19 18:07:42 +02:00
|
|
|
results = cursor.fetchall()
|
2023-12-15 12:12:48 +01:00
|
|
|
return list(map(dict, results))
|
2023-01-06 19:36:14 +01:00
|
|
|
except psycopg2.Error:
|
|
|
|
self._log_query_exception(sql, params)
|
2021-10-06 21:33:25 +02:00
|
|
|
return False
|
|
|
|
|
|
|
|
#
|
|
|
|
# Depreated helpers
|
|
|
|
#
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _quote_value(cls, value):
|
2023-01-16 12:56:12 +01:00
|
|
|
"""Quote a value for SQL query"""
|
2021-10-06 21:33:25 +02:00
|
|
|
if value is None:
|
2023-01-16 12:56:12 +01:00
|
|
|
return "NULL"
|
2021-10-06 21:33:25 +02:00
|
|
|
|
|
|
|
if isinstance(value, (int, float)):
|
|
|
|
return str(value)
|
|
|
|
|
|
|
|
if isinstance(value, datetime.datetime):
|
|
|
|
value = cls._format_datetime(value)
|
|
|
|
elif isinstance(value, datetime.date):
|
|
|
|
value = cls._format_date(value)
|
|
|
|
|
2023-01-06 19:36:14 +01:00
|
|
|
# pylint: disable=consider-using-f-string
|
2023-01-16 12:56:12 +01:00
|
|
|
return "'{}'".format(value.replace("'", "''"))
|
2021-10-06 21:33:25 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _format_datetime(cls, value):
|
2023-01-16 12:56:12 +01:00
|
|
|
"""Format datetime object as string"""
|
2021-10-06 21:33:25 +02:00
|
|
|
assert isinstance(value, datetime.datetime)
|
|
|
|
return value.strftime(cls.datetime_format)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _format_date(cls, value):
|
2023-01-16 12:56:12 +01:00
|
|
|
"""Format date object as string"""
|
2021-10-06 21:33:25 +02:00
|
|
|
assert isinstance(value, (datetime.date, datetime.datetime))
|
|
|
|
return value.strftime(cls.date_format)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def time2datetime(cls, time):
|
2023-01-16 12:56:12 +01:00
|
|
|
"""Convert timestamp to datetime string"""
|
2021-10-06 21:33:25 +02:00
|
|
|
return cls._format_datetime(datetime.datetime.fromtimestamp(int(time)))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def time2date(cls, time):
|
2023-01-16 12:56:12 +01:00
|
|
|
"""Convert timestamp to date string"""
|
2021-10-06 21:33:25 +02:00
|
|
|
return cls._format_date(datetime.date.fromtimestamp(int(time)))
|