lukhed-basic-utils 1.6.2__tar.gz → 1.6.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.
Files changed (31) hide show
  1. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/PKG-INFO +1 -1
  2. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/githubCommon.py +3 -3
  3. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/sqlCommon.py +927 -1
  4. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils.egg-info/PKG-INFO +1 -1
  5. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils.egg-info/requires.txt +1 -0
  6. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/setup.py +4 -3
  7. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/LICENSE +0 -0
  8. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/README.md +0 -0
  9. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/__init__.py +0 -0
  10. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/chartJsCommon.py +0 -0
  11. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/classCommon.py +0 -0
  12. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/fileCommon.py +0 -0
  13. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/listWorkCommon.py +0 -0
  14. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/mathCommon.py +0 -0
  15. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/matplotlibBarCharts.py +0 -0
  16. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/matplotlibBasics.py +0 -0
  17. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/matplotlibFormatting.py +0 -0
  18. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/matplotlibLineCharts.py +0 -0
  19. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/matplotlibPieCharts.py +0 -0
  20. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/matplotlibScatter.py +0 -0
  21. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/matplotlibSpecial.py +0 -0
  22. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/osCommon.py +0 -0
  23. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/requestsCommon.py +0 -0
  24. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/stringCommon.py +0 -0
  25. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils/timeCommon.py +0 -0
  26. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils.egg-info/SOURCES.txt +0 -0
  27. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils.egg-info/dependency_links.txt +0 -0
  28. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/lukhed_basic_utils.egg-info/top_level.txt +0 -0
  29. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/setup.cfg +0 -0
  30. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/tests/test_osCommon.py +0 -0
  31. {lukhed_basic_utils-1.6.2 → lukhed_basic_utils-1.6.4}/tests/test_timeCommon.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lukhed_basic_utils
3
- Version: 1.6.2
3
+ Version: 1.6.4
4
4
  Summary: A collection of basic utility functions
5
5
  Home-page: https://github.com/lukhed/lukhed_basic_utils
6
6
  Author: lukhed
@@ -125,7 +125,7 @@ class GithubHelper:
125
125
  i = input((f'ERROR: There is no project "{self.project}" in the config file. Would you like to go thru setup '
126
126
  'to add a new Github key for this project name? (y/n): '))
127
127
  if i == 'y':
128
- self._guided_setup()
128
+ self._gh_guided_setup()
129
129
  else:
130
130
  print("Ok, exiting...")
131
131
  quit()
@@ -133,7 +133,7 @@ class GithubHelper:
133
133
  def _prompt_for_setup(self):
134
134
  i = input("1. You do not have a valid config file to utilize github. Do you want to go thru easy setup? (y/n):")
135
135
  if i == 'y':
136
- self._guided_setup()
136
+ self._gh_guided_setup()
137
137
  elif i == 'n':
138
138
  print("OK, to use github functions, see https://github.com/lukhed/lukhed_basic_utils for more information.")
139
139
  quit()
@@ -141,7 +141,7 @@ class GithubHelper:
141
141
  print("Did not get an expected result of 'y' or 'n'. Please reinstantiate and try again. Exiting script.")
142
142
  quit()
143
143
 
