Service Postgres added

This commit is contained in:
berkay 2025-01-13 22:17:09 +03:00
parent 3bc0146767
commit 5d8b37179d
26 changed files with 1569 additions and 308 deletions

View File

@ -5,3 +5,9 @@ class HostConfig:
class MainConfig: class MainConfig:
DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss Z" DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss Z"
DATETIME_FORMAT_JS = "YYYY-MM-DD HH:mm:ss +0"
# Timezone Configuration
DEFAULT_TIMEZONE = "GMT+3" # Default timezone for the application
SYSTEM_TIMEZONE = "GMT+0" # System timezone (used for internal operations)
SUPPORTED_TIMEZONES = ["GMT+0", "GMT+3"] # List of supported timezones

3
ApiLibrary/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from ApiLibrary.date_time_actions.date_functions import DateTimeLocal, system_arrow
__all__ = ["DateTimeLocal", "system_arrow"]

View File

@ -0,0 +1,116 @@
import arrow
import calendar
from AllConfigs.main import MainConfig as Config
class DateTimeLocal:
def __init__(self, timezone: str = None, is_client: bool = True):
if timezone and timezone not in Config.SUPPORTED_TIMEZONES:
raise ValueError(
f"Unsupported timezone: {timezone}. Must be one of {Config.SUPPORTED_TIMEZONES}"
)
self.timezone = Config.SYSTEM_TIMEZONE
if is_client:
self.timezone = (timezone or Config.DEFAULT_TIMEZONE).replace("-", "+")
def find_last_day_of_month(self, date_value):
today = self.get(date_value).date()
_, last_day = calendar.monthrange(today.year, today.month)
return self.get(today.year, today.month, last_day, 23, 59, 59).to(self.timezone)
def find_first_day_of_month(self, date_value):
today = self.get(date_value).date()
return self.get(today.year, today.month, 1).to(self.timezone)
def get(self, *args):
return arrow.get(*args).to(str(self.timezone))
def now(self):
return arrow.now().to(str(self.timezone))
def shift(self, date, **kwargs):
return self.get(date).shift(**kwargs)
def date(self, date):
return self.get(date).date()
def time(self, date):
return self.get(date).time()
def string_date(self, date, splitter: str = "-"):
return str(self.get(date).date()).replace("-", splitter)
def string_time_only(self, date):
return self.get(date).format("HH:mm:ss")
def string_date_only(self, date):
return self.get(date).format("YYYY-MM-DD")
def to_timestamp(self, date):
"""Convert datetime to UTC timestamp"""
return self.get(date).timestamp()
def from_timestamp(self, timestamp):
"""Convert timestamp to timezone-aware datetime"""
return arrow.get(timestamp).to(str(self.timezone))
def is_timezone_aware(self, date):
"""Check if a date is timezone-aware"""
return self.get(date).tzinfo is not None
def standardize_timezone(self, date):
"""Ensure date is in the correct timezone"""
if not self.is_timezone_aware(date):
return self.get(date).to(str(self.timezone))
return self.get(date)
def get_expiry_time(self, **kwargs):
"""Get future time for cache expiry
Example: get_expiry_time(hours=1, minutes=30)
"""
return self.now().shift(**kwargs)
def is_expired(self, timestamp):
"""Check if a timestamp is expired"""
if not timestamp:
return True
return self.from_timestamp(timestamp) < self.now()
def get_cache_key(self, base_key, *args):
"""Generate a cache key with timezone info
Example: get_cache_key('user_profile', user_id, 'details')
"""
components = [str(base_key)]
components.extend(str(arg) for arg in args)
components.append(f"tz_{self.timezone}")
return ":".join(components)
def format_for_db(self, date):
"""Format date for database storage"""
return self.get(date).format("YYYY-MM-DD HH:mm:ss.SSSZZ")
def parse_from_db(self, date_str):
"""Parse date from database format"""
if not date_str:
return None
return self.get(date_str)
def get_day_boundaries(self, date=None):
"""Get start and end of day in current timezone"""
dt = self.get(date) if date else self.now()
start = dt.floor("day")
end = dt.ceil("day")
return start, end
def get_month_boundaries(self, date=None):
"""Get start and end of month in current timezone"""
dt = self.get(date) if date else self.now()
start = dt.floor("month")
end = dt.ceil("month")
return start, end
client_arrow = DateTimeLocal(is_client=True)
system_arrow = DateTimeLocal(is_client=False)

View File

@ -0,0 +1,13 @@
from .base_validations import (
BaseModelRegular,
PydanticBaseModel,
ListOptions,
CrudRecords,
)
__all__ = [
"BaseModelRegular",
"PydanticBaseModel",
"ListOptions",
"CrudRecords",
]

View File

@ -0,0 +1,33 @@
from typing import Optional
from ApiValidations.handler import BaseModelRegular
class ListOptions(BaseModelRegular):
page: Optional[int] = 1
size: Optional[int] = 10
order_field: Optional[str] = "id"
order_type: Optional[str] = "asc"
include_joins: Optional[list] = None
query: Optional[dict] = None
class CrudRecords:
uu_id: Optional[str] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
created_by: Optional[str] = None
updated_by: Optional[str] = None
confirmed_by: Optional[str] = None
is_confirmed: Optional[bool] = None
active: Optional[bool] = None
is_notification_send: Optional[bool] = None
is_email_send: Optional[bool] = None
class PydanticBaseModel(BaseModelRegular):
active: Optional[bool] = None
deleted: Optional[bool] = None
expiry_starts: Optional[str] = None
# expiry_ends: Optional[str] = None
is_confirmed: Optional[bool] = None

22
ApiValidations/handler.py Normal file
View File

@ -0,0 +1,22 @@
from pydantic import BaseModel
def rewrite_input_data(data):
return {
item[0]: item[1]
for item in data.items()
if not item[1] == "" and item[1] is not None
}
class BaseModelRegular(BaseModel):
def __init__(self, **kwargs):
super().__init__(**rewrite_input_data(kwargs))
def excluded_dump(self):
return self.model_dump(exclude_unset=True, exclude_none=True)
def dump(self):
return self.model_dump()

View File

@ -0,0 +1,10 @@
from ErrorHandlers.ApiErrorHandlers.api_exc_handler import (
HTTPExceptionApiHandler,
HTTPExceptionApi,
)
__all__ = [
"HTTPExceptionApiHandler",
"HTTPExceptionApi",
]

View File

