database-postgresql-local 0.1.0__tar.gz

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.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: database-postgresql-local
3
+ Version: 0.1.0
4
+ Summary: PyPI Package for Circles database-postgresql-local Python
5
+ Home-page: https://github.com/circles-zone/database-postgresql-local-python-package
6
+ Author: Circles
7
+ Author-email: info@circlez.ai
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: logger-local
12
+ Requires-Dist: database-sql-local
13
+ Requires-Dist: python-sdk-remote
14
+ Requires-Dist: psycopg2
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: requires-dist
22
+ Dynamic: summary
23
+
24
+ PyPI Package for Circles database-postgresql-local Python
@@ -0,0 +1 @@
1
+ TODO Please update the README of the package
@@ -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