new api service and logic implemented

This commit is contained in:
2025-01-23 22:27:25 +03:00
parent d91ecda9df
commit 32022ca521
245 changed files with 28004 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
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,399 @@
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

@@ -0,0 +1,180 @@
"""
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, TypeVar, Type
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
T = TypeVar("T", bound="FilterAttributes")
class ArgumentModel:
__abstract__ = True
@classmethod
def _query(cls: Type[T], db: Session) -> Query:
"""Returns the query to use in the model."""
return cls.pre_query if cls.pre_query else db.query(cls)
@classmethod
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)
)
)
arg_left = lambda arg_obj: getattr(getattr(arg_obj, "left", None), "key", None)
# 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: 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: 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
class QueryModel(ArgumentModel, BaseModel, SmartQueryMixin):
pre_query = None
__abstract__ = True
@classmethod
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 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(
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)
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,
) -> 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)
return PostgresResponse(pre_query=cls._query(db), query=query, is_array=False)
@classmethod
def filter_all_system(
cls: Type[T], *args: BinaryExpression, db: Session
) -> PostgresResponse:
"""
Filter multiple records by expressions without status filtering.
Args:
db: Database session
args: Filter expressions
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,2 @@
class LanguageModel:
__language_model__ = None

View File

@@ -0,0 +1,172 @@
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,185 @@
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

@@ -0,0 +1,91 @@
"""
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, 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
metadata: Additional metadata for the query
Properties:
count: Total count of results
query: Get query object
as_dict: Convert response to dictionary format
"""
def __init__(
self,
pre_query: Query,
query: Query,
is_array: bool = True,
metadata: Any = None,
):
self._is_list = is_array
self._query = query
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:
"""Get query object."""
return self._query
@property
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 {
"metadata": self.metadata,
"is_list": self._is_list,
"query": self.query,
"count": self.count,
}

View File

@@ -0,0 +1,50 @@
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