@ -0,0 +1,45 @@
from typing import Any, Dict, List, Optional
class HTTPExceptionApi(Exception):
def __init__(self, error_code: str, lang: str):
self.error_code = error_code
self.lang = lang
class HTTPExceptionApiHandler:
def __init__(
self,
**kwargs,
):
self.EXCEPTIONS = kwargs.get(
"exceptions"
) # from fastapi.exceptions import HTTPException
self.STATUSES = kwargs.get("statuses") # from fastapi import status
self.EXCEPTION_DICTS: Dict = kwargs.get("exceptions_dict")
self.ERRORS_DICT: Dict = kwargs.get("errors_dict")
self.ERRORS_LANG: Dict = kwargs.get("error_language_dict")
self.RESPONSE_MODEL: Any = kwargs.get("response_model")
def retrieve_error_status_code(self, exc: HTTPExceptionApi):
grab_status = self.ERRORS_DICT.get(str(exc.error_code).upper(), "")
grab_status_code = self.EXCEPTION_DICTS.get(str(grab_status).upper(), "500")
return getattr(
self.STATUSES,
str(grab_status_code),
getattr(self.STATUSES, "HTTP_500_INTERNAL_SERVER_ERROR"),
)
def retrieve_error_message(self, exc: HTTPExceptionApi):
message_by_lang = self.ERRORS_LANG.get(str(exc.lang).lower(), {})
return message_by_lang.get(str(exc.error_code).upper(), "Unknown error")
def handle_exception(self, request, exc: HTTPExceptionApi):
status_code = self.retrieve_error_status_code(exc)
error_message = self.retrieve_error_message(exc)
return self.RESPONSE_MODEL(
status_code=int(status_code),
content={"message": error_message, "lang": exc.lang, "request": request},
)

View File

@ -0,0 +1,4 @@
from ErrorHandlers.LanguageModels.base_languages import BaseErrorLanguageModels
__all__ = ["BaseErrorLanguageModels"]

View File

@ -0,0 +1,38 @@
from ErrorHandlers.bases import ErrorLanguageModelClass, LanguageModelClass
class BaseErrorLanguageModelTurkish(ErrorLanguageModelClass):
NOT_CREATED: str = "Kayıt oluşturulamadı."
NOT_DELETED: str = "Kayıt silinemedi."
NOT_UPDATED: str = "Kayıt güncellenemedi."
NOT_LISTED: str = "Kayıt listelenemedi."
NOT_FOUND: str = "Kayıt bulunamadı."
ALREADY_EXISTS: str = "Kayıt zaten mevcut."
IS_NOT_CONFIRMED: str = "Kayıt onaylanmadı."
NOT_AUTHORIZED: str = "Yetkisiz kullanıcı."
NOT_VALID: str = "Gecersiz veri."
NOT_ACCEPTABLE: str = "Gecersiz veri."
INVALID_DATA: str = "Gecersiz veri."
UNKNOWN_ERROR: str = "Bilinmeyen bir hata oluştu."
class BaseErrorLanguageModelEnglish(ErrorLanguageModelClass):
NOT_CREATED: str = "Not Created."
NOT_DELETED: str = "Not Deleted."
NOT_UPDATED: str = "Not Updated."
NOT_LISTED: str = "Not Listed."
NOT_FOUND: str = "Not Found."
ALREADY_EXISTS: str = "Already Exists."
IS_NOT_CONFIRMED: str = "Not Confirmed."
NOT_AUTHORIZED: str = "Not Authorized."
NOT_VALID: str = "Not Valid."
NOT_ACCEPTABLE: str = "Not Acceptable."
INVALID_DATA: str = "Invalid Data."
UNKNOWN_ERROR: str = "Unknown Error occured."
class BaseErrorLanguageModels(LanguageModelClass):
tr: BaseErrorLanguageModelTurkish = BaseErrorLanguageModelTurkish
en: BaseErrorLanguageModelEnglish = BaseErrorLanguageModelEnglish

16
ErrorHandlers/base.py Normal file
View File

@ -0,0 +1,16 @@
from ErrorHandlers.bases import BaseErrorModelClass
class BaseError(BaseErrorModelClass):
NOT_CREATED: int = 405
NOT_DELETED: int = 405
NOT_UPDATED: int = 405
NOT_LISTED: int = 404
NOT_FOUND: int = 404
ALREADY_EXISTS: int = 400
IS_NOT_CONFIRMED: int = 405
NOT_AUTHORIZED: int = 401
NOT_VALID: int = 406
NOT_ACCEPTABLE: int = 406
INVALID_DATA: int = 422
UNKNOWN_ERROR: int = 502

30
ErrorHandlers/bases.py Normal file
View File

@ -0,0 +1,30 @@
from typing import Optional
class BaseErrorModelClass:
@classmethod
def retrieve_error_by_code(cls, error_code: str):
return getattr(cls, error_code, 502)
class StatusesModelClass:
@classmethod
def retrieve_error_by_code(cls, error_code: str):
return getattr(cls, error_code, 502)
class ErrorLanguageModelClass:
@classmethod
def retrieve_error_header(cls, error_code: str):
return getattr(cls, error_code, "Unknown Error occured.")
class LanguageModelClass:
@classmethod
def retrieve_error_by_code(cls, error_code: str, language: Optional[str] = "tr"):
language_model: ErrorLanguageModelClass = getattr(cls, language, "tr")
return language_model.retrieve_error_header(error_code)

61
ErrorHandlers/statuses.py Normal file
View File

