Skip to content

Commit 3680346

Browse files
committed
adding general execute functions and add future deprecate
1 parent 9b6bf99 commit 3680346

File tree

2 files changed

+182
-5
lines changed

2 files changed

+182
-5
lines changed

dsg_lib/async_database_functions/database_operations.py

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@
2828
Date: 2024/05/16
2929
License: MIT
3030
"""
31-
31+
import functools
3232
import time
33-
from typing import Dict, List, Type
33+
import warnings
34+
from typing import Any, Dict, List, Optional, Tuple, Type, Union
3435

3536
from sqlalchemy import delete
3637
from sqlalchemy.ext.declarative import DeclarativeMeta
38+
from sqlalchemy.sql.elements import ClauseElement
3739

38-
# from loguru import logger
39-
# import logging as logger
4040
from .. import LOGGER as logger
4141
from .__import_sqlalchemy import import_sqlalchemy
4242
# Importing AsyncDatabase class from local module async_database
@@ -107,6 +107,22 @@ def handle_exceptions(ex: Exception) -> Dict[str, str]:
107107
return {"error": "General Exception", "details": str(ex)}
108108

109109

110+
def deprecated(reason):
111+
def decorator(func):
112+
message = f"{func.__name__}() is deprecated: {reason}"
113+
114+
@functools.wraps(func)
115+
async def wrapped(*args, **kwargs):
116+
warnings.warn(
117+
message,
118+
DeprecationWarning,
119+
stacklevel=2,
120+
)
121+
return await func(*args, **kwargs)
122+
return wrapped
123+
return decorator
124+
125+
110126
class DatabaseOperations:
111127
"""
112128
This class provides methods for performing CRUD operations on a database using SQLAlchemy's asynchronous session.
@@ -435,6 +451,7 @@ async def get_table_names(self):
435451
logger.error(f"Exception occurred: {ex}") # pragma: no cover
436452
return handle_exceptions(ex) # pragma: no cover
437453

454+
@deprecated("Use `execute_one` with an INSERT query instead.")
438455
async def create_one(self, record):
439456
"""
440457
Adds a single record to the database.
@@ -506,6 +523,7 @@ async def create_one(self, record):
506523
logger.error(f"Exception occurred: {ex}")
507524
return handle_exceptions(ex)
508525

526+
@deprecated("Use `execute_one` with an INSERT query instead.")
509527
async def create_many(self, records):
510528
"""
511529
Adds multiple records to the database.
@@ -904,6 +922,7 @@ async def read_multi_query(self, queries: Dict[str, str]):
904922
logger.error(f"Exception occurred: {ex}")
905923
return handle_exceptions(ex)
906924

925+
@deprecated("Use `execute_one` with a UPDATE query instead.")
907926
async def update_one(self, table, record_id: str, new_values: dict):
908927
"""
909928
Updates a single record in the database identified by its ID.
@@ -997,6 +1016,7 @@ async def update_one(self, table, record_id: str, new_values: dict):
9971016
logger.error(f"Exception occurred: {ex}")
9981017
return handle_exceptions(ex)
9991018

1019+
@deprecated("Use `execute_many` with a DELETE query instead.")
10001020
async def delete_one(self, table, record_id: str):
10011021
"""
10021022
Deletes a single record from the database based on the provided table
@@ -1100,6 +1120,7 @@ async def delete_one(self, table, record_id: str):
11001120
logger.error(f"Exception occurred: {ex}")
11011121
return handle_exceptions(ex)
11021122

