SQLPyHelper 0.1.4__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 +4 -4
- sqlpyhelper/automation_utils.py +26 -15
- sqlpyhelper/cli.py +120 -67
- sqlpyhelper/db_helper.py +164 -65
- {sqlpyhelper-0.1.4.dist-info → sqlpyhelper-0.1.5.dist-info}/METADATA +28 -7
- sqlpyhelper-0.1.5.dist-info/RECORD +11 -0
- sqlpyhelper-0.1.4.dist-info/RECORD +0 -11
- {sqlpyhelper-0.1.4.dist-info → sqlpyhelper-0.1.5.dist-info}/WHEEL +0 -0
- {sqlpyhelper-0.1.4.dist-info → sqlpyhelper-0.1.5.dist-info}/entry_points.txt +0 -0
- {sqlpyhelper-0.1.4.dist-info → sqlpyhelper-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {sqlpyhelper-0.1.4.dist-info → sqlpyhelper-0.1.5.dist-info}/top_level.txt +0 -0
sqlpyhelper/__init__.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Match the version in setup.py
|
|
2
|
-
__version__ = "0.1.
|
|
2
|
+
__version__ = "0.1.5"
|
|
3
3
|
|
|
4
|
-
from sqlpyhelper.db_helper import (
|
|
5
|
-
|
|
4
|
+
from sqlpyhelper.db_helper import ( # noqa: F401
|
|
5
|
+
BackupError,
|
|
6
6
|
ConnectionError,
|
|
7
7
|
QueryError,
|
|
8
|
-
|
|
8
|
+
SQLPyHelperError,
|
|
9
9
|
)
|
sqlpyhelper/automation_utils.py
CHANGED
|
@@ -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",
|
|
47
|
-
|
|
48
|
-
"-
|
|
48
|
+
"-h",
|
|
49
|
+
host,
|
|
50
|
+
"-p",
|
|
51
|
+
port,
|
|
52
|
+
"-U",
|
|
53
|
+
user,
|
|
49
54
|
db_name,
|
|
50
|
-
"-F",
|
|
51
|
-
"
|
|
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 ==
|
|
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(
|
|
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 ==
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
14
|
-
@click.option(
|
|
15
|
-
@click.option(
|
|
16
|
-
@click.option(
|
|
17
|
-
@click.option(
|
|
18
|
-
@click.option(
|
|
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(
|
|
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(
|
|
30
|
-
@click.option(
|
|
31
|
-
@click.option(
|
|
32
|
-
@click.option(
|
|
33
|
-
@click.option(
|
|
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(
|
|
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(
|
|
54
|
-
@click.option(
|
|
55
|
-
@click.option(
|
|
56
|
-
@click.option(
|
|
57
|
-
@click.option(
|
|
58
|
-
@click.option(
|
|
59
|
-
@click.option(
|
|
60
|
-
@click.option(
|
|
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(
|
|
76
|
-
@click.option(
|
|
77
|
-
@click.option(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@click.option(
|
|
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(
|
|
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(
|
|
92
|
-
@click.option(
|
|
93
|
-
@click.option(
|
|
94
|
-
@click.option(
|
|
95
|
-
@click.option(
|
|
96
|
-
@click.option(
|
|
97
|
-
@click.option(
|
|
98
|
-
@click.option(
|
|
99
|
-
@click.option(
|
|
100
|
-
def detect_missing_periods(
|
|
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(
|
|
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(
|
|
110
|
-
@click.option(
|
|
111
|
-
@click.option(
|
|
112
|
-
@click.option(
|
|
113
|
-
@click.option(
|
|
114
|
-
@click.option(
|
|
115
|
-
@click.option(
|
|
116
|
-
@click.option(
|
|
117
|
-
@click.option(
|
|
118
|
-
@click.option(
|
|
119
|
-
def aggregate(
|
|
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(
|
|
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(
|
|
129
|
-
@click.option(
|
|
130
|
-
@click.option(
|
|
131
|
-
@click.option(
|
|
132
|
-
@click.option(
|
|
133
|
-
@click.option(
|
|
134
|
-
@click.option(
|
|
135
|
-
@click.option(
|
|
136
|
-
@click.option(
|
|
137
|
-
def detect_outliers(
|
|
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(
|
|
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
|
-
|
|
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()
|
|
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
|
|
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__(
|
|
41
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
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(
|
|
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 = {
|
|
158
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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(
|
|
223
|
-
|
|
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
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
271
|
-
|
|
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,6 +1,6 @@
|
|
|
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
5
|
Home-page: https://github.com/adebayopeter/sqlpyhelper
|
|
6
6
|
Author: Adebayo Olaonipekun
|
|
@@ -32,12 +32,12 @@ Requires-Dist: mysql-connector-python; extra == "mysql"
|
|
|
32
32
|
Provides-Extra: sqlserver
|
|
33
33
|
Requires-Dist: pyodbc; extra == "sqlserver"
|
|
34
34
|
Provides-Extra: oracle
|
|
35
|
-
Requires-Dist:
|
|
35
|
+
Requires-Dist: oracledb; extra == "oracle"
|
|
36
36
|
Provides-Extra: all
|
|
37
37
|
Requires-Dist: psycopg2; extra == "all"
|
|
38
38
|
Requires-Dist: mysql-connector-python; extra == "all"
|
|
39
39
|
Requires-Dist: pyodbc; extra == "all"
|
|
40
|
-
Requires-Dist:
|
|
40
|
+
Requires-Dist: oracledb; extra == "all"
|
|
41
41
|
Dynamic: author
|
|
42
42
|
Dynamic: author-email
|
|
43
43
|
Dynamic: classifier
|
|
@@ -60,9 +60,17 @@ Dynamic: summary
|
|
|
60
60
|
[](https://github.com/adebayopeter/sqlpyhelper/blob/main/LICENSE)
|
|
61
61
|
[](https://github.com/adebayopeter/sqlpyhelper)
|
|
62
62
|
|
|
63
|
-
|
|
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.
|
|
64
64
|
|
|
65
|
-
|
|
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
|
+
```
|
|
66
74
|
|
|
67
75
|
## 📖 Table of Contents
|
|
68
76
|
- [🚀 Features](#-features)
|
|
@@ -81,7 +89,7 @@ A Python library for simplified database interactions across **SQLite, PostgreSQ
|
|
|
81
89
|
|
|
82
90
|
---
|
|
83
91
|
|
|
84
|
-
## 🚀 Features in v0.1.
|
|
92
|
+
## 🚀 Features in v0.1.4
|
|
85
93
|
- Unified connection pooling for multiple databases.
|
|
86
94
|
- Automatic reconnection for lost connections.
|
|
87
95
|
- Transaction support (BEGIN, ROLLBACK, COMMIT).
|
|
@@ -92,10 +100,21 @@ A Python library for simplified database interactions across **SQLite, PostgreSQ
|
|
|
92
100
|
|
|
93
101
|
---
|
|
94
102
|
## 📦 Installation
|
|
95
|
-
|
|
103
|
+
|
|
104
|
+
Install the base package (includes SQLite support out of the box):
|
|
96
105
|
```sh
|
|
97
106
|
pip install sqlpyhelper
|
|
98
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
|
+
|
|
99
118
|
📌 Package on PyPI: [SQLPyHelper on PyPI](https://pypi.org/project/SQLPyHelper/)
|
|
100
119
|
|
|
101
120
|
For local development:
|
|
@@ -208,7 +227,9 @@ db.return_connection_to_pool(conn)
|
|
|
208
227
|
| `return_connection_to_pool(conn)` | Returns connection back to pool. |
|
|
209
228
|
| `begin_transaction()` | Begins an **explicit transaction**. |
|
|
210
229
|
| `rollback_transaction()` | Rolls back **uncommitted transactions**. |
|
|
230
|
+
| `commit_transaction()` | Commits the current transaction. |
|
|
211
231
|
| `close()` | Closes the database connection safely. |
|
|
232
|
+
| `__enter__` / `__exit__()` | Use as a context manager — connection closes automatically. |
|
|
212
233
|
|
|
213
234
|
---
|
|
214
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,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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|