SQLPyHelper 0.1.4__py3-none-any.whl → 0.1.6__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.
sqlpyhelper/__init__.py CHANGED
@@ -1,9 +1,9 @@
1
1
  # Match the version in setup.py
2
- __version__ = "0.1.4"
2
+ __version__ = "0.1.6"
3
3
 
4
- from sqlpyhelper.db_helper import (
5
- SQLPyHelperError,
4
+ from sqlpyhelper.db_helper import ( # noqa: F401
5
+ BackupError,
6
6
  ConnectionError,
7
7
  QueryError,
8
- BackupError,
8
+ SQLPyHelperError,
9
9
  )
@@ -1,9 +1,11 @@
1
- import pandas as pd
2
- from sqlpyhelper.db_helper import SQLPyHelper
3
- import subprocess
4
- from datetime import datetime
5
1
  import os
6
2
  import shutil
3
+ import subprocess
4
+ from datetime import datetime
5
+
6
+ import pandas as pd
7
+
8
+ from sqlpyhelper.db_helper import SQLPyHelper
7
9
 
8
10
 
9
11
  class AutomationUtils:
@@ -43,12 +45,17 @@ class AutomationUtils:
43
45
  subprocess.run(
44
46
  [
45
47
  "pg_dump",
46
- "-h", host,
47
- "-p", port,
48
- "-U", user,
48
+ "-h",
49
+ host,
50
+ "-p",
51
+ port,
52
+ "-U",
53
+ user,
49
54
  db_name,
50
- "-F", "c",
51
- "-f", filepath,
55
+ "-F",
56
+ "c",
57
+ "-f",
58
+ filepath,
52
59
  ],
53
60
  check=True,
54
61
  shell=False,
@@ -82,7 +89,7 @@ class AutomationUtils:
82
89
  entity_column (str): Column representing entity ID.
83
90
  date_column (str): Column representing timestamp/date.
84
91
  """
85
- if self.db.db_type == 'sqlite':
92
+ if self.db.db_type == "sqlite":
86
93
  month_expr = f"strftime('%Y-%m', {date_column})"
87
94
  else:
88
95
  month_expr = f"DATE_TRUNC('month', {date_column})"
@@ -95,7 +102,9 @@ class AutomationUtils:
95
102
  self.db.execute_query(query)
96
103
  return self.db.fetch_all()
97
104
 
98
- def aggregate_column(self, table, value_column, group_column=None, time_column=None):
105
+ def aggregate_column(
106
+ self, table, value_column, group_column=None, time_column=None
107
+ ):
99
108
  """
100
109
  Computes sum of any value column grouped by entity or month.
101
110
 
@@ -105,7 +114,7 @@ class AutomationUtils:
105
114
  group_column (str, optional): Entity or category to group by.
106
115
  time_column (str, optional): Timestamp to extract month grouping.
107
116
  """
108
- if self.db.db_type == 'sqlite':
117
+ if self.db.db_type == "sqlite":
109
118
  month_expr = f"strftime('%Y-%m', {time_column})"
110
119
  else:
111
120
  month_expr = f"DATE_TRUNC('month', {time_column})"
@@ -133,11 +142,13 @@ class AutomationUtils:
133
142
  threshold (int): Number of standard deviations from mean to flag as outlier.
134
143
  """
135
144
  query = f"""
136
- SELECT *, {numeric_column}
137
- AS value FROM {table}
145
+ SELECT *, {numeric_column} AS value FROM {table}
138
146
  """
139
147
  self.db.execute_query(query)
140
- data = pd.DataFrame(self.db.fetch_all(), columns=[desc[0] for desc in self.db.cursor.description])
148
+ data = pd.DataFrame(
149
+ self.db.fetch_all(),
150
+ columns=[desc[0] for desc in self.db.cursor.description],
151
+ )
141
152
 
142
153
  mean_val = data["value"].mean()
143
154
  std_val = data["value"].std()
sqlpyhelper/cli.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import click
2
- from sqlpyhelper.db_helper import SQLPyHelper
2
+
3
3
  from sqlpyhelper.automation_utils import AutomationUtils
4
+ from sqlpyhelper.db_helper import SQLPyHelper
4
5
 
5
6
 
6
7
  @click.group()
@@ -10,15 +11,17 @@ def cli():
10
11
 
11
12
 
12
13
  @cli.command()
13
- @click.option('--db_type', help='Type of database (e.g., sqlite, postgres, mysql)')
14
- @click.option('--host', help='Database host')
15
- @click.option('--user', help='Username')
16
- @click.option('--password', help='Password')
17
- @click.option('--database', help='Database name or file')
18
- @click.option('--query', required=True, help='SQL query to run')
14
+ @click.option("--db_type", help="Type of database (e.g., sqlite, postgres, mysql)")
15
+ @click.option("--host", help="Database host")
16
+ @click.option("--user", help="Username")
17
+ @click.option("--password", help="Password")
18
+ @click.option("--database", help="Database name or file")
19
+ @click.option("--query", required=True, help="SQL query to run")
19
20
  def run_query(db_type, host, user, password, database, query):
20
21
  """Run a single SQL query and print results"""
21
- db = SQLPyHelper(db_type=db_type, host=host, user=user, password=password, database=database)
22
+ db = SQLPyHelper(
23
+ db_type=db_type, host=host, user=user, password=password, database=database
24
+ )
22
25
  results = db.execute_query(query)
23
26
  for row in results:
24
27
  click.echo(row)
@@ -26,14 +29,16 @@ def run_query(db_type, host, user, password, database, query):
26
29
 
27
30
 
28
31
  @cli.command()
29
- @click.option('--db_type', required=True)
30
- @click.option('--host')
31
- @click.option('--user')
32
- @click.option('--password')
33
- @click.option('--database', required=True)
32
+ @click.option("--db_type", required=True)
33
+ @click.option("--host")
34
+ @click.option("--user")
35
+ @click.option("--password")
36
+ @click.option("--database", required=True)
34
37
  def interactive_shell(db_type, host, user, password, database):
35
38
  """Launch an interactive SQL shell"""
36
- db = SQLPyHelper(db_type=db_type, host=host, user=user, password=password, database=database)
39
+ db = SQLPyHelper(
40
+ db_type=db_type, host=host, user=user, password=password, database=database
41
+ )
37
42
  click.echo("Interactive shell started. Type your SQL query or 'exit'")
38
43
  while True:
39
44
  query = input("sqlpy> ")
@@ -50,14 +55,14 @@ def interactive_shell(db_type, host, user, password, database):
50
55
 
51
56
 
52
57
  @cli.command()
53
- @click.option('--target', default="local", help="Backup destination")
54
- @click.option('--tag', default="autobackup", help="Tag for backup file naming")
55
- @click.option('--db-type')
56
- @click.option('--host')
57
- @click.option('--user')
58
- @click.option('--password')
59
- @click.option('--database')
60
- @click.option('--port')
58
+ @click.option("--target", default="local", help="Backup destination")
59
+ @click.option("--tag", default="autobackup", help="Tag for backup file naming")
60
+ @click.option("--db-type")
61
+ @click.option("--host")
62
+ @click.option("--user")
63
+ @click.option("--password")
64
+ @click.option("--database")
65
+ @click.option("--port")
61
66
  def backup(target, tag, db_type, host, user, password, database, port):
62
67
  """Create a timestamped backup of the connected database."""
63
68
  utils = AutomationUtils(
@@ -66,77 +71,125 @@ def backup(target, tag, db_type, host, user, password, database, port):
66
71
  user=user,
67
72
  password=password,
68
73
  database=database,
69
- port=port
74
+ port=port,
70
75
  )
71
76
  utils.backup_database(target=target, tag=tag)
72
77
 
73
78
 
74
79
  @cli.command()
75
- @click.option('--file', required=True, help="Path to CSV file")
76
- @click.option('--table', required=True, help="Destination table")
77
- @click.option('--if-exists', default="append", type=click.Choice(["append", "replace"]), help="What to do if table exists")
78
- @click.option('--db-type')
79
- @click.option('--host')
80
- @click.option('--user')
81
- @click.option('--password')
82
- @click.option('--database')
83
- @click.option('--port')
80
+ @click.option("--file", required=True, help="Path to CSV file")
81
+ @click.option("--table", required=True, help="Destination table")
82
+ @click.option(
83
+ "--if-exists",
84
+ default="append",
85
+ type=click.Choice(["append", "replace"]),
86
+ help="What to do if table exists",
87
+ )
88
+ @click.option("--db-type")
89
+ @click.option("--host")
90
+ @click.option("--user")
91
+ @click.option("--password")
92
+ @click.option("--database")
93
+ @click.option("--port")
84
94
  def load_data(file, table, if_exists, db_type, host, user, password, database, port):
85
95
  """Load data from CSV into database table."""
86
- utils = AutomationUtils(db_type=db_type, host=host, user=user, password=password, database=database, port=port)
96
+ utils = AutomationUtils(
97
+ db_type=db_type,
98
+ host=host,
99
+ user=user,
100
+ password=password,
101
+ database=database,
102
+ port=port,
103
+ )
87
104
  utils.load_data_from_csv(file, table, if_exists=if_exists)
88
105
 
89
106
 
90
107
  @cli.command()
91
- @click.option('--table', required=True)
92
- @click.option('--entity-column', required=True)
93
- @click.option('--date-column', required=True)
94
- @click.option('--db-type')
95
- @click.option('--host')
96
- @click.option('--user')
97
- @click.option('--password')
98
- @click.option('--database')
99
- @click.option('--port')
100
- def detect_missing_periods(table, entity_column, date_column, db_type, host, user, password, database, port):
108
+ @click.option("--table", required=True)
109
+ @click.option("--entity-column", required=True)
110
+ @click.option("--date-column", required=True)
111
+ @click.option("--db-type")
112
+ @click.option("--host")
113
+ @click.option("--user")
114
+ @click.option("--password")
115
+ @click.option("--database")
116
+ @click.option("--port")
117
+ def detect_missing_periods(
118
+ table, entity_column, date_column, db_type, host, user, password, database, port
119
+ ):
101
120
  """Flag entities with fewer than 12 months of activity."""
102
- utils = AutomationUtils(db_type=db_type, host=host, user=user, password=password, database=database, port=port)
121
+ utils = AutomationUtils(
122
+ db_type=db_type,
123
+ host=host,
124
+ user=user,
125
+ password=password,
126
+ database=database,
127
+ port=port,
128
+ )
103
129
  results = utils.detect_missing_periods(table, entity_column, date_column)
104
130
  for row in results:
105
131
  click.echo(row)
106
132
 
107
133
 
108
134
  @cli.command()
109
- @click.option('--table', required=True)
110
- @click.option('--value-column', required=True)
111
- @click.option('--group-column')
112
- @click.option('--time-column')
113
- @click.option('--db-type')
114
- @click.option('--host')
115
- @click.option('--user')
116
- @click.option('--password')
117
- @click.option('--database')
118
- @click.option('--port')
119
- def aggregate(table, value_column, group_column, time_column, db_type, host, user, password, database, port):
135
+ @click.option("--table", required=True)
136
+ @click.option("--value-column", required=True)
137
+ @click.option("--group-column")
138
+ @click.option("--time-column")
139
+ @click.option("--db-type")
140
+ @click.option("--host")
141
+ @click.option("--user")
142
+ @click.option("--password")
143
+ @click.option("--database")
144
+ @click.option("--port")
145
+ def aggregate(
146
+ table,
147
+ value_column,
148
+ group_column,
149
+ time_column,
150
+ db_type,
151
+ host,
152
+ user,
153
+ password,
154
+ database,
155
+ port,
156
+ ):
120
157
  """Aggregate numeric column optionally grouped by entity and time."""
121
- utils = AutomationUtils(db_type=db_type, host=host, user=user, password=password, database=database, port=port)
158
+ utils = AutomationUtils(
159
+ db_type=db_type,
160
+ host=host,
161
+ user=user,
162
+ password=password,
163
+ database=database,
164
+ port=port,
165
+ )
122
166
  results = utils.aggregate_column(table, value_column, group_column, time_column)
123
167
  for row in results:
124
168
  click.echo(row)
125
169
 
126
170
 
127
171
  @cli.command()
128
- @click.option('--table', required=True)
129
- @click.option('--numeric-column', required=True)
130
- @click.option('--threshold', default=2, type=int)
131
- @click.option('--db-type')
132
- @click.option('--host')
133
- @click.option('--user')
134
- @click.option('--password')
135
- @click.option('--database')
136
- @click.option('--port')
137
- def detect_outliers(table, numeric_column, threshold, db_type, host, user, password, database, port):
172
+ @click.option("--table", required=True)
173
+ @click.option("--numeric-column", required=True)
174
+ @click.option("--threshold", default=2, type=int)
175
+ @click.option("--db-type")
176
+ @click.option("--host")
177
+ @click.option("--user")
178
+ @click.option("--password")
179
+ @click.option("--database")
180
+ @click.option("--port")
181
+ def detect_outliers(
182
+ table, numeric_column, threshold, db_type, host, user, password, database, port
183
+ ):
138
184
  """Flag rows where values deviate statistically from average."""
139
- utils = AutomationUtils(db_type=db_type, host=host, user=user, password=password, database=database, port=port)
185
+ utils = AutomationUtils(
186
+ db_type=db_type,
187
+ host=host,
188
+ user=user,
189
+ password=password,
190
+ database=database,
191
+ port=port,
192
+ )
140
193
  results = utils.detect_outliers(table, numeric_column, threshold)
141
194
  for row in results:
142
195
  click.echo(row)
sqlpyhelper/db_helper.py CHANGED
@@ -1,9 +1,19 @@
1
1
  import csv
2
- from dotenv import load_dotenv
2
+ import logging
3
3
  import os
4
4
  import re
5
+ from typing import Any, Literal, Optional
6
+
7
+ from dotenv import load_dotenv
5
8
 
6
- load_dotenv() # Load environment variables from .env file
9
+ load_dotenv()
10
+
11
+ logging.basicConfig(
12
+ level=logging.INFO,
13
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
14
+ datefmt="%Y-%m-%d %H:%M:%S",
15
+ )
16
+ logger = logging.getLogger("sqlpyhelper")
7
17
 
8
18
 
9
19
  def _validate_identifier(name: str) -> str:
@@ -12,7 +22,7 @@ def _validate_identifier(name: str) -> str:
12
22
  Allows only alphanumeric characters and underscores.
13
23
  Raises ValueError for anything else, preventing SQL injection via identifiers.
14
24
  """
15
- if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', name):
25
+ if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name):
16
26
  raise ValueError(
17
27
  f"Invalid SQL identifier: {name!r}. "
18
28
  "Only letters, digits, and underscores are allowed."
@@ -37,8 +47,17 @@ class BackupError(SQLPyHelperError):
37
47
 
38
48
 
39
49
  class SQLPyHelper:
40
- def __init__(self, db_type=None, host=None, user=None, password=None,
41
- database=None, driver=None, port=None, oracle_sid=None):
50
+ def __init__(
51
+ self,
52
+ db_type: Optional[str] = None,
53
+ host: Optional[str] = None,
54
+ user: Optional[str] = None,
55
+ password: Optional[str] = None,
56
+ database: Optional[str] = None,
57
+ driver: Optional[str] = None,
58
+ port: Optional[str] = None,
59
+ oracle_sid: Optional[str] = None,
60
+ ) -> None:
42
61
 
43
62
  # Store original params so reconnect() can replay them
44
63
  self._init_kwargs = {
@@ -52,52 +71,71 @@ class SQLPyHelper:
52
71
  "oracle_sid": oracle_sid,
53
72
  }
54
73
 
55
- self.db_type = db_type or os.getenv("DB_TYPE").lower()
56
- self.host = host or os.getenv("DB_HOST")
57
- self.user = user or os.getenv("DB_USER")
58
- self.password = password or os.getenv("DB_PASSWORD")
59
- self.database = database or os.getenv("DB_NAME")
60
- self.driver = driver or os.getenv("DB_DRIVER")
61
- self.port = port or os.getenv("DB_PORT")
62
- self.oracle_sid = oracle_sid or os.getenv("ORACLE_SID")
63
- self.pool = None
74
+ self.db_type: str = (db_type or os.getenv("DB_TYPE") or "").lower()
75
+ self.host: Optional[str] = host or os.getenv("DB_HOST")
76
+ self.user: Optional[str] = user or os.getenv("DB_USER")
77
+ self.password: Optional[str] = password or os.getenv("DB_PASSWORD")
78
+ self.database: Optional[str] = database or os.getenv("DB_NAME")
79
+ self.driver: Optional[str] = driver or os.getenv("DB_DRIVER")
80
+ self.port: Optional[str] = port or os.getenv("DB_PORT")
81
+ self.oracle_sid: Optional[str] = oracle_sid or os.getenv("ORACLE_SID")
82
+ self.pool: Any = None
64
83
 
65
84
  if not self.db_type or not self.database:
66
85
  raise ValueError("Missing required database configuration.")
67
86
 
68
87
  if self.db_type == "sqlite":
69
88
  import sqlite3
89
+
70
90
  self.connection = sqlite3.connect(self.database)
71
91
  elif self.db_type == "postgres":
72
92
  import psycopg2
73
- self.connection = psycopg2.connect(host=self.host, user=self.user,
74
- password=self.password, dbname=self.database)
93
+
94
+ self.connection = psycopg2.connect(
95
+ host=self.host,
96
+ user=self.user,
97
+ password=self.password,
98
+ dbname=self.database,
99
+ )
75
100
  elif self.db_type == "mysql":
76
101
  import mysql.connector
77
- self.connection = mysql.connector.connect(host=self.host, user=self.user,
78
- password=self.password, database=self.database)
102
+
103
+ self.connection = mysql.connector.connect(
104
+ host=self.host,
105
+ user=self.user,
106
+ password=self.password,
107
+ database=self.database,
108
+ ) # type: ignore[assignment]
79
109
  elif self.db_type == "sqlserver":
80
110
  import pyodbc
81
- self.connection = pyodbc.connect(f"DRIVER={self.driver};SERVER={self.host};DATABASE={self.database};"
82
- f"UID={self.user};PWD={self.password}")
111
+
112
+ self.connection = pyodbc.connect(
113
+ f"DRIVER={self.driver};SERVER={self.host};DATABASE={self.database};"
114
+ f"UID={self.user};PWD={self.password}"
115
+ )
83
116
  elif self.db_type == "oracle":
84
- import cx_Oracle
85
- oracle_port = os.getenv("ORACLE_DB_PORT", "1521") # Default to 1521 if not set
86
- dsn = cx_Oracle.makedsn(self.host, oracle_port, self.oracle_sid)
87
- self.connection = cx_Oracle.connect(self.user, self.password, dsn)
117
+ import oracledb
118
+
119
+ oracle_port = os.getenv("ORACLE_DB_PORT", 1521)
120
+ dsn = oracledb.makedsn(
121
+ self.host, oracle_port, sid=self.oracle_sid # type: ignore[arg-type]
122
+ )
123
+ self.connection = oracledb.connect(
124
+ user=self.user, password=self.password, dsn=dsn
125
+ ) # type: ignore[assignment]
88
126
  else:
89
127
  raise ValueError("Unsupported database type")
90
128
 
91
129
  self.cursor = self.connection.cursor()
92
130
 
93
- def __enter__(self):
131
+ def __enter__(self) -> "SQLPyHelper":
94
132
  return self
95
133
 
96
- def __exit__(self, exc_type, exc_val, exc_tb):
134
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Literal[False]:
97
135
  self.close()
98
136
  return False
99
137
 
100
- def execute_query(self, query, params=None):
138
+ def execute_query(self, query: str, params: Optional[tuple] = None) -> None:
101
139
  """Executes a query with optional parameters"""
102
140
  try:
103
141
  if params:
@@ -106,28 +144,33 @@ class SQLPyHelper:
106
144
  self.cursor.execute(query)
107
145
  self.connection.commit()
108
146
  except Exception as e:
109
- if "server has gone away" in str(e): # Example check for MySQL lost connection
147
+ if "server has gone away" in str(
148
+ e
149
+ ): # Example check for MySQL lost connection
110
150
  self.reconnect()
111
- self.cursor.execute(query, params)
151
+ self.cursor.execute(query, params) # type: ignore[arg-type]
112
152
  self.connection.commit()
113
153
  else:
114
154
  raise QueryError(f"Query failed: {e}") from e
115
155
 
116
- def fetch_one(self):
156
+ def fetch_one(self) -> Optional[tuple]:
117
157
  """Fetches a single row"""
118
158
  try:
119
159
  return self.cursor.fetchone()
120
160
  except Exception as e:
121
161
  raise QueryError(f"Failed to fetch row: {e}") from e
122
162
 
123
- def fetch_all(self):
163
+ def fetch_all(self) -> list[tuple]:
124
164
  """Fetches all rows from the last executed query"""
125
165
  try:
126
166
  return self.cursor.fetchall()
127
167
  except Exception as e:
128
168
  raise QueryError(f"Failed to fetch rows: {e}") from e
129
169
 
130
- def fetch_by_param(self, table_name, column_name, value):
170
+ def fetch_by_param(
171
+ self, table_name: str, column_name: str, value: Any
172
+ ) -> list[tuple]:
173
+ """Fetches rows from a table where a column matches the given value."""
131
174
  try:
132
175
  table_name = _validate_identifier(table_name)
133
176
  column_name = _validate_identifier(column_name)
@@ -138,15 +181,15 @@ class SQLPyHelper:
138
181
  except Exception as e:
139
182
  raise QueryError(f"Failed to fetch by param: {e}") from e
140
183
 
141
- def close(self):
142
- """Closes the connection"""
184
+ def close(self) -> None:
185
+ """Closes the cursor and database connection."""
143
186
  try:
144
187
  self.cursor.close()
145
188
  self.connection.close()
146
189
  except Exception as e:
147
190
  raise ConnectionError(f"Failed to close connection: {e}") from e
148
191
 
149
- def create_table(self, table_name, columns):
192
+ def create_table(self, table_name: str, columns: dict[str, str]) -> None:
150
193
  """
151
194
  Creates a table dynamically using a dictionary format.
152
195
  Example:
@@ -154,14 +197,18 @@ class SQLPyHelper:
154
197
  """
155
198
  try:
156
199
  table_name = _validate_identifier(table_name)
157
- validated_cols = {_validate_identifier(col): dtype for col, dtype in columns.items()}
158
- columns_def = ", ".join([f"{col} {dtype}" for col, dtype in validated_cols.items()])
200
+ validated_cols = {
201
+ _validate_identifier(col): dtype for col, dtype in columns.items()
202
+ }
203
+ columns_def = ", ".join(
204
+ [f"{col} {dtype}" for col, dtype in validated_cols.items()]
205
+ )
159
206
  query = f"CREATE TABLE IF NOT EXISTS {table_name} ({columns_def})"
160
207
  self.execute_query(query)
161
208
  except Exception as e:
162
209
  raise QueryError(f"Failed to create table: {e}") from e
163
210
 
164
- def insert_bulk(self, table_name, data):
211
+ def insert_bulk(self, table_name: str, data: list[dict[str, Any]]) -> None:
165
212
  """
166
213
  Inserts multiple rows at once.
167
214
  Example:
@@ -181,7 +228,7 @@ class SQLPyHelper:
181
228
  except Exception as e:
182
229
  raise QueryError(f"Failed to insert bulk rows: {e}") from e
183
230
 
184
- def backup_table(self, table_name, backup_file):
231
+ def backup_table(self, table_name: str, backup_file: str) -> None:
185
232
  """
186
233
  Exports table data into a CSV file.
187
234
  Example:
@@ -195,56 +242,83 @@ class SQLPyHelper:
195
242
 
196
243
  with open(backup_file, mode="w", newline="") as file:
197
244
  writer = csv.writer(file)
198
- writer.writerow([desc[0] for desc in self.cursor.description]) # Column headers
245
+ writer.writerow(
246
+ [desc[0] for desc in self.cursor.description]
247
+ ) # Column headers
199
248
  writer.writerows(rows)
200
249
  except Exception as e:
201
250
  raise BackupError(f"Failed to backup table: {e}") from e
202
251
 
203
- def setup_connection_pool(self, min_conn=1, max_conn=5, pool_size=5):
252
+ def setup_connection_pool(
253
+ self, min_conn: int = 1, max_conn: int = 5, pool_size: int = 5
254
+ ) -> None:
204
255
  """Sets up connection pooling based on the database type"""
205
256
  try:
206
257
  if self.db_type == "postgres":
207
258
  from psycopg2 import pool
208
- self.pool = pool.SimpleConnectionPool(min_conn, max_conn,
209
- host=self.host, user=self.user,
210
- password=self.password, dbname=self.database)
259
+
260
+ self.pool = pool.SimpleConnectionPool(
261
+ min_conn,
262
+ max_conn,
263
+ host=self.host,
264
+ user=self.user,
265
+ password=self.password,
266
+ dbname=self.database,
267
+ )
211
268
 
212
269
  elif self.db_type == "mysql":
213
270
  import mysql.connector.pooling
214
- self.pool = mysql.connector.pooling.MySQLConnectionPool(pool_name="mypool",
215
- pool_size=pool_size, host=self.host,
216
- user=self.user, password=self.password,
217
- database=self.database)
271
+
272
+ self.pool = mysql.connector.pooling.MySQLConnectionPool(
273
+ pool_name="mypool",
274
+ pool_size=pool_size,
275
+ host=self.host,
276
+ user=self.user,
277
+ password=self.password,
278
+ database=self.database,
279
+ )
218
280
 
219
281
  elif self.db_type == "sqlserver":
220
282
  import pyodbc
283
+
221
284
  self.pool = [
222
- pyodbc.connect(f"DRIVER={self.driver};SERVER={self.host};DATABASE={self.database};"
223
- f"UID={self.user};PWD={self.password};ConnectionPooling=Yes")
285
+ pyodbc.connect(
286
+ f"DRIVER={self.driver};SERVER={self.host};DATABASE={self.database};"
287
+ f"UID={self.user};PWD={self.password};ConnectionPooling=Yes"
288
+ )
224
289
  for _ in range(pool_size)
225
290
  ]
226
291
 
227
292
  elif self.db_type == "oracle":
228
- import cx_Oracle
229
- oracle_port = os.getenv("ORACLE_DB_PORT", "1521") # Default Oracle port
230
- dsn = cx_Oracle.makedsn(self.host, oracle_port, self.oracle_sid)
231
- self.pool = cx_Oracle.SessionPool(user=self.user, password=self.password, dsn=dsn,
232
- min=min_conn, max=max_conn, increment=1, threaded=True)
293
+ import oracledb
294
+
295
+ oracle_port = os.getenv("ORACLE_DB_PORT", 1521)
296
+ dsn = oracledb.makedsn(self.host, oracle_port, sid=self.oracle_sid) # type: ignore[arg-type]
297
+ self.pool = oracledb.create_pool(
298
+ user=self.user,
299
+ password=self.password,
300
+ dsn=dsn,
301
+ min=min_conn,
302
+ max=max_conn,
303
+ increment=1,
304
+ )
233
305
 
234
306
  else:
235
307
  raise ValueError(f"Connection pooling not supported for {self.db_type}")
236
308
  except Exception as e:
237
309
  raise ConnectionError(f"Failed to set up connection pool: {e}") from e
238
310
 
239
- def get_connection_from_pool(self):
311
+ def get_connection_from_pool(self) -> Any:
240
312
  """Fetches a connection from the pool."""
241
313
  return self.pool.get_connection()
242
314
 
243
- def return_connection_to_pool(self, connection=None) -> None:
315
+ def return_connection_to_pool(self, connection: Any = None) -> None:
244
316
  """Returns a connection back to the pool."""
245
317
  conn = connection or self.connection
246
318
  if self.pool is None:
247
- raise RuntimeError("No connection pool initialised. Call setup_connection_pool() first.")
319
+ raise RuntimeError(
320
+ "No connection pool initialised. Call setup_connection_pool() first."
321
+ )
248
322
 
249
323
  if self.db_type == "postgres":
250
324
  self.pool.putconn(conn)
@@ -255,22 +329,47 @@ class SQLPyHelper:
255
329
  else:
256
330
  conn.close()
257
331
 
258
- def reconnect(self):
332
+ def reconnect(self) -> None:
259
333
  """Reconnects to the database if connection is lost"""
260
334
  try:
261
335
  self.connection.close()
262
- self.__init__(**self._init_kwargs)
336
+ self.__init__(**self._init_kwargs) # type: ignore[misc]
263
337
  print("Database reconnected successfully.")
264
338
  except Exception as e:
265
339
  raise ConnectionError(f"Reconnection failed: {e}") from e
266
340
 
267
- def begin_transaction(self):
268
- self.execute_query("START TRANSACTION")
341
+ def begin_transaction(self) -> None:
342
+ """Begin an explicit transaction. Works across all supported databases."""
343
+ try:
344
+ if self.db_type == "sqlite":
345
+ self.execute_query("BEGIN")
346
+ elif self.db_type in ("postgres", "mysql"):
347
+ self.execute_query("START TRANSACTION")
348
+ elif self.db_type == "sqlserver":
349
+ self.execute_query("BEGIN TRANSACTION")
350
+ elif self.db_type == "oracle":
351
+ pass # Oracle starts transactions implicitly on first DML statement
352
+ logger.info("Transaction started on %s database", self.db_type)
353
+ except Exception as e:
354
+ raise QueryError(f"Failed to begin transaction: {e}") from e
269
355
 
270
- def rollback_transaction(self):
271
- self.execute_query("ROLLBACK")
356
+ def commit_transaction(self) -> None:
357
+ """Commit the current transaction."""
358
+ try:
359
+ self.connection.commit()
360
+ logger.info("Transaction committed on %s database", self.db_type)
361
+ except Exception as e:
362
+ raise QueryError(f"Failed to commit transaction: {e}") from e
363
+
364
+ def rollback_transaction(self) -> None:
365
+ """Roll back the current transaction."""
366
+ try:
367
+ self.connection.rollback()
368
+ logger.info("Transaction rolled back on %s database", self.db_type)
369
+ except Exception as e:
370
+ raise QueryError(f"Failed to rollback transaction: {e}") from e
272
371
 
273
- def insert_dynamic(self, table, data: dict):
372
+ def insert_dynamic(self, table: str, data: dict[str, Any]) -> None:
274
373
  """
275
374
  Dynamically constructs and executes an INSERT query with database-specific placeholders.
276
375
  """
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SQLPyHelper
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: A simple SQL database helper package for Python.
5
5
  Home-page: https://github.com/adebayopeter/sqlpyhelper
6
6
  Author: Adebayo Olaonipekun
7
7
  Author-email: pekunmi@live.com
8
+ Project-URL: Documentation, https://sqlpyhelper.readthedocs.io/en/latest/
8
9
  Project-URL: Source, https://github.com/adebayopeter/sqlpyhelper
9
10
  Project-URL: Bug Tracker, https://github.com/adebayopeter/sqlpyhelper/issues
10
11
  Project-URL: Changelog, https://github.com/adebayopeter/sqlpyhelper/blob/main/CHANGELOG.md
@@ -32,12 +33,12 @@ Requires-Dist: mysql-connector-python; extra == "mysql"
32
33
  Provides-Extra: sqlserver
33
34
  Requires-Dist: pyodbc; extra == "sqlserver"
34
35
  Provides-Extra: oracle
35
- Requires-Dist: cx_Oracle; extra == "oracle"
36
+ Requires-Dist: oracledb; extra == "oracle"
36
37
  Provides-Extra: all
37
38
  Requires-Dist: psycopg2; extra == "all"
38
39
  Requires-Dist: mysql-connector-python; extra == "all"
39
40
  Requires-Dist: pyodbc; extra == "all"
40
- Requires-Dist: cx_Oracle; extra == "all"
41
+ Requires-Dist: oracledb; extra == "all"
41
42
  Dynamic: author
42
43
  Dynamic: author-email
43
44
  Dynamic: classifier
@@ -59,10 +60,19 @@ Dynamic: summary
59
60
  [![Python versions](https://img.shields.io/pypi/pyversions/sqlpyhelper.svg)](https://pypi.org/project/sqlpyhelper/)
60
61
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/adebayopeter/sqlpyhelper/blob/main/LICENSE)
61
62
  [![GitHub stars](https://img.shields.io/github/stars/adebayopeter/sqlpyhelper?style=social)](https://github.com/adebayopeter/sqlpyhelper)
63
+ [![Documentation](https://readthedocs.org/projects/sqlpyhelper/badge/?version=latest)](https://sqlpyhelper.readthedocs.io/en/latest/)
62
64
 
63
- # 📌 SQLPyHelper v.0.1.4 🚀
65
+ SQLPyHelper is a lightweight Python library that gives you a single, consistent API across **SQLite, PostgreSQL, MySQL, SQL Server, and Oracle** — without the overhead of an ORM.
64
66
 
65
- A Python library for simplified database interactions across **SQLite, PostgreSQL, MySQL, SQL Server, and Oracle**. SQLPyHelper provides an intuitive API for handling queries, connection pooling, transactions, logging, and backups efficiently.
67
+ If you need to run queries, manage transactions, pool connections, or back up tables across multiple database types without learning SQLAlchemy's abstraction layer or wiring up five different drivers manually, SQLPyHelper handles that boilerplate for you.
68
+
69
+ ```python
70
+ # Works identically across all five supported databases
71
+ with SQLPyHelper(db_type="postgres", host="localhost", user="user",
72
+ password="pass", database="mydb") as db:
73
+ db.execute_query("INSERT INTO orders (item) VALUES (%s)", ("Laptop",))
74
+ results = db.fetch_all()
75
+ ```
66
76
 
67
77
  ## 📖 Table of Contents
68
78
  - [🚀 Features](#-features)
@@ -81,7 +91,7 @@ A Python library for simplified database interactions across **SQLite, PostgreSQ
81
91
 
82
92
  ---
83
93
 
84
- ## 🚀 Features in v0.1.3
94
+ ## 🚀 Features in v0.1.4
85
95
  - Unified connection pooling for multiple databases.
86
96
  - Automatic reconnection for lost connections.
87
97
  - Transaction support (BEGIN, ROLLBACK, COMMIT).
@@ -92,10 +102,21 @@ A Python library for simplified database interactions across **SQLite, PostgreSQ
92
102
 
93
103
  ---
94
104
  ## 📦 Installation
95
- #### Install via PyPI:
105
+
106
+ Install the base package (includes SQLite support out of the box):
96
107
  ```sh
97
108
  pip install sqlpyhelper
98
109
  ```
110
+
111
+ Install with your database driver:
112
+ ```sh
113
+ pip install sqlpyhelper[postgres] # PostgreSQL
114
+ pip install sqlpyhelper[mysql] # MySQL
115
+ pip install sqlpyhelper[sqlserver] # SQL Server
116
+ pip install sqlpyhelper[oracle] # Oracle
117
+ pip install sqlpyhelper[all] # All databases
118
+ ```
119
+
99
120
  📌 Package on PyPI: [SQLPyHelper on PyPI](https://pypi.org/project/SQLPyHelper/)
100
121
 
101
122
  For local development:
@@ -208,7 +229,9 @@ db.return_connection_to_pool(conn)
208
229
  | `return_connection_to_pool(conn)` | Returns connection back to pool. |
209
230
  | `begin_transaction()` | Begins an **explicit transaction**. |
210
231
  | `rollback_transaction()` | Rolls back **uncommitted transactions**. |
232
+ | `commit_transaction()` | Commits the current transaction. |
211
233
  | `close()` | Closes the database connection safely. |
234
+ | `__enter__` / `__exit__()` | Use as a context manager — connection closes automatically. |
212
235
 
213
236
  ---
214
237
  ## 🌍 Contributing
@@ -0,0 +1,11 @@
1
+ sqlpyhelper/__init__.py,sha256=7hQ4FRKUaUU_TiGuU1cldnvTmG4S2riZP43SqxA4fUI,183
2
+ sqlpyhelper/automation_utils.py,sha256=pC6pH6bJ-k8iPVeHJ4gUiwEe822dasmKg53ya9bMxyE,5381
3
+ sqlpyhelper/cli.py,sha256=yj0kWJu3oh_JLnmi0L7a5ing2_0x4CQGOKSOhZLAtoY,5646
4
+ sqlpyhelper/db_helper.py,sha256=4DbdBVo86zz1d0hNHtSc4b3Tks7bJGTMTyabsydQyOE,14191
5
+ sqlpyhelper/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ sqlpyhelper-0.1.6.dist-info/licenses/LICENSE,sha256=9XzXxZ_8mWFM9-2TlqyE3L69zvRf4VPY_xIzSj5iU-g,1076
7
+ sqlpyhelper-0.1.6.dist-info/METADATA,sha256=JA0zdZYH5pceNjom3LQQPuUhfGe9kH_6RjXBrKiOi4A,9763
8
+ sqlpyhelper-0.1.6.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ sqlpyhelper-0.1.6.dist-info/entry_points.txt,sha256=uAzSqwkAbbJqQUKHlPNwOebTJVA0FqkOvn2CRP6xSz8,52
10
+ sqlpyhelper-0.1.6.dist-info/top_level.txt,sha256=FrLqTmqTGDa8jHnnf2ZVkYO-gFvLXX9QonpUCE6wKGs,12
11
+ sqlpyhelper-0.1.6.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- sqlpyhelper/__init__.py,sha256=8hopTneD8N4lddcHwD9y-PJHbqElOYw5_aw-HZSMvbo,169
2
- sqlpyhelper/automation_utils.py,sha256=z-9yTdMuOMu1TVUwJ9Q6D3wTrCRh2K6DhCldJ_7MLdI,5227
3
- sqlpyhelper/cli.py,sha256=fJHztOrzufPnQ6agFe7OgepSCNcPT0RcpZw8gg8BgWI,5322
4
- sqlpyhelper/db_helper.py,sha256=Dtpuxa2s67-kitgNQLbAa5JF-AFoN5bC9mLfQ1o4V48,11514
5
- sqlpyhelper/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- sqlpyhelper-0.1.4.dist-info/licenses/LICENSE,sha256=9XzXxZ_8mWFM9-2TlqyE3L69zvRf4VPY_xIzSj5iU-g,1076
7
- sqlpyhelper-0.1.4.dist-info/METADATA,sha256=_gPYGlQmVm6dJS-1z-TYfvWIAKOS4PEsNVzVONUMxnw,8601
8
- sqlpyhelper-0.1.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
- sqlpyhelper-0.1.4.dist-info/entry_points.txt,sha256=uAzSqwkAbbJqQUKHlPNwOebTJVA0FqkOvn2CRP6xSz8,52
10
- sqlpyhelper-0.1.4.dist-info/top_level.txt,sha256=FrLqTmqTGDa8jHnnf2ZVkYO-gFvLXX9QonpUCE6wKGs,12
11
- sqlpyhelper-0.1.4.dist-info/RECORD,,