144
- def _guided_setup(self):
144
+ def _gh_guided_setup(self):
145
145
  input(("\n2. Starting setup\n"
146
146
  "The github key you provide in this setup will be stored locally only. "
147
147
  f"After setup, you can see the config file in your specified destination {self._github_config_file}"
@@ -4,6 +4,9 @@ from mysql.connector.connection import MySQLConnection
4
4
  import mysql.connector
5
5
  import atexit
6
6
  import json
7
+ import psycopg2
8
+ import atexit
9
+ from psycopg2 import sql
7
10
 
8
11
  class SqlHelper(classCommon.LukhedAuth):
9
12
  def __init__(self, datbase_project, datbase_name, key_management='github', auth_type='basic',
@@ -49,9 +52,9 @@ class SqlHelper(classCommon.LukhedAuth):
49
52
  self.cursor = None
50
53
  atexit.register(self.close_connection)
51
54
 
52
-
53
55
  ###################
54
56
  # Auth and Connection
57
+
55
58
  def _auth_setup(self):
56
59
  """
57
60
  Set up authentication for the SQL database.
@@ -1249,11 +1252,934 @@ class SqlHelper(classCommon.LukhedAuth):
1249
1252
  self.cursor.execute(alter_query)
1250
1253
  self.db_connection.commit()
1251
1254
 
1255
+ class PostgresSqlHelper(classCommon.LukhedAuth):
1256
+ def __init__(self, datbase_project, datbase_name, key_management='github', auth_type='basic',
1257
+ auth_dict=None):
1258
+ """
1259
+ Initializes the PostgresSqlHelper class for managing PostgreSQL database connections and operations.
1260
+
1261
+ This class mirrors the structure and behavior of the original MySQL-based SqlHelper but is adapted for use with
1262
+ PostgreSQL databases such as those hosted on Supabase. It provides support for table management, data insertion,
1263
+ querying, and connection handling using psycopg2.
1264
+
1265
+ Parameters
1266
+ ----------
1267
+ datbase_project : str
1268
+ Name of the project or repository where the database is hosted. This class will use this to manage
1269
+ the authentication data going forward. Setup the authentication data for a project one time and then you
1270
+ can re-use it for all future connections to the database by utilizing the database project name.
1271
+ datbase_name : str
1272
+ Name of the PostgreSQL database to connect to. All queries and operations will be performed on this database.
1273
+ You can change the database later by using the `change_database` method.
1274
+ key_management : str, optional
1275
+ Options for storing your authentication data. 'local' to store your auth on your local hardware.
1276
+ 'github' to store it in a private GitHub repository (you will need a GitHub account and GitHub token),
1277
+ by default 'github'
1278
+ auth_type : str, optional
1279
+ Currently supports 'basic' auth, which is authentication by 'host', 'user', and 'password', by default 'basic'
1280
+ auth_dict : dict, optional
1281
+ By default, this class will walk you through providing your authentication data to be stored according
1282
+ to your preference via command prompts. You can also provide the necessary dictionary via this
1283
+ parameter to skip the setup., by default None
1284
+ """
1285
+
1286
+ super().__init__(datbase_project, key_management=key_management)
1287
+
1288
+ self.auth_type = auth_type
1289
+ self._auth_dict = auth_dict
1290
+
1291
+ self.database_project = datbase_project
1292
+ self.database_name = datbase_name
1293
+
1294
+ if self._auth_data is None:
1295
+ self._auth_setup()
1296
+
1297
+ self.db_connection = None
1298
+ self.cursor = None
1299
+ atexit.register(self.close_connection)
1300
+
1301
+
1302
+ ###################
1303
+ # Auth and Connection
1304
+ def _auth_setup(self):
1305
+ if self.auth_type == 'basic':
1306
+ if self._auth_dict is None:
1307
+ input("Basic auth requires the following information: 'host', 'user', 'password', and 'port' "
1308
+ "Press enter to start inputting these values.")
1309
+ host = input("Enter host (e.g. db.xxxxxx.supabase.co): ")
1310
+ user = input("Enter user: ")
1311
+ password = input("Enter password: ")
1312
+ port = input("Enter port: ")
1313
+
1314
+ self._auth_data = {
1315
+ "host": host,
1316
+ "user": user,
1317
+ "password": password,
1318
+ "port": port
1319
+ }
1320
+
1321
+ self.kM.force_update_key_data(self._auth_data)
1322
+ print("Basic auth data has been set up successfully.")
1323
+ else:
1324
+ raise ValueError(f"Unsupported auth_type: {self.auth_type}")
1325
+
1326
+ def _check_connect_db(self):
1327
+ if self.db_connection is None:
1328
+ self.connect()
1329
+
1330
+ def connect(self):
1331
+ self.db_connection = psycopg2.connect(
1332
+ host=self._auth_data['host'],
1333
+ user=self._auth_data['user'],
1334
+ password=self._auth_data['password'],
1335
+ dbname=self.database_name,
1336
+ port=self._auth_data['port']
1337
+ )
1338
+ self.cursor = self.db_connection.cursor()
1339
+
1340
+ def test_connection(self):
1341
+ self._check_connect_db()
1342
+ print(f"Connected to the database {self.database_name}")
1343
+
1344
+ def close_connection(self):
1345
+ if self.cursor:
1346
+ self.cursor.close()
1347
+ if self.db_connection:
1348
+ self.db_connection.close()
1349
+ print(f"Connection was closed to {self.database_name}")
1350
+
1351
+ def change_database(self, database_name):
1352
+ self.close_connection()
1353
+ self.database_name = database_name
1354
+ print("New database is: " + self.database_name)
1355
+
1356
+ def update_auth_data(self, force_new_auth_dict=None):
1357
+ if force_new_auth_dict is not None:
1358
+ self._auth_dict = force_new_auth_dict
1359
+ self._auth_setup()
1360
+
1361
+
1362
+ ##################
1363
+ # Table Management
1364
+ def get_all_tables(self):
1365
+ self._check_connect_db()
1366
+ query = """
1367
+ SELECT tablename
1368
+ FROM pg_catalog.pg_tables
1369
+ WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema';
1370
+ """
1371
+ self.cursor.execute(query)
1372
+ tables = [table[0] for table in self.cursor.fetchall()]
1373
+ return tables
1374
+
1375
+ def create_table(self, table_name, *column_tuples, include_id=False):
1376
+ self._check_connect_db()
1377
+
1378
+ if isinstance(column_tuples[0][0], (tuple, list)):
1379
+ column_tuples = column_tuples[0]
1380
+
1381
+ if include_id:
1382
+ id_column = ('id', 'SERIAL PRIMARY KEY')
1383
+ column_tuples = [id_column] + list(column_tuples)
1384
+
1385
+ columns_str = ', '.join([f'{col_name} {col_type}' for col_name, col_type in column_tuples])
1386
+ query = sql.SQL("CREATE TABLE {} ({})").format(
1387
+ sql.Identifier(table_name),
1388
+ sql.SQL(columns_str)
1389
+ )
1390
+
1391
+ self.cursor.execute(query)
1392
+ self.db_connection.commit()
1393
+
1394
+ def delete_table(self, table_name):
1395
+ self._check_connect_db()
1396
+ query = sql.SQL("DROP TABLE IF EXISTS {} CASCADE").format(sql.Identifier(table_name))
1397
+ self.cursor.execute(query)
1398
+ self.db_connection.commit()
1399
+
1400
+ def table_exists(self, table_name):
1401
+ self._check_connect_db()
1402
+ query = """
1403
+ SELECT EXISTS (
1404
+ SELECT 1
1405
+ FROM information_schema.tables
1406
+ WHERE table_schema = 'public' AND table_name = %s
1407
+ );
1408
+ """
1409
+ self.cursor.execute(query, (table_name,))
1410
+ return self.cursor.fetchone()[0]
1411
+
1412
+ def rename_table(self, old_table_name, new_table_name):
1413
+ self._check_connect_db()
1414
+ query = sql.SQL("ALTER TABLE {} RENAME TO {}").format(
1415
+ sql.Identifier(old_table_name),
1416
+ sql.Identifier(new_table_name)
1417
+ )
1418
+ self.cursor.execute(query)
1419
+ self.db_connection.commit()
1420
+
1421
+ def reset_table(self, table_name, reset_auto_increment=True):
1422
+ """
1423
+ This function deletes all rows from a table and optionally resets the auto-increment (SERIAL) sequence.
1424
+
1425
+ Parameters
1426
+ ----------
1427
+ table_name : str
1428
+ Table name you want to erase from the currently connected db
1429
+ reset_auto_increment : bool, optional
1430
+ If TRUE, resets auto increment of id also (assumes column is named 'id')
1431
+ """
1432
+ self._check_connect_db()
1433
+
1434
+ # Delete all rows from the table
1435
+ query = sql.SQL("DELETE FROM {}").format(sql.Identifier(table_name))
1436
+ self.cursor.execute(query)
1437
+ self.db_connection.commit()
1438
+
1439
+ # Reset auto-increment sequence if requested
1440
+ if reset_auto_increment:
1441
+ sequence_name = f"{table_name}_id_seq"
1442
+ reset_query = sql.SQL("ALTER SEQUENCE {} RESTART WITH 1").format(sql.Identifier(sequence_name))
1443
+ self.cursor.execute(reset_query)
1444
+ self.db_connection.commit()
1445
+
1446
+ def remove_rows_from_table(self, table_name, rows=1, order_column="id"):
1447
+ """
1448
+ Removes a number of rows from a table by ordering with a specified column.
1449
+
1450
+ Parameters
1451
+ ----------
1452
+ table_name : str
1453
+ Name of the table
1454
+ rows : int, optional
1455
+ Number of rows to remove (default is 1)
1456
+ order_column : str, optional
1457
+ Column by which to order the table for row removal (default is "id")
1458
+ """
1459
+ self._check_connect_db()
1460
+
1461
+ query = sql.SQL(
1462
+ "DELETE FROM {} WHERE ctid IN (SELECT ctid FROM {} ORDER BY {} DESC LIMIT %s)"
1463
+ ).format(
1464
+ sql.Identifier(table_name),
1465
+ sql.Identifier(table_name),
1466
+ sql.Identifier(order_column)
1467
+ )
1468
+ self.cursor.execute(query, (rows,))
1469
+ self.db_connection.commit()
1470
+
1471
+
1472
+ ########################
1473
+ # Table Data Management: Data Checks and Queries
1474
+ def display_table_in_console(self, table_name, max_rows=20, min_column_width=15):
1475
+ """
1476
+ Display a formatted table in the console with proper alignment and borders.
1477
+
1478
+ Parameters
1479
+ ----------
1480
+ table_name : str
1481
+ Name of the table to display
1482
+ max_rows : int, optional
1483
+ Maximum number of rows to display (default is 20)
1484
+ min_column_width : int, optional
1485
+ Minimum width for each column (default is 15)
1486
+
1487
+ Returns
1488
+ -------
1489
+ None
1490
+ """
1491
+ self._check_connect_db()
1492
+ select_query = f'SELECT * FROM "{table_name}"'
1493
+ self.cursor.execute(select_query)
1494
+
1495
+ columns = [desc[0] for desc in self.cursor.description]
1496
+ data = self.cursor.fetchmany(max_rows) if max_rows else self.cursor.fetchall()
1497
+
1498
+ col_widths = []
1499
+ for i, col in enumerate(columns):
1500
+ content_width = max(
1501
+ max(len(str(row[i])) for row in data) if data else 0,
1502
+ len(col)
1503
+ )
1504
+ col_widths.append(max(content_width + 2, min_column_width))
1505
+
1506
+ separator = '+' + '+'.join('-' * width for width in col_widths) + '+'
1507
+
1508
+ print(f"\nTable: {table_name}")
1509
+ print(separator)
1510
+ print('|' + '|'.join(
1511
+ f"{col:^{width}}" for col, width in zip(columns, col_widths)
1512
+ ) + '|')
1513
+ print(separator)
1514
+
1515
+ for row in data:
1516
+ print('|' + '|'.join(
1517
+ f"{str(cell):^{width}}" for cell, width in zip(row, col_widths)
1518
+ ) + '|')
1519
+
1520
+ print(separator)
1521
+ if max_rows and len(data) == max_rows:
1522
+ print(f"Note: Showing first {max_rows} rows only.")
1523
+
1524
+ def get_table_as_list(self, table_name, max_rows=None):
1525
+ self._check_connect_db()
1526
+ select_query = f'SELECT * FROM "{table_name}"'
1527
+ self.cursor.execute(select_query)
1528
+
1529
+ columns = [desc[0] for desc in self.cursor.description]
1530
+ data = self.cursor.fetchmany(max_rows) if max_rows else self.cursor.fetchall()
1531
+
1532
+ return [columns] + [list(row) for row in data]
1533
+
1534
+ def get_last_x_entries(self, table_name, x):
1535
+ """
1536
+ This function assumes the table has an 'id' column to order by.
1537
+
1538
+ Parameters
1539
+ ----------
1540
+ table_name : str
1541
+ Name of the table
1542
+ x : int
1543
+ Number of last entries to retrieve
1544
+
1545
+ Returns
1546
+ -------
1547
+ list
1548
+ List of rows including column headers
1549
+ """
1550
+ self._check_connect_db()
1551
+ select_query = f'SELECT * FROM "{table_name}" ORDER BY id DESC LIMIT %s'
1552
+ self.cursor.execute(select_query, (x,))
1553
+
1554
+ columns = [desc[0] for desc in self.cursor.description]
1555
+ data = self.cursor.fetchall()
1556
+ return [columns] + [list(row) for row in data]
1252
1557
 
1558
+ def query_table_by_columns(self, table_name, *condition_tuples):
1559
+ """
1560
+ Query the table by specified columns and conditions.
1253
1561
 
1562
+ Parameters
1563
+ ----------
1564
+ table_name : str
1565
+ Name of the table to query
1566
+ condition_tuples : tuple(s) or list of tuples
1567
+ Conditions for the query, specified as tuples of column-value pairs
1254
1568
 
1569
+ Returns
1570
+ -------
1571
+ list
1572
+ A list of rows matching the query conditions
1573
+ """
1574
+ self._check_connect_db()
1255
1575
 
1576
+ if isinstance(condition_tuples[0][0], (tuple, list)):
1577
+ conditions_list = [
1578
+ " AND ".join([f"{col} = %s" for col, _ in group]) for group in condition_tuples
1579
+ ]
1580
+ values = [val for group in condition_tuples for _, val in group]
1581
+ else:
1582
+ conditions_list = [" AND ".join([f"{col} = %s" for col, _ in condition_tuples])]
1583
+ values = [val for _, val in condition_tuples]
1256
1584
 
1585
+ conditions_sql = " OR ".join(conditions_list)
1586
+ query = f'SELECT * FROM "{table_name}" WHERE {conditions_sql};'
1257
1587
 
1588
+ self.cursor.execute(query, values)
1589
+ return [list(row) for row in self.cursor.fetchall()]
1258
1590
 
1591
+ def query_by_month_day(self, table_name, date_column, date_string):
1592
+ """
1593
+ Query records where the date matches the month and day.
1259
1594
 
1595
+ Parameters
1596
+ ----------
1597
+ table_name : str
1598
+ Name of the table to query
1599
+ date_column : str
1600
+ The column name with date
1601
+ date_string : str
1602
+ Target date (e.g., '2024-01-15')
1603
+
1604
+ Returns
1605
+ -------
1606
+ list
1607
+ A list of rows matching the query conditions
1608
+ """
1609
+ self._check_connect_db()
1610
+
1611
+ query = f'''
1612
+ SELECT *
1613
+ FROM "{table_name}"
1614
+ WHERE EXTRACT(MONTH FROM {date_column}) = EXTRACT(MONTH FROM %s::date)
1615
+ AND EXTRACT(DAY FROM {date_column}) = EXTRACT(DAY FROM %s::date)
1616
+ '''
1617
+ self.cursor.execute(query, (date_string, date_string))
1618
+ return [list(row) for row in self.cursor.fetchall()]
1619
+
1620
+ def get_columns_with_null_values(self, table_name, *lookup_condition_tuples):
1621
+ """
1622
+ Get columns with null values in the specified row.
1623
+
1624
+ Parameters
1625
+ ----------
1626
+ table_name : str
1627
+ Name of the table
1628
+ lookup_condition_tuples : tuple(s) or list of tuples
1629
+ Conditions to identify the row
1630
+
1631
+ Returns
1632
+ -------
1633
+ list of str
1634
+ Column names with NULL values in the row
1635
+ """
1636
+ self._check_connect_db()
1637
+
1638
+ if isinstance(lookup_condition_tuples[0][0], (tuple, list)):
1639
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples[0]]
1640
+ values = [val for _, val in lookup_condition_tuples[0]]
1641
+ else:
1642
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples]
1643
+ values = [val for _, val in lookup_condition_tuples]
1644
+
1645
+ query = f'SELECT * FROM "{table_name}" WHERE {" AND ".join(conditions)} LIMIT 1'
1646
+ self.cursor.execute(query, values)
1647
+ row = self.cursor.fetchone()
1648
+
1649
+ if row is None:
1650
+ return []
1651
+
1652
+ columns = [desc[0] for desc in self.cursor.description]
1653
+ return [col for col, val in zip(columns, row) if val is None]
1654
+
1655
+ def is_value_null(self, table_name, to_check_column, *lookup_condition_tuples):
1656
+ """
1657
+ Check if a value is NULL in the specified table and column.
1658
+
1659
+ Parameters
1660
+ ----------
1661
+ table_name : str
1662
+ Table to check
1663
+ to_check_column : str
1664
+ Column to evaluate
1665
+ lookup_condition_tuples : tuple(s) or list of tuples
1666
+ Conditions to identify the row
1667
+
1668
+ Returns
1669
+ -------
1670
+ bool
1671
+ True if NULL, False otherwise
1672
+ """
1673
+ result = self.get_single_value_from_table(table_name, to_check_column, *lookup_condition_tuples)
1674
+ return result is None
1675
+
1676
+ def check_if_data_exists(self, table_name, columns, values):
1677
+ """
1678
+ Check if a specific row exists based on multiple column values.
1679
+
1680
+ Parameters
1681
+ ----------
1682
+ table_name : str
1683
+ Table name
1684
+ columns : list of str
1685
+ Columns to match
1686
+ values : list
1687
+ Values to match against columns
1688
+
1689
+ Returns
1690
+ -------
1691
+ bool
1692
+ True if a match exists, False otherwise
1693
+ """
1694
+ self._check_connect_db()
1695
+
1696
+ conditions = ' AND '.join([f"{col} = %s" for col in columns])
1697
+ query = f'SELECT COUNT(*) FROM "{table_name}" WHERE {conditions}'
1698
+ self.cursor.execute(query, values)
1699
+ return self.cursor.fetchone()[0] > 0
1700
+
1701
+ def check_if_value_exists_in_column(self, table_name, column, value_to_check):
1702
+ """
1703
+ Check if a value exists in a specific column of a table.
1704
+
1705
+ Parameters
1706
+ ----------
1707
+ table_name : str
1708
+ Table to search
1709
+ column : str
1710
+ Column name
1711
+ value_to_check : any
1712
+ Value to search for
1713
+
1714
+ Returns
1715
+ -------
1716
+ bool
1717
+ True if the value exists, False otherwise
1718
+ """
1719
+ self._check_connect_db()
1720
+
1721
+ query = f'SELECT COUNT(*) FROM "{table_name}" WHERE {column} = %s'
1722
+ self.cursor.execute(query, (value_to_check,))
1723
+ return self.cursor.fetchone()[0] > 0
1724
+
1725
+ #######################
1726
+ # Table Data Management: Inserts and Updates
1727
+ def insert_data(self, table_name, columns, values, check_for_unique=False):
1728
+ """
1729
+ Inserts a row into a PostgreSQL table. Optionally checks for uniqueness.
1730
+
1731
+ Parameters
1732
+ ----------
1733
+ table_name : str
1734
+ columns : list of str
1735
+ values : list
1736
+ check_for_unique : bool
1737
+
1738
+ Returns
1739
+ -------
1740
+ bool
1741
+ """
1742
+ add_data = True
1743
+ if check_for_unique:
1744
+ add_data = not self.check_if_data_exists(table_name, columns, values)
1745
+
1746
+ if add_data:
1747
+ self._check_connect_db()
1748
+ query = sql.SQL("INSERT INTO {} ({}) VALUES ({})").format(
1749
+ sql.Identifier(table_name),
1750
+ sql.SQL(', ').join(map(sql.Identifier, columns)),
1751
+ sql.SQL(', ').join(sql.Placeholder() * len(values))
1752
+ )
1753
+ self.cursor.execute(query, values)
1754
+ self.db_connection.commit()
1755
+ return True
1756
+ else:
1757
+ print("Data already exists in the table matching the information you are trying to add.")
1758
+ return False
1759
+
1760
+ def insert_data_as_table(self, table_name, table_columns, table_rows_list_of_list):
1761
+ """
1762
+ Insert multiple rows into a PostgreSQL table.
1763
+
1764
+ Parameters
1765
+ ----------
1766
+ table_name : str
1767
+ table_columns : list of str
1768
+ table_rows_list_of_list : list of list
1769
+ """
1770
+ self._check_connect_db()
1771
+
1772
+ query = sql.SQL("INSERT INTO {} ({}) VALUES ({})").format(
1773
+ sql.Identifier(table_name),
1774
+ sql.SQL(', ').join(map(sql.Identifier, table_columns)),
1775
+ sql.SQL(', ').join(sql.Placeholder() * len(table_columns))
1776
+ )
1777
+
1778
+ try:
1779
+ for row_values in table_rows_list_of_list:
1780
+ self.cursor.execute(query, row_values)
1781
+ self.db_connection.commit()
1782
+ except Exception as e:
1783
+ self.db_connection.rollback()
1784
+ print(f"An error occurred: {e}")
1785
+
1786
+ def update_single_value_in_table(self, table_name, column_to_update, new_value, *lookup_condition_tuples):
1787
+ """
1788
+ Update a single column's value in a specific row.
1789
+
1790
+ Parameters
1791
+ ----------
1792
+ table_name : str
1793
+ column_to_update : str
1794
+ new_value : any
1795
+ lookup_condition_tuples : tuple(s)
1796
+ """
1797
+ self._check_connect_db()
1798
+
1799
+ if isinstance(lookup_condition_tuples[0][0], (tuple, list)):
1800
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples[0]]
1801
+ values = [val for _, val in lookup_condition_tuples[0]]
1802
+ else:
1803
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples]
1804
+ values = [val for _, val in lookup_condition_tuples]
1805
+
1806
+ where_clause = " AND ".join(conditions)
1807
+ query = sql.SQL("UPDATE {} SET {} = %s WHERE " + where_clause).format(
1808
+ sql.Identifier(table_name),
1809
+ sql.Identifier(column_to_update)
1810
+ )
1811
+ self.cursor.execute(query, [new_value] + values)
1812
+ self.db_connection.commit()
1813
+
1814
+ def update_multiple_values_in_table(self, table_name, columns_to_update_list, values_to_update_list, *lookup_condition_tuples):
1815
+ """
1816
+ Update multiple columns in a single row using WHERE conditions.
1817
+
1818
+ Parameters
1819
+ ----------
1820
+ table_name : str
1821
+ columns_to_update_list : list of str
1822
+ values_to_update_list : list
1823
+ lookup_condition_tuples : tuple(s)
1824
+ """
1825
+ self._check_connect_db()
1826
+
1827
+ if isinstance(lookup_condition_tuples[0][0], (tuple, list)):
1828
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples[0]]
1829
+ values = [val for _, val in lookup_condition_tuples[0]]
1830
+ else:
1831
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples]
1832
+ values = [val for _, val in lookup_condition_tuples]
1833
+
1834
+ set_clause = ', '.join([f"{col} = %s" for col in columns_to_update_list])
1835
+ where_clause = " AND ".join(conditions)
1836
+
1837
+ query = sql.SQL(f"UPDATE {{}} SET {set_clause} WHERE {where_clause}").format(
1838
+ sql.Identifier(table_name)
1839
+ )
1840
+ self.cursor.execute(query, values_to_update_list + values)
1841
+ self.db_connection.commit()
1842
+
1843
+
1844
+ #######################
1845
+ # Table Data Management: Columns
1846
+ def get_columns_in_table(self, table_name):
1847
+ """
1848
+ Get the list of column names in the specified PostgreSQL table.
1849
+
1850
+ Parameters
1851
+ ----------
1852
+ table_name : str
1853
+
1854
+ Returns
1855
+ -------
1856
+ list of str
1857
+ Column names
1858
+ """
1859
+ self._check_connect_db()
1860
+ query = """
1861
+ SELECT column_name
1862
+ FROM information_schema.columns
1863
+ WHERE table_schema = 'public' AND table_name = %s;
1864
+ """
1865
+ self.cursor.execute(query, (table_name,))
1866
+ return [row[0] for row in self.cursor.fetchall()]
1867
+
1868
+ def add_column_to_table(self, table_name, column_name, column_type, after_column=None):
1869
+ """
1870
+ Add a column to the specified PostgreSQL table.
1871
+
1872
+ Parameters
1873
+ ----------
1874
+ table_name : str
1875
+ column_name : str
1876
+ column_type : str
1877
+ after_column : str, optional (ignored in PostgreSQL)
1878
+ """
1879
+ self._check_connect_db()
1880
+
1881
+ query = sql.SQL("ALTER TABLE {} ADD COLUMN {} {}").format(
1882
+ sql.Identifier(table_name),
1883
+ sql.Identifier(column_name),
1884
+ sql.SQL(column_type)
1885
+ )
1886
+ self.cursor.execute(query)
1887
+ self.db_connection.commit()
1888
+
1889
+ def rename_column(self, table_name, old_column_name, new_column_name):
1890
+ """
1891
+ Rename a column in a PostgreSQL table.
1892
+
1893
+ Parameters
1894
+ ----------
1895
+ table_name : str
1896
+ old_column_name : str
1897
+ new_column_name : str
1898
+ """
1899
+ self._check_connect_db()
1900
+ query = sql.SQL("ALTER TABLE {} RENAME COLUMN {} TO {}").format(
1901
+ sql.Identifier(table_name),
1902
+ sql.Identifier(old_column_name),
1903
+ sql.Identifier(new_column_name)
1904
+ )
1905
+ self.cursor.execute(query)
1906
+ self.db_connection.commit()
1907
+
1908
+ def change_column_type(self, table_name, column_name, new_type):
1909
+ """
1910
+ Change the data type of a column in PostgreSQL.
1911
+
1912
+ Parameters
1913
+ ----------
1914
+ table_name : str
1915
+ column_name : str
1916
+ new_type : str
1917
+ """
1918
+ self._check_connect_db()
1919
+ query = sql.SQL("ALTER TABLE {} ALTER COLUMN {} TYPE {}").format(
1920
+ sql.Identifier(table_name),
1921
+ sql.Identifier(column_name),
1922
+ sql.SQL(new_type)
1923
+ )
1924
+ self.cursor.execute(query)
1925
+ self.db_connection.commit()
1926
+
1927
+ def change_column_charset(self, table_name, column_name, new_charset, new_collation):
1928
+ """
1929
+ Change the collation of a column in PostgreSQL (character sets are cluster-level).
1930
+
1931
+ Parameters
1932
+ ----------
1933
+ table_name : str
1934
+ column_name : str
1935
+ new_charset : str (ignored – PostgreSQL uses UTF-8)
1936
+ new_collation : str (PostgreSQL collation name)
1937
+ """
1938
+ self._check_connect_db()
1939
+ query = sql.SQL("ALTER TABLE {} ALTER COLUMN {} SET COLLATION {}").format(
1940
+ sql.Identifier(table_name),
1941
+ sql.Identifier(column_name),
1942
+ sql.Identifier(new_collation)
1943
+ )
1944
+ self.cursor.execute(query)
1945
+ self.db_connection.commit()
1946
+
1947
+ def delete_column(self, table_name, column_name):
1948
+ """
1949
+ Delete a column from a PostgreSQL table.
1950
+ """
1951
+ self._check_connect_db()
1952
+ query = sql.SQL("ALTER TABLE {} DROP COLUMN {}").format(
1953
+ sql.Identifier(table_name),
1954
+ sql.Identifier(column_name)
1955
+ )
1956
+ self.cursor.execute(query)
1957
+ self.db_connection.commit()
1958
+
1959
+ def get_column_data_as_list(self, table_name, column_name):
1960
+ """
1961
+ Retrieve data from a column as a list.
1962
+ """
1963
+ self._check_connect_db()
1964
+ query = sql.SQL("SELECT {} FROM {}").format(
1965
+ sql.Identifier(column_name),
1966
+ sql.Identifier(table_name)
1967
+ )
1968
+ self.cursor.execute(query)
1969
+ return [row[0] for row in self.cursor.fetchall()]
1970
+
1971
+ def initialize_values_in_column_to_provided_value(self, table, column_name, value):
1972
+ """
1973
+ Initialize all values in a column to the provided value.
1974
+ """
1975
+ self._check_connect_db()
1976
+ query = sql.SQL("UPDATE {} SET {} = %s").format(
1977
+ sql.Identifier(table),
1978
+ sql.Identifier(column_name)
1979
+ )
1980
+ self.cursor.execute(query, (value,))
1981
+ self.db_connection.commit()
1982
+
1983
+ def append_value_to_column(self, table_name, column_to_append, value_to_append, *lookup_condition_tuples):
1984
+ """
1985
+ Append a value to a JSONB array in a PostgreSQL column.
1986
+ """
1987
+ self._check_connect_db()
1988
+
1989
+ if isinstance(lookup_condition_tuples[0][0], (tuple, list)):
1990
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples[0]]
1991
+ values = [val for _, val in lookup_condition_tuples[0]]
1992
+ else:
1993
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples]
1994
+ values = [val for _, val in lookup_condition_tuples]
1995
+
1996
+ condition_sql = " AND ".join(conditions)
1997
+ query = f"""
1998
+ UPDATE {table_name}
1999
+ SET {column_to_append} = COALESCE({column_to_append}, '[]'::jsonb) || to_jsonb(%s::jsonb)
2000
+ WHERE {condition_sql}
2001
+ """
2002
+ self.cursor.execute(query, [json.dumps(value_to_append)] + values)
2003
+ self.db_connection.commit()
2004
+
2005
+ def append_values_to_columns(self, table_name, columns_to_append_to, values_to_append, *lookup_condition_tuples):
2006
+ """
2007
+ Append values to multiple JSONB columns in PostgreSQL.
2008
+ """
2009
+ self._check_connect_db()
2010
+
2011
+ if isinstance(lookup_condition_tuples[0][0], (tuple, list)):
2012
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples[0]]
2013
+ values = [val for _, val in lookup_condition_tuples[0]]
2014
+ else:
2015
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples]
2016
+ values = [val for _, val in lookup_condition_tuples]
2017
+
2018
+ condition_sql = " AND ".join(conditions)
2019
+ update_parts = []
2020
+ value_placeholders = []
2021
+
2022
+ for col, val in zip(columns_to_append_to, values_to_append):
2023
+ update_parts.append(
2024
+ f"{col} = COALESCE({col}, '[]'::jsonb) || to_jsonb(%s::jsonb)"
2025
+ )
2026
+ value_placeholders.append(json.dumps(val))
2027
+
2028
+ query = f"""
2029
+ UPDATE {table_name}
2030
+ SET {', '.join(update_parts)}
2031
+ WHERE {condition_sql}
2032
+ """
2033
+ self.cursor.execute(query, value_placeholders + values)
2034
+ self.db_connection.commit()
2035
+
2036
+ def append_values_to_columns_concat(self, table_name, columns_to_append_to, values_to_append, *lookup_condition_tuples):
2037
+ """
2038
+ Append values to multiple columns using string concatenation.
2039
+ """
2040
+ self._check_connect_db()
2041
+
2042
+ if isinstance(lookup_condition_tuples[0][0], (tuple, list)):
2043
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples[0]]
2044
+ values = [val for _, val in lookup_condition_tuples[0]]
2045
+ else:
2046
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples]
2047
+ values = [val for _, val in lookup_condition_tuples]
2048
+
2049
+ condition_sql = " AND ".join(conditions)
2050
+
2051
+ for col, val in zip(columns_to_append_to, values_to_append):
2052
+ query = f"""
2053
+ UPDATE {table_name}
2054
+ SET {col} = COALESCE({col}, '') || ', {json.dumps(val)}'
2055
+ WHERE {condition_sql}
2056
+ """
2057
+ self.cursor.execute(query, values)
2058
+ self.db_connection.commit()
2059
+
2060
+ #######################
2061
+ # Table Data Management: Rows
2062
+ def get_total_rows(self, table_name):
2063
+ """
2064
+ Get the total number of rows in the specified table.
2065
+ """
2066
+ self._check_connect_db()
2067
+ query = sql.SQL("SELECT COUNT(*) FROM {}").format(sql.Identifier(table_name))
2068
+ self.cursor.execute(query)
2069
+ return self.cursor.fetchone()[0]
2070
+
2071
+ def delete_first_rows(self, table_name, x):
2072
+ """
2073
+ Delete the first x rows from the specified table.
2074
+ NOTE: Requires a primary key or ordering column for deterministic behavior.
2075
+ """
2076
+ self._check_connect_db()
2077
+ # Replace 'id' with your table's primary key if needed
2078
+ query = sql.SQL("""
2079
+ DELETE FROM {table}
2080
+ WHERE ctid IN (
2081
+ SELECT ctid FROM {table}
2082
+ ORDER BY ctid
2083
+ LIMIT %s
2084
+ )
2085
+ """).format(table=sql.Identifier(table_name))
2086
+ self.cursor.execute(query, (x,))
2087
+ self.db_connection.commit()
2088
+
2089
+ def get_single_value_from_table(self, table_name, column_to_retrieve, *lookup_condition_tuples):
2090
+ """
2091
+ Retrieve a single value from the specified table.
2092
+ """
2093
+ self._check_connect_db()
2094
+
2095
+ if isinstance(lookup_condition_tuples[0][0], (tuple, list)):
2096
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples[0]]
2097
+ values = [val for _, val in lookup_condition_tuples[0]]
2098
+ else:
2099
+ conditions = [f"{col} = %s" for col, _ in lookup_condition_tuples]
2100
+ values = [val for _, val in lookup_condition_tuples]
2101
+
2102
+ condition_sql = " AND ".join(conditions)
2103
+ query = sql.SQL("SELECT {} FROM {} WHERE {} LIMIT 1").format(
2104
+ sql.Identifier(column_to_retrieve),
2105
+ sql.Identifier(table_name),
2106
+ sql.SQL(condition_sql)
2107
+ )
2108
+
2109
+ self.cursor.execute(query, values)
2110
+ result = self.cursor.fetchone()
2111
+
2112
+ return result[0] if result and len(result) == 1 else result
2113
+
2114
+ def bulk_update_rows(self, table_name, columns_to_update, rows_to_update):
2115
+ """
2116
+ Perform a bulk update of rows in the specified table.
2117
+ This method assumes the first column in columns_to_update is the unique key.
2118
+ """
2119
+ self._check_connect_db()
2120
+
2121
+ key_column = columns_to_update[0]
2122
+ set_columns = columns_to_update[1:]
2123
+
2124
+ cases = {col: [] for col in set_columns}
2125
+ ids = []
2126
+
2127
+ for row in rows_to_update:
2128
+ row_dict = dict(zip(columns_to_update, row))
2129
+ key = row_dict[key_column]
2130
+ ids.append(key)
2131
+ for col in set_columns:
2132
+ cases[col].append((key, row_dict[col]))
2133
+
2134
+ set_clauses = []
2135
+ values = []
2136
+
2137
+ for col, case_list in cases.items():
2138
+ case_sql = f"{col} = CASE {key_column} " + \
2139
+ ' '.join([f"WHEN %s THEN %s" for _ in case_list]) + " ELSE {col} END"
2140
+ set_clauses.append(case_sql)
2141
+ for pair in case_list:
2142
+ values.extend(pair)
2143
+
2144
+ query = f"""
2145
+ UPDATE {table_name}
2146
+ SET {', '.join(set_clauses)}
2147
+ WHERE {key_column} IN ({', '.join(['%s'] * len(ids))});
2148
+ """
2149
+ values.extend(ids)
2150
+ self.cursor.execute(query, values)
2151
+ self.db_connection.commit()
2152
+
2153
+
2154
+ ###################
2155
+ # General Query Execution
2156
+ def execute_query(self, query, params=None):
2157
+ """
2158
+ Execute a SQL query with optional parameters.
2159
+
2160
+ Parameters
2161
+ ----------
2162
+ query : str
2163
+ The SQL query to be executed
2164
+ params : tuple or list, optional
2165
+ Parameters to be passed to the query
2166
+
2167
+ Returns
2168
+ -------
2169
+ cursor
2170
+ For SELECT queries, returns cursor for fetching results
2171
+ None
2172
+ For other queries
2173
+ """
2174
+ self._check_connect_db()
2175
+
2176
+ if params is None:
2177
+ self.cursor.execute(query)
2178
+ else:
2179
+ self.cursor.execute(query, params)
2180
+
2181
+ if query.strip().upper().startswith('SELECT'):
2182
+ return self.cursor
2183
+
2184
+ self.db_connection.commit()
2185
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lukhed-basic-utils
3
- Version: 1.6.2
3
+ Version: 1.6.4
4
4
  Summary: A collection of basic utility functions
5
5
  Home-page: https://github.com/lukhed/lukhed_basic_utils
6
6
  Author: lukhed
@@ -9,3 +9,4 @@ numpy>=2.2.4
9
9
  pandas>=2.2.3
10
10
  scipy>=1.15.2
11
11
  mysql-connector-python>=8.0.0
12
+ psycopg2-binary>=2.9.10
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="lukhed_basic_utils",
5
- version="1.6.2",
5
+ version="1.6.4",
6
6
  description="A collection of basic utility functions",
7
7
  long_description=open("README.md").read(),
8
8
  long_description_content_type="text/markdown",
@@ -21,12 +21,13 @@ setup(
21
21
  "requests>=2.32.3",
22
22
  "beautifulsoup4>=4.12.3",
23
23
  "fake-useragent>=2.0.3",
24
- "tzdata>=2023.3",
24
+ "tzdata>=2023.3",
25
25
  "PyGithub>= 2.5.0",
26
26
  "matplotlib>=3.10.1",
27
27
  "numpy>=2.2.4",
28
28
  "pandas>=2.2.3",
29
29
  "scipy>=1.15.2",
30
- "mysql-connector-python>=8.0.0"
30
+ "mysql-connector-python>=8.0.0",
31
+ "psycopg2-binary>=2.9.10"
31
32
  ],
32
33
  )