1123+
@deprecated("User 'execute_many' with a DELETE query instead.")
11031124
async def delete_many(
11041125
self,
11051126
table: Type[DeclarativeMeta],
@@ -1185,3 +1206,93 @@ async def delete_many(
11851206
# Handle any exceptions that occur during the record deletion
11861207
logger.error(f"Exception occurred: {ex}")
11871208
return handle_exceptions(ex)
1209+
1210+
1211+
async def execute_one(
1212+
self,
1213+
query: ClauseElement,
1214+
values: Optional[Dict[str, Any]] = None
1215+
) -> Union[str, Dict[str, str]]:
1216+
"""
1217+
Executes a single non-read SQL query asynchronously.
1218+
1219+
This method executes a single SQL statement that modifies the database,
1220+
such as INSERT, UPDATE, or DELETE. It handles the execution within an
1221+
asynchronous session and commits the transaction upon success.
1222+
1223+
Args:
1224+
query (ClauseElement): An SQLAlchemy query object representing the SQL statement to execute.
1225+
values (Optional[Dict[str, Any]]): A dictionary of parameter values to bind to the query.
1226+
Defaults to None.
1227+
1228+
Returns:
1229+
Union[str, Dict[str, str]]: "complete" if the query executed and committed successfully,
1230+
or an error dictionary if an exception occurred.
1231+
1232+
Example:
1233+
```python
1234+
from sqlalchemy import insert
1235+
1236+
query = insert(User).values(name='John Doe')
1237+
result = await db_ops.execute_one(query)
1238+
```
1239+
"""
1240+
logger.debug("Starting execute_one operation")
1241+
try:
1242+
async with self.async_db.get_db_session() as session:
1243+
logger.debug(f"Executing query: {query}")
1244+
await session.execute(query, params=values)
1245+
await session.commit()
1246+
logger.debug("Query executed successfully")
1247+
return "complete"
1248+
except Exception as ex:
1249+
logger.error(f"Exception occurred: {ex}")
1250+
return handle_exceptions(ex)
1251+
1252+
async def execute_many(
1253+
self,
1254+
queries: List[Tuple[ClauseElement, Optional[Dict[str, Any]]]]
1255+
) -> Union[str, Dict[str, str]]:
1256+
"""
1257+
Executes multiple non-read SQL queries asynchronously within a single transaction.
1258+
1259+
This method executes a list of SQL statements that modify the database,
1260+
such as multiple INSERTs, UPDATEs, or DELETEs. All queries are executed
1261+
within the same transaction, which is committed if all succeed, or rolled
1262+
back if any fail.
1263+
1264+
Args:
1265+
queries (List[Tuple[ClauseElement, Optional[Dict[str, Any]]]]): A list of tuples, each containing
1266+
a query and an optional dictionary of parameter values. Each tuple should be of the form
1267+
`(query, values)` where:
1268+
- `query` is an SQLAlchemy query object.
1269+
- `values` is a dictionary of parameters to bind to the query (or None).
1270+
1271+
Returns:
1272+
Union[str, Dict[str, str]]: "complete" if all queries executed and committed successfully,
1273+
or an error dictionary if an exception occurred.
1274+
1275+
Example:
1276+
```python
1277+
from sqlalchemy import insert
1278+
1279+
queries = [
1280+
(insert(User), {'name': 'User1'}),
1281+
(insert(User), {'name': 'User2'}),
1282+
(insert(User), {'name': 'User3'}),
1283+
]
1284+
result = await db_ops.execute_many(queries)
1285+
```
1286+
"""
1287+
logger.debug("Starting execute_many operation")
1288+
try:
1289+
async with self.async_db.get_db_session() as session:
1290+
for query, values in queries:
1291+
logger.debug(f"Executing query: {query}")
1292+
await session.execute(query, params=values)
1293+
await session.commit()
1294+
logger.debug("All queries executed successfully")
1295+
return "complete"
1296+
except Exception as ex:
1297+
logger.error(f"Exception occurred: {ex}")
1298+
return handle_exceptions(ex)

tests/test_database_functions/test_async_database.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import secrets
44

55
import pytest
6-
from sqlalchemy import Column, Integer, String, select
6+
from sqlalchemy import Column, Integer, String, delete, insert, select
77
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
88

99
from dsg_lib.async_database_functions.async_database import AsyncDatabase
@@ -439,3 +439,69 @@ async def test_delete_many_exception(self, db_ops):
439439
)
440440
# assert result contains "error"
441441
assert "error" in result
442+
443+
@pytest.fixture(scope="class", autouse=True)
444+
async def setup_database(self):
445+
await async_db.create_tables()
446+
yield
447+
await async_db.drop_tables()
448+
449+
@pytest.fixture(scope="function", autouse=True)
450+
async def setup_teardown(self):
451+
# Clean the database before each test
452+
await db_ops.execute_one(delete(User))
453+
yield
454+
# Clean the database after each test
455+
await db_ops.execute_one(delete(User))
456+
457+
@pytest.mark.asyncio
458+
async def test_execute_one_insert(self):
459+
query = insert(User).values(name='Test User')
460+
result = await db_ops.execute_one(query)
461+
assert result == "complete"
462+
r_query = select(User).where(User.name == 'Test User')
463+
user = await db_ops.read_one_record(query=r_query)
464+
assert user.name == 'Test User'
465+
466+
@pytest.mark.asyncio
467+
async def test_execute_many_insert(self):
468+
queries = [
469+
(insert(User), {'name': f'User {i}'}) for i in range(1, 6)
470+
]
471+
result = await db_ops.execute_many(queries)
472+
assert result == "complete"
473+
r_query = select(User)
474+
users = await db_ops.read_query(query=r_query)
475+
assert len(users) >= 5
476+
477+
@pytest.mark.asyncio
478+
async def test_execute_one_delete(self):
479+
query = insert(User).values(name='Test User')
480+
await db_ops.execute_one(query)
481+
query = delete(User).where(User.name == 'Test User')
482+
result = await db_ops.execute_one(query)
483+
assert result == "complete"
484+
r_query = select(User).where(User.name == 'Test User')
485+
user = await db_ops.read_one_record(query=r_query)
486+
assert user is None
487+
488+
@pytest.mark.asyncio
489+
async def test_execute_many_delete(self):
490+
# Insert users to delete
491+
queries = [
492+
(insert(User), {'name': f'User {i}'}) for i in range(1, 6)
493+
]
494+
await db_ops.execute_many(queries)
495+
# Fetch all users
496+
r_query = select(User)
497+
users = await db_ops.read_query(query=r_query)
498+
# Create delete queries based on pkid
499+
user_pkids = [user.pkid for user in users]
500+
queries = [
501+
(delete(User).where(User.pkid == pkid), None) for pkid in user_pkids
502+
]
503+
result = await db_ops.execute_many(queries)
504+
assert result == "complete"
505+
# Verify all users are deleted
506+
users = await db_ops.read_query(query=r_query)
507+
assert len(users) == 0

0 commit comments

Comments
 (0)