Skip to content

Commit 5ecfcce

Browse files
authored
Merge pull request #5 from Pymetheus/update-main
Add full CRUD support and query operator utilities
2 parents 2eee951 + 09a7448 commit 5ecfcce

14 files changed

Lines changed: 347 additions & 60 deletions

File tree

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
**A modular toolkit for building, configuring, and managing databases using SQLAlchemy**
44

55
The SQLAlchemy Database Toolkit simplifies the setup and management across different relational databases.
6-
Currently, it handles configuration loading, engine creation, ORM base registration, and session management.
6+
Currently, it handles configuration loading, engine creation, ORM base registration, session management and CRUD operations.
77
It provides an extensible foundation for rapid database development, prototyping, and integration into data pipelines or applications.
88

99
Supported DBMS under current version:
@@ -81,7 +81,7 @@ Engine Factory Example:
8181
```python
8282
from sqlalchemy_dbtoolkit.engine.factory import AlchemyEngineFactory
8383

84-
engine = AlchemyEngineFactory(dbms="mysql", db_name="analytics_db", config_path='../.config/config.ini').engine
84+
engine = AlchemyEngineFactory(dbms='mysql', db_name='analytics_db', config_path='../.config/config.ini').engine
8585
```
8686

8787
ORM Table Management Example:
@@ -106,14 +106,29 @@ ORM Session Insert Example:
106106
from sqlalchemy_dbtoolkit.query.create import InsertManager
107107