@@ -0,0 +1,372 @@
"""
Response handlers for SQLAlchemy query results with pagination support.
This module provides a set of response classes for handling different types of data:
- Single PostgreSQL records
- Multiple SQLAlchemy records
- List data
- Dictionary data
Each response includes pagination information and supports data transformation
through response models.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Type, TypeVar, Protocol, Generic
from dataclasses import dataclass
from fastapi import status
from fastapi.responses import JSONResponse
from ApiLibrary.common.line_number import get_line_number_for_error
from Services.PostgresDb.Models.response import PostgresResponse
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
from Services.pagination import Pagination, PaginationConfig
T = TypeVar("T")
DataT = TypeVar("DataT")
@dataclass
class ResponseConfig(Generic[T]):
"""Configuration for response formatting.
Attributes:
status_code: HTTP status code (default: "HTTP_200_OK")
message: Response message to include in the response
completed: Operation completion status flag
cls_object: Class object for error handling context
response_model: Optional response model class for data transformation
"""
status_code: str = "HTTP_200_OK"
message: str = ""
completed: bool = True
cls_object: Optional[Any] = None
response_model: Optional[Type[T]] = None
class ResponseProtocol(Protocol):
"""Protocol defining required methods for response models."""
def dump(self) -> Dict[str, Any]:
"""Convert model to dictionary format."""
...
class BaseJsonResponse(Generic[T]):
"""Base class for JSON response handling.
Provides common functionality for all response types including:
- Response formatting with consistent structure
- Pagination handling and configuration
- Data transformation through response models
"""
def __init__(
self,
message: str,
result: Any,
response_model: Optional[Type[T]] = None,
status_code: str = "HTTP_200_OK",
completed: bool = True,
cls_object: Optional[Any] = None,
filter_attributes: Optional[Any] = None,
) -> None:
"""Initialize response handler.
Args:
message: Response message
result: Query result or data
response_model: Optional model for data transformation
status_code: HTTP status code
completed: Operation completion status
cls_object: Class object for error context
filter_attributes: Optional pagination and filtering attributes
"""
self.status_code = getattr(status, status_code, status.HTTP_200_OK)
self.message = message
self.completed = completed
self.filter_attributes = filter_attributes
self.response_model = response_model
self.cls_object = cls_object
self.result = result
def _create_pagination(self) -> Pagination:
"""Create and configure pagination instance.
Returns:
Configured Pagination instance
"""
pagination = Pagination()
if self.filter_attributes:
pagination.change(
PaginationConfig(
page=self.filter_attributes.page,
size=self.filter_attributes.size,
order_field=self.filter_attributes.order_field,
order_type=self.filter_attributes.order_type,
)
)
return pagination
def _format_response(self, pagination: Pagination, data: Any) -> JSONResponse:
"""Format final JSON response with pagination.
Args:
pagination: Pagination instance with configuration
data: Response data to include
Returns:
Formatted JSONResponse
"""
return JSONResponse(
status_code=self.status_code,
content={
"pagination": pagination.as_dict(),
"completed": self.completed,
"message": self.message,
"data": data,
},
)
def _transform_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Transform data using response model if provided.
Args:
data: Raw data dictionary
Returns:
Transformed data dictionary
"""
if self.response_model:
return self.response_model(**data).dump()
return data
@staticmethod
def _validate_data(data: Any, expected_type: Type, cls_object: Any) -> None:
"""Validate data type and raise exception if invalid.
Args:
data: Data to validate
expected_type: Expected type of data
cls_object: Class object for error context
Raises:
HTTPExceptionApi: If data type is invalid
"""
if not isinstance(data, expected_type):
raise HTTPExceptionApi(
lang=cls_object.lang,
error_code="HTTP_400_BAD_REQUEST",
loc=get_line_number_for_error(),
sys_msg=f"Invalid data type: {type(data)}",
)
class SinglePostgresResponse(BaseJsonResponse[T]):
"""Handler for single record responses from PostgreSQL queries."""
def __new__(
cls,
message: str,
result: PostgresResponse,
response_model: Optional[Type[T]] = None,
status_code: str = "HTTP_200_OK",
completed: bool = True,
cls_object: Optional[Any] = None,
filter_attributes: Optional[Any] = None,
) -> JSONResponse:
"""Create response for single PostgreSQL record.
Args:
message: Response message
result: PostgreSQL query result
response_model: Optional model for data transformation
status_code: HTTP status code
completed: Operation completion status
cls_object: Class object for error context
filter_attributes: Optional pagination and filtering attributes
Returns:
Formatted JSON response
Raises:
HTTPExceptionApi: If result is invalid or empty
"""
cls._validate_data(result, PostgresResponse, cls_object)
if not result.first:
raise HTTPExceptionApi(
lang=cls_object.lang,
error_code="HTTP_400_BAD_REQUEST",
loc=get_line_number_for_error(),
sys_msg="No data found",
)
instance = super().__new__(cls)
instance.__init__(
message=message,
result=result,
response_model=response_model,
status_code=status_code,
completed=completed,
cls_object=cls_object,
filter_attributes=filter_attributes,
)
pagination = instance._create_pagination()
data = instance._transform_data(result.data.get_dict())
return instance._format_response(pagination, data)
class AlchemyJsonResponse(BaseJsonResponse[T]):
"""Handler for multiple record responses from SQLAlchemy queries."""
def __new__(
cls,
message: str,
result: PostgresResponse,
response_model: Optional[Type[T]] = None,
status_code: str = "HTTP_200_OK",
completed: bool = True,
cls_object: Optional[Any] = None,
filter_attributes: Optional[Any] = None,
) -> JSONResponse:
"""Create response for multiple SQLAlchemy records.
Args:
message: Response message
result: PostgreSQL query result
response_model: Optional model for data transformation
status_code: HTTP status code
completed: Operation completion status
cls_object: Class object for error context
filter_attributes: Optional pagination and filtering attributes
Returns:
Formatted JSON response
Raises:
HTTPExceptionApi: If result is invalid
"""
cls._validate_data(result, PostgresResponse, cls_object)
if result.first:
raise HTTPExceptionApi(
lang=cls_object.lang,
error_code="HTTP_400_BAD_REQUEST",
loc=get_line_number_for_error(),
sys_msg="No data found",
)
instance = super().__new__(cls)
instance.__init__(
message=message,
result=result,
response_model=response_model,
status_code=status_code,
completed=completed,
cls_object=cls_object,
filter_attributes=filter_attributes,
)
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)
class ListJsonResponse(BaseJsonResponse[T]):
"""Handler for list data responses."""
def __new__(
cls,
message: str,
result: List[Any],
response_model: Optional[Type[T]] = None,
status_code: str = "HTTP_200_OK",
completed: bool = True,
cls_object: Optional[Any] = None,
filter_attributes: Optional[Any] = None,
) -> JSONResponse:
"""Create response for list data.
Args:
message: Response message
result: List of data items
response_model: Optional model for data transformation
status_code: HTTP status code
completed: Operation completion status
cls_object: Class object for error context
filter_attributes: Optional pagination and filtering attributes
Returns:
Formatted JSON response
"""
cls._validate_data(result, list, cls_object)
instance = super().__new__(cls)
instance.__init__(
message=message,
result=result,
response_model=response_model,
status_code=status_code,
completed=completed,
cls_object=cls_object,
filter_attributes=filter_attributes,
)
pagination = instance._create_pagination()
data = [instance._transform_data(item) for item in result]
pagination.feed(data)
return instance._format_response(pagination, data)
class DictJsonResponse(BaseJsonResponse[T]):
"""Handler for dictionary data responses."""
def __new__(
cls,
message: str,
result: Dict[str, Any],
response_model: Optional[Type[T]] = None,
status_code: str = "HTTP_200_OK",
completed: bool = True,
cls_object: Optional[Any] = None,
filter_attributes: Optional[Any] = None,
) -> JSONResponse:
"""Create response for dictionary data.
Args:
message: Response message
result: Dictionary data
response_model: Optional model for data transformation
status_code: HTTP status code
completed: Operation completion status
cls_object: Class object for error context
filter_attributes: Optional pagination and filtering attributes
Returns:
Formatted JSON response
"""
cls._validate_data(result, dict, cls_object)
instance = super().__new__(cls)
instance.__init__(
message=message,
result=result,
response_model=response_model,
status_code=status_code,
completed=completed,
cls_object=cls_object,
filter_attributes=filter_attributes,
)
pagination = instance._create_pagination()
data = instance._transform_data(result)
return instance._format_response(pagination, data)