@ -0,0 +1,61 @@
from ErrorHandlers.bases import StatusesModelClass
class Statuses(StatusesModelClass):
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_102_PROCESSING = 102
HTTP_103_EARLY_HINTS = 103
HTTP_200_OK = 200
HTTP_201_CREATED = 201
HTTP_202_ACCEPTED = 202
HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
HTTP_204_NO_CONTENT = 204
HTTP_205_RESET_CONTENT = 205
HTTP_206_PARTIAL_CONTENT = 206
HTTP_207_MULTI_STATUS = 207
HTTP_208_ALREADY_REPORTED = 208
HTTP_226_IM_USED = 226
HTTP_300_MULTIPLE_CHOICES = 300
HTTP_301_MOVED_PERMANENTLY = 301
HTTP_302_FOUND = 302
HTTP_303_SEE_OTHER = 303
HTTP_304_NOT_MODIFIED = 304
HTTP_305_USE_PROXY = 305
HTTP_306_RESERVED = 306
HTTP_307_TEMPORARY_REDIRECT = 307
HTTP_308_PERMANENT_REDIRECT = 308
HTTP_400_BAD_REQUEST = 400
HTTP_401_UNAUTHORIZED = 401
HTTP_402_PAYMENT_REQUIRED = 402
HTTP_403_FORBIDDEN = 403
HTTP_404_NOT_FOUND = 404
HTTP_405_METHOD_NOT_ALLOWED = 405
HTTP_406_NOT_ACCEPTABLE = 406
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
HTTP_408_REQUEST_TIMEOUT = 408
HTTP_409_CONFLICT = 409
HTTP_410_GONE = 410
HTTP_411_LENGTH_REQUIRED = 411
HTTP_412_PRECONDITION_FAILED = 412
HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
HTTP_414_REQUEST_URI_TOO_LONG = 414
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
HTTP_417_EXPECTATION_FAILED = 417
HTTP_418_IM_A_TEAPOT = 418
HTTP_421_MISDIRECTED_REQUEST = 421
HTTP_422_UNPROCESSABLE_ENTITY = 422
HTTP_423_LOCKED = 423
HTTP_424_FAILED_DEPENDENCY = 424
HTTP_426_UPGRADE_REQUIRED = 426
HTTP_428_PRECONDITION_REQUIRED = 428
HTTP_429_TOO_MANY_REQUESTS = 429
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451
HTTP_500_INTERNAL_SERVER_ERROR = 500
HTTP_502_BAD_GATEWAY = 502
@classmethod
def retrieve_error_by_code(cls, error_code: str):
return getattr(cls, error_code, 502)

View File

View File

@ -1,68 +0,0 @@
from datetime import datetime
from typing import Any, Dict, List, Optional, TypeVar, Union
from sqlalchemy import Column, DateTime, Integer, inspect
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import Session
from Services.Postgres.database import Base, get_db
T = TypeVar('T')
class PostgresBase(Base):
"""Base class for all PostgreSQL models."""
__abstract__ = True
id = Column(Integer, primary_key=True, index=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
deleted_at = Column(DateTime, nullable=True)
@declared_attr
def __tablename__(cls) -> str:
"""Generate table name automatically from class name."""
return cls.__name__.lower()
def as_dict(self) -> Dict[str, Any]:
"""Convert model instance to dictionary."""
return {c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs}
@classmethod
def filter_non_deleted(cls, db: Session):
"""Filter out soft-deleted records."""
return db.query(cls).filter(cls.deleted_at.is_(None))
@classmethod
def get_by_id(cls, db: Session, id: int) -> Optional['PostgresBase']:
"""Get record by ID if not deleted."""
return cls.filter_non_deleted(db).filter(cls.id == id).first()
def soft_delete(self) -> None:
"""Soft delete the record."""
self.deleted_at = datetime.utcnow()
with get_db() as db:
self.save(db)
def save(self, db: Session) -> None:
"""Save the current record."""
if not self.id:
db.add(self)
db.commit()
db.refresh(self)
def update(self, **kwargs: Dict[str, Any]) -> None:
"""Update record with given values."""
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
with get_db() as db:
self.save(db)
@classmethod
def create(cls, **kwargs: Dict[str, Any]) -> 'PostgresBase':
"""Create a new record."""
instance = cls(**kwargs)
with get_db() as db:
instance.save(db)
return instance

View File

@ -1,49 +0,0 @@
from typing import Any, Dict, List, Optional, TypeVar, Generic
T = TypeVar('T')
class PostgresResponse(Generic[T]):
"""Base class for Postgres response handling."""
def __init__(
self,
status: bool,
message: str,
data: Optional[T] = None,
error: Optional[str] = None,
):
self.status = status
self.message = message
self.data = data
self.error = error
if isinstance(data, dict):
self.data_type = "dict"
elif isinstance(data, list):
self.data_type = "list"
else:
self.data_type = None
def as_dict(self) -> Dict[str, Any]:
"""Convert response to dictionary format."""
return {
"status": self.status,
"message": self.message,
"data": self.data,
"dataType": self.data_type,
"error": self.error,
}
@property
def all(self) -> Optional[List[T]]:
"""Get all data items if data is a list."""
if isinstance(self.data, list):
return self.data
return None
@property
def first(self) -> Optional[T]:
"""Get first data item if data is a list."""
if isinstance(self.data, list) and self.data:
return self.data[0]
return None

View File

@ -0,0 +1,379 @@
"""
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, Union, TypeVar, Protocol
from dataclasses import dataclass
from fastapi import status
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Query
from ApiValidations.Request.base_validations import PydanticBaseModel
from ApiValidations.handler import BaseModelRegular
from Services.Postgres.Models.response import PostgresResponse
from ErrorHandlers.ApiErrorHandlers.api_exc_handler import HTTPExceptionApi
T = TypeVar("T")
class DataValidator(Protocol):
"""Protocol for data validation methods."""
@staticmethod
def validate_data(data: Any, cls_object: Any) -> None:
"""Validate data and raise HTTPExceptionApi if invalid."""
...
@dataclass
class PaginationConfig:
"""
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: str = "id"
order_type: str = "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 = 10
MIN_SIZE = 10
MAX_SIZE = 40
def __init__(self):
self.size: int = self.DEFAULT_SIZE
self.page: int = 1
self.order_field: str = "id"
self.order_type: str = "asc"
self.page_count: int = 1
self.total_count: int = 0
self.total_pages: int = 1
def change(self, config: PaginationConfig) -> None:
"""Update pagination settings from config."""
self.size = (
config.size
if self.MIN_SIZE <= config.size <= self.MAX_SIZE
else self.DEFAULT_SIZE
)
self.page = config.page
self.order_field = config.order_field
self.order_type = config.order_type
self._update_page_counts()
def feed(self, data: Union[List[Any], PostgresResponse, Query]) -> None:
"""Calculate pagination based on data source."""
self.total_count = (
len(data)
if isinstance(data, list)
else data.count if isinstance(data, PostgresResponse) else data.count()
)
self._update_page_counts()
def _update_page_counts(self) -> None:
"""Update page counts and validate current page."""
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 as_dict(self) -> Dict[str, Any]:
"""Convert pagination state to dictionary format."""
return {
"size": self.size,
"page": self.page,
"totalCount": self.total_count,
"totalPages": self.total_pages,
"pageCount": self.page_count,
"orderField": self.order_field,
"orderType": self.order_type,
}
@dataclass
class ResponseConfig:
"""
Configuration for response formatting.
Attributes:
status_code: HTTP status code (default: "HTTP_200_OK")
message: Response message
completed: Operation completion status
cls_object: Class object for error handling
"""
status_code: str = "HTTP_200_OK"
message: str = ""
completed: bool = True
cls_object: Optional[Any] = None
class BaseJsonResponse:
"""
Base class for JSON response handling.
Provides common functionality for all response types:
- Response formatting
- Pagination handling
- Data transformation
"""
def __init__(
self,
config: ResponseConfig,
response_model: Optional[Type[T]] = None,
filter_attributes: Optional[Any] = None,
):
self.status_code = getattr(status, config.status_code, status.HTTP_200_OK)
self.message = config.message
self.completed = config.completed
self.filter_attributes = filter_attributes
self.response_model = response_model
self.cls_object = config.cls_object
def _create_pagination(self) -> Pagination:
"""Create and configure 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."""
return JSONResponse(
status_code=self.status_code,
content={
"pagination": pagination.as_dict(),
"completed": self.completed,
"message": self.message,
"data": data,
},
)
@staticmethod
def _validate_data(data: Any, expected_type: Type, cls_object: Any) -> None:
"""Validate data type and raise exception if invalid."""
if not isinstance(data, expected_type):
raise HTTPExceptionApi(
lang=cls_object.lang,
error_code="HTTP_400_BAD_REQUEST",
)
class SinglePostgresResponse(BaseJsonResponse):
"""
Handles single record responses from PostgreSQL queries.
Used when expecting a single record from a database query.
Validates that the result is a PostgresResponse and contains exactly one record.
"""
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,
) -> JSONResponse:
cls._validate_data(result, PostgresResponse, cls_object)
if not result.first:
raise HTTPExceptionApi(
lang=cls_object.lang,
error_code="HTTP_400_BAD_REQUEST",
)
instance = cls()
instance.__init__(
ResponseConfig(
status_code=status_code,
message=message,
completed=completed,
cls_object=cls_object,
),
response_model=response_model,
)
pagination = instance._create_pagination()
data = result.data.get_dict()
if response_model:
data = response_model(**data).dump()
return instance._format_response(pagination, data)
class AlchemyJsonResponse(BaseJsonResponse):
"""
Handles multiple record responses from SQLAlchemy queries.
Used for database queries returning multiple records.
Validates that the result is a PostgresResponse and contains multiple records.
Supports pagination and data transformation through response models.
"""
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:
cls._validate_data(result, PostgresResponse, cls_object)
if result.first:
raise HTTPExceptionApi(
lang=cls_object.lang,
error_code="HTTP_400_BAD_REQUEST",
)
instance = cls()
instance.__init__(
ResponseConfig(
status_code=status_code,
message=message,
completed=completed,
cls_object=cls_object,
),
response_model=response_model,
filter_attributes=filter_attributes,
)
pagination = instance._create_pagination()
data = [
(
response_model(**item.get_dict()).dump()
if response_model
else item.get_dict()
)
for item in result.data
]
pagination.feed(data)
return instance._format_response(pagination, data)
class ListJsonResponse(BaseJsonResponse):
"""
Handles responses for list data.
Used when working with Python lists that need to be paginated
and optionally transformed through a response model.
Validates that the input is a list.
"""
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:
cls._validate_data(result, list, cls_object)
instance = cls()
instance.__init__(
ResponseConfig(
status_code=status_code,
message=message,
completed=completed,
cls_object=cls_object,
),
response_model=response_model,
filter_attributes=filter_attributes,
)
pagination = instance._create_pagination()
data = [
response_model(**item).dump() if response_model else item for item in result
]
pagination.feed(data)
return instance._format_response(pagination, data)
class DictJsonResponse(BaseJsonResponse):
"""
Handles responses for dictionary data.
Used when working with single dictionary objects that need to be
transformed through a response model. Validates that the input
is a dictionary.
"""
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:
cls._validate_data(result, dict, cls_object)
instance = cls()
instance.__init__(
ResponseConfig(
status_code=status_code,
message=message,
completed=completed,
cls_object=cls_object,
),
response_model=response_model,
filter_attributes=filter_attributes,
)
pagination = instance._create_pagination()
data = response_model(**result).dump() if response_model else result
return instance._format_response(pagination, data)

