SQLPyHelper 0.1.3__py3-none-any.whl → 0.1.5__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 +8 -1
- sqlpyhelper/automation_utils.py +156 -0
- sqlpyhelper/cli.py +199 -0
- sqlpyhelper/db_helper.py +259 -85
- sqlpyhelper/py.typed +0 -0
- {sqlpyhelper-0.1.3.dist-info → sqlpyhelper-0.1.5.dist-info}/METADATA +65 -22
- sqlpyhelper-0.1.5.dist-info/RECORD +11 -0
- {sqlpyhelper-0.1.3.dist-info → sqlpyhelper-0.1.5.dist-info}/WHEEL +1 -1
- sqlpyhelper-0.1.5.dist-info/entry_points.txt +2 -0
- sqlpyhelper-0.1.3.dist-info/RECORD +0 -7
- {sqlpyhelper-0.1.3.dist-info → sqlpyhelper-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {sqlpyhelper-0.1.3.dist-info → sqlpyhelper-0.1.5.dist-info}/top_level.txt +0 -0
sqlpyhelper/__init__.py
CHANGED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import os
|
|
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
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AutomationUtils:
|
|
12
|
+
def __init__(self, db=None, **db_kwargs):
|
|
13
|
+
"""
|
|
14
|
+
Optionally accepts db instance or connection parameters like:
|
|
15
|
+
db_type, host, user, password, database, port, driver.
|
|
16
|
+
"""
|
|
17
|
+
self.db = db or SQLPyHelper(**db_kwargs)
|
|
18
|
+
|
|
19
|
+
def backup_database(self, target="local", tag="autobackup"):
|
|
20
|
+
"""
|
|
21
|
+
Backs up the active PostgreSQL database using pg_dump.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
target (str): Backup destination ("local" only for now).
|
|
25
|
+
tag (str): Custom tag for backup file naming.
|
|
26
|
+
"""
|
|
27
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
|
|
28
|
+
filename = f"{tag}_{timestamp}.sql"
|
|
29
|
+
backup_dir = "backups"
|
|
30
|
+
os.makedirs(backup_dir, exist_ok=True)
|
|
31
|
+
filepath = os.path.join(backup_dir, filename)
|
|
32
|
+
|
|
33
|
+
db_name = self.db.database
|
|
34
|
+
user = self.db.user
|
|
35
|
+
host = self.db.host or "localhost"
|
|
36
|
+
port = str(self.db.port or "5432")
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
if self.db.db_type == "sqlite":
|
|
40
|
+
filename2 = f"{tag}_{timestamp}.db"
|
|
41
|
+
sqlite_filepath = os.path.join(backup_dir, filename2)
|
|
42
|
+
shutil.copy2(self.db.database, sqlite_filepath)
|
|
43
|
+
else:
|
|
44
|
+
print(f"📦 Backing up database to {filepath}")
|
|
45
|
+
subprocess.run(
|
|
46
|
+
[
|
|
47
|
+
"pg_dump",
|
|
48
|
+
"-h",
|
|
49
|
+
host,
|
|
50
|
+
"-p",
|
|
51
|
+
port,
|
|
52
|
+
"-U",
|
|
53
|
+
user,
|
|
54
|
+
db_name,
|
|
55
|
+
"-F",
|
|
56
|
+
"c",
|
|
57
|
+
"-f",
|
|
58
|
+
filepath,
|
|
59
|
+
],
|
|
60
|
+
check=True,
|
|
61
|
+
shell=False,
|
|
62
|
+
)
|
|
63
|
+
except subprocess.CalledProcessError as e:
|
|
64
|
+
print("❌ Backup failed:", e)
|
|
65
|
+
|
|
66
|
+
def load_data_from_csv(self, file_path, table_name, if_exists="append"):
|
|
67
|
+
"""
|
|
68
|
+
Loads a CSV file into the specified database table.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
file_path (str): Path to the CSV file.
|
|
72
|
+
table_name (str): Destination table name in the database.
|
|
73
|
+
if_exists (str): 'append' or 'replace'. Default is 'append'.
|
|
74
|
+
"""
|
|
75
|
+
df = pd.read_csv(file_path)
|
|
76
|
+
|
|
77
|
+
if if_exists == "replace":
|
|
78
|
+
self.db.execute_query(f"DROP TABLE IF EXISTS {table_name}")
|
|
79
|
+
|
|
80
|
+
for _, row in df.iterrows():
|
|
81
|
+
self.db.insert_dynamic(table_name, row.to_dict())
|
|
82
|
+
|
|
83
|
+
def detect_missing_periods(self, table, entity_column, date_column):
|
|
84
|
+
"""
|
|
85
|
+
Flags rows where recurring time periods (e.g. monthly) are missing per entity.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
table (str): Table name to query.
|
|
89
|
+
entity_column (str): Column representing entity ID.
|
|
90
|
+
date_column (str): Column representing timestamp/date.
|
|
91
|
+
"""
|
|
92
|
+
if self.db.db_type == "sqlite":
|
|
93
|
+
month_expr = f"strftime('%Y-%m', {date_column})"
|
|
94
|
+
else:
|
|
95
|
+
month_expr = f"DATE_TRUNC('month', {date_column})"
|
|
96
|
+
query = f"""
|
|
97
|
+
SELECT {entity_column}, COUNT(DISTINCT {month_expr}) AS recorded_months
|
|
98
|
+
FROM {table}
|
|
99
|
+
GROUP BY {entity_column}
|
|
100
|
+
HAVING COUNT(DISTINCT {month_expr}) < 12
|
|
101
|
+
"""
|
|
102
|
+
self.db.execute_query(query)
|
|
103
|
+
return self.db.fetch_all()
|
|
104
|
+
|
|
105
|
+
def aggregate_column(
|
|
106
|
+
self, table, value_column, group_column=None, time_column=None
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Computes sum of any value column grouped by entity or month.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
table (str): Table name.
|
|
113
|
+
value_column (str): Numeric column to aggregate.
|
|
114
|
+
group_column (str, optional): Entity or category to group by.
|
|
115
|
+
time_column (str, optional): Timestamp to extract month grouping.
|
|
116
|
+
"""
|
|
117
|
+
if self.db.db_type == "sqlite":
|
|
118
|
+
month_expr = f"strftime('%Y-%m', {time_column})"
|
|
119
|
+
else:
|
|
120
|
+
month_expr = f"DATE_TRUNC('month', {time_column})"
|
|
121
|
+
|
|
122
|
+
if group_column and time_column:
|
|
123
|
+
query = f"""
|
|
124
|
+
SELECT {group_column}, {month_expr} AS month, SUM({value_column}) AS total
|
|
125
|
+
FROM {table}
|
|
126
|
+
GROUP BY {group_column}, month
|
|
127
|
+
ORDER BY month
|
|
128
|
+
"""
|
|
129
|
+
else:
|
|
130
|
+
query = f"SELECT SUM({value_column}) FROM {table}"
|
|
131
|
+
|
|
132
|
+
self.db.execute_query(query)
|
|
133
|
+
return self.db.fetch_all()
|
|
134
|
+
|
|
135
|
+
def detect_outliers(self, table, numeric_column, threshold=2):
|
|
136
|
+
"""
|
|
137
|
+
Detects statistical outliers based on deviation from mean.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
table (str): Table name.
|
|
141
|
+
numeric_column (str): Column to analyze.
|
|
142
|
+
threshold (int): Number of standard deviations from mean to flag as outlier.
|
|
143
|
+
"""
|
|
144
|
+
query = f"""
|
|
145
|
+
SELECT *, {numeric_column} AS value FROM {table}
|
|
146
|
+
"""
|
|
147
|
+
self.db.execute_query(query)
|
|
148
|
+
data = pd.DataFrame(
|
|
149
|
+
self.db.fetch_all(),
|
|
150
|
+
columns=[desc[0] for desc in self.db.cursor.description],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
mean_val = data["value"].mean()
|
|
154
|
+
std_val = data["value"].std()
|
|
155
|
+
outliers = data[abs(data["value"] - mean_val) > threshold * std_val]
|
|
156
|
+
return outliers.values.tolist()
|
sqlpyhelper/cli.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from sqlpyhelper.automation_utils import AutomationUtils
|
|
4
|
+
from sqlpyhelper.db_helper import SQLPyHelper
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
def cli():
|
|
9
|
+
"""SQLPyHelper Command Line Interface"""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@cli.command()
|
|
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")
|
|
20
|
+
def run_query(db_type, host, user, password, database, query):
|
|
21
|
+
"""Run a single SQL query and print results"""
|
|
22
|
+
db = SQLPyHelper(
|
|
23
|
+
db_type=db_type, host=host, user=user, password=password, database=database
|
|
24
|
+
)
|
|
25
|
+
results = db.execute_query(query)
|
|
26
|
+
for row in results:
|
|
27
|
+
click.echo(row)
|
|
28
|
+
db.close()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@cli.command()
|
|
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)
|
|
37
|
+
def interactive_shell(db_type, host, user, password, database):
|
|
38
|
+
"""Launch an interactive SQL shell"""
|
|
39
|
+
db = SQLPyHelper(
|
|
40
|
+
db_type=db_type, host=host, user=user, password=password, database=database
|
|
41
|
+
)
|
|
42
|
+
click.echo("Interactive shell started. Type your SQL query or 'exit'")
|
|
43
|
+
while True:
|
|
44
|
+
query = input("sqlpy> ")
|
|
45
|
+
if query.lower() in ("exit", "quit"):
|
|
46
|
+
break
|
|
47
|
+
try:
|
|
48
|
+
db.execute_query(query)
|
|
49
|
+
results = db.fetch_all()
|
|
50
|
+
for row in results:
|
|
51
|
+
click.echo(row)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
click.echo(f"Error: {e}")
|
|
54
|
+
db.close()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@cli.command()
|
|
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")
|
|
66
|
+
def backup(target, tag, db_type, host, user, password, database, port):
|
|
67
|
+
"""Create a timestamped backup of the connected database."""
|
|
68
|
+
utils = AutomationUtils(
|
|
69
|
+
db_type=db_type,
|
|
70
|
+
host=host,
|
|
71
|
+
user=user,
|
|
72
|
+
password=password,
|
|
73
|
+
database=database,
|
|
74
|
+
port=port,
|
|
75
|
+
)
|
|
76
|
+
utils.backup_database(target=target, tag=tag)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@cli.command()
|
|
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")
|
|
94
|
+
def load_data(file, table, if_exists, db_type, host, user, password, database, port):
|
|
95
|
+
"""Load data from CSV into database table."""
|
|
96
|
+
utils = AutomationUtils(
|
|
97
|
+
db_type=db_type,
|
|
98
|
+
host=host,
|
|
99
|
+
user=user,
|
|
100
|
+
password=password,
|
|
101
|
+
database=database,
|
|
102
|
+
port=port,
|
|
103
|
+
)
|
|
104
|
+
utils.load_data_from_csv(file, table, if_exists=if_exists)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@cli.command()
|
|
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
|
+
):
|
|
120
|
+
"""Flag entities with fewer than 12 months of activity."""
|
|
121
|
+
utils = AutomationUtils(
|
|
122
|
+
db_type=db_type,
|
|
123
|
+
host=host,
|
|
124
|
+
user=user,
|
|
125
|
+
password=password,
|
|
126
|
+
database=database,
|
|
127
|
+
port=port,
|
|
128
|
+
)
|
|
129
|
+
results = utils.detect_missing_periods(table, entity_column, date_column)
|
|
130
|
+
for row in results:
|
|
131
|
+
click.echo(row)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@cli.command()
|
|
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
|
+
):
|
|
157
|
+
"""Aggregate numeric column optionally grouped by entity and time."""
|
|
158
|
+
utils = AutomationUtils(
|
|
159
|
+
db_type=db_type,
|
|
160
|
+
host=host,
|
|
161
|
+
user=user,
|
|
162
|
+
password=password,
|
|
163
|
+
database=database,
|
|
164
|
+
port=port,
|
|
165
|
+
)
|
|
166
|
+
results = utils.aggregate_column(table, value_column, group_column, time_column)
|
|
167
|
+
for row in results:
|
|
168
|
+
click.echo(row)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@cli.command()
|
|
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
|
+
):
|
|
184
|
+
"""Flag rows where values deviate statistically from average."""
|
|
185
|
+
utils = AutomationUtils(
|
|
186
|
+
db_type=db_type,
|
|
187
|
+
host=host,
|
|
188
|
+
user=user,
|
|
189
|
+
password=password,
|
|
190
|
+
database=database,
|
|
191
|
+
port=port,
|
|
192
|
+
)
|
|
193
|
+
results = utils.detect_outliers(table, numeric_column, threshold)
|
|
194
|
+
for row in results:
|
|
195
|
+
click.echo(row)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
cli()
|
sqlpyhelper/db_helper.py
CHANGED
|
@@ -1,53 +1,141 @@
|
|
|
1
1
|
import csv
|
|
2
|
-
|
|
2
|
+
import logging
|
|
3
3
|
import os
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
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")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _validate_identifier(name: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Validate a SQL identifier (table or column name).
|
|
22
|
+
Allows only alphanumeric characters and underscores.
|
|
23
|
+
Raises ValueError for anything else, preventing SQL injection via identifiers.
|
|
24
|
+
"""
|
|
25
|
+
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name):
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"Invalid SQL identifier: {name!r}. "
|
|
28
|
+
"Only letters, digits, and underscores are allowed."
|
|
29
|
+
)
|
|
30
|
+
return name
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SQLPyHelperError(Exception):
|
|
34
|
+
"""Base exception for SQLPyHelper errors."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConnectionError(SQLPyHelperError):
|
|
38
|
+
"""Raised when a database connection fails."""
|
|
39
|
+
|
|
4
40
|
|
|
5
|
-
|
|
41
|
+
class QueryError(SQLPyHelperError):
|
|
42
|
+
"""Raised when a query fails to execute."""
|
|
6
43
|
|
|
7
44
|
|
|
8
|
-
|
|
9
|
-
"""
|
|
10
|
-
with open("query_log.txt", "a") as f:
|
|
11
|
-
f.write(query + "\n")
|
|
45
|
+
class BackupError(SQLPyHelperError):
|
|
46
|
+
"""Raised when a backup operation fails."""
|
|
12
47
|
|
|
13
48
|
|
|
14
49
|
class SQLPyHelper:
|
|
15
|
-
def __init__(
|
|
16
|
-
self
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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:
|
|
61
|
+
|
|
62
|
+
# Store original params so reconnect() can replay them
|
|
63
|
+
self._init_kwargs = {
|
|
64
|
+
"db_type": db_type,
|
|
65
|
+
"host": host,
|
|
66
|
+
"user": user,
|
|
67
|
+
"password": password,
|
|
68
|
+
"database": database,
|
|
69
|
+
"driver": driver,
|
|
70
|
+
"port": port,
|
|
71
|
+
"oracle_sid": oracle_sid,
|
|
72
|
+
}
|
|
73
|
+
|
|
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
|
|
83
|
+
|
|
84
|
+
if not self.db_type or not self.database:
|
|
85
|
+
raise ValueError("Missing required database configuration.")
|
|
24
86
|
|
|
25
87
|
if self.db_type == "sqlite":
|
|
26
88
|
import sqlite3
|
|
89
|
+
|
|
27
90
|
self.connection = sqlite3.connect(self.database)
|
|
28
91
|
elif self.db_type == "postgres":
|
|
29
92
|
import psycopg2
|
|
30
|
-
|
|
31
|
-
|
|
93
|
+
|
|
94
|
+
self.connection = psycopg2.connect(
|
|
95
|
+
host=self.host,
|
|
96
|
+
user=self.user,
|
|
97
|
+
password=self.password,
|
|
98
|
+
dbname=self.database,
|
|
99
|
+
)
|
|
32
100
|
elif self.db_type == "mysql":
|
|
33
101
|
import mysql.connector
|
|
34
|
-
|
|
35
|
-
|
|
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]
|
|
36
109
|
elif self.db_type == "sqlserver":
|
|
37
110
|
import pyodbc
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
)
|
|
40
116
|
elif self.db_type == "oracle":
|
|
41
|
-
import
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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]
|
|
45
126
|
else:
|
|
46
127
|
raise ValueError("Unsupported database type")
|
|
47
128
|
|
|
48
129
|
self.cursor = self.connection.cursor()
|
|
49
130
|
|
|
50
|
-
def
|
|
131
|
+
def __enter__(self) -> "SQLPyHelper":
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Literal[False]:
|
|
135
|
+
self.close()
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def execute_query(self, query: str, params: Optional[tuple] = None) -> None:
|
|
51
139
|
"""Executes a query with optional parameters"""
|
|
52
140
|
try:
|
|
53
141
|
if params:
|
|
@@ -56,154 +144,240 @@ class SQLPyHelper:
|
|
|
56
144
|
self.cursor.execute(query)
|
|
57
145
|
self.connection.commit()
|
|
58
146
|
except Exception as e:
|
|
59
|
-
if "server has gone away" in str(
|
|
147
|
+
if "server has gone away" in str(
|
|
148
|
+
e
|
|
149
|
+
): # Example check for MySQL lost connection
|
|
60
150
|
self.reconnect()
|
|
61
|
-
self.cursor.execute(query, params)
|
|
151
|
+
self.cursor.execute(query, params) # type: ignore[arg-type]
|
|
62
152
|
self.connection.commit()
|
|
63
153
|
else:
|
|
64
|
-
|
|
154
|
+
raise QueryError(f"Query failed: {e}") from e
|
|
65
155
|
|
|
66
|
-
def fetch_one(self):
|
|
156
|
+
def fetch_one(self) -> Optional[tuple]:
|
|
67
157
|
"""Fetches a single row"""
|
|
68
158
|
try:
|
|
69
159
|
return self.cursor.fetchone()
|
|
70
160
|
except Exception as e:
|
|
71
|
-
|
|
72
|
-
return None
|
|
161
|
+
raise QueryError(f"Failed to fetch row: {e}") from e
|
|
73
162
|
|
|
74
|
-
def fetch_all(self):
|
|
163
|
+
def fetch_all(self) -> list[tuple]:
|
|
75
164
|
"""Fetches all rows from the last executed query"""
|
|
76
165
|
try:
|
|
77
166
|
return self.cursor.fetchall()
|
|
78
167
|
except Exception as e:
|
|
79
|
-
|
|
80
|
-
return None
|
|
168
|
+
raise QueryError(f"Failed to fetch rows: {e}") from e
|
|
81
169
|
|
|
82
|
-
def fetch_by_param(
|
|
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."""
|
|
83
174
|
try:
|
|
84
|
-
|
|
175
|
+
table_name = _validate_identifier(table_name)
|
|
176
|
+
column_name = _validate_identifier(column_name)
|
|
177
|
+
placeholder = "?" if self.db_type == "sqlite" else "%s"
|
|
178
|
+
query = f"SELECT * FROM {table_name} WHERE {column_name} = {placeholder}"
|
|
85
179
|
self.cursor.execute(query, (value,))
|
|
86
180
|
return self.cursor.fetchall()
|
|
87
181
|
except Exception as e:
|
|
88
|
-
|
|
89
|
-
return None
|
|
182
|
+
raise QueryError(f"Failed to fetch by param: {e}") from e
|
|
90
183
|
|
|
91
|
-
def close(self):
|
|
92
|
-
"""Closes the connection"""
|
|
184
|
+
def close(self) -> None:
|
|
185
|
+
"""Closes the cursor and database connection."""
|
|
93
186
|
try:
|
|
94
187
|
self.cursor.close()
|
|
95
188
|
self.connection.close()
|
|
96
189
|
except Exception as e:
|
|
97
|
-
|
|
98
|
-
return None
|
|
190
|
+
raise ConnectionError(f"Failed to close connection: {e}") from e
|
|
99
191
|
|
|
100
|
-
def create_table(self, table_name, columns):
|
|
192
|
+
def create_table(self, table_name: str, columns: dict[str, str]) -> None:
|
|
101
193
|
"""
|
|
102
194
|
Creates a table dynamically using a dictionary format.
|
|
103
195
|
Example:
|
|
104
196
|
columns = {'id': 'INTEGER PRIMARY KEY', 'name': 'TEXT', 'age': 'INTEGER'}
|
|
105
197
|
"""
|
|
106
198
|
try:
|
|
107
|
-
|
|
108
|
-
|
|
199
|
+
table_name = _validate_identifier(table_name)
|
|
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
|
+
)
|
|
206
|
+
query = f"CREATE TABLE IF NOT EXISTS {table_name} ({columns_def})"
|
|
109
207
|
self.execute_query(query)
|
|
110
208
|
except Exception as e:
|
|
111
|
-
|
|
112
|
-
return None
|
|
209
|
+
raise QueryError(f"Failed to create table: {e}") from e
|
|
113
210
|
|
|
114
|
-
def insert_bulk(self, table_name, data):
|
|
211
|
+
def insert_bulk(self, table_name: str, data: list[dict[str, Any]]) -> None:
|
|
115
212
|
"""
|
|
116
213
|
Inserts multiple rows at once.
|
|
117
214
|
Example:
|
|
118
215
|
data = [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
|
|
119
216
|
"""
|
|
120
217
|
try:
|
|
121
|
-
|
|
122
|
-
|
|
218
|
+
table_name = _validate_identifier(table_name)
|
|
219
|
+
col_names = [_validate_identifier(col) for col in data[0].keys()]
|
|
220
|
+
columns = ", ".join(col_names)
|
|
221
|
+
placeholder = "?" if self.db_type == "sqlite" else "%s"
|
|
222
|
+
placeholders = ", ".join([placeholder] * len(data[0]))
|
|
123
223
|
query = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
|
|
124
224
|
values = [tuple(row.values()) for row in data]
|
|
125
225
|
self.cursor.executemany(query, values)
|
|
126
226
|
self.connection.commit()
|
|
127
227
|
|
|
128
228
|
except Exception as e:
|
|
129
|
-
|
|
130
|
-
return None
|
|
229
|
+
raise QueryError(f"Failed to insert bulk rows: {e}") from e
|
|
131
230
|
|
|
132
|
-
def backup_table(self, table_name, backup_file):
|
|
231
|
+
def backup_table(self, table_name: str, backup_file: str) -> None:
|
|
133
232
|
"""
|
|
134
233
|
Exports table data into a CSV file.
|
|
135
234
|
Example:
|
|
136
235
|
backup_table('users', 'users_backup.csv')
|
|
137
236
|
"""
|
|
138
237
|
try:
|
|
238
|
+
table_name = _validate_identifier(table_name)
|
|
139
239
|
query = f"SELECT * FROM {table_name}"
|
|
140
240
|
self.execute_query(query)
|
|
141
241
|
rows = self.fetch_all()
|
|
142
242
|
|
|
143
243
|
with open(backup_file, mode="w", newline="") as file:
|
|
144
244
|
writer = csv.writer(file)
|
|
145
|
-
writer.writerow(
|
|
245
|
+
writer.writerow(
|
|
246
|
+
[desc[0] for desc in self.cursor.description]
|
|
247
|
+
) # Column headers
|
|
146
248
|
writer.writerows(rows)
|
|
147
249
|
except Exception as e:
|
|
148
|
-
|
|
149
|
-
return None
|
|
250
|
+
raise BackupError(f"Failed to backup table: {e}") from e
|
|
150
251
|
|
|
151
|
-
def setup_connection_pool(
|
|
252
|
+
def setup_connection_pool(
|
|
253
|
+
self, min_conn: int = 1, max_conn: int = 5, pool_size: int = 5
|
|
254
|
+
) -> None:
|
|
152
255
|
"""Sets up connection pooling based on the database type"""
|
|
153
256
|
try:
|
|
154
257
|
if self.db_type == "postgres":
|
|
155
258
|
from psycopg2 import pool
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
)
|
|
159
268
|
|
|
160
269
|
elif self.db_type == "mysql":
|
|
161
270
|
import mysql.connector.pooling
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
)
|
|
166
280
|
|
|
167
281
|
elif self.db_type == "sqlserver":
|
|
168
282
|
import pyodbc
|
|
283
|
+
|
|
169
284
|
self.pool = [
|
|
170
|
-
pyodbc.connect(
|
|
171
|
-
|
|
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
|
+
)
|
|
172
289
|
for _ in range(pool_size)
|
|
173
290
|
]
|
|
174
291
|
|
|
175
292
|
elif self.db_type == "oracle":
|
|
176
|
-
import
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
)
|
|
181
305
|
|
|
182
306
|
else:
|
|
183
307
|
raise ValueError(f"Connection pooling not supported for {self.db_type}")
|
|
184
308
|
except Exception as e:
|
|
185
|
-
|
|
186
|
-
self.pool = None # Prevent broken pool usage
|
|
309
|
+
raise ConnectionError(f"Failed to set up connection pool: {e}") from e
|
|
187
310
|
|
|
188
|
-
def get_connection_from_pool(self):
|
|
311
|
+
def get_connection_from_pool(self) -> Any:
|
|
189
312
|
"""Fetches a connection from the pool."""
|
|
190
313
|
return self.pool.get_connection()
|
|
191
314
|
|
|
192
|
-
def return_connection_to_pool(self):
|
|
315
|
+
def return_connection_to_pool(self, connection: Any = None) -> None:
|
|
193
316
|
"""Returns a connection back to the pool."""
|
|
194
|
-
self.connection
|
|
317
|
+
conn = connection or self.connection
|
|
318
|
+
if self.pool is None:
|
|
319
|
+
raise RuntimeError(
|
|
320
|
+
"No connection pool initialised. Call setup_connection_pool() first."
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if self.db_type == "postgres":
|
|
324
|
+
self.pool.putconn(conn)
|
|
325
|
+
elif self.db_type == "mysql":
|
|
326
|
+
conn.close()
|
|
327
|
+
elif self.db_type == "oracle":
|
|
328
|
+
self.pool.release(conn)
|
|
329
|
+
else:
|
|
330
|
+
conn.close()
|
|
195
331
|
|
|
196
|
-
def reconnect(self):
|
|
332
|
+
def reconnect(self) -> None:
|
|
197
333
|
"""Reconnects to the database if connection is lost"""
|
|
198
334
|
try:
|
|
199
|
-
self.connection.close()
|
|
200
|
-
self.__init__() #
|
|
335
|
+
self.connection.close()
|
|
336
|
+
self.__init__(**self._init_kwargs) # type: ignore[misc]
|
|
201
337
|
print("Database reconnected successfully.")
|
|
202
338
|
except Exception as e:
|
|
203
|
-
|
|
339
|
+
raise ConnectionError(f"Reconnection failed: {e}") from e
|
|
204
340
|
|
|
205
|
-
def begin_transaction(self):
|
|
206
|
-
|
|
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
|
|
355
|
+
|
|
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
|
|
371
|
+
|
|
372
|
+
def insert_dynamic(self, table: str, data: dict[str, Any]) -> None:
|
|
373
|
+
"""
|
|
374
|
+
Dynamically constructs and executes an INSERT query with database-specific placeholders.
|
|
375
|
+
"""
|
|
376
|
+
table = _validate_identifier(table)
|
|
377
|
+
columns = ", ".join(_validate_identifier(col) for col in data.keys())
|
|
378
|
+
placeholders_style = "?" if self.db_type == "sqlite" else "%s"
|
|
379
|
+
placeholders = ", ".join([placeholders_style] * len(data))
|
|
380
|
+
values = tuple(data.values())
|
|
207
381
|
|
|
208
|
-
|
|
209
|
-
self.execute_query(
|
|
382
|
+
sql = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
|
|
383
|
+
self.execute_query(sql, values)
|
sqlpyhelper/py.typed
ADDED
|
File without changes
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SQLPyHelper
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: A simple SQL database helper package for Python.
|
|
5
|
+
Home-page: https://github.com/adebayopeter/sqlpyhelper
|
|
5
6
|
Author: Adebayo Olaonipekun
|
|
6
7
|
Author-email: pekunmi@live.com
|
|
8
|
+
Project-URL: Source, https://github.com/adebayopeter/sqlpyhelper
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/adebayopeter/sqlpyhelper/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/adebayopeter/sqlpyhelper/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: database,sql,sqlite,postgresql,mysql,sqlserver,oracle,db,query,helper
|
|
7
12
|
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier:
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
19
|
Classifier: Intended Audience :: Developers
|
|
10
20
|
Classifier: Topic :: Database :: Database Engines/Servers
|
|
11
21
|
Classifier: Operating System :: OS Independent
|
|
@@ -13,34 +23,54 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
13
23
|
Requires-Python: >=3.8
|
|
14
24
|
Description-Content-Type: text/markdown
|
|
15
25
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: psycopg2
|
|
17
|
-
Requires-Dist: mysql-connector-python
|
|
18
|
-
Requires-Dist: pyodbc
|
|
19
|
-
Requires-Dist: cx_Oracle
|
|
20
26
|
Requires-Dist: python-dotenv
|
|
21
|
-
|
|
22
|
-
Requires-Dist: mysql-connector-python; extra == "mysql"
|
|
27
|
+
Requires-Dist: click
|
|
23
28
|
Provides-Extra: postgres
|
|
24
29
|
Requires-Dist: psycopg2; extra == "postgres"
|
|
25
|
-
Provides-Extra:
|
|
26
|
-
Requires-Dist:
|
|
30
|
+
Provides-Extra: mysql
|
|
31
|
+
Requires-Dist: mysql-connector-python; extra == "mysql"
|
|
27
32
|
Provides-Extra: sqlserver
|
|
28
33
|
Requires-Dist: pyodbc; extra == "sqlserver"
|
|
29
|
-
Provides-Extra:
|
|
34
|
+
Provides-Extra: oracle
|
|
35
|
+
Requires-Dist: oracledb; extra == "oracle"
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Requires-Dist: psycopg2; extra == "all"
|
|
38
|
+
Requires-Dist: mysql-connector-python; extra == "all"
|
|
39
|
+
Requires-Dist: pyodbc; extra == "all"
|
|
40
|
+
Requires-Dist: oracledb; extra == "all"
|
|
30
41
|
Dynamic: author
|
|
31
42
|
Dynamic: author-email
|
|
32
43
|
Dynamic: classifier
|
|
33
44
|
Dynamic: description
|
|
34
45
|
Dynamic: description-content-type
|
|
46
|
+
Dynamic: home-page
|
|
47
|
+
Dynamic: keywords
|
|
35
48
|
Dynamic: license-file
|
|
49
|
+
Dynamic: project-url
|
|
36
50
|
Dynamic: provides-extra
|
|
37
51
|
Dynamic: requires-dist
|
|
38
52
|
Dynamic: requires-python
|
|
39
53
|
Dynamic: summary
|
|
40
54
|
|
|
41
|
-
#
|
|
55
|
+
# SQLPyHelper
|
|
56
|
+
|
|
57
|
+
[](https://pypi.org/project/sqlpyhelper/)
|
|
58
|
+
[](https://pypi.org/project/sqlpyhelper/)
|
|
59
|
+
[](https://pypi.org/project/sqlpyhelper/)
|
|
60
|
+
[](https://github.com/adebayopeter/sqlpyhelper/blob/main/LICENSE)
|
|
61
|
+
[](https://github.com/adebayopeter/sqlpyhelper)
|
|
62
|
+
|
|
63
|
+
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.
|
|
42
64
|
|
|
43
|
-
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# Works identically across all five supported databases
|
|
69
|
+
with SQLPyHelper(db_type="postgres", host="localhost", user="user",
|
|
70
|
+
password="pass", database="mydb") as db:
|
|
71
|
+
db.execute_query("INSERT INTO orders (item) VALUES (%s)", ("Laptop",))
|
|
72
|
+
results = db.fetch_all()
|
|
73
|
+
```
|
|
44
74
|
|
|
45
75
|
## 📖 Table of Contents
|
|
46
76
|
- [🚀 Features](#-features)
|
|
@@ -59,21 +89,32 @@ A Python library for simplified database interactions across **SQLite, PostgreSQ
|
|
|
59
89
|
|
|
60
90
|
---
|
|
61
91
|
|
|
62
|
-
## 🚀 Features in v0.1.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
92
|
+
## 🚀 Features in v0.1.4
|
|
93
|
+
- Unified connection pooling for multiple databases.
|
|
94
|
+
- Automatic reconnection for lost connections.
|
|
95
|
+
- Transaction support (BEGIN, ROLLBACK, COMMIT).
|
|
96
|
+
- Secure parameterized queries to prevent SQL injection.
|
|
97
|
+
- Bulk insertion & dynamic table creation.
|
|
98
|
+
- Logging & error handling for better debugging.
|
|
99
|
+
- CSV export & database backups.
|
|
70
100
|
|
|
71
101
|
---
|
|
72
102
|
## 📦 Installation
|
|
73
|
-
|
|
103
|
+
|
|
104
|
+
Install the base package (includes SQLite support out of the box):
|
|
74
105
|
```sh
|
|
75
106
|
pip install sqlpyhelper
|
|
76
107
|
```
|
|
108
|
+
|
|
109
|
+
Install with your database driver:
|
|
110
|
+
```sh
|
|
111
|
+
pip install sqlpyhelper[postgres] # PostgreSQL
|
|
112
|
+
pip install sqlpyhelper[mysql] # MySQL
|
|
113
|
+
pip install sqlpyhelper[sqlserver] # SQL Server
|
|
114
|
+
pip install sqlpyhelper[oracle] # Oracle
|
|
115
|
+
pip install sqlpyhelper[all] # All databases
|
|
116
|
+
```
|
|
117
|
+
|
|
77
118
|
📌 Package on PyPI: [SQLPyHelper on PyPI](https://pypi.org/project/SQLPyHelper/)
|
|
78
119
|
|
|
79
120
|
For local development:
|
|
@@ -186,7 +227,9 @@ db.return_connection_to_pool(conn)
|
|
|
186
227
|
| `return_connection_to_pool(conn)` | Returns connection back to pool. |
|
|
187
228
|
| `begin_transaction()` | Begins an **explicit transaction**. |
|
|
188
229
|
| `rollback_transaction()` | Rolls back **uncommitted transactions**. |
|
|
230
|
+
| `commit_transaction()` | Commits the current transaction. |
|
|
189
231
|
| `close()` | Closes the database connection safely. |
|
|
232
|
+
| `__enter__` / `__exit__()` | Use as a context manager — connection closes automatically. |
|
|
190
233
|
|
|
191
234
|
---
|
|
192
235
|
## 🌍 Contributing
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
sqlpyhelper/__init__.py,sha256=nkFTrXkOtL4V7KVPw_bdjl5afqWroUG81WRVdCm0-NQ,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.5.dist-info/licenses/LICENSE,sha256=9XzXxZ_8mWFM9-2TlqyE3L69zvRf4VPY_xIzSj5iU-g,1076
|
|
7
|
+
sqlpyhelper-0.1.5.dist-info/METADATA,sha256=s_qc18w4ulfBx46s9azplWDBZsFUSvqnTVDw_32EnkE,9555
|
|
8
|
+
sqlpyhelper-0.1.5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
sqlpyhelper-0.1.5.dist-info/entry_points.txt,sha256=uAzSqwkAbbJqQUKHlPNwOebTJVA0FqkOvn2CRP6xSz8,52
|
|
10
|
+
sqlpyhelper-0.1.5.dist-info/top_level.txt,sha256=FrLqTmqTGDa8jHnnf2ZVkYO-gFvLXX9QonpUCE6wKGs,12
|
|
11
|
+
sqlpyhelper-0.1.5.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
sqlpyhelper/__init__.py,sha256=v4m1w8m6t3Ypvteu4Ukto47uZG_pAHHkBFcKjWrk__Q,54
|
|
2
|
-
sqlpyhelper/db_helper.py,sha256=hRgwzPM5Ze_xKwgphQLJovQmkVpVhKffmAa1C7Lje9g,8447
|
|
3
|
-
sqlpyhelper-0.1.3.dist-info/licenses/LICENSE,sha256=9XzXxZ_8mWFM9-2TlqyE3L69zvRf4VPY_xIzSj5iU-g,1076
|
|
4
|
-
sqlpyhelper-0.1.3.dist-info/METADATA,sha256=zYkK-eNm3BBm4T0bStWMSWdIR44pyWQgJOjUHh6lnQ8,7253
|
|
5
|
-
sqlpyhelper-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
sqlpyhelper-0.1.3.dist-info/top_level.txt,sha256=FrLqTmqTGDa8jHnnf2ZVkYO-gFvLXX9QonpUCE6wKGs,12
|
|
7
|
-
sqlpyhelper-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|