Skip to content

Adding new general execute queries and adding deprecation #459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,072 changes: 557 additions & 515 deletions coverage.xml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dsg_lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"""
from datetime import date

__version__ = "2024.10.20.1"
__version__ = "2024-11-28-001"
__author__ = "Mike Ryan"
__license__ = "MIT"
__copyright__ = f"Copyright© 2021-{date.today().year}"
Expand Down
119 changes: 115 additions & 4 deletions dsg_lib/async_database_functions/database_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@
Date: 2024/05/16
License: MIT
"""

import functools
import time
from typing import Dict, List, Type
import warnings
from typing import Any, Dict, List, Optional, Tuple, Type, Union

from sqlalchemy import delete
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.sql.elements import ClauseElement

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


def deprecated(reason):
def decorator(func):
message = f"{func.__name__}() is deprecated: {reason}"

@functools.wraps(func)
async def wrapped(*args, **kwargs):
warnings.warn(
message,
DeprecationWarning,
stacklevel=2,
)
return await func(*args, **kwargs)
return wrapped
return decorator


class DatabaseOperations:
"""
This class provides methods for performing CRUD operations on a database using SQLAlchemy's asynchronous session.
Expand Down Expand Up @@ -435,6 +451,7 @@ async def get_table_names(self):
logger.error(f"Exception occurred: {ex}") # pragma: no cover
return handle_exceptions(ex) # pragma: no cover

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

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

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

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