View File

@ -117,4 +117,4 @@ class BaseModel:
# db: Session = Depends(get_db_session) # db: Session = Depends(get_db_session)
# ): # ):
# with User.create_with_session(**user_data) as new_user: # with User.create_with_session(**user_data) as new_user:
# return new_user # return new_user

View File

@ -0,0 +1,526 @@
"""
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 ErrorHandlers.ApiErrorHandlers 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",
)
@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",
)
@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",
)
@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",
)
@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], db: Session, *args: BinaryExpression, 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],
db: Session,
*args: BinaryExpression,
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",
)

View File

@ -17,9 +17,9 @@ from Services.Postgres.database import get_db
# Example Model Definition # Example Model Definition
class User(CrudCollection): class User(CrudCollection):
"""Example user model demonstrating CrudCollection usage.""" """Example user model demonstrating CrudCollection usage."""
__tablename__ = "users" __tablename__ = "users"
# Additional fields (id and other common fields come from CrudCollection) # Additional fields (id and other common fields come from CrudCollection)
username: Mapped[str] = mapped_column(String(50), unique=True, index=True) username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
email: Mapped[str] = mapped_column(String(100), unique=True) email: Mapped[str] = mapped_column(String(100), unique=True)
@ -28,15 +28,13 @@ class User(CrudCollection):
# Example Usage # Example Usage
def example_create(): def example_create():
"""Example of creating a new record.""" """Example of creating a new record."""
with get_db() as db: with get_db() as db:
# Create a new user # Create a new user
user = User.find_or_create( user = User.find_or_create(
db, db, username="john_doe", email="john@example.com", age=30
username="john_doe",
email="john@example.com",
age=30
) )
db.commit() db.commit()
return user return user
@ -50,12 +48,10 @@ def example_batch_create():
users = [] users = []
for i in range(3): for i in range(3):
user = User.find_or_create( user = User.find_or_create(
db, db, username=f"user_{i}", email=f"user_{i}@example.com"
username=f"user_{i}",
email=f"user_{i}@example.com"
) )
users.append(user) users.append(user)
db.commit() db.commit()
return users return users
except Exception: except Exception:
@ -69,11 +65,7 @@ def example_update():
# Find user and update # Find user and update
user = db.query(User).filter(User.username == "john_doe").first() user = db.query(User).filter(User.username == "john_doe").first()
if user: if user:
user.update( user.update(db, email="john.doe@newdomain.com", age=31)
db,
email="john.doe@newdomain.com",
age=31
)
db.commit() db.commit()
return user return user
@ -94,11 +86,7 @@ def example_query():
# Get active (non-deleted) users # Get active (non-deleted) users
active_users = ( active_users = (
db.query(User) db.query(User)
.filter( .filter(User.active == True, User.deleted == False, User.age >= 18)
User.active == True,
User.deleted == False,
User.age >= 18
)
.order_by(User.created_at.desc()) .order_by(User.created_at.desc())
.all() .all()
) )
@ -110,25 +98,21 @@ def example_complex_transaction():
with get_db() as db: with get_db() as db:
try: try:
# Multiple operations in single transaction # Multiple operations in single transaction
user = User.find_or_create( user = User.find_or_create(db, username="new_user", email="new@example.com")
db,
username="new_user",
email="new@example.com"
)
# Update existing user # Update existing user
other_user = db.query(User).filter(User.username == "old_user").first() other_user = db.query(User).filter(User.username == "old_user").first()
if other_user: if other_user:
other_user.update(db, email="updated@example.com") other_user.update(db, email="updated@example.com")
# Soft delete another user # Soft delete another user
deleted_user = db.query(User).filter(User.username == "to_delete").first() deleted_user = db.query(User).filter(User.username == "to_delete").first()
if deleted_user: if deleted_user:
deleted_user.update(db, deleted=True) deleted_user.update(db, deleted=True)
# Commit all changes at once # Commit all changes at once
db.commit() db.commit()
except Exception: except Exception:
# Rollback all changes if any operation fails # Rollback all changes if any operation fails
db.rollback() db.rollback()
@ -141,19 +125,12 @@ def example_serialization():
user = db.query(User).first() user = db.query(User).first()
if user: if user:
# Get all fields except specified ones # Get all fields except specified ones
dict_with_excludes = user.get_dict( dict_with_excludes = user.get_dict(exclude=["created_at", "updated_at"])
exclude=["created_at", "updated_at"]
)
# Get only specified fields # Get only specified fields
dict_with_includes = user.get_dict( dict_with_includes = user.get_dict(include=["id", "username", "email"])
include=["id", "username", "email"]
) return {"excluded": dict_with_excludes, "included": dict_with_includes}
return {
"excluded": dict_with_excludes,
"included": dict_with_includes
}
def example_confirmation(): def example_confirmation():
@ -176,17 +153,17 @@ def example_error_handling():
user = User.find_or_create( user = User.find_or_create(
db, db,
username="existing_user", # This might cause unique constraint violation username="existing_user", # This might cause unique constraint violation
email="exists@example.com" email="exists@example.com",
) )
db.commit() db.commit()
return {"status": "success", "user": user.get_dict()} return {"status": "success", "user": user.get_dict()}
except Exception as e: except Exception as e:
db.rollback() db.rollback()
return { return {
"status": "error", "status": "error",
"message": str(e), "message": str(e),
"error_type": e.__class__.__name__ "error_type": e.__class__.__name__,
} }
@ -199,18 +176,15 @@ def example_date_handling():
db.query(User) db.query(User)
.filter( .filter(
User.expiry_starts <= datetime.utcnow(), User.expiry_starts <= datetime.utcnow(),
User.expiry_ends > datetime.utcnow() User.expiry_ends > datetime.utcnow(),
) )
.all() .all()
) )
# Set expiry for a user # Set expiry for a user
user = db.query(User).first() user = db.query(User).first()
if user: if user:
user.update( user.update(db, expiry_ends=datetime(2024, 12, 31, 23, 59, 59))
db,
expiry_ends=datetime(2024, 12, 31, 23, 59, 59)
)
db.commit() db.commit()
return current_users return current_users

View File

@ -25,25 +25,23 @@ from sqlalchemy import (
Boolean, Boolean,
SmallInteger, SmallInteger,
) )
from sqlalchemy.orm import ( from sqlalchemy.orm import Mapped, mapped_column, Session
Mapped,
mapped_column,
Session
)
from sqlalchemy_mixins.serialize import SerializeMixin from sqlalchemy_mixins.serialize import SerializeMixin
from sqlalchemy_mixins.repr import ReprMixin from sqlalchemy_mixins.repr import ReprMixin
from sqlalchemy_mixins.smartquery import SmartQueryMixin from sqlalchemy_mixins.smartquery import SmartQueryMixin
from api_library import DateTimeLocal, system_arrow from ApiLibrary import DateTimeLocal, system_arrow
from Services.Postgres.database import Base, get_db from Services.Postgres.database import Base
from Services.PostgresDb.Models.filter_functions import FilterAttributes
# Type variable for class methods returning self # Type variable for class methods returning self
T = TypeVar('T', bound='CrudMixin') T = TypeVar("T", bound="CrudMixin")
class CrudMixin(Base, SmartQueryMixin, SerializeMixin): class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
""" """
Base mixin providing CRUD operations and common fields for PostgreSQL models. Base mixin providing CRUD operations and common fields for PostgreSQL models.
Features: Features:
- Automatic timestamps (created_at, updated_at) - Automatic timestamps (created_at, updated_at)
- Soft delete capability - Soft delete capability
@ -51,30 +49,54 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
- Data serialization - Data serialization
- Multi-language support - Multi-language support
""" """
__abstract__ = True __abstract__ = True
# System fields that should be handled automatically during creation # System fields that should be handled automatically during creation
__system__fields__create__ = ( __system__fields__create__ = (
"created_at", "updated_at", "cryp_uu_id", "created_at",
"created_by", "created_by_id", "updated_by", "updated_by_id", "updated_at",
"replication_id", "confirmed_by", "confirmed_by_id", "cryp_uu_id",
"is_confirmed", "deleted", "active", "created_by",
"is_notification_send", "is_email_send", "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 that should be handled automatically during updates
__system__fields__update__ = ( __system__fields__update__ = (
"cryp_uu_id", "created_at", "updated_at", "cryp_uu_id",
"created_by", "created_by_id", "confirmed_by", "confirmed_by_id", "created_at",
"updated_by", "updated_by_id", "replication_id", "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 # Default fields to exclude from serialization
__system_default_model__ = [ __system_default_model__ = [
"cryp_uu_id", "is_confirmed", "deleted", "cryp_uu_id",
"is_notification_send", "replication_id", "is_email_send", "is_confirmed",
"confirmed_by_id", "confirmed_by", "updated_by_id", "created_by_id", "deleted",
"is_notification_send",
"replication_id",
"is_email_send",
"confirmed_by_id",
"confirmed_by",
"updated_by_id",
"created_by_id",
] ]
# User credentials and preferences # User credentials and preferences
@ -89,20 +111,20 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
TIMESTAMP(timezone=True), TIMESTAMP(timezone=True),
server_default=func.now(), server_default=func.now(),
nullable=False, nullable=False,
comment="Record validity start timestamp" comment="Record validity start timestamp",
) )
expiry_ends: Mapped[TIMESTAMP] = mapped_column( expiry_ends: Mapped[TIMESTAMP] = mapped_column(
TIMESTAMP(timezone=True), TIMESTAMP(timezone=True),
default="2099-12-31", default="2099-12-31",
server_default="2099-12-31", server_default="2099-12-31",
comment="Record validity end timestamp" comment="Record validity end timestamp",
) )
@classmethod @classmethod
def set_user_define_properties(cls, token: Any) -> None: def set_user_define_properties(cls, token: Any) -> None:
""" """
Set user-specific properties from the authentication token. Set user-specific properties from the authentication token.
Args: Args:
token: Authentication token containing user preferences token: Authentication token containing user preferences
""" """
@ -114,10 +136,10 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
def remove_non_related_inputs(cls, kwargs: Dict[str, Any]) -> Dict[str, Any]: def remove_non_related_inputs(cls, kwargs: Dict[str, Any]) -> Dict[str, Any]:
""" """
Filter out inputs that don't correspond to model fields. Filter out inputs that don't correspond to model fields.
Args: Args:
kwargs: Dictionary of field names and values kwargs: Dictionary of field names and values
Returns: Returns:
Dictionary containing only valid model fields Dictionary containing only valid model fields
""" """
@ -128,21 +150,22 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
} }
@classmethod @classmethod
def extract_system_fields(cls, filter_kwargs: dict, create: bool = True) -> Dict[str, Any]: def extract_system_fields(
cls, filter_kwargs: dict, create: bool = True
) -> Dict[str, Any]:
""" """
Remove system-managed fields from input dictionary. Remove system-managed fields from input dictionary.
Args: Args:
filter_kwargs: Input dictionary of fields filter_kwargs: Input dictionary of fields
create: If True, use creation field list, else use update field list create: If True, use creation field list, else use update field list
Returns: Returns:
Dictionary with system fields removed Dictionary with system fields removed
""" """
system_fields = filter_kwargs.copy() system_fields = filter_kwargs.copy()
extract_fields = ( extract_fields = (
cls.__system__fields__create__ if create cls.__system__fields__create__ if create else cls.__system__fields__update__
else cls.__system__fields__update__
) )
for field in extract_fields: for field in extract_fields:
system_fields.pop(field, None) system_fields.pop(field, None)
@ -152,30 +175,30 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
def iterate_over_variables(cls, val: Any, key: str) -> tuple[bool, Optional[Any]]: 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. Process a field value based on its type and convert it to the appropriate format.
Args: Args:
val: Field value val: Field value
key: Field name key: Field name
Returns: Returns:
Tuple of (should_include, processed_value) Tuple of (should_include, processed_value)
""" """
key_ = cls.__annotations__.get(key, None) key_ = cls.__annotations__.get(key, None)
is_primary = key in cls.primary_keys is_primary = key in cls.primary_keys
row_attr = bool(getattr(getattr(cls, key), "foreign_keys", None)) row_attr = bool(getattr(getattr(cls, key), "foreign_keys", None))
# Skip primary keys and foreign keys # Skip primary keys and foreign keys
if is_primary or row_attr: if is_primary or row_attr:
return False, None return False, None
# Handle None values # Handle None values
if val is None: if val is None:
return True, None return True, None
# Special handling for UUID fields # Special handling for UUID fields
if str(key[-5:]).lower() == "uu_id": if str(key[-5:]).lower() == "uu_id":
return True, str(val) return True, str(val)
# Handle typed fields # Handle typed fields
if key_: if key_:
if key_ == Mapped[int]: if key_ == Mapped[int]:
@ -190,7 +213,7 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
) )
elif key_ == Mapped[str]: elif key_ == Mapped[str]:
return True, str(val) return True, str(val)
# Handle based on Python types # Handle based on Python types
else: else:
if isinstance(val, datetime.datetime): if isinstance(val, datetime.datetime):
@ -207,35 +230,35 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
return True, str(val) return True, str(val)
elif val is None: elif val is None:
return True, None return True, None
return False, None return False, None
@classmethod @classmethod
def find_or_create(cls: Type[T], db: Session, **kwargs) -> T: def find_or_create(cls: Type[T], db: Session, **kwargs) -> T:
""" """
Find an existing record matching the criteria or create a new one. Find an existing record matching the criteria or create a new one.
Args: Args:
db: Database session db: Database session
**kwargs: Search/creation criteria **kwargs: Search/creation criteria
Returns: Returns:
Existing or newly created record Existing or newly created record
""" """
check_kwargs = cls.extract_system_fields(kwargs) check_kwargs = cls.extract_system_fields(kwargs)
# Search for existing record # Search for existing record
query = db.query(cls).filter( query = db.query(cls).filter(
cls.expiry_ends > str(system_arrow.now()), cls.expiry_ends > str(system_arrow.now()),
cls.expiry_starts <= str(system_arrow.now()), cls.expiry_starts <= str(system_arrow.now()),
) )
for key, value in check_kwargs.items(): for key, value in check_kwargs.items():
if hasattr(cls, key): if hasattr(cls, key):
query = query.filter(getattr(cls, key) == value) query = query.filter(getattr(cls, key) == value)
already_record = query.first() already_record = query.first()
# Handle existing record # Handle existing record
if already_record: if already_record:
if already_record.deleted: if already_record.deleted:
@ -252,7 +275,7 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
"message": "", "message": "",
} }
return already_record return already_record
already_record.meta_data = { already_record.meta_data = {
"created": False, "created": False,
"error_case": "AlreadyExists", "error_case": "AlreadyExists",
@ -263,75 +286,81 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
# Create new record # Create new record
check_kwargs = cls.remove_non_related_inputs(check_kwargs) check_kwargs = cls.remove_non_related_inputs(check_kwargs)
created_record = cls() created_record = cls()
for key, value in check_kwargs.items(): for key, value in check_kwargs.items():
setattr(created_record, key, value) setattr(created_record, key, value)
if getattr(cls.creds, "person_id", None) and getattr(cls.creds, "person_name", None): 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_id = cls.creds.person_id
created_record.created_by = cls.creds.person_name created_record.created_by = cls.creds.person_name
db.add(created_record) db.add(created_record)
db.flush() db.flush()
created_record.meta_data = {"created": True, "error_case": None, "message": ""} created_record.meta_data = {"created": True, "error_case": None, "message": ""}
return created_record return created_record
def update(self, db: Session, **kwargs) -> 'CrudMixin': def update(self, db: Session, **kwargs) -> "CrudMixin":
""" """
Update the record with new values. Update the record with new values.
Args: Args:
db: Database session db: Database session
**kwargs: Fields to update **kwargs: Fields to update
Returns: Returns:
Updated record Updated record
Raises: Raises:
ValueError: If attempting to update is_confirmed with other fields ValueError: If attempting to update is_confirmed with other fields
""" """
check_kwargs = self.remove_non_related_inputs(kwargs) check_kwargs = self.remove_non_related_inputs(kwargs)
is_confirmed_argument = kwargs.get("is_confirmed", None) is_confirmed_argument = kwargs.get("is_confirmed", None)
if is_confirmed_argument and not len(kwargs) == 1: if is_confirmed_argument and not len(kwargs) == 1:
raise ValueError("Confirm field cannot be updated with other fields") raise ValueError("Confirm field cannot be updated with other fields")
check_kwargs = self.extract_system_fields(check_kwargs, create=False) check_kwargs = self.extract_system_fields(check_kwargs, create=False)
for key, value in check_kwargs.items(): for key, value in check_kwargs.items():
setattr(self, key, value) setattr(self, key, value)
# Update confirmation or modification tracking # Update confirmation or modification tracking
if is_confirmed_argument: if is_confirmed_argument:
if getattr(self.creds, "person_id", None) and getattr(self.creds, "person_name", None): 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_id = self.creds.person_id
self.confirmed_by = self.creds.person_name self.confirmed_by = self.creds.person_name
else: else:
if getattr(self.creds, "person_id", None) and getattr(self.creds, "person_name", None): 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_id = self.creds.person_id
self.updated_by = self.creds.person_name self.updated_by = self.creds.person_name
db.flush() db.flush()
return self return self
def get_dict( def get_dict(
self, self,
exclude: Optional[List[str]] = None, exclude: Optional[List[str]] = None,
include: Optional[List[str]] = None, include: Optional[List[str]] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Convert model instance to dictionary with customizable fields. Convert model instance to dictionary with customizable fields.
Args: Args:
exclude: List of fields to exclude exclude: List of fields to exclude
include: List of fields to include (takes precedence over exclude) include: List of fields to include (takes precedence over exclude)
Returns: Returns:
Dictionary representation of the model Dictionary representation of the model
""" """
return_dict: Dict[str, Any] = {} return_dict: Dict[str, Any] = {}
if include: if include:
# Handle explicitly included fields # Handle explicitly included fields
exclude_list = [ exclude_list = [
@ -341,22 +370,30 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
] ]
columns_include_list = list(set(include).difference(set(exclude_list))) columns_include_list = list(set(include).difference(set(exclude_list)))
columns_include_list.extend(["uu_id"]) columns_include_list.extend(["uu_id"])
for key in columns_include_list: for key in columns_include_list:
val = getattr(self, key) val = getattr(self, key)
correct, value_of_database = self.iterate_over_variables(val, key) correct, value_of_database = self.iterate_over_variables(val, key)
if correct: if correct:
return_dict[key] = value_of_database return_dict[key] = value_of_database
elif exclude: elif exclude:
# Handle explicitly excluded fields # Handle explicitly excluded fields
exclude.extend(list(set(getattr(self, '__exclude__fields__', []) or []).difference(exclude))) exclude.extend(
exclude.extend([ list(
element set(getattr(self, "__exclude__fields__", []) or []).difference(
for element in self.__system_default_model__ exclude
if str(element)[-2:] == "id" )
]) )
)
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 = list(set(self.columns).difference(set(exclude)))
columns_excluded_list.extend(["uu_id", "active"]) columns_excluded_list.extend(["uu_id", "active"])
@ -367,37 +404,41 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
return_dict[key] = value_of_database return_dict[key] = value_of_database
else: else:
# Handle default field selection # Handle default field selection
exclude_list = (getattr(self, '__exclude__fields__', []) or []) + self.__system_default_model__ exclude_list = (
getattr(self, "__exclude__fields__", []) or []
) + self.__system_default_model__
columns_list = list(set(self.columns).difference(set(exclude_list))) 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 = [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"]) columns_list.extend(
[col for col in self.columns if str(col)[-5:].lower() == "uu_id"]
)
for remove_field in self.__system_default_model__: for remove_field in self.__system_default_model__:
if remove_field in columns_list: if remove_field in columns_list:
columns_list.remove(remove_field) columns_list.remove(remove_field)
for key in columns_list: for key in columns_list:
val = getattr(self, key) val = getattr(self, key)
correct, value_of_database = self.iterate_over_variables(val, key) correct, value_of_database = self.iterate_over_variables(val, key)
if correct: if correct:
return_dict[key] = value_of_database return_dict[key] = value_of_database
return return_dict return return_dict
class BaseCollection(CrudMixin, ReprMixin): class BaseCollection(CrudMixin, ReprMixin):
"""Base model class with minimal fields.""" """Base model class with minimal fields."""
__abstract__ = True __abstract__ = True
__repr__ = ReprMixin.__repr__ __repr__ = ReprMixin.__repr__
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
class CrudCollection(CrudMixin, SmartQueryMixin): class CrudCollection(CrudMixin, SmartQueryMixin, FilterAttributes):
""" """
Full-featured model class with all common fields. Full-featured model class with all common fields.
Includes: Includes:
- UUID and reference ID - UUID and reference ID
- Timestamps - Timestamps
@ -406,7 +447,7 @@ class CrudCollection(CrudMixin, SmartQueryMixin):
- Soft delete - Soft delete
- Notification flags - Notification flags
""" """
__abstract__ = True __abstract__ = True
__repr__ = ReprMixin.__repr__ __repr__ = ReprMixin.__repr__
@ -417,22 +458,19 @@ class CrudCollection(CrudMixin, SmartQueryMixin):
server_default=text("gen_random_uuid()"), server_default=text("gen_random_uuid()"),
index=True, index=True,
unique=True, unique=True,
comment="Unique identifier UUID" comment="Unique identifier UUID",
) )
ref_id: Mapped[str] = mapped_column( ref_id: Mapped[str] = mapped_column(
String(100), String(100), nullable=True, index=True, comment="External reference ID"
nullable=True,
index=True,
comment="External reference ID"
) )
# Timestamps # Timestamps
created_at: Mapped[TIMESTAMP] = mapped_column( created_at: Mapped[TIMESTAMP] = mapped_column(
TIMESTAMP(timezone=True), TIMESTAMP(timezone=True),
server_default=func.now(), server_default=func.now(),
nullable=False, nullable=False,
index=True, index=True,
comment="Record creation timestamp" comment="Record creation timestamp",
) )
updated_at: Mapped[TIMESTAMP] = mapped_column( updated_at: Mapped[TIMESTAMP] = mapped_column(
TIMESTAMP(timezone=True), TIMESTAMP(timezone=True),
@ -440,95 +478,70 @@ class CrudCollection(CrudMixin, SmartQueryMixin):
onupdate=func.now(), onupdate=func.now(),
nullable=False, nullable=False,
index=True, index=True,
comment="Last update timestamp" comment="Last update timestamp",
) )
# Cryptographic and user tracking # Cryptographic and user tracking
cryp_uu_id: Mapped[str] = mapped_column( cryp_uu_id: Mapped[str] = mapped_column(
String, String, nullable=True, index=True, comment="Cryptographic UUID"
nullable=True,
index=True,
comment="Cryptographic UUID"
) )
created_by: Mapped[str] = mapped_column( created_by: Mapped[str] = mapped_column(
String, String, nullable=True, comment="Creator name"
nullable=True,
comment="Creator name"
) )
created_by_id: Mapped[int] = mapped_column( created_by_id: Mapped[int] = mapped_column(
Integer, Integer, nullable=True, comment="Creator ID"
nullable=True,
comment="Creator ID"
) )
updated_by: Mapped[str] = mapped_column( updated_by: Mapped[str] = mapped_column(
String, String, nullable=True, comment="Last modifier name"
nullable=True,
comment="Last modifier name"
) )
updated_by_id: Mapped[int] = mapped_column( updated_by_id: Mapped[int] = mapped_column(
Integer, Integer, nullable=True, comment="Last modifier ID"
nullable=True,
comment="Last modifier ID"
) )
confirmed_by: Mapped[str] = mapped_column( confirmed_by: Mapped[str] = mapped_column(
String, String, nullable=True, comment="Confirmer name"
nullable=True,
comment="Confirmer name"
) )
confirmed_by_id: Mapped[int] = mapped_column( confirmed_by_id: Mapped[int] = mapped_column(
Integer, Integer, nullable=True, comment="Confirmer ID"
nullable=True,
comment="Confirmer ID"
) )
# Status flags # Status flags
is_confirmed: Mapped[bool] = mapped_column( is_confirmed: Mapped[bool] = mapped_column(
Boolean, Boolean, server_default="0", comment="Record confirmation status"
server_default="0",
comment="Record confirmation status"
) )
replication_id: Mapped[int] = mapped_column( replication_id: Mapped[int] = mapped_column(
SmallInteger, SmallInteger, server_default="0", comment="Replication identifier"
server_default="0",
comment="Replication identifier"
) )
deleted: Mapped[bool] = mapped_column( deleted: Mapped[bool] = mapped_column(
Boolean, Boolean, server_default="0", comment="Soft delete flag"
server_default="0",
comment="Soft delete flag"
) )
active: Mapped[bool] = mapped_column( active: Mapped[bool] = mapped_column(
Boolean, Boolean, server_default="1", comment="Record active status"
server_default="1",
comment="Record active status"
) )
is_notification_send: Mapped[bool] = mapped_column( is_notification_send: Mapped[bool] = mapped_column(
Boolean, Boolean, server_default="0", comment="Notification sent flag"
server_default="0",
comment="Notification sent flag"
) )
is_email_send: Mapped[bool] = mapped_column( is_email_send: Mapped[bool] = mapped_column(
Boolean, Boolean, server_default="0", comment="Email sent flag"
server_default="0",
comment="Email sent flag"
) )
@classmethod @classmethod
def retrieve_language_model(cls, lang: str, response_model: Any) -> Dict[str, str]: def retrieve_language_model(cls, lang: str, response_model: Any) -> Dict[str, str]:
""" """
Retrieve language-specific model headers and validation messages. Retrieve language-specific model headers and validation messages.
Args: Args:
lang: Language code lang: Language code
response_model: Model containing language annotations response_model: Model containing language annotations
Returns: Returns:
Dictionary of field names to localized headers Dictionary of field names to localized headers
""" """
headers_and_validation = {} headers_and_validation = {}
__language_model__ = getattr(cls.__language_model__, lang, "tr") __language_model__ = getattr(cls.__language_model__, lang, "tr")
for field in response_model.__annotations__.keys(): for field in response_model.__annotations__.keys():
headers_and_validation[field] = getattr(__language_model__, field, "Lang Not found") headers_and_validation[field] = getattr(
__language_model__, field, "Lang Not found"
)
return headers_and_validation return headers_and_validation

View File

@ -3,7 +3,8 @@ from typing import Any, List, Optional, TypeVar, Union
from sqlalchemy.orm import Query from sqlalchemy.orm import Query
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
T = TypeVar('T') T = TypeVar("T")
class QueryResponse: class QueryResponse:
"""Handler for SQLAlchemy query results with error handling.""" """Handler for SQLAlchemy query results with error handling."""

View File

@ -0,0 +1,81 @@
"""
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 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_item(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

@ -22,6 +22,7 @@ SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
Base = declarative_base() Base = declarative_base()
@contextmanager @contextmanager
def get_db() -> Generator[Session, None, None]: def get_db() -> Generator[Session, None, None]:
"""Get database session with context management.""" """Get database session with context management."""