Service Postgres added
This commit is contained in:
parent
3bc0146767
commit
5d8b37179d
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from ApiLibrary.date_time_actions.date_functions import DateTimeLocal, system_arrow
|
||||||
|
|
||||||
|
__all__ = ["DateTimeLocal", "system_arrow"]
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
from .base_validations import (
|
||||||
|
BaseModelRegular,
|
||||||
|
PydanticBaseModel,
|
||||||
|
ListOptions,
|
||||||
|
CrudRecords,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"BaseModelRegular",
|
||||||
|
"PydanticBaseModel",
|
||||||
|
"ListOptions",
|
||||||
|
"CrudRecords",
|
||||||
|
]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from ErrorHandlers.ApiErrorHandlers.api_exc_handler import (
|
||||||
|
HTTPExceptionApiHandler,
|
||||||
|
HTTPExceptionApi,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"HTTPExceptionApiHandler",
|
||||||
|
"HTTPExceptionApi",
|
||||||
|
]
|
||||||
|
|
@ -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},
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
from ErrorHandlers.LanguageModels.base_languages import BaseErrorLanguageModels
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["BaseErrorLanguageModels"]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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",
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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."""
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from Services.PostgresDb.Models.mixins import CrudCollection, BaseCollection
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CrudCollection",
|
||||||
|
"BaseCollection",
|
||||||
|
]
|
||||||
|
|
@ -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."""
|
||||||
Loading…
Reference in New Issue