View File

@@ -0,0 +1,254 @@
from contextlib import contextmanager
from typing import Any, Dict, Optional, Generator
from sqlalchemy.orm import Session
from sqlalchemy import inspect
from Services.PostgresDb.database import Base
class BaseModel(Base):
"""Base model class with common utility functions and SQLAlchemy integration.
This class serves as the foundation for all database models, providing:
- SQLAlchemy ORM integration through Base
- Session management utilities
- CRUD operations (create, update)
- Bulk operation support
"""
__abstract__ = True # Marks this as a base class, won't create a table
@classmethod
def new_session(cls) -> Session:
"""Get database session."""
from Services.PostgresDb.database import get_db
with get_db() as session:
return session
def update(
self, session: Optional[Session] = None, **kwargs: Dict[str, Any]
) -> "BaseModel":
"""Update model instance with given attributes.
Args:
session: Optional existing session to use. If not provided, creates a new one.
**kwargs: Attributes to update
Returns:
Updated model instance
Example:
# Using an existing session
with get_db() as session:
model.update(session=session, name="new name")
model2.update(session=session, status="active")
# Both updates use the same transaction
# Creating a new session automatically
model.update(name="new name") # Creates and manages its own session
"""
should_close_session = session is None
if session is None:
session = self.get_session()
try:
# Remove unrelated fields
check_kwargs = self.remove_non_related_inputs(kwargs)
# Get all table columns
mapper = inspect(self.__class__)
columns = [column.key for column in mapper.columns]
# Get relationship fields
relationships = [rel.key for rel in mapper.relationships]
# Handle confirmation logic
is_confirmed_argument = kwargs.get("is_confirmed", None)
if is_confirmed_argument and not len(kwargs) == 1:
self.raise_http_exception(
status_code="HTTP_406_NOT_ACCEPTABLE",
error_case="ConfirmError",
data=kwargs,
message="Confirm field cannot be updated with other fields",
)
# Process system fields
check_kwargs = self.extract_system_fields(check_kwargs, create=False)
# Update columns
for key, value in check_kwargs.items():
if key in columns:
setattr(self, key, value)
elif key in relationships:
# Handle relationship updates
related_obj = getattr(self, key)
if isinstance(related_obj, list):
# Handle many-to-many or one-to-many relationships
if isinstance(value, list):
setattr(self, key, value)
else:
# Handle many-to-one or one-to-one relationships
setattr(self, key, value)
# Handle user tracking
if hasattr(self, "creds"):
person_id = getattr(self.creds, "person_id", None)
person_name = getattr(self.creds, "person_name", None)
if person_id and person_name:
if is_confirmed_argument:
self.confirmed_by_id = self.creds.get("person_id", "Unknown")
self.confirmed_by = self.creds.get("person_name", "Unknown")
else:
self.updated_by_id = self.creds.get("person_id", "Unknown")
self.updated_by = self.creds.get("person_name", "Unknown")
session.add(self)
session.flush()
return self
except Exception:
if should_close_session:
session.rollback()
raise
finally:
if should_close_session:
session.close()
@classmethod
def create(
cls, session: Optional[Session] = None, **kwargs: Dict[str, Any]
) -> "BaseModel":
"""Create new instance with optional session reuse.
Args:
session: Optional existing session to use. If not provided, creates a new one.
**kwargs: Attributes for the new instance
Returns:
Created model instance
Example:
# Using an existing session for multiple creates
with get_db() as session:
user1 = User.create(session=session, name="John")
user2 = User.create(session=session, name="Jane")
# Both creates use the same transaction
# Creating with auto-managed session
user = User.create(name="John") # Creates and manages its own session
"""
instance = cls()
should_close_session = session is None
if session is None:
session = instance.get_session()
try:
check_kwargs = cls.remove_non_related_inputs(instance, kwargs)
check_kwargs = cls.extract_system_fields(
instance, check_kwargs, create=True
)
# Get all table columns and relationships
mapper = inspect(cls)
columns = [column.key for column in mapper.columns]
relationships = [rel.key for rel in mapper.relationships]
# Set attributes
for key, value in check_kwargs.items():
if key in columns:
setattr(instance, key, value)
elif key in relationships:
# Handle relationship assignments
if isinstance(value, list):
# Handle many-to-many or one-to-many relationships
setattr(instance, key, value)
else:
# Handle many-to-one or one-to-one relationships
setattr(instance, key, value)
# Handle user tracking
if hasattr(instance, "creds"):
person_id = getattr(instance.creds, "person_id", None)
person_name = getattr(instance.creds, "person_name", None)
if person_id and person_name:
instance.created_by_id = instance.creds.get("person_id", "Unknown")
instance.created_by = instance.creds.get("person_name", "Unknown")
session.add(instance)
session.flush()
if should_close_session:
session.commit()
return instance
except Exception:
if should_close_session:
session.rollback()
raise
finally:
if should_close_session:
session.close()
@classmethod
@contextmanager
def bulk_create(
cls, session: Optional[Session] = None
) -> Generator[Session, None, None]:
"""Context manager for bulk creating instances.
Args:
session: Optional existing session to use. If not provided, creates a new one.
Yields:
SQLAlchemy session for creating multiple instances
Example:
# Bulk create multiple instances in one transaction
with User.bulk_create() as session:
user1 = User.create(session=session, name="John")
user2 = User.create(session=session, name="Jane")
# Both creates share the same transaction
"""
should_close_session = session is None
if session is None:
session = cls().get_session()
try:
yield session
if should_close_session:
session.commit()
except Exception:
if should_close_session:
session.rollback()
raise
finally:
if should_close_session:
session.close()
# @router.put("/users/{user_id}")
# async def update_user(
# user_id: str,
# update_data: Dict[str, Any],
# db: Session = Depends(get_db_session)
# ):
# user = db.query(User).filter(User.id == user_id).first()
# if not user:
# raise HTTPException(status_code=404, detail="User not found")
#
# updated_user = user.update(**update_data)
# return updated_user
#
#
# @router.post("/users")
# async def create_user(
# user_data: Dict[str, Any],
# db: Session = Depends(get_db_session)
# ):
# with User.create_with_session(**user_data) as new_user:
# return new_user

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,549 @@
"""
PostgreSQL Base Models Module
This module provides base classes for PostgreSQL models with common functionality such as:
- CRUD operations with session management
- Soft delete capability
- Automatic timestamps
- User tracking (created_by, updated_by)
- Data serialization
- Multi-language support
"""
import datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast
from sqlalchemy import (
TIMESTAMP,
NUMERIC,
func,
text,
UUID,
String,
Integer,
Boolean,
SmallInteger,
)
from sqlalchemy.orm import Mapped, mapped_column, Session
from sqlalchemy_mixins.serialize import SerializeMixin
from sqlalchemy_mixins.repr import ReprMixin
from sqlalchemy_mixins.smartquery import SmartQueryMixin
from ApiLibrary import DateTimeLocal, system_arrow
from Services.PostgresDb.Models.base_model import BaseModel
from Services.PostgresDb.Models.filter_functions import FilterAttributes
# Type variable for class methods returning self
T = TypeVar("T", bound="CrudMixin")
class CrudMixin(
BaseModel, SmartQueryMixin, SerializeMixin, ReprMixin, FilterAttributes
):
"""
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
# 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",
]
# User credentials and preferences
creds = None
lang: str = "tr"
client_arrow: Optional[DateTimeLocal] = None
valid_record_dict: Dict[str, bool] = {"active": True, "deleted": False}
meta_data: Dict[str, Any] = {}
# 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",
)
@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
"""
cls.creds = token.credentials
cls.client_arrow = DateTimeLocal(is_client=True, timezone=token.timezone)
cls.lang = str(token.lang).lower()
@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 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 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(
cls.client_arrow.get(str(val)).format("DD-MM-YYYY HH:mm:ss +0")
)
elif key_ == Mapped[str]:
return True, str(val)
# Handle based on Python types
else:
if isinstance(val, datetime.datetime):
return True, str(
cls.client_arrow.get(str(val)).format("DD-MM-YYYY HH:mm:ss +0")
)
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
@classmethod
def find_or_create(cls: Type[T], db: Session, **kwargs) -> T:
"""
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:
already_record.meta_data = {
"created": False,
"error_case": "DeletedRecord",
"message": "",
}
return already_record
elif not already_record.is_confirmed:
already_record.meta_data = {
"created": False,
"error_case": "IsNotConfirmed",
"message": "",
}
return already_record
already_record.meta_data = {
"created": False,
"error_case": "AlreadyExists",
"message": "",
}
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)
if getattr(cls.creds, "person_id", None) and getattr(
cls.creds, "person_name", None
):
created_record.created_by_id = cls.creds.person_id
created_record.created_by = cls.creds.person_name
db.add(created_record)
db.flush()
created_record.meta_data = {"created": True, "error_case": None, "message": ""}
return created_record
def update(self, db: Session, **kwargs) -> "CrudMixin":
"""
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)
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")
check_kwargs = self.extract_system_fields(check_kwargs, create=False)
for key, value in check_kwargs.items():
setattr(self, key, value)
# Update confirmation or modification tracking
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
db.flush()
return self
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 []
) + 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 BaseCollection(CrudMixin):
"""Base model class with minimal fields."""
__abstract__ = True
__repr__ = ReprMixin.__repr__
id: Mapped[int] = mapped_column(Integer, primary_key=True)
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__
# 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",
)
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,44 @@
from typing import Any, List, Optional, TypeVar, Union
from sqlalchemy.orm import Query
from sqlalchemy.orm.session import Session
T = TypeVar("T")
class QueryResponse:
"""Handler for SQLAlchemy query results with error handling."""
def __init__(self, db: Session, query: Query, first: bool = False):
self.db = db
self.first = first
self.__query = query
def get(self, index: int) -> Optional[T]:
"""Get item at specific index if it exists."""
count = self.count
if count and not index > count:
return self.data[index - 1]
return None
@property
def data(self) -> Union[Optional[T], List[T]]:
"""Get query results with error handling."""
try:
if self.first:
return self.__query.first()
return self.__query.all()
except Exception as e:
# Handle any database errors by rolling back
self.db.rollback()
return None if self.first else []
@property
def count(self) -> int:
"""Get total count of query results."""
return self.__query.count()
@property
def query(self) -> Query:
"""Get the underlying SQLAlchemy query."""
return self.__query

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,6 @@
from Services.PostgresDb.Models.mixins import CrudCollection, BaseCollection
__all__ = [
"CrudCollection",
"BaseCollection",
]

