SQLPyHelper 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sqlpyhelper/__init__.py CHANGED
@@ -1,2 +1,9 @@
1
1
  # Match the version in setup.py
2
- __version__ = "0.1.2"
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()
sqlpyhelper/cli.py ADDED
@@ -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()
sqlpyhelper/db_helper.py CHANGED
@@ -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:
@@ -56,24 +106,45 @@ class SQLPyHelper:
56
106
  self.cursor.execute(query)
57
107
  self.connection.commit()
58
108
  except Exception as e:
59
- print(f"Error executing query: {e}")
109
+ if "server has gone away" in str(e): # Example check for MySQL lost connection
110
+ self.reconnect()
111
+ self.cursor.execute(query, params)
112
+ self.connection.commit()
113
+ else:
114
+ raise QueryError(f"Query failed: {e}") from e
60
115
 
61
116
  def fetch_one(self):
62
- return self.cursor.fetchone()
117
+ """Fetches a single row"""
118
+ try:
119
+ return self.cursor.fetchone()
120
+ except Exception as e:
121
+ raise QueryError(f"Failed to fetch row: {e}") from e
63
122
 
64
123
  def fetch_all(self):
65
124
  """Fetches all rows from the last executed query"""
66
- return self.cursor.fetchall()
125
+ try:
126
+ return self.cursor.fetchall()
127
+ except Exception as e:
128
+ raise QueryError(f"Failed to fetch rows: {e}") from e
67
129
 
68
130
  def fetch_by_param(self, table_name, column_name, value):
69
- query = f"SELECT * FROM {table_name} WHERE {column_name} = %s"
70
- self.cursor.execute(query, (value,))
71
- return self.cursor.fetchall() # Fetch matching rows
131
+ try:
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}"
136
+ self.cursor.execute(query, (value,))
137
+ return self.cursor.fetchall()
138
+ except Exception as e:
139
+ raise QueryError(f"Failed to fetch by param: {e}") from e
72
140
 
73
141
  def close(self):
74
142
  """Closes the connection"""
75
- self.cursor.close()
76
- self.connection.close()
143
+ try:
144
+ self.cursor.close()
145
+ self.connection.close()
146
+ except Exception as e:
147
+ raise ConnectionError(f"Failed to close connection: {e}") from e
77
148
 
78
149
  def create_table(self, table_name, columns):
79
150
  """
@@ -81,9 +152,14 @@ class SQLPyHelper:
81
152
  Example:
82
153
  columns = {'id': 'INTEGER PRIMARY KEY', 'name': 'TEXT', 'age': 'INTEGER'}
83
154
  """
84
- column_defs = ", ".join(f"{col} {dtype}" for col, dtype in columns.items())
85
- query = f"CREATE TABLE {table_name} ({column_defs})"
86
- self.execute_query(query)
155
+ try:
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})"
160
+ self.execute_query(query)
161
+ except Exception as e:
162
+ raise QueryError(f"Failed to create table: {e}") from e
87
163
 
88
164
  def insert_bulk(self, table_name, data):
89
165
  """
@@ -91,12 +167,19 @@ class SQLPyHelper:
91
167
  Example:
92
168
  data = [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
93
169
  """
94
- columns = ", ".join(data[0].keys()) # Extract column names
95
- placeholders = ", ".join(["%s" for _ in data[0].keys()]) # Generate placeholders
96
- query = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
97
- values = [tuple(row.values()) for row in data] # Convert dictionaries to tuples
98
- self.cursor.executemany(query, values)
99
- self.connection.commit()
170
+ try:
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]))
176
+ query = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
177
+ values = [tuple(row.values()) for row in data]
178
+ self.cursor.executemany(query, values)
179
+ self.connection.commit()
180
+
181
+ except Exception as e:
182
+ raise QueryError(f"Failed to insert bulk rows: {e}") from e
100
183
 
101
184
  def backup_table(self, table_name, backup_file):
