database-postgresql-local 0.1.0__py3-none-any.whl
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.
- database_postgresql_local/__init__.py +1 -0
- database_postgresql_local/connector.py +156 -0
- database_postgresql_local/constants_src.py +36 -0
- database_postgresql_local/cursor.py +142 -0
- database_postgresql_local/generic_crud_postgresql.py +967 -0
- database_postgresql_local/table_columns.py +4 -0
- database_postgresql_local/table_definition.py +4 -0
- database_postgresql_local/utils.py +263 -0
- database_postgresql_local-0.1.0.dist-info/METADATA +24 -0
- database_postgresql_local-0.1.0.dist-info/RECORD +12 -0
- database_postgresql_local-0.1.0.dist-info/WHEEL +5 -0
- database_postgresql_local-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Database PostgreSQL Local Package
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations # Required for type hinting in class methods
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from os.path import expanduser
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import psycopg2
|
|
8
|
+
from psycopg2 import pool
|
|
9
|
+
from logger_local.MetaLogger import MetaLogger
|
|
10
|
+
|
|
11
|
+
from .constants_src import LOGGER_CONNECTOR_CODE_OBJECT
|
|
12
|
+
from .cursor import Cursor
|
|
13
|
+
from .utils import (get_sql_hostname, get_sql_password, get_sql_username, get_sql_port)
|
|
14
|
+
|
|
15
|
+
connections_pool = defaultdict(dict)
|
|
16
|
+
home = expanduser('~')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Connector(metaclass=MetaLogger, object=LOGGER_CONNECTOR_CODE_OBJECT):
|
|
20
|
+
@staticmethod
|
|
21
|
+
def connect(schema_name: str, *, user: str = get_sql_username(), ignore_cache: bool = False) -> Connector:
|
|
22
|
+
if ignore_cache and schema_name in connections_pool[user]:
|
|
23
|
+
connections_pool[user].pop(schema_name)
|
|
24
|
+
|
|
25
|
+
if connections_pool[user].get(schema_name):
|
|
26
|
+
# Check if connection is still valid
|
|
27
|
+
connector = None
|
|
28
|
+
try:
|
|
29
|
+
# Test if connection is still alive
|
|
30
|
+
connections_pool[user][schema_name].connection.cursor().execute("SELECT 1")
|
|
31
|
+
connector = connections_pool[user][schema_name]
|
|
32
|
+
except (Exception, psycopg2.DatabaseError):
|
|
33
|
+
# Reconnect if connection is closed
|
|
34
|
+
try:
|
|
35
|
+
connections_pool[user][schema_name].connection.close()
|
|
36
|
+
except:
|
|
37
|
+
pass
|
|
38
|
+
connections_pool[user].pop(schema_name, None)
|
|
39
|
+
connector = Connector(schema_name)
|
|
40
|
+
connections_pool[user][schema_name] = connector
|
|
41
|
+
else:
|
|
42
|
+
connector = Connector(schema_name)
|
|
43
|
+
connections_pool[user][schema_name] = connector
|
|
44
|
+
|
|
45
|
+
return connector
|
|
46
|
+
|
|
47
|
+
def __init__(self, schema_name: str, *,
|
|
48
|
+
host: str = get_sql_hostname(),
|
|
49
|
+
user: str = get_sql_username(),
|
|
50
|
+
password: str = get_sql_password(),
|
|
51
|
+
port: str = get_sql_port()) -> None:
|
|
52
|
+
self.host = host
|
|
53
|
+
self.schema = schema_name
|
|
54
|
+
self.user = user
|
|
55
|
+
self.password = password
|
|
56
|
+
self.port = port
|
|
57
|
+
|
|
58
|
+
# Checking host suffix
|
|
59
|
+
if not (self.host.endswith("circ.zone") or self.host.endswith("circlez.ai")):
|
|
60
|
+
self.logger.warning(
|
|
61
|
+
f"Your POSTGRESQL_HOSTNAME={self.host} which is not what is expected")
|
|
62
|
+
self.connection = None
|
|
63
|
+
self._cursor = None
|
|
64
|
+
self._connect_to_db()
|
|
65
|
+
connections_pool[user][schema_name] = self
|
|
66
|
+
|
|
67
|
+
def reconnect(self) -> None:
|
|
68
|
+
# Called when the connection is lost
|
|
69
|
+
self._connect_to_db()
|
|
70
|
+
self._cursor = self.cursor(close_previous=True)
|
|
71
|
+
self.set_schema(self.schema)
|
|
72
|
+
|
|
73
|
+
def _connect_to_db(self):
|
|
74
|
+
try:
|
|
75
|
+
# PostgreSQL uses a database parameter instead of schema
|
|
76
|
+
# We'll connect to the default database (usually 'postgres') and then set the schema
|
|
77
|
+
self.connection = psycopg2.connect(
|
|
78
|
+
host=self.host,
|
|
79
|
+
user=self.user,
|
|
80
|
+
password=self.password,
|
|
81
|
+
port=self.port,
|
|
82
|
+
dbname='postgres' # Connect to default database
|
|
83
|
+
)
|
|
84
|
+
self.connection.autocommit = False # Explicit transaction control
|
|
85
|
+
self.logger.info(f"Connected to the database: {self.database_info()}")
|
|
86
|
+
self._cursor = self.connection.cursor()
|
|
87
|
+
self.set_schema(self.schema)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
self.logger.error(f"Failed to connect to PostgreSQL: {e}")
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
def database_info(self) -> str:
|
|
93
|
+
database_info_str = f"host={self.host}, user={self.user}, schema={self.schema}, port={self.port}"
|
|
94
|
+
return database_info_str
|
|
95
|
+
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
try:
|
|
98
|
+
if self._cursor:
|
|
99
|
+
self._cursor.close()
|
|
100
|
+
self.logger.info(f"Cursor closed successfully for schema: {self.schema}")
|
|
101
|
+
except Exception as exception:
|
|
102
|
+
self.logger.error(object={"exception": exception})
|
|
103
|
+
connections_pool[self.user].pop(self.schema, None)
|
|
104
|
+
if self.connection:
|
|
105
|
+
try:
|
|
106
|
+
self.connection.close()
|
|
107
|
+
self.logger.info("Connection closed successfully.")
|
|
108
|
+
except:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def cursor(self, *, close_previous: bool = True, cache_previous: bool = False, dictionary: bool = None,
|
|
112
|
+
buffered: bool = None, raw: bool = None,
|
|
113
|
+
prepared: bool = None, named_tuple: bool = None, cursor_class=None) -> Cursor:
|
|
114
|
+
# PostgreSQL doesn't support all the same cursor options as MySQL
|
|
115
|
+
# We'll create a basic cursor and wrap it in our Cursor class
|
|
116
|
+
cursor_instance = Cursor(self.connection.cursor())
|
|
117
|
+
return cursor_instance
|
|
118
|
+
|
|
119
|
+
def commit(self) -> None:
|
|
120
|
+
self.connection.commit()
|
|
121
|
+
|
|
122
|
+
def set_schema(self, new_schema: Optional[str]) -> None:
|
|
123
|
+
if not new_schema:
|
|
124
|
+
return
|
|
125
|
+
if self.schema == new_schema:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
if self._cursor and self.connection:
|
|
129
|
+
try:
|
|
130
|
+
# First check if schema exists
|
|
131
|
+
self._cursor.execute(f"SELECT schema_name FROM information_schema.schemata WHERE schema_name = %s", (new_schema,))
|
|
132
|
+
if not self._cursor.fetchone():
|
|
133
|
+
# Create schema if it doesn't exist
|
|
134
|
+
self._cursor.execute(f'CREATE SCHEMA IF NOT EXISTS "{new_schema}"')
|
|
135
|
+
self.connection.commit()
|
|
136
|
+
|
|
137
|
+
# Set search path to use the schema
|
|
138
|
+
self._cursor.execute(f'SET search_path TO "{new_schema}", public')
|
|
139
|
+
connections_pool[self.user][new_schema] = self
|
|
140
|
+
connections_pool[self.user].pop(self.schema, None)
|
|
141
|
+
self.schema = new_schema
|
|
142
|
+
self.logger.info(f"Switched to schema: {new_schema}")
|
|
143
|
+
except Exception as e:
|
|
144
|
+
self.logger.error(f"Error setting schema: {e}")
|
|
145
|
+
raise
|
|
146
|
+
else:
|
|
147
|
+
raise Exception(
|
|
148
|
+
"Connection is not established. The database will be used on the next connect.")
|
|
149
|
+
|
|
150
|
+
def rollback(self):
|
|
151
|
+
self.connection.rollback()
|
|
152
|
+
|
|
153
|
+
def start_transaction(self):
|
|
154
|
+
# PostgreSQL doesn't have an explicit start_transaction method
|
|
155
|
+
# Transactions begin automatically when you execute a query
|
|
156
|
+
pass
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from logger_local.LoggerComponentEnum import LoggerComponentEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# TODO Move everything related to sync to separate directory called
|
|
7
|
+
# sync_data_source (preferable with it's own src and tests directories)
|
|
8
|
+
class UpdateStatus(Enum):
|
|
9
|
+
UPDATE_DATA_SOURCE = -1
|
|
10
|
+
DONT_UPDATE = 0
|
|
11
|
+
UPDATE_CIRCLEZ = 1 # TODO Don't use CIRCLEZ
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# connector / cursor
|
|
15
|
+
DATABASE_POSTGRESQL_PYTHON_PACKAGE_COMPONENT_ID = 113
|
|
16
|
+
DATABASE_POSTGRESQL_PYTHON_PACKAGE_COMPONENT_NAME = 'database_postgresql_local\\connector'
|
|
17
|
+
CONNECTOR_DEVELOPER_EMAIL = 'idan.a@circ.zone'
|
|
18
|
+
LOGGER_CONNECTOR_CODE_OBJECT = {
|
|
19
|
+
'component_id': DATABASE_POSTGRESQL_PYTHON_PACKAGE_COMPONENT_ID,
|
|
20
|
+
'component_name': DATABASE_POSTGRESQL_PYTHON_PACKAGE_COMPONENT_NAME,
|
|
21
|
+
'component_category': LoggerComponentEnum.ComponentCategory.Code.value,
|
|
22
|
+
'developer_email': CONNECTOR_DEVELOPER_EMAIL
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# generic_crud
|
|
26
|
+
DATABASE_POSTGRESQL_PYTHON_GENERIC_CRUD_COMPONENT_ID = 207
|
|
27
|
+
DATABASE_POSTGRESQL_PYTHON_GENERIC_CRUD_COMPONENT_NAME = 'database_postgresql_local\\generic_crud_postgresql'
|
|
28
|
+
GENERIC_CRUD_DEVELOPER_EMAIL = 'akiva.s@circ.zone'
|
|
29
|
+
CRUD_POSTGRESQL_CODE_LOGGER_OBJECT = {
|
|
30
|
+
'component_id': DATABASE_POSTGRESQL_PYTHON_GENERIC_CRUD_COMPONENT_ID,
|
|
31
|
+
'component_name': DATABASE_POSTGRESQL_PYTHON_GENERIC_CRUD_COMPONENT_NAME,
|
|
32
|
+
'component_category': LoggerComponentEnum.ComponentCategory.Code.value,
|
|
33
|
+
'developer_email': GENERIC_CRUD_DEVELOPER_EMAIL
|
|
34
|
+
}
|
|
35
|
+
CRUD_POSTGRESQL_TEST_LOGGER_OBJECT = CRUD_POSTGRESQL_CODE_LOGGER_OBJECT.copy()
|
|
36
|
+
CRUD_POSTGRESQL_TEST_LOGGER_OBJECT['component_category'] = LoggerComponentEnum.ComponentCategory.Unit_Test.value
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from logger_local.MetaLogger import MetaLogger
|
|
4
|
+
from psycopg2.extensions import cursor as PostgreSQLCursor
|
|
5
|
+
import psycopg2
|
|
6
|
+
from .constants_src import LOGGER_CONNECTOR_CODE_OBJECT
|
|
7
|
+
|
|
8
|
+
# PostgreSQL version
|
|
9
|
+
POSTGRESQL_VERSION = psycopg2.__version__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def version_tuple(version):
|
|
13
|
+
return tuple(map(int, version.split("."))) # Converts "2.9.5" → (2, 9, 5)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Cursor(metaclass=MetaLogger, object=LOGGER_CONNECTOR_CODE_OBJECT):
|
|
17
|
+
def __init__(self, cursor: PostgreSQLCursor) -> None:
|
|
18
|
+
self.cursor = cursor
|
|
19
|
+
self.__is_closed = False
|
|
20
|
+
|
|
21
|
+
def execute(self, sql_statement: str, sql_parameters: tuple | list = None, multi: bool = False) -> None:
|
|
22
|
+
# PostgreSQL doesn't support the multi parameter
|
|
23
|
+
# Convert MySQL-style backtick quotes to PostgreSQL double quotes
|
|
24
|
+
sql_statement = sql_statement.replace('`', '"')
|
|
25
|
+
|
|
26
|
+
if sql_parameters:
|
|
27
|
+
# PostgreSQL uses %s for all parameter types
|
|
28
|
+
formatted_sql = sql_statement
|
|
29
|
+
sql_parameters_str = ", ".join(str(param) for param in sql_parameters)
|
|
30
|
+
else:
|
|
31
|
+
formatted_sql = sql_statement
|
|
32
|
+
sql_parameters_str = "None"
|
|
33
|
+
|
|
34
|
+
self.logger.info(object={
|
|
35
|
+
"formatted_sql": formatted_sql.replace("\n", " "),
|
|
36
|
+
"sql_parameters": sql_parameters_str,
|
|
37
|
+
"sql_statement": sql_statement.replace("\n", " ")
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
self.cursor.execute(sql_statement, sql_parameters)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
self.logger.error(f"Error executing SQL: {e}")
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
def executemany(self, sql_statement: str, sql_parameters: tuple | list = None) -> None:
|
|
47
|
+
try:
|
|
48
|
+
# Convert MySQL-style backtick quotes to PostgreSQL double quotes
|
|
49
|
+
sql_statement = sql_statement.replace('`', '"')
|
|
50
|
+
|
|
51
|
+
if sql_parameters:
|
|
52
|
+
sql_parameters_str = str(sql_parameters)
|
|
53
|
+
formatted_sql = sql_statement
|
|
54
|
+
else:
|
|
55
|
+
formatted_sql = sql_statement
|
|
56
|
+
sql_parameters_str = "None"
|
|
57
|
+
|
|
58
|
+
self.logger.info(object={
|
|
59
|
+
"formatted_sql": formatted_sql.replace("\n", " "),
|
|
60
|
+
"sql_parameters": sql_parameters_str,
|
|
61
|
+
"sql_statement": sql_statement.replace("\n", " ")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
self.cursor.executemany(sql_statement, sql_parameters)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
self.logger.warning('Unable to execute statement', object={
|
|
67
|
+
"sql_statement": sql_statement,
|
|
68
|
+
"sql_parameters": sql_parameters,
|
|
69
|
+
"error": str(e)
|
|
70
|
+
})
|
|
71
|
+
raise
|
|
72
|
+
|
|
73
|
+
def fetchall(self) -> Any:
|
|
74
|
+
result = self.cursor.fetchall()
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
def fetchmany(self, size: int) -> Any:
|
|
78
|
+
result = self.cursor.fetchmany(size)
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
def fetchone(self) -> Any:
|
|
82
|
+
result = self.cursor.fetchone()
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
def description(self) -> list[tuple]:
|
|
86
|
+
"""Returns description of columns in a result
|
|
87
|
+
|
|
88
|
+
This property returns a list of tuples describing the columns
|
|
89
|
+
in a result set. A tuple is described as follows::
|
|
90
|
+
|
|
91
|
+
(column_name,
|
|
92
|
+
type,
|
|
93
|
+
None,
|
|
94
|
+
None,
|
|
95
|
+
None,
|
|
96
|
+
None,
|
|
97
|
+
null_ok)
|
|
98
|
+
"""
|
|
99
|
+
result = self.cursor.description
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
def column_names(self) -> tuple:
|
|
103
|
+
"""Returns the column names from the last query"""
|
|
104
|
+
if self.cursor.description:
|
|
105
|
+
return tuple(desc[0] for desc in self.cursor.description)
|
|
106
|
+
return tuple()
|
|
107
|
+
|
|
108
|
+
def lastrowid(self) -> int | None:
|
|
109
|
+
"""Returns the value generated for an AUTO_INCREMENT column by the previous INSERT or UPDATE statement."""
|
|
110
|
+
# PostgreSQL doesn't have a direct lastrowid property
|
|
111
|
+
# We need to use RETURNING clause in the SQL or currval() function
|
|
112
|
+
# This is a simplified implementation that may need to be adjusted based on actual usage
|
|
113
|
+
try:
|
|
114
|
+
self.cursor.execute("SELECT lastval()")
|
|
115
|
+
result = self.cursor.fetchone()[0]
|
|
116
|
+
return result
|
|
117
|
+
except Exception as e:
|
|
118
|
+
self.logger.warning(f"Error getting lastrowid: {e}")
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def get_affected_row_count(self) -> int:
|
|
122
|
+
"""Returns the number of rows produced or affected"""
|
|
123
|
+
result = self.cursor.rowcount
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
def get_last_executed_statement(self) -> str:
|
|
127
|
+
# PostgreSQL cursor doesn't store the last executed statement
|
|
128
|
+
# We would need to track this ourselves
|
|
129
|
+
return "Statement not available"
|
|
130
|
+
|
|
131
|
+
def close(self) -> None:
|
|
132
|
+
if self.__is_closed:
|
|
133
|
+
self.logger.warning('Cursor is already closed')
|
|
134
|
+
try:
|
|
135
|
+
self.cursor.close()
|
|
136
|
+
self.__is_closed = True
|
|
137
|
+
except Exception as exception:
|
|
138
|
+
self.logger.error('Unable to close cursor', object={"exception": exception})
|
|
139
|
+
|
|
140
|
+
def is_closed(self) -> bool:
|
|
141
|
+
result = self.__is_closed
|
|
142
|
+
return result
|