View File

@@ -0,0 +1,60 @@
from contextlib import contextmanager
from functools import lru_cache
from typing import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, Session
from AllConfigs.SqlDatabase.configs import WagDatabase
# Configure the database engine with proper pooling
engine = create_engine(
WagDatabase.DATABASE_URL,
pool_pre_ping=True, # Verify connection before using
pool_size=20, # Maximum number of permanent connections
max_overflow=10, # Maximum number of additional connections
pool_recycle=3600, # Recycle connections after 1 hour
pool_timeout=30, # Wait up to 30 seconds for a connection
echo=False, # Set to True for debugging SQL queries
)
Base = declarative_base()
# Create a cached session factory
@lru_cache()
def get_session_factory() -> scoped_session:
"""Create a thread-safe session factory."""
session_local = sessionmaker(
bind=engine,
autocommit=False,
autoflush=False,
expire_on_commit=False, # Prevent expired object issues
)
return scoped_session(session_local)
@contextmanager
def get_db() -> Generator[Session, None, None]:
"""Get database session with proper connection management.
This context manager ensures:
- Proper connection pooling
- Session cleanup
- Connection return to pool
- Thread safety
Yields:
Session: SQLAlchemy session object
"""
session_factory = get_session_factory()
session = session_factory()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
session_factory.remove() # Clean up the session from the registry

View File

@@ -0,0 +1,82 @@
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)