102
185
  """
@@ -104,32 +187,98 @@ class SQLPyHelper:
104
187
  Example:
105
188
  backup_table('users', 'users_backup.csv')
106
189
  """
107
- query = f"SELECT * FROM {table_name}"
108
- self.execute_query(query)
109
- rows = self.fetch_all()
190
+ try:
191
+ table_name = _validate_identifier(table_name)
192
+ query = f"SELECT * FROM {table_name}"
193
+ self.execute_query(query)
194
+ rows = self.fetch_all()
110
195
 
111
- with open(backup_file, mode="w", newline="") as file:
112
- writer = csv.writer(file)
113
- writer.writerow([desc[0] for desc in self.cursor.description]) # Column headers
114
- writer.writerows(rows)
196
+ with open(backup_file, mode="w", newline="") as file:
197
+ writer = csv.writer(file)
198
+ writer.writerow([desc[0] for desc in self.cursor.description]) # Column headers
199
+ writer.writerows(rows)
200
+ except Exception as e:
201
+ raise BackupError(f"Failed to backup table: {e}") from e
115
202
 
116
- def setup_postgres_pool(self, min_conn=1, max_conn=5):
117
- """
118
- Creates a connection pool for PostgreSQL.
119
- Example:
120
- setup_postgres_pool(min_conn=2, max_conn=10)
121
- """
122
- from psycopg2 import pool
123
- self.pool = pool.SimpleConnectionPool(min_conn, max_conn,
124
- host=self.host,
125
- user=self.user,
126
- password=self.password,
127
- dbname=self.database)
203
+ def setup_connection_pool(self, min_conn=1, max_conn=5, pool_size=5):
204
+ """Sets up connection pooling based on the database type"""
205
+ try:
206
+ if self.db_type == "postgres":
207
+ from psycopg2 import pool
208
+ self.pool = pool.SimpleConnectionPool(min_conn, max_conn,
209
+ host=self.host, user=self.user,
210
+ password=self.password, dbname=self.database)
211
+
212
+ elif self.db_type == "mysql":
213
+ import mysql.connector.pooling
214
+ self.pool = mysql.connector.pooling.MySQLConnectionPool(pool_name="mypool",
215
+ pool_size=pool_size, host=self.host,
216
+ user=self.user, password=self.password,
217
+ database=self.database)
218
+
219
+ elif self.db_type == "sqlserver":
220
+ import pyodbc
221
+ self.pool = [
222
+ pyodbc.connect(f"DRIVER={self.driver};SERVER={self.host};DATABASE={self.database};"
223
+ f"UID={self.user};PWD={self.password};ConnectionPooling=Yes")
224
+ for _ in range(pool_size)
225
+ ]
226
+
227
+ elif self.db_type == "oracle":
228
+ import cx_Oracle
229
+ oracle_port = os.getenv("ORACLE_DB_PORT", "1521") # Default Oracle port
230
+ dsn = cx_Oracle.makedsn(self.host, oracle_port, self.oracle_sid)
231
+ self.pool = cx_Oracle.SessionPool(user=self.user, password=self.password, dsn=dsn,
232
+ min=min_conn, max=max_conn, increment=1, threaded=True)
233
+
234
+ else:
235
+ raise ValueError(f"Connection pooling not supported for {self.db_type}")
236
+ except Exception as e:
237
+ raise ConnectionError(f"Failed to set up connection pool: {e}") from e
128
238
 
129
239
  def get_connection_from_pool(self):
130
240
  """Fetches a connection from the pool."""
131
- return self.pool.getconn()
241
+ return self.pool.get_connection()
132
242
 
133
- def return_connection_to_pool(self, conn):
243
+ def return_connection_to_pool(self, connection=None) -> None:
134
244
  """Returns a connection back to the pool."""
