sqlalchemy updated

This commit is contained in:
2025-01-21 19:35:34 +03:00
parent 8e34497c80
commit 87e5f5ab06
54 changed files with 2549 additions and 540 deletions

View File

@@ -0,0 +1,148 @@
from typing import Type, TypeVar
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ApiLibrary import get_line_number_for_error
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
# Type variable for class methods returning self
T = TypeVar("T", bound="FilterAttributes")
class BaseAlchemyModel:
"""
Controller of alchemy to database transactions.
Query: Query object for model
Session: Session object for model
Actions: save, flush, rollback, commit
"""
__abstract__ = True
@classmethod
def new_session(cls) -> Session:
"""Get database session."""
from Services.PostgresDb.database import get_db
with get_db() as session:
return session
@classmethod
def flush(cls: Type[T], db: Session) -> T:
"""
Flush the current session to the database.
Args:
db: Database session
Returns:
Self instance
Raises:
HTTPException: If database operation fails
"""
try:
db.flush()
return cls
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
def destroy(self: Type[T], db: Session) -> None:
"""
Delete the record from the database.
Args:
db: Database session
"""
db.delete(self)
@classmethod
def save_via_metadata(cls: Type[T], db: Session) -> None:
"""
Save or rollback based on metadata.
Args:
db: Database session
Raises:
HTTPException: If save operation fails
"""
try:
if cls.is_created:
db.commit()
db.flush()
db.rollback()
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
@classmethod
def save(cls: Type[T], db: Session) -> None:
"""
Commit changes to database.
Args:
db: Database session
Raises:
HTTPException: If commit fails
"""
try:
db.commit()
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
except Exception as e:
raise HTTPExceptionApi(
error_code="HTTP_500_INTERNAL_SERVER_ERROR",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
@classmethod
def save_and_confirm(cls: Type[T], db: Session) -> None:
"""
Save changes and mark record as confirmed.
Args:
db: Database session
Raises:
HTTPException: If operation fails
"""
try:
cls.save(db)
cls.update(db, is_confirmed=True)
cls.save(db)
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
@classmethod
def rollback(cls: Type[T], db: Session) -> None:
"""
Rollback current transaction.
Args:
db: Database session
"""
db.rollback()

View File

@@ -0,0 +1,395 @@
import datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional
from sqlalchemy import TIMESTAMP, NUMERIC
from sqlalchemy.orm import Session, Mapped
from pydantic import BaseModel
from ApiLibrary import system_arrow, get_line_number_for_error, client_arrow
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from Services.PostgresDb.Models.core_alchemy import BaseAlchemyModel
from Services.PostgresDb.Models.system_fields import SystemFields
class MetaDataRow(BaseModel):
created: Optional[bool] = False
message: Optional[str] = None
error_case: Optional[str] = None
class Credentials(BaseModel):
person_id: int
person_name: str
class CrudActions(SystemFields):
@classmethod
def extract_system_fields(cls, filter_kwargs: dict, create: bool = True) -> Dict[str, Any]:
"""
Remove system-managed fields from input dictionary.
Args:
filter_kwargs: Input dictionary of fields
create: If True, use creation field list, else use update field list
Returns:
Dictionary with system fields removed
"""
system_fields = filter_kwargs.copy()
extract_fields = (
cls.__system__fields__create__ if create else cls.__system__fields__update__
)
for field in extract_fields:
system_fields.pop(field, None)
return system_fields
@classmethod
def remove_non_related_inputs(cls, kwargs: Dict[str, Any]) -> Dict[str, Any]:
"""
Filter out inputs that don't correspond to model fields.
Args:
kwargs: Dictionary of field names and values
Returns:
Dictionary containing only valid model fields
"""
return {
key: value
for key, value in kwargs.items()
if key in cls.columns + cls.hybrid_properties + cls.settable_relations
}
@classmethod
def iterate_over_variables(cls, val: Any, key: str) -> tuple[bool, Optional[Any]]:
"""
Process a field value based on its type and convert it to the appropriate format.
Args:
val: Field value
key: Field name
Returns:
Tuple of (should_include, processed_value)
"""
key_ = cls.__annotations__.get(key, None)
is_primary = key in cls.primary_keys
row_attr = bool(getattr(getattr(cls, key), "foreign_keys", None))
# Skip primary keys and foreign keys
if is_primary or row_attr:
return False, None
# Handle None values
if val is None:
return True, None
# Special handling for UUID fields
if str(key[-5:]).lower() == "uu_id":
return True, str(val)
# Handle typed fields
if key_:
if key_ == Mapped[int]:
return True, int(val)
elif key_ == Mapped[bool]:
return True, bool(val)
elif key_ == Mapped[float] or key_ == Mapped[NUMERIC]:
return True, round(float(val), 3)
elif key_ == Mapped[TIMESTAMP]:
return True, str(
system_arrow.get(str(val)).format("YYYY-MM-DD HH:mm:ss ZZ")
)
elif key_ == Mapped[str]:
return True, str(val)
# Handle based on Python types
else:
if isinstance(val, datetime.datetime):
return True, str(
system_arrow.get(str(val)).format("YYYY-MM-DD HH:mm:ss ZZ")
)
elif isinstance(val, bool):
return True, bool(val)
elif isinstance(val, (float, Decimal)):
return True, round(float(val), 3)
elif isinstance(val, int):
return True, int(val)
elif isinstance(val, str):
return True, str(val)
elif val is None:
return True, None
return False, None
def get_dict(
self,
exclude: Optional[List[str]] = None,
include: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""
Convert model instance to dictionary with customizable fields.
Args:
exclude: List of fields to exclude
include: List of fields to include (takes precedence over exclude)
Returns:
Dictionary representation of the model
"""
return_dict: Dict[str, Any] = {}
if include:
# Handle explicitly included fields
exclude_list = [
element
for element in self.__system_default_model__
if str(element)[-2:] == "id" and str(element)[-5:].lower() == "uu_id"
]
columns_include_list = list(set(include).difference(set(exclude_list)))
columns_include_list.extend(["uu_id"])
for key in columns_include_list:
val = getattr(self, key)
correct, value_of_database = self.iterate_over_variables(val, key)
if correct:
return_dict[key] = value_of_database
elif exclude:
# Handle explicitly excluded fields
exclude.extend(
list(
set(getattr(self, "__exclude__fields__", []) or []).difference(
exclude
)
)
)
exclude.extend(
[
element
for element in self.__system_default_model__
if str(element)[-2:] == "id"
]
)
columns_excluded_list = list(set(self.columns).difference(set(exclude)))
columns_excluded_list.extend(["uu_id", "active"])
for key in columns_excluded_list:
val = getattr(self, key)
correct, value_of_database = self.iterate_over_variables(val, key)
if correct:
return_dict[key] = value_of_database
else:
# Handle default field selection
exclude_list = (
getattr(self, "__exclude__fields__", []) or []
) + list(self.__system_default_model__)
columns_list = list(set(self.columns).difference(set(exclude_list)))
columns_list = [col for col in columns_list if str(col)[-2:] != "id"]
columns_list.extend(
[col for col in self.columns if str(col)[-5:].lower() == "uu_id"]
)
for remove_field in self.__system_default_model__:
if remove_field in columns_list:
columns_list.remove(remove_field)
for key in columns_list:
val = getattr(self, key)
correct, value_of_database = self.iterate_over_variables(val, key)
if correct:
return_dict[key] = value_of_database
return return_dict
class CRUDModel(BaseAlchemyModel, CrudActions):
__abstract__ = True
meta_data: MetaDataRow
creds: Credentials = None
@property
def is_created(self):
return self.meta_data.created
@classmethod
def create_credentials(cls, record_created) -> None:
"""
Save user credentials for tracking.
Args:
record_created: Record that created or updated
"""
if getattr(cls.creds, "person_id", None) and getattr(
cls.creds, "person_name", None
):
record_created.created_by_id = cls.creds.person_id
record_created.created_by = cls.creds.person_name
return
@classmethod
def update_metadata(cls, created: bool, error_case: str = None, message: str = None) -> None:
cls.meta_data = MetaDataRow(
created=created,
error_case=error_case,
message=message
)
@classmethod
def raise_exception(cls):
raise HTTPExceptionApi(
error_code=cls.meta_data.error_case,
lang=cls.lang,
loc=get_line_number_for_error(),
sys_msg=cls.meta_data.message
)
@classmethod
def create_or_abort(cls, db: Session, **kwargs):
"""
Create a new record or abort if it already exists.
Args:
db: Database session
**kwargs: Record fields
Returns:
New record if successfully created
"""
check_kwargs = cls.extract_system_fields(kwargs)
# Search for existing record
query = db.query(cls).filter(
cls.expiry_ends > str(system_arrow.now()),
cls.expiry_starts <= str(system_arrow.now()),
)
for key, value in check_kwargs.items():
if hasattr(cls, key):
query = query.filter(getattr(cls, key) == value)
already_record = query.first()
# Handle existing record
if already_record:
if already_record.deleted:
cls.update_metadata(created=False, error_case="DeletedRecord")
cls.raise_exception()
elif not already_record.is_confirmed:
cls.update_metadata(created=False, error_case="IsNotConfirmed")
cls.raise_exception()
cls.update_metadata(created=False, error_case="AlreadyExists")
cls.raise_exception()
# Create new record
check_kwargs = cls.remove_non_related_inputs(check_kwargs)
created_record = cls()
for key, value in check_kwargs.items():
setattr(created_record, key, value)
cls.create_credentials(created_record)
db.add(created_record)
db.flush()
cls.update_metadata(created=True)
return created_record
@classmethod
def find_or_create(cls, db: Session, **kwargs):
"""
Find an existing record matching the criteria or create a new one.
Args:
db: Database session
**kwargs: Search/creation criteria
Returns:
Existing or newly created record
"""
check_kwargs = cls.extract_system_fields(kwargs)
# Search for existing record
query = db.query(cls).filter(
cls.expiry_ends > str(system_arrow.now()),
cls.expiry_starts <= str(system_arrow.now()),
)
for key, value in check_kwargs.items():
if hasattr(cls, key):
query = query.filter(getattr(cls, key) == value)
already_record = query.first()
# Handle existing record
if already_record:
if already_record.deleted:
cls.update_metadata(created=False, error_case="DeletedRecord")
return already_record
elif not already_record.is_confirmed:
cls.update_metadata(created=False, error_case="IsNotConfirmed")
return already_record
cls.update_metadata(created=False, error_case="AlreadyExists")
return already_record
# Create new record
check_kwargs = cls.remove_non_related_inputs(check_kwargs)
created_record = cls()
for key, value in check_kwargs.items():
setattr(created_record, key, value)
cls.create_credentials(created_record)
db.add(created_record)
db.flush()
cls.update_metadata(created=True)
return created_record
def update(self, db: Session, **kwargs):
"""
Update the record with new values.
Args:
db: Database session
**kwargs: Fields to update
Returns:
Updated record
Raises:
ValueError: If attempting to update is_confirmed with other fields
"""
check_kwargs = self.remove_non_related_inputs(kwargs)
check_kwargs = self.extract_system_fields(check_kwargs, create=False)
for key, value in check_kwargs.items():
setattr(self, key, value)
self.update_credentials(kwargs=kwargs)
db.flush()
return self
def update_credentials(self, **kwargs) -> None:
"""
Save user credentials for tracking.
Args:
record_updated: Record that created or updated
"""
# Update confirmation or modification tracking
is_confirmed_argument = kwargs.get("is_confirmed", None)
if is_confirmed_argument and not len(kwargs) == 1:
raise ValueError("Confirm field cannot be updated with other fields")
if is_confirmed_argument:
if getattr(self.creds, "person_id", None) and getattr(self.creds, "person_name", None):
self.confirmed_by_id = self.creds.person_id
self.confirmed_by = self.creds.person_name
else:
if getattr(self.creds, "person_id", None) and getattr(self.creds, "person_name", None):
self.updated_by_id = self.creds.person_id
self.updated_by = self.creds.person_name
return

View File

@@ -6,420 +6,83 @@ including pagination, ordering, and complex query building.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, Tuple, Protocol
from dataclasses import dataclass
from json import dumps
from typing import Any, TypeVar, Type
from sqlalchemy import BinaryExpression, desc, asc
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Query, Session
from sqlalchemy.sql.elements import BinaryExpression
from sqlalchemy_mixins.smartquery import SmartQueryMixin
from Services.PostgresDb.Models.response import PostgresResponse
from Services.PostgresDb.Models_old.base_model import BaseModel
from ApiLibrary import system_arrow
from ApiLibrary.common.line_number import get_line_number_for_error
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from Services.PostgresDb.Models.response import PostgresResponse
# Type variable for class methods returning self
T = TypeVar("T", bound="FilterAttributes")
class HTTPException(Exception):
"""Base exception for HTTP errors."""
def __init__(self, status_code: str, detail: str):
self.status_code = status_code
self.detail = detail
super().__init__(detail)
class HTTPStatus(Protocol):
"""Protocol defining required HTTP status codes."""
HTTP_400_BAD_REQUEST: str
HTTP_404_NOT_FOUND: str
HTTP_304_NOT_MODIFIED: str
@dataclass
class FilterConfig:
"""Configuration for filtering and pagination."""
page: int = 1
size: int = 10
order_field: str = "id"
order_type: str = "asc"
include_joins: List[str] = None
query: Dict[str, Any] = None
def __post_init__(self):
"""Initialize default values for None fields."""
self.include_joins = self.include_joins or []
self.query = self.query or {}
class QueryConfig:
"""Configuration for query building and execution."""
def __init__(
self,
pre_query: Optional[Query] = None,
filter_config: Optional[FilterConfig] = None,
http_exception: Optional[Type[HTTPException]] = HTTPException,
status: Optional[Type[HTTPStatus]] = None,
):
self.pre_query = pre_query
self.filter_config = filter_config or FilterConfig()
self.http_exception = http_exception
self.status = status
self.total_count: Optional[int] = None
def update_filter_config(self, **kwargs) -> None:
"""Update filter configuration parameters."""
for key, value in kwargs.items():
if hasattr(self.filter_config, key):
setattr(self.filter_config, key, value)
def set_total_count(self, count: int) -> None:
"""Set the total count of records."""
self.total_count = count
class FilterAttributes:
"""
Advanced filtering capabilities for SQLAlchemy models.
Features:
- Pagination and ordering
- Complex query building
- Active/deleted/confirmed status filtering
- Expiry date handling
- Transaction management
Usage:
# Initialize configuration
config = QueryConfig(filter_config=FilterConfig(page=1, size=10))
# Create model with configuration
class User(FilterAttributes):
query_config = config
# Filter multiple records
users = User.filter_by_all(db, name="John").data
# Update configuration
User.query_config.update_filter_config(page=2, size=20)
next_users = User.filter_all(db).data
"""
class ArgumentModel:
__abstract__ = True
# Class-level configuration
query_config: QueryConfig = QueryConfig()
@classmethod
def flush(cls: Type[T], db: Session) -> T:
"""
Flush the current session to the database.
Args:
db: Database session
Returns:
Self instance
Raises:
HTTPException: If database operation fails
"""
try:
db.flush()
return cls
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
@classmethod
def destroy(cls: Type[T], db: Session) -> None:
"""
Delete the record from the database.
Args:
db: Database session
"""
db.delete(cls)
db.commit()
@classmethod
def save_via_metadata(cls: Type[T], db: Session) -> None:
"""
Save or rollback based on metadata.
Args:
db: Database session
Raises:
HTTPException: If save operation fails
"""
try:
meta_data = getattr(cls, "meta_data", {})
if meta_data.get("created", False):
db.commit()
db.rollback()
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
@classmethod
def save(cls: Type[T], db: Session) -> None:
"""
Commit changes to database.
Args:
db: Database session
Raises:
HTTPException: If commit fails
"""
try:
db.commit()
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
@classmethod
def rollback(cls: Type[T], db: Session) -> None:
"""
Rollback current transaction.
Args:
db: Database session
"""
db.rollback()
@classmethod
def save_and_confirm(cls: Type[T], db: Session) -> None:
"""
Save changes and mark record as confirmed.
Args:
db: Database session
Raises:
HTTPException: If operation fails
"""
try:
cls.save(db)
cls.update(db, is_confirmed=True)
cls.save(db)
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
@classmethod
def _query(cls: Type[T], db: Session) -> Query:
"""
Get base query for model.
Args:
db: Database session
Returns:
SQLAlchemy Query object
"""
"""Returns the query to use in the model."""
return (
cls.query_config.pre_query if cls.query_config.pre_query else db.query(cls)
cls.pre_query if cls.pre_query else db.query(cls)
)
@classmethod
def add_query_to_filter(
cls: Type[T], query: Query, filter_list: Dict[str, Any]
) -> Query:
"""
Add pagination and ordering to query.
Args:
query: Base query
filter_list: Dictionary containing pagination and ordering parameters
Returns:
Modified query with pagination and ordering
"""
order_field = getattr(cls, filter_list.get("order_field"))
order_func = desc if str(filter_list.get("order_type"))[0] == "d" else asc
return (
query.order_by(order_func(order_field))
.limit(filter_list.get("size"))
.offset((filter_list.get("page") - 1) * filter_list.get("size"))
.populate_existing()
def add_new_arg_to_args(cls: Type[T], args_list, argument, value):
new_arg_list = list(
set(
args_
for args_ in list(args_list)
if isinstance(args_, BinaryExpression)
)
)
@classmethod
def get_filter_attributes(cls) -> Dict[str, Any]:
"""
Get filter configuration from attributes.
Returns:
Dictionary containing pagination and filtering parameters
"""
return {
"page": getattr(cls.query_config.filter_config, "page", 1),
"size": getattr(cls.query_config.filter_config, "size", 10),
"order_field": getattr(cls.query_config.filter_config, "order_field", "id"),
"order_type": getattr(cls.query_config.filter_config, "order_type", "asc"),
"include_joins": getattr(
cls.query_config.filter_config, "include_joins", []
),
"query": getattr(cls.query_config.filter_config, "query", {}),
}
@classmethod
def add_new_arg_to_args(
cls,
args_list: Tuple[BinaryExpression, ...],
argument: str,
value: BinaryExpression,
) -> Tuple[BinaryExpression, ...]:
"""
Add new argument to filter arguments if not exists.
Args:
args_list: Current filter arguments
argument: Argument name to check
value: New argument to add
Returns:
Updated argument tuple
"""
new_args = [arg for arg in args_list if isinstance(arg, BinaryExpression)]
arg_left = lambda arg_obj: getattr(getattr(arg_obj, "left", None), "key", None)
if not any(arg_left(arg) == argument for arg in new_args):
new_args.append(value)
return tuple(new_args)
# arg_right = lambda arg_obj: getattr(getattr(arg_obj, "right", None), "value", None)
if not any(True for arg in new_arg_list if arg_left(arg_obj=arg) == argument):
new_arg_list.append(value)
return tuple(new_arg_list)
@classmethod
def get_not_expired_query_arg(
cls, args: Tuple[BinaryExpression, ...]
) -> Tuple[BinaryExpression, ...]:
"""
Add expiry date conditions to query.
Args:
args: Current query arguments
Returns:
Updated arguments with expiry conditions
"""
current_time = str(system_arrow.now())
args = cls.add_new_arg_to_args(
args, "expiry_ends", cls.expiry_ends > current_time
)
args = cls.add_new_arg_to_args(
args, "expiry_starts", cls.expiry_starts <= current_time
)
return args
def get_not_expired_query_arg(cls: Type[T], arg):
"""Add expiry_starts and expiry_ends to the query."""
starts = cls.expiry_starts <= str(system_arrow.now())
ends = cls.expiry_ends > str(system_arrow.now())
arg = cls.add_new_arg_to_args(arg, "expiry_ends", ends)
arg = cls.add_new_arg_to_args(arg, "expiry_starts", starts)
return arg
@classmethod
def get_active_and_confirmed_query_arg(
cls, args: Tuple[BinaryExpression, ...]
) -> Tuple[BinaryExpression, ...]:
"""
Add status conditions to query.
def get_active_and_confirmed_query_arg(cls: Type[T], arg):
"""Add active and confirmed to the query."""
arg = cls.add_new_arg_to_args(arg, "is_confirmed", cls.is_confirmed == True)
arg = cls.add_new_arg_to_args(arg, "active", cls.active == True)
arg = cls.add_new_arg_to_args(arg, "deleted", cls.deleted == False)
return arg
Args:
args: Current query arguments
Returns:
Updated arguments with status conditions
"""
args = cls.add_new_arg_to_args(args, "is_confirmed", cls.is_confirmed == True)
args = cls.add_new_arg_to_args(args, "active", cls.active == True)
args = cls.add_new_arg_to_args(args, "deleted", cls.deleted == False)
return args
class QueryModel(ArgumentModel, BaseModel, SmartQueryMixin):
pre_query = None
__abstract__ = True
@classmethod
def select_only(
cls: Type[T],
db: Session,
*args: BinaryExpression,
select_args: List[Any],
order_by: Optional[Any] = None,
limit: Optional[int] = None,
system: bool = False,
) -> PostgresResponse:
"""
Select specific columns from filtered query.
Args:
db: Database session
args: Filter conditions
select_args: Columns to select
order_by: Optional ordering
limit: Optional result limit
system: If True, skip status filtering
Returns:
Query response with selected columns
"""
if not system:
args = cls.get_active_and_confirmed_query_arg(args)
args = cls.get_not_expired_query_arg(args)
query = cls._query(db).filter(*args).with_entities(*select_args)
cls.query_config.set_total_count(query.count())
if order_by is not None:
query = query.order_by(order_by)
if limit:
query = query.limit(limit)
return PostgresResponse(query=query, first=False)
def produce_query_to_add(cls: Type[T], filter_list):
if filter_list.get("query"):
for smart_iter in cls.filter_expr(**filter_list["query"]):
if key := getattr(getattr(smart_iter, "left", None), "key", None):
args = cls.add_new_arg_to_args(args, key, smart_iter)
@classmethod
def filter_by_all(
cls: Type[T], db: Session, system: bool = False, **kwargs
) -> PostgresResponse:
"""
Filter multiple records by keyword arguments.
Args:
db: Database session
system: If True, skip status filtering
**kwargs: Filter criteria
Returns:
Query response with matching records
"""
if "is_confirmed" not in kwargs and not system:
kwargs["is_confirmed"] = True
kwargs.pop("system", None)
query = cls._query(db).filter_by(**kwargs)
cls.query_config.set_total_count(query.count())
if cls.query_config.filter_config:
filter_list = cls.get_filter_attributes()
query = cls.add_query_to_filter(query, filter_list)
return PostgresResponse(query=query, first=False)
def convert(
cls: Type[T], smart_options: dict, validate_model: Any = None
) -> tuple[BinaryExpression]:
if not validate_model:
return tuple(cls.filter_expr(**smart_options))
@classmethod
def filter_by_one(
@@ -439,53 +102,16 @@ class FilterAttributes:
if "is_confirmed" not in kwargs and not system:
kwargs["is_confirmed"] = True
kwargs.pop("system", None)
query = cls._query(db).filter_by(**kwargs)
cls.query_config.set_total_count(1)
return PostgresResponse(query=query, first=True)
@classmethod
def filter_all(
cls: Type[T], *args: Any, db: Session, system: bool = False
) -> PostgresResponse:
"""
Filter multiple records by expressions.
Args:
db: Database session
args: Filter expressions
system: If True, skip status filtering
Returns:
Query response with matching records
"""
if not system:
args = cls.get_active_and_confirmed_query_arg(args)
args = cls.get_not_expired_query_arg(args)
filter_list = cls.get_filter_attributes()
if filter_list.get("query"):
for smart_iter in cls.filter_expr(**filter_list["query"]):
if key := getattr(getattr(smart_iter, "left", None), "key", None):
args = cls.add_new_arg_to_args(args, key, smart_iter)
query = cls._query(db)
cls.query_config.set_total_count(query.count())
query = query.filter(*args)
if cls.query_config.filter_config:
query = cls.add_query_to_filter(query, filter_list)
return PostgresResponse(query=query, first=False)
return PostgresResponse(pre_query=cls._query(db), query=query, is_array=False)
@classmethod
def filter_one(
cls: Type[T],
*args: Any,
db: Session,
system: bool = False,
expired: bool = False,
cls: Type[T],
*args: Any,
db: Session,
system: bool = False,
expired: bool = False,
) -> PostgresResponse:
"""
Filter single record by expressions.
@@ -501,35 +127,61 @@ class FilterAttributes:
"""
if not system:
args = cls.get_active_and_confirmed_query_arg(args)
if not expired:
args = cls.get_not_expired_query_arg(args)
if not expired:
args = cls.get_not_expired_query_arg(args)
query = cls._query(db).filter(*args)
cls.query_config.set_total_count(1)
return PostgresResponse(pre_query=cls._query(db), query=query, is_array=False)
return PostgresResponse(query=query, first=True)
# @classmethod
# def raise_http_exception(
# cls,
# status_code: str,
# error_case: str,
# data: Dict[str, Any],
# message: str,
# ) -> None:
# """
# Raise HTTP exception with formatted error details.
@classmethod
def filter_all_system(
cls: Type[T], *args: BinaryExpression, db: Session
) -> PostgresResponse:
"""
Filter multiple records by expressions without status filtering.
# Args:
# status_code: HTTP status code string
# error_case: Error type
# data: Additional error data
# message: Error message
Args:
db: Database session
args: Filter expressions
# Raises:
# HTTPException: With formatted error details
# """
# raise HTTPExceptionApi(
# error_code="HTTP_304_NOT_MODIFIED",
# lang=cls.lang or "tr", loc=get_line_number_for_error()
# )
Returns:
Query response with matching records
"""
query = cls._query(db)
query = query.filter(*args)
return PostgresResponse(pre_query=cls._query(db), query=query, is_array=True)
@classmethod
def filter_all(
cls: Type[T], *args: Any, db: Session
) -> PostgresResponse:
"""
Filter multiple records by expressions.
Args:
db: Database session
args: Filter expressions
Returns:
Query response with matching records
"""
args = cls.get_active_and_confirmed_query_arg(args)
args = cls.get_not_expired_query_arg(args)
query = cls._query(db).filter(*args)
return PostgresResponse(pre_query=cls._query(db), query=query, is_array=True)
@classmethod
def filter_by_all_system(
cls: Type[T], db: Session, **kwargs
) -> PostgresResponse:
"""
Filter multiple records by keyword arguments.
Args:
db: Database session
**kwargs: Filter criteria
Returns:
Query response with matching records
"""
query = cls._query(db).filter_by(**kwargs)
return PostgresResponse(pre_query=cls._query(db), query=query, is_array=True)

View File

@@ -0,0 +1,7 @@
class LanguageModel:
__language_model__ = None

View File

@@ -0,0 +1,176 @@
from sqlalchemy import (
TIMESTAMP,
NUMERIC,
func,
text,
UUID,
String,
Integer,
Boolean,
SmallInteger,
)
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy_mixins.serialize import SerializeMixin
from sqlalchemy_mixins.repr import ReprMixin
from Services.PostgresDb.Models.crud_alchemy import CRUDModel
from Services.PostgresDb.Models.filter_functions import QueryModel
class BasicMixin(CRUDModel, QueryModel):
__abstract__ = True
__repr__ = ReprMixin.__repr__
class CrudMixin(BasicMixin, SerializeMixin, ReprMixin):
"""
Base mixin providing CRUD operations and common fields for PostgreSQL models.
Features:
- Automatic timestamps (created_at, updated_at)
- Soft delete capability
- User tracking (created_by, updated_by)
- Data serialization
- Multi-language support
"""
__abstract__ = True
# Primary and reference fields
id: Mapped[int] = mapped_column(Integer, primary_key=True)
uu_id: Mapped[str] = mapped_column(
UUID,
server_default=text("gen_random_uuid()"),
index=True,
unique=True,
comment="Unique identifier UUID",
)
# Common timestamp fields for all models
expiry_starts: Mapped[TIMESTAMP] = mapped_column(
type_=TIMESTAMP(timezone=True),
server_default=func.now(),
nullable=False,
comment="Record validity start timestamp",
)
expiry_ends: Mapped[TIMESTAMP] = mapped_column(
type_=TIMESTAMP(timezone=True),
default="2099-12-31",
server_default="2099-12-31",
comment="Record validity end timestamp",
)
class BaseCollection(CrudMixin):
"""Base model class with minimal fields."""
__abstract__ = True
__repr__ = ReprMixin.__repr__
class CrudCollection(CrudMixin):
"""
Full-featured model class with all common fields.
Includes:
- UUID and reference ID
- Timestamps
- User tracking
- Confirmation status
- Soft delete
- Notification flags
"""
__abstract__ = True
__repr__ = ReprMixin.__repr__
ref_id: Mapped[str] = mapped_column(
String(100), nullable=True, index=True, comment="External reference ID"
)
# Timestamps
created_at: Mapped[TIMESTAMP] = mapped_column(
TIMESTAMP(timezone=True),
server_default=func.now(),
nullable=False,
index=True,
comment="Record creation timestamp",
)
updated_at: Mapped[TIMESTAMP] = mapped_column(
TIMESTAMP(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
index=True,
comment="Last update timestamp",
)
# Cryptographic and user tracking
cryp_uu_id: Mapped[str] = mapped_column(
String, nullable=True, index=True, comment="Cryptographic UUID"
)
created_by: Mapped[str] = mapped_column(
String, nullable=True, comment="Creator name"
)
created_by_id: Mapped[int] = mapped_column(
Integer, nullable=True, comment="Creator ID"
)
updated_by: Mapped[str] = mapped_column(
String, nullable=True, comment="Last modifier name"
)
updated_by_id: Mapped[int] = mapped_column(
Integer, nullable=True, comment="Last modifier ID"
)
confirmed_by: Mapped[str] = mapped_column(
String, nullable=True, comment="Confirmer name"
)
confirmed_by_id: Mapped[int] = mapped_column(
Integer, nullable=True, comment="Confirmer ID"
)
# Status flags
is_confirmed: Mapped[bool] = mapped_column(
Boolean, server_default="0", comment="Record confirmation status"
)
replication_id: Mapped[int] = mapped_column(
SmallInteger, server_default="0", comment="Replication identifier"
)
deleted: Mapped[bool] = mapped_column(
Boolean, server_default="0", comment="Soft delete flag"
)
active: Mapped[bool] = mapped_column(
Boolean, server_default="1", comment="Record active status"
)
is_notification_send: Mapped[bool] = mapped_column(
Boolean, server_default="0", comment="Notification sent flag"
)
is_email_send: Mapped[bool] = mapped_column(
Boolean, server_default="0", comment="Email sent flag"
)
# @classmethod
# def retrieve_language_model(cls, lang: str, response_model: Any) -> Dict[str, str]:
# """
# Retrieve language-specific model headers and validation messages.
#
# Args:
# lang: Language code
# response_model: Model containing language annotations
#
# Returns:
# Dictionary of field names to localized headers
# """
# headers_and_validation = {}
# __language_model__ = getattr(cls.__language_model__, lang, "tr")
#
# for field in response_model.__annotations__.keys():
# headers_and_validation[field] = getattr(
# __language_model__, field, "Lang Not found"
# )
#
# return headers_and_validation

View File

@@ -0,0 +1,176 @@
from __future__ import annotations
from typing import Any, Dict, Optional, Union
from sqlalchemy import desc, asc
from pydantic import BaseModel
from AllConfigs.SqlDatabase.configs import PaginateConfig
from Services.PostgresDb.Models.response import PostgresResponse
class PaginationConfig(BaseModel):
"""
Configuration for pagination settings.
Attributes:
page: Current page number (default: 1)
size: Items per page (default: 10)
order_field: Field to order by (default: "id")
order_type: Order direction (default: "asc")
"""
page: int = 1
size: int = 10
order_field: Optional[Union[tuple[str], list[str]]] = None
order_type: Optional[Union[tuple[str], list[str]]] = None
def __init__(self, **data):
super().__init__(**data)
if self.order_field is None:
self.order_field = ["uu_id"]
if self.order_type is None:
self.order_type = ["asc"]
class Pagination:
"""
Handles pagination logic for query results.
Manages page size, current page, ordering, and calculates total pages
and items based on the data source.
Attributes:
DEFAULT_SIZE: Default number of items per page (10)
MIN_SIZE: Minimum allowed page size (10)
MAX_SIZE: Maximum allowed page size (40)
"""
DEFAULT_SIZE = PaginateConfig.DEFAULT_SIZE
MIN_SIZE = PaginateConfig.MIN_SIZE
MAX_SIZE = PaginateConfig.MAX_SIZE
def __init__(self, data: PostgresResponse):
self.data = data
self.size: int = self.DEFAULT_SIZE
self.page: int = 1
self.orderField: Optional[Union[tuple[str], list[str]]] = ["uu_id"]
self.orderType: Optional[Union[tuple[str], list[str]]] = ["asc"]
self.page_count: int = 1
self.total_count: int = 0
self.all_count: int = 0
self.total_pages: int = 1
self._update_page_counts()
def change(self, **kwargs) -> None:
"""Update pagination settings from config."""
config = PaginationConfig(**kwargs)
self.size = (
config.size
if self.MIN_SIZE <= config.size <= self.MAX_SIZE
else self.DEFAULT_SIZE
)
self.page = config.page
self.orderField = config.order_field
self.orderType = config.order_type
self._update_page_counts()
def feed(self, data: PostgresResponse) -> None:
"""Calculate pagination based on data source."""
self.data = data
self._update_page_counts()
def _update_page_counts(self) -> None:
"""Update page counts and validate current page."""
if self.data:
self.total_count = self.data.count
self.all_count = self.data.total_count
self.size = (
self.size
if self.MIN_SIZE <= self.size <= self.MAX_SIZE
else self.DEFAULT_SIZE
)
self.total_pages = max(1, (self.total_count + self.size - 1) // self.size)
self.page = max(1, min(self.page, self.total_pages))
self.page_count = (
self.total_count % self.size
if self.page == self.total_pages and self.total_count % self.size
else self.size
)
def refresh(self) -> None:
"""Reset pagination state to defaults."""
self._update_page_counts()
def reset(self) -> None:
"""Reset pagination state to defaults."""
self.size = self.DEFAULT_SIZE
self.page = 1
self.orderField = "uu_id"
self.orderType = "asc"
def as_dict(self) -> Dict[str, Any]:
"""Convert pagination state to dictionary format."""
self.refresh()
return {
"size": self.size,
"page": self.page,
"allCount": self.all_count,
"totalCount": self.total_count,
"totalPages": self.total_pages,
"pageCount": self.page_count,
"order_field": self.orderField,
"order_type": self.orderType,
}
class PaginationResult:
"""
Result of a paginated query.
Contains the query result and pagination state.
data: PostgresResponse of query results
pagination: Pagination state
Attributes:
_query: Original query object
pagination: Pagination state
"""
def __init__(self, data: PostgresResponse, pagination: Pagination):
self._query = data.query
self.pagination = pagination
self.response_type = data.is_list
self.limit = self.pagination.size
self.offset = self.pagination.size * (self.pagination.page - 1)
self.order_by = self.pagination.orderField
def dynamic_order_by(self):
"""
Dynamically order a query by multiple fields.
Returns:
Ordered query object.
"""
if not len(self.order_by) == len(self.pagination.orderType):
raise ValueError(
"Order by fields and order types must have the same length."
)
order_criteria = zip(self.order_by, self.pagination.orderType)
for field, direction in order_criteria:
if hasattr(self._query.column_descriptions[0]['entity'], field):
if direction.lower().startswith("d"):
self._query = self._query.order_by(
desc(getattr(self._query.column_descriptions[0]['entity'], field))
)
else:
self._query = self._query.order_by(
asc(getattr(self._query.column_descriptions[0]['entity'], field))
)
return self._query
@property
def data(self) -> Union[list | dict]:
"""Get query object."""
query_ordered = self.dynamic_order_by()
query_paginated = query_ordered.limit(self.limit).offset(self.offset)
queried_data = query_paginated.all() if self.response_type else query_paginated.first()
return [result.get_dict() for result in queried_data] if self.response_type else queried_data.get_dict()

View File

@@ -5,7 +5,7 @@ This module provides a wrapper class for SQLAlchemy query results,
adding convenience methods for accessing data and managing query state.
"""
from typing import Any, Dict, List, Optional, TypeVar, Generic, Union
from typing import Any, Dict, Optional, TypeVar, Generic, Union
from sqlalchemy.orm import Query
T = TypeVar("T")
@@ -17,30 +17,59 @@ class PostgresResponse(Generic[T]):
Attributes:
query: SQLAlchemy query object
first: Whether to return first result only
data: Query results (lazy loaded)
count: Total count of results
metadata: Additional metadata for the query
Properties:
all: All results as list
first_item: First result only
count: Total count of results
query: Get query object
as_dict: Convert response to dictionary format
"""
def __init__(
self,
query: Query,
first: bool = False,
status: bool = True,
message: str = "",
error: Optional[str] = None,
self,
pre_query: Query,
query: Query,
is_array: bool = True,
metadata: Any = None,
):
self._is_list = is_array
self._query = query
self._first = first
self.status = status
self.message = message
self.error = error
self._data: Optional[Union[List[T], T]] = None
self._pre_query = pre_query
self._count: Optional[int] = None
self.metadata = metadata
@property
def data(self) -> Union[T, list[T]]:
"""Get query results."""
if not self.is_list:
first_item = self._query.first()
return first_item if first_item else None
return self._query.all() if self._query.all() else []
@property
def data_as_dict(self) -> Union[Dict[str, Any], list[Dict[str, Any]]]:
"""Get query results as dictionary."""
if self.is_list:
first_item = self._query.first()
return first_item.get_dict() if first_item.first() else None
all_items = self._query.all()
return [result.get_dict() for result in all_items] if all_items else []
@property
def total_count(self) -> int:
"""Lazy load and return total count of results."""
if self.is_list:
return self._pre_query.count() if self._pre_query else 0
return 1
@property
def count(self) -> int:
"""Lazy load and return total count of results."""
if self.is_list and self._count is None:
self._count = self._query.count()
elif not self.is_list:
self._count = 1
return self._count
@property
def query(self) -> Query:
@@ -48,43 +77,15 @@ class PostgresResponse(Generic[T]):
return self._query
@property
def data(self) -> Union[List[T], T, None]:
"""
Lazy load and return query results.
Returns first item if first=True, otherwise returns all results.
"""
if self._data is None:
results = self._query.all()
self._data = results[0] if self._first and results else results
return self._data
@property
def count(self) -> int:
"""Lazy load and return total count of results."""
if self._count is None:
self._count = self._query.count()
return self._count
@property
def all(self) -> List[T]:
"""Get all results as list."""
return (
self.data
if isinstance(self.data, list)
else [self.data] if self.data else []
)
@property
def first(self) -> Optional[T]:
"""Get first result only."""
return self.data if self._first else (self.data[0] if self.data else None)
def is_list(self) -> bool:
"""Check if response is a list."""
return self._is_list
def as_dict(self) -> Dict[str, Any]:
"""Convert response to dictionary format."""
return {
"status": self.status,
"message": self.message,
"data": self.data,
"metadata": self.metadata,
"is_list": self._is_list,
"query": self.query,
"count": self.count,
"error": self.error,
}

View File

@@ -0,0 +1,53 @@
class SystemFields:
__abstract__ = True
# System fields that should be handled automatically during creation
__system__fields__create__ = (
"created_at",
"updated_at",
"cryp_uu_id",
"created_by",
"created_by_id",
"updated_by",
"updated_by_id",
"replication_id",
"confirmed_by",
"confirmed_by_id",
"is_confirmed",
"deleted",
"active",
"is_notification_send",
"is_email_send",
)
# System fields that should be handled automatically during updates
__system__fields__update__ = (
"cryp_uu_id",
"created_at",
"updated_at",
"created_by",
"created_by_id",
"confirmed_by",
"confirmed_by_id",
"updated_by",
"updated_by_id",
"replication_id",
)
# Default fields to exclude from serialization
__system_default_model__ = (
"cryp_uu_id",
"is_confirmed",
"deleted",
"is_notification_send",
"replication_id",
"is_email_send",
"confirmed_by_id",
"confirmed_by",
"updated_by_id",
"created_by_id",
)

View File

@@ -0,0 +1,39 @@
from typing import TypeVar, Dict, Any
from dataclasses import dataclass
from ApiLibrary import get_line_number_for_error
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
# Type variable for class methods returning self
T = TypeVar("T", bound="FilterAttributes")
@dataclass
class TokenModel:
lang: str
credentials: Dict[str, str]
timezone: str
def __post_init__(self):
self.lang = str(self.lang or "tr").lower()
self.credentials = self.credentials or {}
if 'GMT' in self.timezone:
raise HTTPExceptionApi(
error_code="HTTP_400_BAD_REQUEST",
lang=self.lang,
loc=get_line_number_for_error(),
sys_msg="Invalid timezone format",
)
@classmethod
def set_user_define_properties(cls, token: Any) -> None:
"""
Set user-specific properties from the authentication token.
Args:
token: Authentication token containing user preferences
"""
from ApiLibrary.date_time_actions.date_functions import DateTimeLocal
cls.credentials = token.credentials
cls.client_arrow = DateTimeLocal(is_client=True, timezone=token.timezone)
cls.lang = str(token.lang).lower()

View File

@@ -276,7 +276,6 @@ class AlchemyJsonResponse(BaseJsonResponse[T]):
pagination = instance._create_pagination()
data = [instance._transform_data(item.get_dict()) for item in result.data]
pagination.feed(data)
return instance._format_response(pagination, data)

View File

@@ -0,0 +1,535 @@
"""
Advanced filtering functionality for SQLAlchemy models.
This module provides a comprehensive set of filtering capabilities for SQLAlchemy models,
including pagination, ordering, and complex query building.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, Tuple, Protocol
from dataclasses import dataclass
from json import dumps
from sqlalchemy import BinaryExpression, desc, asc
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Query, Session
from sqlalchemy.sql.elements import BinaryExpression
from ApiLibrary import system_arrow
from ApiLibrary.common.line_number import get_line_number_for_error
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from Services.PostgresDb.Models.response import PostgresResponse
# Type variable for class methods returning self
T = TypeVar("T", bound="FilterAttributes")
class HTTPException(Exception):
"""Base exception for HTTP errors."""
def __init__(self, status_code: str, detail: str):
self.status_code = status_code
self.detail = detail
super().__init__(detail)
class HTTPStatus(Protocol):
"""Protocol defining required HTTP status codes."""
HTTP_400_BAD_REQUEST: str
HTTP_404_NOT_FOUND: str
HTTP_304_NOT_MODIFIED: str
@dataclass
class FilterConfig:
"""Configuration for filtering and pagination."""
page: int = 1
size: int = 10
order_field: str = "id"
order_type: str = "asc"
include_joins: List[str] = None
query: Dict[str, Any] = None
def __post_init__(self):
"""Initialize default values for None fields."""
self.include_joins = self.include_joins or []
self.query = self.query or {}
class QueryConfig:
"""Configuration for query building and execution."""
def __init__(
self,
pre_query: Optional[Query] = None,
filter_config: Optional[FilterConfig] = None,
http_exception: Optional[Type[HTTPException]] = HTTPException,
status: Optional[Type[HTTPStatus]] = None,
):
self.pre_query = pre_query
self.filter_config = filter_config or FilterConfig()
self.http_exception = http_exception
self.status = status
self.total_count: Optional[int] = None
def update_filter_config(self, **kwargs) -> None:
"""Update filter configuration parameters."""
for key, value in kwargs.items():
if hasattr(self.filter_config, key):
setattr(self.filter_config, key, value)
def set_total_count(self, count: int) -> None:
"""Set the total count of records."""
self.total_count = count
class FilterAttributes:
"""
Advanced filtering capabilities for SQLAlchemy models.
Features:
- Pagination and ordering
- Complex query building
- Active/deleted/confirmed status filtering
- Expiry date handling
- Transaction management
Usage:
# Initialize configuration
config = QueryConfig(filter_config=FilterConfig(page=1, size=10))
# Create model with configuration
class User(FilterAttributes):
query_config = config
# Filter multiple records
users = User.filter_by_all(db, name="John").data
# Update configuration
User.query_config.update_filter_config(page=2, size=20)
next_users = User.filter_all(db).data
"""
__abstract__ = True
# Class-level configuration
query_config: QueryConfig = QueryConfig()
@classmethod
def flush(cls: Type[T], db: Session) -> T:
"""
Flush the current session to the database.
Args:
db: Database session
Returns:
Self instance
Raises:
HTTPException: If database operation fails
"""
try:
db.flush()
return cls
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
@classmethod
def destroy(cls: Type[T], db: Session) -> None:
"""
Delete the record from the database.
Args:
db: Database session
"""
db.delete(cls)
db.commit()
@classmethod
def save_via_metadata(cls: Type[T], db: Session) -> None:
"""
Save or rollback based on metadata.
Args:
db: Database session
Raises:
HTTPException: If save operation fails
"""
try:
meta_data = getattr(cls, "meta_data", {})
if meta_data.get("created", False):
db.commit()
db.rollback()
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
@classmethod
def save(cls: Type[T], db: Session) -> None:
"""
Commit changes to database.
Args:
db: Database session
Raises:
HTTPException: If commit fails
"""
try:
db.commit()
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
@classmethod
def rollback(cls: Type[T], db: Session) -> None:
"""
Rollback current transaction.
Args:
db: Database session
"""
db.rollback()
@classmethod
def save_and_confirm(cls: Type[T], db: Session) -> None:
"""
Save changes and mark record as confirmed.
Args:
db: Database session
Raises:
HTTPException: If operation fails
"""
try:
cls.save(db)
cls.update(db, is_confirmed=True)
cls.save(db)
except SQLAlchemyError as e:
raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr",
loc=get_line_number_for_error(),
sys_msg=str(e),
)
@classmethod
def _query(cls: Type[T], db: Session) -> Query:
"""
Get base query for model.
Args:
db: Database session
Returns:
SQLAlchemy Query object
"""
return (
cls.query_config.pre_query if cls.query_config.pre_query else db.query(cls)
)
@classmethod
def add_query_to_filter(
cls: Type[T], query: Query, filter_list: Dict[str, Any]
) -> Query:
"""
Add pagination and ordering to query.
Args:
query: Base query
filter_list: Dictionary containing pagination and ordering parameters
Returns:
Modified query with pagination and ordering
"""
order_field = getattr(cls, filter_list.get("order_field"))
order_func = desc if str(filter_list.get("order_type"))[0] == "d" else asc
return (
query.order_by(order_func(order_field))
.limit(filter_list.get("size"))
.offset((filter_list.get("page") - 1) * filter_list.get("size"))
.populate_existing()
)
@classmethod
def get_filter_attributes(cls) -> Dict[str, Any]:
"""
Get filter configuration from attributes.
Returns:
Dictionary containing pagination and filtering parameters
"""
return {
"page": getattr(cls.query_config.filter_config, "page", 1),
"size": getattr(cls.query_config.filter_config, "size", 10),
"order_field": getattr(cls.query_config.filter_config, "order_field", "id"),
"order_type": getattr(cls.query_config.filter_config, "order_type", "asc"),
"include_joins": getattr(
cls.query_config.filter_config, "include_joins", []
),
"query": getattr(cls.query_config.filter_config, "query", {}),
}
@classmethod
def add_new_arg_to_args(
cls,
args_list: Tuple[BinaryExpression, ...],
argument: str,
value: BinaryExpression,
) -> Tuple[BinaryExpression, ...]:
"""
Add new argument to filter arguments if not exists.
Args:
args_list: Current filter arguments
argument: Argument name to check
value: New argument to add
Returns:
Updated argument tuple
"""
new_args = [arg for arg in args_list if isinstance(arg, BinaryExpression)]
arg_left = lambda arg_obj: getattr(getattr(arg_obj, "left", None), "key", None)
if not any(arg_left(arg) == argument for arg in new_args):
new_args.append(value)
return tuple(new_args)
@classmethod
def get_not_expired_query_arg(
cls, args: Tuple[BinaryExpression, ...]
) -> Tuple[BinaryExpression, ...]:
"""
Add expiry date conditions to query.
Args:
args: Current query arguments
Returns:
Updated arguments with expiry conditions
"""
current_time = str(system_arrow.now())
args = cls.add_new_arg_to_args(
args, "expiry_ends", cls.expiry_ends > current_time
)
args = cls.add_new_arg_to_args(
args, "expiry_starts", cls.expiry_starts <= current_time
)
return args
@classmethod
def get_active_and_confirmed_query_arg(
cls, args: Tuple[BinaryExpression, ...]
) -> Tuple[BinaryExpression, ...]:
"""
Add status conditions to query.
Args:
args: Current query arguments
Returns:
Updated arguments with status conditions
"""
args = cls.add_new_arg_to_args(args, "is_confirmed", cls.is_confirmed == True)
args = cls.add_new_arg_to_args(args, "active", cls.active == True)
args = cls.add_new_arg_to_args(args, "deleted", cls.deleted == False)
return args
@classmethod
def select_only(
cls: Type[T],
db: Session,
*args: BinaryExpression,
select_args: List[Any],
order_by: Optional[Any] = None,
limit: Optional[int] = None,
system: bool = False,
) -> PostgresResponse:
"""
Select specific columns from filtered query.
Args:
db: Database session
args: Filter conditions
select_args: Columns to select
order_by: Optional ordering
limit: Optional result limit
system: If True, skip status filtering
Returns:
Query response with selected columns
"""
if not system:
args = cls.get_active_and_confirmed_query_arg(args)
args = cls.get_not_expired_query_arg(args)
query = cls._query(db).filter(*args).with_entities(*select_args)
cls.query_config.set_total_count(query.count())
if order_by is not None:
query = query.order_by(order_by)
if limit:
query = query.limit(limit)
return PostgresResponse(query=query, first=False)
@classmethod
def filter_by_all(
cls: Type[T], db: Session, system: bool = False, **kwargs
) -> PostgresResponse:
"""
Filter multiple records by keyword arguments.
Args:
db: Database session
system: If True, skip status filtering
**kwargs: Filter criteria
Returns:
Query response with matching records
"""
if "is_confirmed" not in kwargs and not system:
kwargs["is_confirmed"] = True
kwargs.pop("system", None)
query = cls._query(db).filter_by(**kwargs)
cls.query_config.set_total_count(query.count())
if cls.query_config.filter_config:
filter_list = cls.get_filter_attributes()
query = cls.add_query_to_filter(query, filter_list)
return PostgresResponse(query=query, first=False)
@classmethod
def filter_by_one(
cls: Type[T], db: Session, system: bool = False, **kwargs
) -> PostgresResponse:
"""
Filter single record by keyword arguments.
Args:
db: Database session
system: If True, skip status filtering
**kwargs: Filter criteria
Returns:
Query response with single record
"""
if "is_confirmed" not in kwargs and not system:
kwargs["is_confirmed"] = True
kwargs.pop("system", None)
query = cls._query(db).filter_by(**kwargs)
cls.query_config.set_total_count(1)
return PostgresResponse(query=query, first=True)
@classmethod
def filter_all(
cls: Type[T], *args: Any, db: Session, system: bool = False
) -> PostgresResponse:
"""
Filter multiple records by expressions.
Args:
db: Database session
args: Filter expressions
system: If True, skip status filtering
Returns:
Query response with matching records
"""
if not system:
args = cls.get_active_and_confirmed_query_arg(args)
args = cls.get_not_expired_query_arg(args)
filter_list = cls.get_filter_attributes()
if filter_list.get("query"):
for smart_iter in cls.filter_expr(**filter_list["query"]):
if key := getattr(getattr(smart_iter, "left", None), "key", None):
args = cls.add_new_arg_to_args(args, key, smart_iter)
query = cls._query(db)
cls.query_config.set_total_count(query.count())
query = query.filter(*args)
if cls.query_config.filter_config:
query = cls.add_query_to_filter(query, filter_list)
return PostgresResponse(query=query, first=False)
@classmethod
def filter_one(
cls: Type[T],
*args: Any,
db: Session,
system: bool = False,
expired: bool = False,
) -> PostgresResponse:
"""
Filter single record by expressions.
Args:
db: Database session
args: Filter expressions
system: If True, skip status filtering
expired: If True, include expired records
Returns:
Query response with single record
"""
if not system:
args = cls.get_active_and_confirmed_query_arg(args)
if not expired:
args = cls.get_not_expired_query_arg(args)
query = cls._query(db).filter(*args)
cls.query_config.set_total_count(1)
return PostgresResponse(query=query, first=True)
# @classmethod
# def raise_http_exception(
# cls,
# status_code: str,
# error_case: str,
# data: Dict[str, Any],
# message: str,
# ) -> None:
# """
# Raise HTTP exception with formatted error details.
# Args:
# status_code: HTTP status code string
# error_case: Error type
# data: Additional error data
# message: Error message
# Raises:
# HTTPException: With formatted error details
# """
# raise HTTPExceptionApi(
# error_code="HTTP_304_NOT_MODIFIED",
# lang=cls.lang or "tr", loc=get_line_number_for_error()
# )

View File

@@ -0,0 +1,90 @@
"""
Response handler for PostgreSQL query results.
This module provides a wrapper class for SQLAlchemy query results,
adding convenience methods for accessing data and managing query state.
"""
from typing import Any, Dict, List, Optional, TypeVar, Generic, Union
from sqlalchemy.orm import Query
T = TypeVar("T")
class PostgresResponse(Generic[T]):
"""
Wrapper for PostgreSQL/SQLAlchemy query results.
Attributes:
query: SQLAlchemy query object
first: Whether to return first result only
data: Query results (lazy loaded)
count: Total count of results
Properties:
all: All results as list
first_item: First result only
"""
def __init__(
self,
query: Query,
first: bool = False,
status: bool = True,
message: str = "",
error: Optional[str] = None,
):
self._query = query
self._first = first
self.status = status
self.message = message
self.error = error
self._data: Optional[Union[List[T], T]] = None
self._count: Optional[int] = None
@property
def query(self) -> Query:
"""Get query object."""
return self._query
@property
def data(self) -> Union[List[T], T, None]:
"""
Lazy load and return query results.
Returns first item if first=True, otherwise returns all results.
"""
if self._data is None:
results = self._query.all()
self._data = results[0] if self._first and results else results
return self._data
@property
def count(self) -> int:
"""Lazy load and return total count of results."""
if self._count is None:
self._count = self._query.count()
return self._count
@property
def all(self) -> List[T]:
"""Get all results as list."""
return (
self.data
if isinstance(self.data, list)
else [self.data] if self.data else []
)
@property
def first(self) -> Optional[T]:
"""Get first result only."""
return self.data if self._first else (self.data[0] if self.data else None)
def as_dict(self) -> Dict[str, Any]:
"""Convert response to dictionary format."""
return {
"status": self.status,
"message": self.message,
"data": self.data,
"count": self.count,
"error": self.error,
}

View File

@@ -0,0 +1,81 @@
from Schemas import AddressNeighborhood
from Services.PostgresDb.Models.crud_alchemy import Credentials
from Services.PostgresDb.Models.mixin import BasicMixin
from Services.PostgresDb.Models.pagination import Pagination, PaginationResult
listing = False
creating = False
updating = True
new_session = AddressNeighborhood.new_session()
new_session_test = AddressNeighborhood.new_session()
BasicMixin.creds = Credentials(person_id=10, person_name='Berkay Super User')
if listing:
"""List Options and Queries """
AddressNeighborhood.pre_query = AddressNeighborhood.filter_all(
AddressNeighborhood.neighborhood_code.icontains('10'),
db=new_session,
).query
query_of_list_options = {
"neighborhood_name__ilike": "A%",
"neighborhood_code__contains": "3",
}
address_neighborhoods = AddressNeighborhood.filter_all(
*AddressNeighborhood.convert(query_of_list_options),
db=new_session,
)
pagination = Pagination(data=address_neighborhoods)
pagination.page = 9
pagination.size = 10
pagination.orderField = ['type_code','neighborhood_code']
pagination.orderType = ['desc', 'asc']
pagination_result = PaginationResult(data=address_neighborhoods, pagination=pagination)
print(pagination_result.pagination.as_dict())
print(pagination_result.data)
if creating:
"""Create Queries """
find_or_create = AddressNeighborhood.find_or_create(
neighborhood_code='100',
neighborhood_name='Test',
locality_id=15334,
db=new_session,
)
find_or_create.save_via_metadata(db=new_session)
find_or_create.destroy(db=new_session)
find_or_create.save_via_metadata(db=new_session)
find_or_create = AddressNeighborhood.find_or_create(
neighborhood_code='100',
neighborhood_name='Test',
locality_id=15334,
db=new_session,
)
find_or_create.save_via_metadata(db=new_session)
if updating:
"""Update Queries """
query_of_list_options = {
"uu_id": str("33a89767-d2dc-4531-8f66-7b650e22a8a7"),
}
print('query_of_list_options', query_of_list_options)
address_neighborhoods_one = AddressNeighborhood.filter_one(
*AddressNeighborhood.convert(query_of_list_options),
db=new_session,
).data
address_neighborhoods_one.update(
neighborhood_name='Test 44',
db=new_session,
)
address_neighborhoods_one.save(db=new_session)
address_neighborhoods_one = AddressNeighborhood.filter_one(
*AddressNeighborhood.convert(query_of_list_options),
db=new_session,
).data_as_dict
print('address_neighborhoods_one', address_neighborhoods_one)