108108
inserter = InsertManager(engine)
109-
inserter.add_row(YourTable, {"column_1": "value", "column_2": 42})
109+
inserter.add_row(YourTable, {'column_1': 'value', 'column_2': 42})
110110
```
111111

112112
ORM Session Select Example:
113113
```python
114114
from sqlalchemy_dbtoolkit.query.read import SelectManager
115115
selector = SelectManager(engine)
116-
selection = selector.select_one_by_column(Table=YourTable, column_name="column_1", value="value")
116+
selection = selector.select_one_by_column(Table=YourTable, column_name='column_1', column_value='value', operator_name='eq')
117+
```
118+
119+
ORM Session Update Example:
120+
```python
121+
from sqlalchemy_dbtoolkit.query.update import UpdateManager
122+
updater = UpdateManager(engine)
123+
updates = {'column_2': 43}
124+
updated_rows = updater.update_rows(Table=YourTable, column_name='column_1', column_value='value', update_dict=updates, operator_name='eq')
125+
```
126+
127+
ORM Session Delete Example:
128+
```python
129+
from sqlalchemy_dbtoolkit.query.delete import DeleteManager
130+
deleter = DeleteManager(engine)
131+
deleted_rows = deleter.delete_rows_by_filter(Table=YourTable, column_name='column_1', column_value='value', operator_name='eq')
117132
```
118133

119134
Inspector Example:
@@ -129,7 +144,7 @@ for table in table_names:
129144
## Roadmap
130145

131146
- [ ] Pandas Integration: Enable conversion between database queries and pandas DataFrames for analysis and data manipulation
132-
- [ ] Full CRUD Support: Expand the query layer to include read, update, and delete operations
147+
- [X] Full CRUD Support: Expand the query layer to include read, update, and delete operations
133148
- [ ] SQLAlchemy Core Support: Provide additional utilities to support low-level, fine-grained database interactions
134149
- [ ] Integrated Logging: Add structured logging across all components to improve debugging
135150
- [ ] Integrate DBMSs: Include support for additional DBMS like mariadb, mssql and oracle

docs/SECURITY.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ If you believe you have found a security vulnerability, please report it to us a
44

55
## Supported Versions
66

7-
Currently only the 0.1.7 version of the sqlalchemy-dbtoolkit is supported.
7+
Currently only the 0.1.8 version of the sqlalchemy-dbtoolkit is supported.
88

99
| Version | Supported |
1010
|---------| ------------------ |
11-
| 0.1.7 | :white_check_mark: |
11+
| 0.1.8 | :white_check_mark: |
1212
| others | :x: |
1313

1414
## Reporting a Vulnerability
@@ -17,4 +17,4 @@ Currently only the 0.1.7 version of the sqlalchemy-dbtoolkit is supported.
1717

1818
Please report security vulnerabilities via [Email](mailto:github.senate902@passfwd.com).
1919
You will receive a response from us within 48 hours.
20-
If the issue is confirmed, we will release a patch as soon as possible depending on complexity
20+
If the issue is confirmed, we will release a patch as soon as possible depending on complexity.

setup.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
from setuptools import setup, find_packages
22

3-
with open("README.md", "r", encoding="utf-8") as fh:
3+
with open('README.md', 'r', encoding='utf-8') as fh:
44
long_description = fh.read()
55

66
setup(
7-
name="sqlalchemy-dbtoolkit",
8-
version="0.1.7",
9-
description="A toolkit for building SQLAlchemy database engines and utilities.",
7+
name='sqlalchemy-dbtoolkit',
8+
version='0.1.8',
9+
description='A toolkit for building SQLAlchemy database engines and utilities.',
1010
long_description=long_description,
11-
long_description_content_type="text/markdown",
12-
author="Pymetheus",
13-
author_email="github.senate902@passfwd.com",
14-
url="https://github.com/Pymetheus/sqlalchemy-dbtoolkit",
15-
license="MIT",
11+
long_description_content_type='text/markdown',
12+
author='Pymetheus',
13+
author_email='github.senate902@passfwd.com',
14+
url='https://github.com/Pymetheus/sqlalchemy-dbtoolkit',
15+
license='MIT',
1616
packages=find_packages(),
1717
include_package_data=False,
1818
install_requires=[
19-
"sqlalchemy>=2.0",
20-
"mysql-connector-python>=9.3.0",
21-
"psycopg2>=2.9.10",
22-
"pandas>=2.2.0"
19+
'sqlalchemy>=2.0',
20+
'mysql-connector-python>=9.3.0',
21+
'psycopg2>=2.9.10',
22+
'pandas>=2.2.0'
2323
],
24-
python_requires=">=3.8"
24+
python_requires='>=3.8'
2525
)

sqlalchemy_dbtoolkit/engine/builder.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ class BaseEngine(ABC):
1111
"""
1212

1313
DEFAULT_DB_PORTS = {
14-
"mariadb": 3306,
15-
"mssql": 1433,
16-
"mysql": 3306,
17-
"oracle": 1521,
18-
"postgresql": 5432
14+
'mariadb': 3306,
15+
'mssql': 1433,
16+
'mysql': 3306,
17+
'oracle': 1521,
18+
'postgresql': 5432
1919
}
2020

2121
def __init__(self, db_name, config_path='../../.config/config.ini'):
@@ -74,7 +74,7 @@ def sanitize_db_name(self, name):
7474
if len(name) > 63:
7575
raise ValueError(f"Database name is too long: {len(name)} Max length is 63 characters.")
7676

77-
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name):
77+
if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', name):
7878
raise ValueError(f"{name} must start with letter/underscore. Only letters, numbers, underscores allowed.")
7979

8080
def create_connection_url(self):
@@ -87,7 +87,7 @@ def create_connection_url(self):
8787
"""
8888

8989
connection_url = URL.create(
90-
drivername=f"{self.dialect}+{self.driver}",
90+
drivername=f'{self.dialect}+{self.driver}',
9191
username=self.username,
9292
password=self.password,
9393
host=self.host,

sqlalchemy_dbtoolkit/engine/factory.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def validate_supported_dbms(self):
3232
"""
3333
Validates that the provided DBMS is supported.
3434
"""
35-
supported_dbms = ["mysql", "postgresql", "sqlite"]
35+
supported_dbms = ['mysql', 'postgresql', 'sqlite']
3636
if self.dbms not in supported_dbms:
3737
raise ValueError(f"{self.dbms} is not in supported DBMS: {supported_dbms}")
3838

@@ -45,15 +45,15 @@ def initialize_engine(self):
4545
sqlalchemy.engine.Engine: Initialized SQLAlchemy engine.
4646
"""
4747

48-
if self.dbms == "mysql":
48+
if self.dbms == 'mysql':
4949
engine_instance = MysqlEngine(db_name=self.db_name, config_path=self.config_path)
5050
engine_instance.establish_db_connection()
5151
return engine_instance.engine
52-
elif self.dbms == "postgresql":
52+
elif self.dbms == 'postgresql':
5353
engine_instance = PostgreSQLEngine(db_name=self.db_name, config_path=self.config_path)
5454
engine_instance.establish_db_connection()
5555
return engine_instance.engine
56-
elif self.dbms == "sqlite":
56+
elif self.dbms == 'sqlite':
5757
engine_instance = SqliteEngine(db_name=self.db_name, config_path=self.config_path)
5858
engine_instance.establish_db_connection()
5959
return engine_instance.engine

sqlalchemy_dbtoolkit/engine/mysql_engine.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def __init__(self, db_name, config_path='../../.config/config.ini'):
2020
"""
2121

2222
super().__init__(db_name, config_path)
23-
self.driver = "mysqlconnector"
23+
self.driver = 'mysqlconnector'
2424
self.load_config()
2525

2626
@property
@@ -31,7 +31,7 @@ def dialect(self):
3131
Returns:
3232
str: Always returns 'mysql'.
3333
"""
34-
return "mysql"
34+
return 'mysql'
3535

3636
@property
3737
def fallback_database(self):
@@ -42,7 +42,7 @@ def fallback_database(self):
4242
str: The name of the fallback database, 'information_schema'.
4343
"""
4444

45-
return "information_schema"
45+
return 'information_schema'
4646

4747
def load_config(self):
4848
"""
@@ -82,7 +82,7 @@ def database_exists(self):
8282
try:
8383
temp_engine = self.connect_to_fallback_db()
8484
with temp_engine.begin() as temp_connection:
85-
check_db_query = "SHOW DATABASES;"
85+
check_db_query = 'SHOW DATABASES;'
8686
result = temp_connection.execute(text(check_db_query))
8787

8888
for item in result:
@@ -107,7 +107,7 @@ def create_new_database(self, new_db):
107107
try:
108108
temp_engine = self.connect_to_fallback_db()
109109
with temp_engine.connect() as temp_connection:
110-
create_db_query = f"CREATE DATABASE IF NOT EXISTS {new_db}"
110+
create_db_query = f'CREATE DATABASE IF NOT EXISTS {new_db}'
111111
temp_connection.execute(text(create_db_query))
112112
temp_connection.commit()
113113
except Exception as e:

sqlalchemy_dbtoolkit/engine/postgresql_engine.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def __init__(self, db_name, config_path='../../.config/config.ini'):
2020
"""
2121

2222
super().__init__(db_name, config_path)
23-
self.driver = "psycopg2"
23+
self.driver = 'psycopg2'
2424
self.load_config()
2525

2626
@property
@@ -31,7 +31,7 @@ def dialect(self):
3131
Returns:
3232
str: Always returns 'postgresql'.
3333
"""
34-
return "postgresql"
34+
return 'postgresql'
3535

3636
@property
3737
def fallback_database(self):
@@ -41,7 +41,7 @@ def fallback_database(self):
4141
Returns:
4242
str: The name of the fallback database, 'postgres'.
4343
"""
44-
return "postgres"
44+
return 'postgres'
4545

4646
def load_config(self):
4747
"""
@@ -81,7 +81,7 @@ def database_exists(self):
8181
try:
8282
temp_engine = self.connect_to_fallback_db()
8383
with temp_engine.begin() as temp_connection:
84-
check_db_query = "SELECT datname FROM pg_database WHERE datistemplate = false;"
84+
check_db_query = 'SELECT datname FROM pg_database WHERE datistemplate = false;'
8585
result = temp_connection.execute(text(check_db_query))
8686

8787
for item in result:
@@ -104,8 +104,8 @@ def create_new_database(self, new_db):
104104
temp_engine = None
105105
try:
106106
temp_engine = self.connect_to_fallback_db()
107-
with temp_engine.connect().execution_options(isolation_level="AUTOCOMMIT") as temp_connection:
108-
create_db_query = f"CREATE DATABASE {new_db}"
107+
with temp_engine.connect().execution_options(isolation_level='AUTOCOMMIT') as temp_connection:
108+
create_db_query = f'CREATE DATABASE {new_db}'
109109
temp_connection.execute(text(create_db_query))
110110
temp_connection.commit()
111111
except Exception as e:

sqlalchemy_dbtoolkit/engine/sqlite_engine.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def dialect(self):
3333
str: Always returns 'sqlite'.
3434
"""
3535

36-
return "sqlite"
36+
return 'sqlite'
3737

3838
@property
3939
def fallback_database(self):
@@ -66,7 +66,7 @@ def create_connection_url(self):
6666
sqlalchemy.engine.URL: The SQLite connection URL.
6767
"""
6868

69-
database_path = os.path.join(self.sqlite_dir_path, f"{self.db_name}.db")
69+
database_path = os.path.join(self.sqlite_dir_path, f'{self.db_name}.db')
7070
connection_url = URL.create(
7171
drivername=self.dialect,
7272
database=database_path
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from sqlalchemy_dbtoolkit.orm.session import ORMSessionManager
2+
from sqlalchemy_dbtoolkit.utils.query_operators import get_filter_operator
3+
4+
5+
class DeleteManager:
6+
"""
7+
Handles database delete operations using SQLAlchemy ORM sessions.
8+
"""
9+
10+
def __init__(self, engine):
11+
"""
12+
Initializes the DeleteManager with a SQLAlchemy engine.
13+
14+
Args:
15+
engine (sqlalchemy.Engine): An initialized SQLAlchemy engine.
16+
"""
17+
18+
self.session_manager = ORMSessionManager(engine)
19+
20+
def delete_row(self, row_instance):
21+
"""
22+
Deletes a single ORM object from the database.
23+
24+
Args:
25+
row_instance (Base): An instance of a SQLAlchemy ORM model to delete.
26+
27+
Returns:
28+
One
29+
"""
30+
31+
if row_instance is None:
32+
raise ValueError("Cannot delete a None object")
33+
34+
with self.session_manager.session_scope() as session:
35+
session.delete(row_instance)
36+
37+
return 1
38+
39+
def delete_rows(self, row_instances):
40+
"""
41+
Deletes multiple ORM objects at once.
42+
43+
Args:
44+
row_instances (list[Base]): A list of SQLAlchemy ORM model instances to delete.
45+
46+
Returns:
47+
int: The number of rows deleted.
48+
"""
49+
50+
if row_instances is None:
51+
raise ValueError("Must be a non-empty list of ORM objects")
52+
53+
with self.session_manager.session_scope() as session:
54+
for instance in row_instances:
55+
session.delete(instance)
56+
57+
return len(row_instances)
58+
59+
def delete_rows_by_filter(self, Table, column_name, column_value, operator_name='eq'):
60+
"""
61+
Deletes rows from the specified table based on a filter condition.
62+
63+
Args:
64+
Table (Base): A SQLAlchemy ORM model/table class.
65+
column_name (str): The column name to filter by.
66+
column_value (Any): The value to match in the specified column.
67+
operator_name (str, optional): The filter operator to use (default 'eq').
68+
Supported operators: eq, ne, gt, lt, ge, le, like, in, etc.
69+
70+
Returns:
71+
int: The number of rows deleted.
72+
"""
73+
74+
with self.session_manager.session_scope() as session:
75+
column_attr = getattr(Table, column_name, None)
76+
if column_attr is None:
77+
raise AttributeError(f"{column_name} is not a valid column of {Table.__name__}")
78+
79+
operator_func = get_filter_operator(operator_name=operator_name)
80+
deleted_rows = session.query(Table).filter(operator_func(column_attr, column_value)).delete(
81+
synchronize_session=False)
82+
83+
return deleted_rows

0 commit comments

Comments
 (0)