135
- self.pool.putconn(conn)
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()
257
+
258
+ def reconnect(self):
259
+ """Reconnects to the database if connection is lost"""
260
+ try:
261
+ self.connection.close()
262
+ self.__init__(**self._init_kwargs)
263
+ print("Database reconnected successfully.")
264
+ except Exception as e:
265
+ raise ConnectionError(f"Reconnection failed: {e}") from e
266
+
267
+ def begin_transaction(self):
268
+ self.execute_query("START TRANSACTION")
269
+
270
+ def rollback_transaction(self):
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)
sqlpyhelper/py.typed ADDED
File without changes
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: SQLPyHelper
3
+ Version: 0.1.4
4
+ Summary: A simple SQL database helper package for Python.
5
+ Home-page: https://github.com/adebayopeter/sqlpyhelper
6
+ Author: Adebayo Olaonipekun
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
12
+ Classifier: Programming Language :: Python :: 3
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
19
+ Classifier: Intended Audience :: Developers
20
+ Classifier: Topic :: Database :: Database Engines/Servers
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: License :: OSI Approved :: MIT License
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: python-dotenv
27
+ Requires-Dist: click
28
+ Provides-Extra: postgres
29
+ Requires-Dist: psycopg2; extra == "postgres"
30
+ Provides-Extra: mysql
31
+ Requires-Dist: mysql-connector-python; extra == "mysql"
32
+ Provides-Extra: sqlserver
33
+ Requires-Dist: pyodbc; extra == "sqlserver"
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"
41
+ Dynamic: author
42
+ Dynamic: author-email
43
+ Dynamic: classifier
44
+ Dynamic: description
45
+ Dynamic: description-content-type
46
+ Dynamic: home-page
47
+ Dynamic: keywords
48
+ Dynamic: license-file
49
+ Dynamic: project-url
50
+ Dynamic: provides-extra
51
+ Dynamic: requires-dist
52
+ Dynamic: requires-python
53
+ Dynamic: summary
54
+
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 🚀
64
+
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.
66
+
67
+ ## 📖 Table of Contents
68
+ - [🚀 Features](#-features)
69
+ - [📦 Installation](#-installation)
70
+ - [⚙️ Setup Using `.env`](#️-setup-using-env)
71
+ - [🛠 Usage Examples](#-usage-examples)
72
+ - [SQLite Example](#sqlite-example)
73
+ - [PostgreSQL Example](#postgresql-example)
74
+ - [MySQL Example](#mysql-example)
75
+ - [SQL Server Example](#sql-server-example)
76
+ - [Oracle Example](#oracle-example)
77
+ - [📂 Project Structure](#-project-structure)
78
+ - [📌 Available Methods in SQLPyHelper](#-available-methods-in-sqlpyhelper)
79
+ - [🌍 Contributing](#-contributing)
80
+ - [☕ Support the Project](#-support-the-project)
81
+
82
+ ---
83
+
84
+ ## 🚀 Features in v0.1.3
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.
92
+
93
+ ---
94
+ ## 📦 Installation
95
+ #### Install via PyPI:
96
+ ```sh
97
+ pip install sqlpyhelper
98
+ ```
99
+ 📌 Package on PyPI: [SQLPyHelper on PyPI](https://pypi.org/project/SQLPyHelper/)
100
+
101
+ For local development:
102
+ ```sh
103
+ git clone https://github.com/adebayopeter/sqlpyhelper.git
104
+ cd sqlpyhelper
105
+ pip install -r requirements.txt
106
+ ```
107
+
108
+ ---
109
+
110
+ ## ⚙️ Setup Using `.env`
111
+ Create a `.env` file in your project root to manage database configurations securely by renaming `.env_example`.
112
+
113
+ ```sh
114
+ # .env_example (Rename to .env)
115
+ DB_TYPE=postgres
116
+ DB_HOST=localhost
117
+ DB_USER=your_user
118
+ DB_PASSWORD=your_secure_password
119
+ DB_NAME=database_name
120
+ DB_DRIVER={ODBC Driver 17 for SQL Server}
121
+ ORACLE_SID=XE
122
+ ORACLE_DB_PORT=1521
123
+ ```
124
+ ### Loading `.env` in Code
125
+ ```pycon
126
+ from dotenv import load_dotenv
127
+ import os
128
+
129
+ load_dotenv()
130
+ db_type = os.getenv("DB_TYPE")
131
+ host = os.getenv("DB_HOST")
132
+ user = os.getenv("DB_USER")
133
+ password = os.getenv("DB_PASSWORD")
134
+ database = os.getenv("DB_NAME")
135
+ ```
136
+ ---
137
+ ## 🛠 Usage Examples
138
+ ### Initialize SQLPyHelper
139
+ ```pycon
140
+ from sqlpyhelper.db_helper import SQLPyHelper
141
+ db = SQLPyHelper() # Auto-detects database type based on `DB_TYPE`
142
+ ```
143
+ ### SQLite Example
144
+ ```pycon
145
+ db.execute_query("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
146
+ db.execute_query("INSERT INTO users (name) VALUES (?)", ("Alice",))
147
+ print(db.fetch_all()) # Expected Output: [(1, 'Alice')]
148
+ db.close()
149
+ ```
150
+ ### PostgreSQL Example
151
+ ```pycon
152
+ db.execute_query("CREATE TABLE customers (id SERIAL PRIMARY KEY, name TEXT)")
153
+ db.execute_query("INSERT INTO customers (name) VALUES (%s)", ("Bob",))
154
+ db.begin_transaction()
155
+ db.execute_query("DELETE FROM customers WHERE name=%s", ("Bob",))
156
+ db.rollback_transaction() # Undo delete
157
+ ```
158
+ ### MySQL Example
159
+ ```pycon
160
+ db.execute_query("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))")
161
+ db.execute_query("INSERT INTO users (id, name) VALUES (%s, %s)", (1, "Alice"))
162
+ print(db.fetch_by_param("users", "id", 1)) # Expected Output: [(1, 'Alice')]
163
+ db.close()
164
+ ```
165
+ ### SQL Server Example
166
+ ```pycon
167
+ db.execute_query("CREATE TABLE orders (order_id INT PRIMARY KEY, item NVARCHAR(100))")
168
+ db.insert_bulk("orders", [{"order_id": 1, "item": "Laptop"}, {"order_id": 2, "item": "Mouse"}])
169
+ db.backup_table("orders", "orders_backup.csv") # Export data to CSV
170
+ ```
171
+ ### Oracle Example
172
+ ```pycon
173
+ db.execute_query("CREATE TABLE employees (id NUMBER PRIMARY KEY, name VARCHAR2(100))")
174
+ db.execute_query("INSERT INTO employees (id, name) VALUES (:1, :2)", (1, "Charlie"))
175
+ db.setup_connection_pool(min_conn=2, max_conn=10) # Enable pooling for better performance
176
+ conn = db.get_connection_from_pool()
177
+ db.return_connection_to_pool(conn)
178
+ ```
179
+
180
+ ## 📂 Project Structure
181
+ ```
182
+ 📦 SQLPyHelper/
183
+ ├─ sqlpyhelper/
184
+ │  ├─ __init__.py
185
+ │  └─ db_helper.py
186
+ ├─ tests/
187
+ │  └─ test_sqlpyhelper.py
188
+ ├─ .env_example
189
+ ├─ .gitignore
190
+ ├─ setup.py
191
+ ├─ README.md
192
+ └─ requirements.txt
193
+ ```
194
+ ---
195
+ ## 📌 Available Methods in SQLPyHelper
196
+
197
+ | Method | Description |
198
+ |--------|-------------|
199
+ | `execute_query(query, params=None)` | Executes a SQL query with optional parameters. |
200
+ | `fetch_one()` | Retrieves a **single row** from query results. |
201
+ | `fetch_all()` | Retrieves **all rows** from query results. |
202
+ | `fetch_by_param(table, column, value)` | Fetches **rows dynamically** based on a given parameter. |
203
+ | `create_table(table_name, columns_dict)` | Creates a table dynamically with a dictionary format. |
204
+ | `insert_bulk(table, data_list)` | Inserts **multiple rows at once** efficiently. |
205
+ | `backup_table(table, backup_file.csv)` | Exports table data to **CSV format**. |
206
+ | `setup_connection_pool()` | Initializes **database connection pooling**. |
207
+ | `get_connection_from_pool()` | Fetches a connection from the pool. |
208
+ | `return_connection_to_pool(conn)` | Returns connection back to pool. |
209
+ | `begin_transaction()` | Begins an **explicit transaction**. |
210
+ | `rollback_transaction()` | Rolls back **uncommitted transactions**. |
211
+ | `close()` | Closes the database connection safely. |
212
+
213
+ ---
214
+ ## 🌍 Contributing
215
+ We welcome contributions from the **open-source community**! Follow these steps to contribute:
216
+
217
+ 1. Fork the repo: [SQLPyHelper GitHub Repository](https://github.com/adebayopeter/sqlpyhelper)
218
+ 2. Clone your fork:
219
+ ```sh
220
+ git clone https://github.com/adebayopeter/sqlpyhelper.git
221
+ ```
222
+ 3. Create a new branch:
223
+ ```sh
224
+ git checkout -b feature-new-functionality
225
+ ```
226
+ 4. Make changes, commit, and push:
227
+ ```sh
228
+ git commit -m "Added new feature"
229
+ git push origin feature-new-functionality
230
+ ```
231
+ 5. Submit a Pull Request!
232
+
233
+ ---
234
+ ## ☕ Support the Project
235
+
236
+ If you find SQLPyHelper useful, consider buying me a coffee to support continued development!
237
+ Donate Here: [PayPal](https://paypal.me/adebayopeter?country.x=GB&locale.x=en_GB)
238
+ ---
@@ -0,0 +1,11 @@
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sqlpyhelper = sqlpyhelper.cli:cli
@@ -1,211 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: SQLPyHelper
3
- Version: 0.1.2
4
- Summary: A simple SQL database helper package for Python.
5
- Author: Adebayo Olaonipekun
6
- Author-email: pekunmi@live.com
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: Development Status :: 5 - Production/Stable
9
- Classifier: Intended Audience :: Developers
10
- Classifier: Topic :: Database :: Database Engines/Servers
11
- Classifier: Operating System :: OS Independent
12
- Classifier: License :: OSI Approved :: MIT License
13
- Requires-Python: >=3.8
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Requires-Dist: psycopg2
17
- Requires-Dist: mysql-connector-python
18
- Requires-Dist: pyodbc
19
- Requires-Dist: cx_Oracle
20
- Requires-Dist: python-dotenv
21
- Provides-Extra: mysql
22
- Requires-Dist: mysql-connector-python; extra == "mysql"
23
- Provides-Extra: postgres
24
- Requires-Dist: psycopg2; extra == "postgres"
25
- Provides-Extra: oracle
26
- Requires-Dist: cx_Oracle; extra == "oracle"
27
- Provides-Extra: sqlserver
28
- Requires-Dist: pyodbc; extra == "sqlserver"
29
- Provides-Extra: sqlite
30
- Dynamic: author
31
- Dynamic: author-email
32
- Dynamic: classifier
33
- Dynamic: description
34
- Dynamic: description-content-type
35
- Dynamic: license-file
36
- Dynamic: provides-extra
37
- Dynamic: requires-dist
38
- Dynamic: requires-python
39
- Dynamic: summary
40
-
41
- # 📌 SQLPyHelper
42
-
43
- A Python library for simplified database interactions across **SQLite, PostgreSQL, MySQL, SQL Server, and Oracle**. This open-source package provides an intuitive API for handling database operations efficiently.
44
-
45
- ## 📖 Table of Contents
46
- - [🚀 Features](#-features)
47
- - [📦 Installation](#-installation)
48
- - [⚙️ Setup Using `.env`](#️-setup-using-env)
49
- - [🛠 Usage Examples](#-usage-examples)
50
- - [SQLite Example](#sqlite-example)
51
- - [PostgreSQL Example](#postgresql-example)
52
- - [MySQL Example](#mysql-example)
53
- - [SQL Server Example](#sql-server-example)
54
- - [Oracle Example](#oracle-example)
55
- - [📂 Project Structure](#-project-structure)
56
- - [🌍 Contributing](#-contributing)
57
- - [☕ Support the Project](#-support-the-project)
58
-
59
- ---
60
-
61
- ## 🚀 Features
62
- - **Unified Interface** for multiple databases
63
- - **Connection pooling support** for PostgreSQL
64
- - **Bulk insertion & dynamic table creation**
65
- - **Automated logging & query execution**
66
- - **CSV export & backup functionality**
67
-
68
- ---
69
- ## 📦 Installation
70
- #### Install via PyPI:
71
- ```sh
72
- pip install sqlpyhelper
73
- ```
74
- 📌 Package on PyPI: [SQLPyHelper on PyPI](https://pypi.org/project/SQLPyHelper/)
75
-
76
- Or, if working from source:
77
- ```sh
78
- git clone https://github.com/adebayopeter/sqlpyhelper.git
79
- cd sqlpyhelper
80
- pip install -r requirements.txt
81
- ```
82
-
83
- ---
84
-
85
- ## ⚙️ Setup Using `.env`
86
- Create a `.env` file in your project root to manage database configurations securely by renaming `.env_example`.
87
-
88
- ```sh
89
- # .env_example (Rename to .env)
90
- DB_TYPE=postgres
91
- DB_HOST=localhost
92
- DB_USER=your_user
93
- DB_PASSWORD=your_secure_password
94
- DB_NAME=database_name
95
- DB_DRIVER={ODBC Driver 17 for SQL Server}
96
- ORACLE_SID=XE
97
- ORACLE_DB_PORT=1521
98
- ```
99
- ### Loading `.env` in Code
100
- ```pycon
101
- from dotenv import load_dotenv
102
- import os
103
-
104
- load_dotenv()
105
- db_type = os.getenv("DB_TYPE")
106
- host = os.getenv("DB_HOST")
107
- user = os.getenv("DB_USER")
108
- password = os.getenv("DB_PASSWORD")
109
- database = os.getenv("DB_NAME")
110
- ```
111
- ---
112
- ## 🛠 Usage Examples
113
- ### SQLite Example
114
- ```pycon
115
- from sqlpyhelper.db_helper import SQLPyHelper
116
-
117
- db = SQLPyHelper()
118
- db.execute_query("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
119
- db.execute_query("INSERT INTO users (name) VALUES (?)", ("Alice",))
120
- db.execute_query("SELECT * FROM users")
121
- print(db.fetch_all())
122
- db.close()
123
- ```
124
- ### PostgreSQL Example
125
- ```pycon
126
- db = SQLPyHelper()
127
- db.execute_query("CREATE TABLE IF NOT EXISTS employees (id SERIAL PRIMARY KEY, name VARCHAR(100))")
128
- db.execute_query("INSERT INTO employees (name) VALUES (%s)", ("Charlie",))
129
- db.execute_query("SELECT * FROM employees")
130
- print(db.fetch_all())
131
- db.close()
132
- ```
133
- ### MySQL Example
134
- ```pycon
135
- db = SQLPyHelper()
136
- db.execute_query("CREATE TABLE IF NOT EXISTS customers (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100))")
137
- db.execute_query("INSERT INTO customers (name) VALUES (%s)", ("David",))
138
- db.execute_query("SELECT * FROM customers")
139
- print(db.fetch_all())
140
- db.close()
141
- ```
142
- ```pycon
143
- db = SQLPyHelper()
144
-
145
- # Fetch rows where customer_id = 3
146
- customers = db.fetch_by_param("customers", "id", 3)
147
- print(customers)
148
-
149
- db.close()
150
- ```
151
-
152
- ### SQL Server Example
153
- ```pycon
154
- db = SQLPyHelper()
155
- db.execute_query("CREATE TABLE IF NOT EXISTS orders (id INT PRIMARY KEY, product VARCHAR(100))")
156
- db.execute_query("INSERT INTO orders (id, product) VALUES (?, ?)", (1, "Laptop"))
157
- db.execute_query("SELECT * FROM orders")
158
- print(db.fetch_all())
159
- db.close()
160
- ```
161
- ### Oracle Example
162
- ```pycon
163
- db = SQLPyHelper()
164
- db.execute_query("CREATE TABLE employees (id NUMBER PRIMARY KEY, name VARCHAR2(100))")
165
- db.execute_query("INSERT INTO employees (id, name) VALUES (:1, :2)", (1, "Emily"))
166
- db.execute_query("SELECT * FROM employees")
167
- print(db.fetch_all())
168
- db.close()
169
- ```
170
-
171
- ## 📂 Project Structure
172
- ```
173
- 📦 SQLPyHelper/
174
- ├─ sqlpyhelper/
175
- │  ├─ __init__.py
176
- │  └─ db_helper.py
177
- ├─ tests/
178
- │  └─ test_sqlpyhelper.py
179
- ├─ .env_example
180
- ├─ .gitignore
181
- ├─ setup.py
182
- ├─ README.md
183
- └─ requirements.txt
184
- ```
185
-
186
- ---
187
- ## 🌍 Contributing
188
- We welcome contributions from the **open-source community**! Follow these steps to contribute:
189
-
190
- 1. Fork the repo: [SQLPyHelper GitHub Repository](https://github.com/adebayopeter/sqlpyhelper)
191
- 2. Clone your fork:
192
- ```sh
193
- git clone https://github.com/adebayopeter/sqlpyhelper.git
194
- ```
195
- 3. Create a new branch:
196
- ```sh
197
- git checkout -b feature-new-functionality
198
- ```
199
- 4. Make changes, commit, and push:
200
- ```sh
201
- git commit -m "Added new feature"
202
- git push origin feature-new-functionality
203
- ```
204
- 5. Submit a Pull Request!
205
-
206
- ---
207
- ## ☕ Support the Project
208
-
209
- If you find SQLPyHelper useful, consider buying me a coffee to support continued development!
210
- Donate Here: [PayPal](https://paypal.me/adebayopeter?country.x=GB&locale.x=en_GB)
211
- ---
@@ -1,7 +0,0 @@
1
- sqlpyhelper/__init__.py,sha256=kJA4mgypZmLd3SnwwTNkVwB0yIJ8PKP4h9cjOCIPfTA,54
2
- sqlpyhelper/db_helper.py,sha256=TT93q3f1EVjkgUSXA4iXCy9Uw5gdGuRbqH-Wl-8OjeI,5269
3
- sqlpyhelper-0.1.2.dist-info/licenses/LICENSE,sha256=9XzXxZ_8mWFM9-2TlqyE3L69zvRf4VPY_xIzSj5iU-g,1076
4
- sqlpyhelper-0.1.2.dist-info/METADATA,sha256=ATC92p-IcBVUFEBHKXqqMBkYC524FsdQpxDVBmhQ3JI,5954
5
- sqlpyhelper-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- sqlpyhelper-0.1.2.dist-info/top_level.txt,sha256=FrLqTmqTGDa8jHnnf2ZVkYO-gFvLXX9QonpUCE6wKGs,12
7
- sqlpyhelper-0.1.2.dist-info/RECORD,,