SQLPyHelper 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SQLPyHelper
3
- Version: 0.1.3
3
+ Version: 0.1.4
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: Development Status :: 5 - Production/Stable
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,32 +23,44 @@ 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
- Provides-Extra: mysql
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: oracle
26
- Requires-Dist: cx_Oracle; extra == "oracle"
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: sqlite
34
+ Provides-Extra: oracle
35
+ Requires-Dist: cx_Oracle; 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: cx_Oracle; 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
- # 📌 SQLPyHelper v.0.1.3 🚀
55
+ # SQLPyHelper
56
+
57
+ [![PyPI version](https://img.shields.io/pypi/v/sqlpyhelper.svg)](https://pypi.org/project/sqlpyhelper/)
58
+ [![PyPI downloads](https://img.shields.io/pypi/dm/sqlpyhelper.svg)](https://pypi.org/project/sqlpyhelper/)
59
+ [![Python versions](https://img.shields.io/pypi/pyversions/sqlpyhelper.svg)](https://pypi.org/project/sqlpyhelper/)
60
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/adebayopeter/sqlpyhelper/blob/main/LICENSE)
61
+ [![GitHub stars](https://img.shields.io/github/stars/adebayopeter/sqlpyhelper?style=social)](https://github.com/adebayopeter/sqlpyhelper)
62
+
63
+ # 📌 SQLPyHelper v.0.1.4 🚀
42
64
 
43
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.
44
66
 
@@ -60,13 +82,13 @@ A Python library for simplified database interactions across **SQLite, PostgreSQ
60
82
  ---
61
83
 
62
84
  ## 🚀 Features in v0.1.3
63
- Unified connection pooling for multiple databases.
64
- Automatic reconnection for lost connections.
65
- Transaction support (BEGIN, ROLLBACK, COMMIT).
66
- Secure parameterized queries to prevent SQL injection.
67
- Bulk insertion & dynamic table creation.
68
- Logging & error handling for better debugging.
69
- CSV export & database backups.
85
+ - Unified connection pooling for multiple databases.
86
+ - Automatic reconnection for lost connections.
87
+ - Transaction support (BEGIN, ROLLBACK, COMMIT).
88
+ - Secure parameterized queries to prevent SQL injection.
89
+ - Bulk insertion & dynamic table creation.
90
+ - Logging & error handling for better debugging.
91
+ - CSV export & database backups.
70
92
 
71
93
  ---
72
94
  ## 📦 Installation
@@ -1,4 +1,12 @@
1
- # 📌 SQLPyHelper v.0.1.3 🚀
1
+ # SQLPyHelper
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/sqlpyhelper.svg)](https://pypi.org/project/sqlpyhelper/)
4
+ [![PyPI downloads](https://img.shields.io/pypi/dm/sqlpyhelper.svg)](https://pypi.org/project/sqlpyhelper/)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/sqlpyhelper.svg)](https://pypi.org/project/sqlpyhelper/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/adebayopeter/sqlpyhelper/blob/main/LICENSE)
7
+ [![GitHub stars](https://img.shields.io/github/stars/adebayopeter/sqlpyhelper?style=social)](https://github.com/adebayopeter/sqlpyhelper)
8
+
9
+ # 📌 SQLPyHelper v.0.1.4 🚀
2
10
 
3
11
  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.
4
12
 
@@ -20,13 +28,13 @@ A Python library for simplified database interactions across **SQLite, PostgreSQ
20
28
  ---
21
29
 
22
30
  ## 🚀 Features in v0.1.3
23
- Unified connection pooling for multiple databases.
24
- Automatic reconnection for lost connections.
25
- Transaction support (BEGIN, ROLLBACK, COMMIT).
26
- Secure parameterized queries to prevent SQL injection.
27
- Bulk insertion & dynamic table creation.
28
- Logging & error handling for better debugging.
29
- CSV export & database backups.
31
+ - Unified connection pooling for multiple databases.
32
+ - Automatic reconnection for lost connections.
33
+ - Transaction support (BEGIN, ROLLBACK, COMMIT).
34
+ - Secure parameterized queries to prevent SQL injection.
35
+ - Bulk insertion & dynamic table creation.
36
+ - Logging & error handling for better debugging.
37
+ - CSV export & database backups.
30
38
 
31
39
  ---
32
40
  ## 📦 Installation
@@ -1,11 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SQLPyHelper
3
- Version: 0.1.3
3
+ Version: 0.1.4
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: Development Status :: 5 - Production/Stable
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,32 +23,44 @@ 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
- Provides-Extra: mysql
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: oracle
26
- Requires-Dist: cx_Oracle; extra == "oracle"
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: sqlite
34
+ Provides-Extra: oracle
35
+ Requires-Dist: cx_Oracle; 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: cx_Oracle; 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
- # 📌 SQLPyHelper v.0.1.3 🚀
55
+ # SQLPyHelper
56
+
57
+ [![PyPI version](https://img.shields.io/pypi/v/sqlpyhelper.svg)](https://pypi.org/project/sqlpyhelper/)
58
+ [![PyPI downloads](https://img.shields.io/pypi/dm/sqlpyhelper.svg)](https://pypi.org/project/sqlpyhelper/)
59
+ [![Python versions](https://img.shields.io/pypi/pyversions/sqlpyhelper.svg)](https://pypi.org/project/sqlpyhelper/)
60
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/adebayopeter/sqlpyhelper/blob/main/LICENSE)
61
+ [![GitHub stars](https://img.shields.io/github/stars/adebayopeter/sqlpyhelper?style=social)](https://github.com/adebayopeter/sqlpyhelper)
62
+
63
+ # 📌 SQLPyHelper v.0.1.4 🚀
42
64
 
43
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.
44
66
 
@@ -60,13 +82,13 @@ A Python library for simplified database interactions across **SQLite, PostgreSQ
60
82
  ---
61
83
 
62
84
  ## 🚀 Features in v0.1.3
63
- Unified connection pooling for multiple databases.
64
- Automatic reconnection for lost connections.
65
- Transaction support (BEGIN, ROLLBACK, COMMIT).
66
- Secure parameterized queries to prevent SQL injection.
67
- Bulk insertion & dynamic table creation.
68
- Logging & error handling for better debugging.
69
- CSV export & database backups.
85
+ - Unified connection pooling for multiple databases.
86
+ - Automatic reconnection for lost connections.
87
+ - Transaction support (BEGIN, ROLLBACK, COMMIT).
88
+ - Secure parameterized queries to prevent SQL injection.
89
+ - Bulk insertion & dynamic table creation.
90
+ - Logging & error handling for better debugging.
91
+ - CSV export & database backups.
70
92
 
71
93
  ---
72
94
  ## 📦 Installation
@@ -4,8 +4,13 @@ setup.py
4
4
  SQLPyHelper.egg-info/PKG-INFO
5
5
  SQLPyHelper.egg-info/SOURCES.txt
6
6
  SQLPyHelper.egg-info/dependency_links.txt
7
+ SQLPyHelper.egg-info/entry_points.txt
7
8
  SQLPyHelper.egg-info/requires.txt
8
9
  SQLPyHelper.egg-info/top_level.txt
9
10
  sqlpyhelper/__init__.py
11
+ sqlpyhelper/automation_utils.py
12
+ sqlpyhelper/cli.py
10
13
  sqlpyhelper/db_helper.py
14
+ sqlpyhelper/py.typed
15
+ test/test_automation.py
11
16
  test/test_sqlpyhelper.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sqlpyhelper = sqlpyhelper.cli:cli
@@ -1,8 +1,11 @@
1
+ python-dotenv
2
+ click
3
+
4
+ [all]
1
5
  psycopg2
2
6
  mysql-connector-python
3
7
  pyodbc
4
8
  cx_Oracle
5
- python-dotenv
6
9
 
7
10
  [mysql]
8
11
  mysql-connector-python
@@ -13,7 +16,5 @@ cx_Oracle
13
16
  [postgres]
14
17
  psycopg2
15
18
 
16
- [sqlite]
17
-
18
19
  [sqlserver]
19
20
  pyodbc
@@ -0,0 +1,63 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ with open("README.md", "r", encoding="utf-8") as f:
4
+ long_description = f.read()
5
+
6
+ setup(
7
+ name='SQLPyHelper',
8
+ version='0.1.4',
9
+ description='A simple SQL database helper package for Python.',
10
+ long_description=long_description,
11
+ long_description_content_type="text/markdown",
12
+ author='Adebayo Olaonipekun',
13
+ author_email='pekunmi@live.com',
14
+ url='https://github.com/adebayopeter/sqlpyhelper',
15
+ packages=find_packages(),
16
+ package_data={
17
+ "sqlpyhelper": ["py.typed"],
18
+ },
19
+ python_requires=">=3.8",
20
+ install_requires=[
21
+ 'python-dotenv',
22
+ 'click'
23
+ ],
24
+ extras_require={
25
+ "postgres": ["psycopg2"],
26
+ "mysql": ["mysql-connector-python"],
27
+ "sqlserver": ["pyodbc"],
28
+ "oracle": ["cx_Oracle"],
29
+ "all": [
30
+ "psycopg2",
31
+ "mysql-connector-python",
32
+ "pyodbc",
33
+ "cx_Oracle",
34
+ ],
35
+ },
36
+ keywords=[
37
+ "database", "sql", "sqlite", "postgresql", "mysql",
38
+ "sqlserver", "oracle", "db", "query", "helper",
39
+ ],
40
+ project_urls={
41
+ "Source": "https://github.com/adebayopeter/sqlpyhelper",
42
+ "Bug Tracker": "https://github.com/adebayopeter/sqlpyhelper/issues",
43
+ "Changelog": "https://github.com/adebayopeter/sqlpyhelper/blob/main/CHANGELOG.md",
44
+ },
45
+ classifiers=[
46
+ "Programming Language :: Python :: 3",
47
+ "Programming Language :: Python :: 3.8",
48
+ "Programming Language :: Python :: 3.9",
49
+ "Programming Language :: Python :: 3.10",
50
+ "Programming Language :: Python :: 3.11",
51
+ "Programming Language :: Python :: 3.12",
52
+ "Development Status :: 4 - Beta",
53
+ "Intended Audience :: Developers",
54
+ "Topic :: Database :: Database Engines/Servers",
55
+ "Operating System :: OS Independent",
56
+ "License :: OSI Approved :: MIT License",
57
+ ],
58
+ entry_points={
59
+ 'console_scripts': [
60
+ 'sqlpyhelper=sqlpyhelper.cli:cli',
61
+ ],
62
+ },
63
+ )
@@ -0,0 +1,9 @@
1
+ # Match the version in setup.py
2
+ __version__ = "0.1.4"
3
+
4
+ from sqlpyhelper.db_helper import (
5
+ SQLPyHelperError,
6
+ ConnectionError,
7
+ QueryError,
8
+ BackupError,
9
+ )
@@ -0,0 +1,145 @@
1
+ import pandas as pd
2
+ from sqlpyhelper.db_helper import SQLPyHelper
3
+ import subprocess
4
+ from datetime import datetime
5
+ import os
6
+ import shutil
7
+
8
+
9
+ class AutomationUtils:
10
+ def __init__(self, db=None, **db_kwargs):
11
+ """
12
+ Optionally accepts db instance or connection parameters like:
13
+ db_type, host, user, password, database, port, driver.
14
+ """
15
+ self.db = db or SQLPyHelper(**db_kwargs)
16
+
17
+ def backup_database(self, target="local", tag="autobackup"):
18
+ """
19
+ Backs up the active PostgreSQL database using pg_dump.
20
+
21
+ Args:
22
+ target (str): Backup destination ("local" only for now).
23
+ tag (str): Custom tag for backup file naming.
24
+ """
25
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M")
26
+ filename = f"{tag}_{timestamp}.sql"
27
+ backup_dir = "backups"
28
+ os.makedirs(backup_dir, exist_ok=True)
29
+ filepath = os.path.join(backup_dir, filename)
30
+
31
+ db_name = self.db.database
32
+ user = self.db.user
33
+ host = self.db.host or "localhost"
34
+ port = str(self.db.port or "5432")
35
+
36
+ try:
37
+ if self.db.db_type == "sqlite":
38
+ filename2 = f"{tag}_{timestamp}.db"
39
+ sqlite_filepath = os.path.join(backup_dir, filename2)
40
+ shutil.copy2(self.db.database, sqlite_filepath)
41
+ else:
42
+ print(f"📦 Backing up database to {filepath}")
43
+ subprocess.run(
44
+ [
45
+ "pg_dump",
46
+ "-h", host,
47
+ "-p", port,
48
+ "-U", user,
49
+ db_name,
50
+ "-F", "c",
51
+ "-f", filepath,
52
+ ],
53
+ check=True,
54
+ shell=False,
55
+ )
56
+ except subprocess.CalledProcessError as e:
57
+ print("❌ Backup failed:", e)
58
+
59
+ def load_data_from_csv(self, file_path, table_name, if_exists="append"):
60
+ """
61
+ Loads a CSV file into the specified database table.
62
+
63
+ Args:
64
+ file_path (str): Path to the CSV file.
65
+ table_name (str): Destination table name in the database.
66
+ if_exists (str): 'append' or 'replace'. Default is 'append'.
67
+ """
68
+ df = pd.read_csv(file_path)
69
+
70
+ if if_exists == "replace":
71
+ self.db.execute_query(f"DROP TABLE IF EXISTS {table_name}")
72
+
73
+ for _, row in df.iterrows():
74
+ self.db.insert_dynamic(table_name, row.to_dict())
75
+
76
+ def detect_missing_periods(self, table, entity_column, date_column):
77
+ """
78
+ Flags rows where recurring time periods (e.g. monthly) are missing per entity.
79
+
80
+ Args:
81
+ table (str): Table name to query.
82
+ entity_column (str): Column representing entity ID.
83
+ date_column (str): Column representing timestamp/date.
84
+ """
85
+ if self.db.db_type == 'sqlite':
86
+ month_expr = f"strftime('%Y-%m', {date_column})"
87
+ else:
88
+ month_expr = f"DATE_TRUNC('month', {date_column})"
89
+ query = f"""
90
+ SELECT {entity_column}, COUNT(DISTINCT {month_expr}) AS recorded_months
91
+ FROM {table}
92
+ GROUP BY {entity_column}
93
+ HAVING COUNT(DISTINCT {month_expr}) < 12
94
+ """
95
+ self.db.execute_query(query)
96
+ return self.db.fetch_all()
97
+
98
+ def aggregate_column(self, table, value_column, group_column=None, time_column=None):
99
+ """
100
+ Computes sum of any value column grouped by entity or month.
101
+
102
+ Args:
103
+ table (str): Table name.
104
+ value_column (str): Numeric column to aggregate.
105
+ group_column (str, optional): Entity or category to group by.
106
+ time_column (str, optional): Timestamp to extract month grouping.
107
+ """
108
+ if self.db.db_type == 'sqlite':
109
+ month_expr = f"strftime('%Y-%m', {time_column})"
110
+ else:
111
+ month_expr = f"DATE_TRUNC('month', {time_column})"
112
+
113
+ if group_column and time_column:
114
+ query = f"""
115
+ SELECT {group_column}, {month_expr} AS month, SUM({value_column}) AS total
116
+ FROM {table}
117
+ GROUP BY {group_column}, month
118
+ ORDER BY month
119
+ """
120
+ else:
121
+ query = f"SELECT SUM({value_column}) FROM {table}"
122
+
123
+ self.db.execute_query(query)
124
+ return self.db.fetch_all()
125
+
126
+ def detect_outliers(self, table, numeric_column, threshold=2):
127
+ """
128
+ Detects statistical outliers based on deviation from mean.
129
+
130
+ Args:
131
+ table (str): Table name.
132
+ numeric_column (str): Column to analyze.
133
+ threshold (int): Number of standard deviations from mean to flag as outlier.
134
+ """
135
+ query = f"""
136
+ SELECT *, {numeric_column}
137
+ AS value FROM {table}
138
+ """
139
+ self.db.execute_query(query)
140
+ data = pd.DataFrame(self.db.fetch_all(), columns=[desc[0] for desc in self.db.cursor.description])
141
+
142
+ mean_val = data["value"].mean()
143
+ std_val = data["value"].std()
144
+ outliers = data[abs(data["value"] - mean_val) > threshold * std_val]
145
+ return outliers.values.tolist()
@@ -0,0 +1,146 @@
1
+ import click
2
+ from sqlpyhelper.db_helper import SQLPyHelper
3
+ from sqlpyhelper.automation_utils import AutomationUtils
4
+
5
+
6
+ @click.group()
7
+ def cli():
8
+ """SQLPyHelper Command Line Interface"""
9
+ pass
10
+
11
+
12
+ @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')
19
+ def run_query(db_type, host, user, password, database, query):
20
+ """Run a single SQL query and print results"""
21
+ db = SQLPyHelper(db_type=db_type, host=host, user=user, password=password, database=database)
22
+ results = db.execute_query(query)
23
+ for row in results:
24
+ click.echo(row)
25
+ db.close()
26
+
27
+
28
+ @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)
34
+ def interactive_shell(db_type, host, user, password, database):
35
+ """Launch an interactive SQL shell"""
36
+ db = SQLPyHelper(db_type=db_type, host=host, user=user, password=password, database=database)
37
+ click.echo("Interactive shell started. Type your SQL query or 'exit'")
38
+ while True:
39
+ query = input("sqlpy> ")
40
+ if query.lower() in ("exit", "quit"):
41
+ break
42
+ try:
43
+ db.execute_query(query)
44
+ results = db.fetch_all()
45
+ for row in results:
46
+ click.echo(row)
47
+ except Exception as e:
48
+ click.echo(f"Error: {e}")
49
+ db.close()
50
+
51
+
52
+ @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')
61
+ def backup(target, tag, db_type, host, user, password, database, port):
62
+ """Create a timestamped backup of the connected database."""
63
+ utils = AutomationUtils(
64
+ db_type=db_type,
65
+ host=host,
66
+ user=user,
67
+ password=password,
68
+ database=database,
69
+ port=port
70
+ )
71
+ utils.backup_database(target=target, tag=tag)
72
+
73
+
74
+ @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')
84
+ def load_data(file, table, if_exists, db_type, host, user, password, database, port):
85
+ """Load data from CSV into database table."""
86
+ utils = AutomationUtils(db_type=db_type, host=host, user=user, password=password, database=database, port=port)
87
+ utils.load_data_from_csv(file, table, if_exists=if_exists)
88
+
89
+
90
+ @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):
101
+ """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)
103
+ results = utils.detect_missing_periods(table, entity_column, date_column)
104
+ for row in results:
105
+ click.echo(row)
106
+
107
+
108
+ @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):
120
+ """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)
122
+ results = utils.aggregate_column(table, value_column, group_column, time_column)
123
+ for row in results:
124
+ click.echo(row)
125
+
126
+
127
+ @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):
138
+ """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)
140
+ results = utils.detect_outliers(table, numeric_column, threshold)
141
+ for row in results:
142
+ click.echo(row)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ cli()
@@ -1,27 +1,70 @@
1
1
  import csv
2
2
  from dotenv import load_dotenv
3
3
  import os
4
+ import re
4
5
 
5
6
  load_dotenv() # Load environment variables from .env file
6
7
 
7
8
 
8
- def log_query(query):
9
- """Logs queries for debugging purposes."""
10
- with open("query_log.txt", "a") as f:
11
- f.write(query + "\n")
9
+ def _validate_identifier(name: str) -> str:
10
+ """
11
+ Validate a SQL identifier (table or column name).
12
+ Allows only alphanumeric characters and underscores.
13
+ Raises ValueError for anything else, preventing SQL injection via identifiers.
14
+ """
15
+ if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', name):
16
+ raise ValueError(
17
+ f"Invalid SQL identifier: {name!r}. "
18
+ "Only letters, digits, and underscores are allowed."
19
+ )
20
+ return name
21
+
22
+
23
+ class SQLPyHelperError(Exception):
24
+ """Base exception for SQLPyHelper errors."""
25
+
26
+
27
+ class ConnectionError(SQLPyHelperError):
28
+ """Raised when a database connection fails."""
29
+
30
+
31
+ class QueryError(SQLPyHelperError):
32
+ """Raised when a query fails to execute."""
33
+
34
+
35
+ class BackupError(SQLPyHelperError):
36
+ """Raised when a backup operation fails."""
12
37
 
13
38
 
14
39
  class SQLPyHelper:
15
- def __init__(self):
16
- self.db_type = os.getenv("DB_TYPE").lower()
17
- self.host = os.getenv("DB_HOST")
18
- self.user = os.getenv("DB_USER")
19
- self.password = os.getenv("DB_PASSWORD")
20
- self.database = os.getenv("DB_NAME")
21
- self.driver = os.getenv("DB_DRIVER")
22
- self.oracle_sid = os.getenv("ORACLE_SID")
40
+ def __init__(self, db_type=None, host=None, user=None, password=None,
41
+ database=None, driver=None, port=None, oracle_sid=None):
42
+
43
+ # Store original params so reconnect() can replay them
44
+ self._init_kwargs = {
45
+ "db_type": db_type,
46
+ "host": host,
47
+ "user": user,
48
+ "password": password,
49
+ "database": database,
50
+ "driver": driver,
51
+ "port": port,
52
+ "oracle_sid": oracle_sid,
53
+ }
54
+
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")
23
63
  self.pool = None
24
64
 
65
+ if not self.db_type or not self.database:
66
+ raise ValueError("Missing required database configuration.")
67
+
25
68
  if self.db_type == "sqlite":
26
69
  import sqlite3
27
70
  self.connection = sqlite3.connect(self.database)
@@ -47,6 +90,13 @@ class SQLPyHelper:
47
90
 
48
91
  self.cursor = self.connection.cursor()
49
92
 
93
+ def __enter__(self):
94
+ return self
95
+
96
+ def __exit__(self, exc_type, exc_val, exc_tb):
97
+ self.close()
98
+ return False
99
+
50
100
  def execute_query(self, query, params=None):
51
101
  """Executes a query with optional parameters"""
52
102
  try:
@@ -61,32 +111,32 @@ class SQLPyHelper:
61
111
  self.cursor.execute(query, params)
62
112
  self.connection.commit()
63
113
  else:
64
- print(f"Error executing query: {e}")
114
+ raise QueryError(f"Query failed: {e}") from e
65
115
 
66
116
  def fetch_one(self):
67
117
  """Fetches a single row"""
68
118
  try:
69
119
  return self.cursor.fetchone()
70
120
  except Exception as e:
71
- print(f"Error fetching row: {e}")
72
- return None
121
+ raise QueryError(f"Failed to fetch row: {e}") from e
73
122
 
74
123
  def fetch_all(self):
75
124
  """Fetches all rows from the last executed query"""
76
125
  try:
77
126
  return self.cursor.fetchall()
78
127
  except Exception as e:
79
- print(f"Error fetching rows: {e}")
80
- return None
128
+ raise QueryError(f"Failed to fetch rows: {e}") from e
81
129
 
82
130
  def fetch_by_param(self, table_name, column_name, value):
83
131
  try:
84
- query = f"SELECT * FROM {table_name} WHERE {column_name} = %s"
132
+ table_name = _validate_identifier(table_name)
133
+ column_name = _validate_identifier(column_name)
134
+ placeholder = "?" if self.db_type == "sqlite" else "%s"
135
+ query = f"SELECT * FROM {table_name} WHERE {column_name} = {placeholder}"
85
136
  self.cursor.execute(query, (value,))
86
137
  return self.cursor.fetchall()
87
138
  except Exception as e:
88
- print(f"Error fetching row(s): {e}")
89
- return None
139
+ raise QueryError(f"Failed to fetch by param: {e}") from e
90
140
 
91
141
  def close(self):
92
142
  """Closes the connection"""
@@ -94,8 +144,7 @@ class SQLPyHelper:
94
144
  self.cursor.close()
95
145
  self.connection.close()
96
146
  except Exception as e:
97
- print(f"Error closing connection: {e}")
98
- return None
147
+ raise ConnectionError(f"Failed to close connection: {e}") from e
99
148
 
100
149
  def create_table(self, table_name, columns):
101
150
  """
@@ -104,12 +153,13 @@ class SQLPyHelper:
104
153
  columns = {'id': 'INTEGER PRIMARY KEY', 'name': 'TEXT', 'age': 'INTEGER'}
105
154
  """
106
155
  try:
107
- column_defs = ", ".join(f"{col} {dtype}" for col, dtype in columns.items())
108
- query = f"CREATE TABLE {table_name} ({column_defs})"
156
+ 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()])
159
+ query = f"CREATE TABLE IF NOT EXISTS {table_name} ({columns_def})"
109
160
  self.execute_query(query)
110
161
  except Exception as e:
111
- print(f"Error creating table: {e}")
112
- return None
162
+ raise QueryError(f"Failed to create table: {e}") from e
113
163
 
114
164
  def insert_bulk(self, table_name, data):
115
165
  """
@@ -118,16 +168,18 @@ class SQLPyHelper:
118
168
  data = [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
119
169
  """
120
170
  try:
121
- columns = ", ".join(data[0].keys()) # Extract column names
122
- placeholders = ", ".join(["%s" for _ in data[0].keys()]) # Generate placeholders
171
+ table_name = _validate_identifier(table_name)
172
+ col_names = [_validate_identifier(col) for col in data[0].keys()]
173
+ columns = ", ".join(col_names)
174
+ placeholder = "?" if self.db_type == "sqlite" else "%s"
175
+ placeholders = ", ".join([placeholder] * len(data[0]))
123
176
  query = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
124
177
  values = [tuple(row.values()) for row in data]
125
178
  self.cursor.executemany(query, values)
126
179
  self.connection.commit()
127
180
 
128
181
  except Exception as e:
129
- print(f"Error inserting bulk rows: {e}")
130
- return None
182
+ raise QueryError(f"Failed to insert bulk rows: {e}") from e
131
183
 
132
184
  def backup_table(self, table_name, backup_file):
133
185
  """
@@ -136,6 +188,7 @@ class SQLPyHelper:
136
188
  backup_table('users', 'users_backup.csv')
137
189
  """
138
190
  try:
191
+ table_name = _validate_identifier(table_name)
139
192
  query = f"SELECT * FROM {table_name}"
140
193
  self.execute_query(query)
141
194
  rows = self.fetch_all()
@@ -145,8 +198,7 @@ class SQLPyHelper:
145
198
  writer.writerow([desc[0] for desc in self.cursor.description]) # Column headers
146
199
  writer.writerows(rows)
147
200
  except Exception as e:
148
- print(f"Error backing up table: {e}")
149
- return None
201
+ raise BackupError(f"Failed to backup table: {e}") from e
150
202
 
151
203
  def setup_connection_pool(self, min_conn=1, max_conn=5, pool_size=5):
152
204
  """Sets up connection pooling based on the database type"""
@@ -182,28 +234,51 @@ class SQLPyHelper:
182
234
  else:
183
235
  raise ValueError(f"Connection pooling not supported for {self.db_type}")
184
236
  except Exception as e:
185
- print(f"⚠️ Error setting up connection pool: {e}")
186
- self.pool = None # Prevent broken pool usage
237
+ raise ConnectionError(f"Failed to set up connection pool: {e}") from e
187
238
 
188
239
  def get_connection_from_pool(self):
189
240
  """Fetches a connection from the pool."""
190
241
  return self.pool.get_connection()
191
242
 
192
- def return_connection_to_pool(self):
243
+ def return_connection_to_pool(self, connection=None) -> None:
193
244
  """Returns a connection back to the pool."""
194
- self.connection.close()
245
+ conn = connection or self.connection
246
+ if self.pool is None:
247
+ raise RuntimeError("No connection pool initialised. Call setup_connection_pool() first.")
248
+
249
+ if self.db_type == "postgres":
250
+ self.pool.putconn(conn)
251
+ elif self.db_type == "mysql":
252
+ conn.close()
253
+ elif self.db_type == "oracle":
254
+ self.pool.release(conn)
255
+ else:
256
+ conn.close()
195
257
 
196
258
  def reconnect(self):
197
259
  """Reconnects to the database if connection is lost"""
198
260
  try:
199
- self.connection.close() # Close existing connection
200
- self.__init__() # Reinitialize the connection
261
+ self.connection.close()
262
+ self.__init__(**self._init_kwargs)
201
263
  print("Database reconnected successfully.")
202
264
  except Exception as e:
203
- print(f"Error during reconnection: {e}")
265
+ raise ConnectionError(f"Reconnection failed: {e}") from e
204
266
 
205
267
  def begin_transaction(self):
206
268
  self.execute_query("START TRANSACTION")
207
269
 
208
270
  def rollback_transaction(self):
209
271
  self.execute_query("ROLLBACK")
272
+
273
+ def insert_dynamic(self, table, data: dict):
274
+ """
275
+ Dynamically constructs and executes an INSERT query with database-specific placeholders.
276
+ """
277
+ table = _validate_identifier(table)
278
+ columns = ", ".join(_validate_identifier(col) for col in data.keys())
279
+ placeholders_style = "?" if self.db_type == "sqlite" else "%s"
280
+ placeholders = ", ".join([placeholders_style] * len(data))
281
+ values = tuple(data.values())
282
+
283
+ sql = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
284
+ self.execute_query(sql, values)
File without changes
@@ -0,0 +1,15 @@
1
+ from sqlpyhelper.automation_utils import AutomationUtils
2
+
3
+ utils = AutomationUtils()
4
+
5
+ print("✅ Loading test data...")
6
+ utils.load_data_from_csv("sample_data.csv", "contributors")
7
+
8
+ print("\n📊 Contribution breakdown:")
9
+ print(utils.aggregate_column("contributors", "contribution", "name", "timestamp"))
10
+
11
+ print("\n⚠️ Missing months:")
12
+ print(utils.detect_missing_periods("contributors", "name", "timestamp"))
13
+
14
+ print("\n🚨 Outliers:")
15
+ print(utils.detect_outliers("contributors", "contribution"))
@@ -1,38 +0,0 @@
1
- from setuptools import setup, find_packages
2
-
3
- with open("README.md", "r", encoding="utf-8") as f:
4
- long_description = f.read()
5
-
6
- setup(
7
- name='SQLPyHelper',
8
- version='0.1.3',
9
- description='A simple SQL database helper package for Python.',
10
- long_description=long_description,
11
- long_description_content_type="text/markdown",
12
- author='Adebayo Olaonipekun',
13
- author_email='pekunmi@live.com',
14
- packages=find_packages(),
15
- install_requires=[
16
- 'psycopg2',
17
- 'mysql-connector-python',
18
- 'pyodbc',
19
- 'cx_Oracle',
20
- 'python-dotenv'
21
- ],
22
- extras_require={
23
- "mysql": ["mysql-connector-python"],
24
- "postgres": ["psycopg2"],
25
- "oracle": ["cx_Oracle"],
26
- "sqlserver": ["pyodbc"],
27
- "sqlite": []
28
- },
29
- python_requires=">=3.8",
30
- classifiers=[
31
- "Programming Language :: Python :: 3",
32
- "Development Status :: 5 - Production/Stable",
33
- "Intended Audience :: Developers",
34
- "Topic :: Database :: Database Engines/Servers",
35
- "Operating System :: OS Independent",
36
- "License :: OSI Approved :: MIT License"
37
- ],
38
- )
@@ -1,2 +0,0 @@
1
- # Match the version in setup.py
2
- __version__ = "0.1.3"
File without changes
File without changes