@deprecated("User 'execute_many' with a DELETE query instead.")
async def delete_many(
self,
table: Type[DeclarativeMeta],
Expand Down Expand Up @@ -1185,3 +1206,93 @@ async def delete_many(
# Handle any exceptions that occur during the record deletion
logger.error(f"Exception occurred: {ex}")
return handle_exceptions(ex)


async def execute_one(
self,
query: ClauseElement,
values: Optional[Dict[str, Any]] = None
) -> Union[str, Dict[str, str]]:
"""
Executes a single non-read SQL query asynchronously.

This method executes a single SQL statement that modifies the database,
such as INSERT, UPDATE, or DELETE. It handles the execution within an
asynchronous session and commits the transaction upon success.

Args:
query (ClauseElement): An SQLAlchemy query object representing the SQL statement to execute.
values (Optional[Dict[str, Any]]): A dictionary of parameter values to bind to the query.
Defaults to None.

Returns:
Union[str, Dict[str, str]]: "complete" if the query executed and committed successfully,
or an error dictionary if an exception occurred.

Example:
```python
from sqlalchemy import insert

query = insert(User).values(name='John Doe')
result = await db_ops.execute_one(query)
```
"""
logger.debug("Starting execute_one operation")
try:
async with self.async_db.get_db_session() as session:
logger.debug(f"Executing query: {query}")
await session.execute(query, params=values)
await session.commit()
logger.debug("Query executed successfully")
return "complete"
except Exception as ex:
logger.error(f"Exception occurred: {ex}")
return handle_exceptions(ex)

async def execute_many(
self,
queries: List[Tuple[ClauseElement, Optional[Dict[str, Any]]]]
) -> Union[str, Dict[str, str]]:
"""
Executes multiple non-read SQL queries asynchronously within a single transaction.

This method executes a list of SQL statements that modify the database,
such as multiple INSERTs, UPDATEs, or DELETEs. All queries are executed
within the same transaction, which is committed if all succeed, or rolled
back if any fail.

Args:
queries (List[Tuple[ClauseElement, Optional[Dict[str, Any]]]]): A list of tuples, each containing
a query and an optional dictionary of parameter values. Each tuple should be of the form
`(query, values)` where:
- `query` is an SQLAlchemy query object.
- `values` is a dictionary of parameters to bind to the query (or None).

Returns:
Union[str, Dict[str, str]]: "complete" if all queries executed and committed successfully,
or an error dictionary if an exception occurred.

Example:
```python
from sqlalchemy import insert

queries = [
(insert(User), {'name': 'User1'}),
(insert(User), {'name': 'User2'}),
(insert(User), {'name': 'User3'}),
]
result = await db_ops.execute_many(queries)
```
"""
logger.debug("Starting execute_many operation")
try:
async with self.async_db.get_db_session() as session:
for query, values in queries:
logger.debug(f"Executing query: {query}")
await session.execute(query, params=values)
await session.commit()
logger.debug("All queries executed successfully")
return "complete"
except Exception as ex:
logger.error(f"Exception occurred: {ex}")
return handle_exceptions(ex)
30 changes: 28 additions & 2 deletions examples/fastapi_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from fastapi.responses import RedirectResponse
from loguru import logger
from pydantic import BaseModel, EmailStr
from sqlalchemy import Column, ForeignKey, Select, String
from sqlalchemy import Column, ForeignKey, Select, String, insert
from sqlalchemy.orm import relationship
from tqdm import tqdm

Expand Down Expand Up @@ -99,7 +99,7 @@ async def lifespan(app: FastAPI):

create_users = True
if create_users:
await create_a_bunch_of_users(single_entry=2000, many_entries=20000)
await create_a_bunch_of_users(single_entry=2, many_entries=100)
yield
logger.info("shutting down")
await async_db.disconnect()
Expand Down Expand Up @@ -341,6 +341,32 @@ async def read_list_of_records(
return records_list


@app.post("/database/execute-one", tags=["Database Examples"])
async def execute_query(query: str = Body(...)):
# add a user with execute_one
logger.info(f"Executing query: {query}")

query = insert(User).values(first_name='John', last_name='Doe',email='x@abc.com')
result = await db_ops.execute_one(query)
logger.info(f"Executed query: {result}")
query_return = await db_ops.read_query(Select(User).where(User.first_name == 'John'))
return query_return

@app.post("/database/execute-many", tags=["Database Examples"])
async def execute_many(query: str = Body(...)):
# multiple users with execute_many
logger.info(f"Executing query: {query}")
queries = []

for i in range(10):
query = insert(User).values(first_name=f'User{i}', last_name='Doe',email='x@abc.com')
queries.append(query)

results = await db_ops.execute_many(queries)
logger.info(f"Executed query: {results}")
query_return = await db_ops.read_query(Select(User))
return query_return

if __name__ == "__main__":
import uvicorn

Expand Down
47 changes: 7 additions & 40 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,49 +1,16 @@
[build-system]
requires = ["hatchling"]
requires = [ "hatchling",]
build-backend = "hatchling.build"

[project]
name = "devsetgo_lib"
version = "2024.10.20.1"
version = "2024.11.28.1"
requires-python = ">=3.9"
description = """
DevSetGo Library is a Python library offering reusable functions for efficient coding. It includes file operations, calendar utilities, pattern matching, advanced logging with loguru, FastAPI endpoints, async database handling, and email validation. Designed for ease of use and versatility, it's a valuable tool for Python developers.
"""
keywords = [
"python",
"library",
"reusable functions",
"file operations",
"calendar utilities",
"pattern matching",
"logging",
"loguru",
"FastAPI",
"async database",
"CRUD operations",
"email validation",
"development tools",
]
description = "DevSetGo Library is a Python library offering reusable functions for efficient coding. It includes file operations, calendar utilities, pattern matching, advanced logging with loguru, FastAPI endpoints, async database handling, and email validation. Designed for ease of use and versatility, it's a valuable tool for Python developers.\n"
keywords = [ "python", "library", "reusable functions", "file operations", "calendar utilities", "pattern matching", "logging", "loguru", "FastAPI", "async database", "CRUD operations", "email validation", "development tools",]
readme = "README.md"
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
# "Programming Language :: Python :: 3.13",
"Operating System :: POSIX :: Linux",
"Operating System :: POSIX",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
]
dependencies = [
"loguru>=0.7.0",
"packaging>=20.0",
"email-validator>=2.1.1",
]
classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Operating System :: POSIX :: Linux", "Operating System :: POSIX", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows",]
dependencies = [ "loguru>=0.7.0", "packaging>=20.0", "email-validator>=2.1.1",]
[[project.authors]]
name = "Mike Ryan"
email = "mikeryan56@gmail.com"
Expand Down Expand Up @@ -129,7 +96,7 @@ exclude_lines = [ "pragma: no cover", "if __name__", "def main", "import_sqlalch
norecursedirs = [ "/tests",]
testpaths = [ "tests",]
python_files = [ "test_*.py", "*_test.py",]
addopts = [ "--cov=./", "--cov-report=html", "--cov-report=xml", "--junitxml=report.xml", "-ra", "--strict-markers", "--tb=short", "-p pytester",]
addopts = [ "--cov=./", "--cov-report=html", "--cov-report=xml", "--junitxml=report.xml", "--html=htmlcov/_test_report.html", "--self-contained-html", "-ra", "--strict-markers", "--tb=short", "-p", "pytester",]

[tool.hatch.build.targets.sdist]
include = [ "/dsg_lib",]
Expand Down
2 changes: 1 addition & 1 deletion report.xml

Large diffs are not rendered by default.

32 changes: 17 additions & 15 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,42 +1,44 @@
aiomysql==0.2.0 # Vulnerabilities: None
aiosqlite==0.20.0 # Vulnerabilities: None
asyncpg==0.29.0 # Vulnerabilities: None
asyncpg==0.30.0 # From 0.29.0 | Vulnerabilities: None
autoflake==2.3.1 # Vulnerabilities: None
autopep8==2.3.1 # Vulnerabilities: None
black==24.10.0 # Vulnerabilities: None
bumpcalver==2024.10.20.4 # Vulnerabilities: None
bumpcalver==2024.11.8 # From 2024.10.20.4 | Vulnerabilities: None
click==8.1.7 # Vulnerabilities: None
fastapi[all]==0.115.4 # Vulnerabilities: None
fastapi[all]==0.115.5 # From 0.115.2 | Vulnerabilities: None
flake8==7.1.1 # Vulnerabilities: None
genbadge[all]==1.1.1 # Vulnerabilities: None
hatchling==1.25.0 # Vulnerabilities: None
hatchling==1.26.3 # From 1.25.0 | Vulnerabilities: None
loguru==0.7.2 # Vulnerabilities: None
mike==2.1.3 # Vulnerabilities: None
mkdocs-material==9.5.41 # Vulnerabilities: None
mkdocs-material==9.5.46 # From 9.5.41 | Vulnerabilities: None
mkdocs-print-site-plugin==2.6.0 # Vulnerabilities: None
mkdocstrings[python,shell]==0.26.2 # Vulnerabilities: None
oracledb==2.4.1 # Vulnerabilities: None
packaging==24.1 # Vulnerabilities: None
mkdocstrings[python,shell]==0.27.0 # From 0.26.2 | Vulnerabilities: None
oracledb==2.5.0 # From 2.4.1 | Vulnerabilities: None
packaging==24.2 # From 24.1 | Vulnerabilities: None
pre-commit==4.0.1 # Vulnerabilities: None
psycopg2==2.9.10 # Vulnerabilities: None
Pygments==2.18.0 # Vulnerabilities: None
pylint==3.3.1 # Vulnerabilities: None
pymdown-extensions==10.12 # Vulnerabilities: None
pymdown-extensions==10.12 # From 10.11.2 | Vulnerabilities: None
pytest==8.3.3 # Vulnerabilities: None
pytest-asyncio==0.24.0 # Vulnerabilities: None
pytest-cov==5.0.0 # Vulnerabilities: None
pytest-cov==6.0.0 # From 5.0.0 | Vulnerabilities: None
pytest-html==4.1.1 # From none | Vulnerabilities: None
pytest-mock==3.14.0 # Vulnerabilities: None
pytest-runner==6.0.1 # Vulnerabilities: None
pytest-xdist==3.6.1 # Vulnerabilities: None
python-json-logger==2.0.7 # Vulnerabilities: None
PyYAML==6.0.2 # Vulnerabilities: None
ruff==0.7.1 # Vulnerabilities: None
ruff==0.8.0 # From 0.7.0 | Vulnerabilities: None
SQLAlchemy==2.0.36 # Vulnerabilities: None
structlog==24.4.0 # Vulnerabilities: None
toml==0.10.2 # Vulnerabilities: None
tox==4.23.2 # Vulnerabilities: None
tqdm==4.66.6 # Vulnerabilities: None
tox==4.23.2 # From 4.23.0 | Vulnerabilities: None
tqdm==4.67.1 # From 4.66.5 | Vulnerabilities: None
twine==5.1.1 # Vulnerabilities: None
watchdog==5.0.3 # Vulnerabilities: None
wheel==0.44.0 # Vulnerabilities: None
watchdog==6.0.0 # From 5.0.3 | Vulnerabilities: None
wheel==0.45.1 # From 0.44.0 | Vulnerabilities: None
xmltodict==0.14.2 # Vulnerabilities: None

2 changes: 1 addition & 1 deletion tests-badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading