test file added & mongo tested

This commit is contained in:
2025-01-14 17:01:33 +03:00
parent 5a23d41eef
commit 08b1815156
36 changed files with 2497 additions and 506 deletions

View File

@@ -12,161 +12,92 @@ through response models.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Type, Union, TypeVar, Protocol
from typing import Any, Dict, List, Optional, Type, TypeVar, Protocol, Generic
from dataclasses import dataclass
from fastapi import status
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Query
from ApiValidations.Request.base_validations import PydanticBaseModel
from ApiValidations.handler import BaseModelRegular
from Services.PostgresDb.Models.response import PostgresResponse
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
from Services.pagination import Pagination, PaginationConfig
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."""
...
DataT = TypeVar("DataT")
@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.
class ResponseConfig(Generic[T]):
"""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
message: Response message to include in the response
completed: Operation completion status flag
cls_object: Class object for error handling context
response_model: Optional response model class for data transformation
"""
status_code: str = "HTTP_200_OK"
message: str = ""
completed: bool = True
cls_object: Optional[Any] = None
response_model: Optional[Type[T]] = None
class BaseJsonResponse:
"""
Base class for JSON response handling.
class ResponseProtocol(Protocol):
"""Protocol defining required methods for response models."""
Provides common functionality for all response types:
- Response formatting
- Pagination handling
- Data transformation
def dump(self) -> Dict[str, Any]:
"""Convert model to dictionary format."""
...
class BaseJsonResponse(Generic[T]):
"""Base class for JSON response handling.
Provides common functionality for all response types including:
- Response formatting with consistent structure
- Pagination handling and configuration
- Data transformation through response models
"""
def __init__(
self,
config: ResponseConfig,
message: str,
result: Any,
response_model: Optional[Type[T]] = None,
status_code: str = "HTTP_200_OK",
completed: bool = True,
cls_object: Optional[Any] = None,
filter_attributes: Optional[Any] = None,
):
self.status_code = getattr(status, config.status_code, status.HTTP_200_OK)
self.message = config.message
self.completed = config.completed
) -> None:
"""Initialize response handler.
Args:
message: Response message
result: Query result or data
response_model: Optional model for data transformation
status_code: HTTP status code
completed: Operation completion status
cls_object: Class object for error context
filter_attributes: Optional pagination and filtering attributes
"""
self.status_code = getattr(status, status_code, status.HTTP_200_OK)
self.message = message
self.completed = completed
self.filter_attributes = filter_attributes
self.response_model = response_model
self.cls_object = config.cls_object
self.cls_object = cls_object
self.result = result
def _create_pagination(self) -> Pagination:
"""Create and configure pagination instance."""
"""Create and configure pagination instance.
Returns:
Configured Pagination instance
"""
pagination = Pagination()
if self.filter_attributes:
pagination.change(
@@ -180,7 +111,15 @@ class BaseJsonResponse:
return pagination
def _format_response(self, pagination: Pagination, data: Any) -> JSONResponse:
"""Format final JSON response with pagination."""
"""Format final JSON response with pagination.
Args:
pagination: Pagination instance with configuration
data: Response data to include
Returns:
Formatted JSONResponse
"""
return JSONResponse(
status_code=self.status_code,
content={
@@ -191,9 +130,31 @@ class BaseJsonResponse:
},
)
def _transform_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Transform data using response model if provided.
Args:
data: Raw data dictionary
Returns:
Transformed data dictionary
"""
if self.response_model:
return self.response_model(**data).dump()
return data
@staticmethod
def _validate_data(data: Any, expected_type: Type, cls_object: Any) -> None:
"""Validate data type and raise exception if invalid."""
"""Validate data type and raise exception if invalid.
Args:
data: Data to validate
expected_type: Expected type of data
cls_object: Class object for error context
Raises:
HTTPExceptionApi: If data type is invalid
"""
if not isinstance(data, expected_type):
raise HTTPExceptionApi(
lang=cls_object.lang,
@@ -201,58 +162,8 @@ class BaseJsonResponse:
)
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.
"""
class SinglePostgresResponse(BaseJsonResponse[T]):
"""Handler for single record responses from PostgreSQL queries."""
def __new__(
cls,
@@ -264,6 +175,78 @@ class AlchemyJsonResponse(BaseJsonResponse):
cls_object: Optional[Any] = None,
filter_attributes: Optional[Any] = None,
) -> JSONResponse:
"""Create response for single PostgreSQL record.
Args:
message: Response message
result: PostgreSQL query result
response_model: Optional model for data transformation
status_code: HTTP status code
completed: Operation completion status
cls_object: Class object for error context
filter_attributes: Optional pagination and filtering attributes
Returns:
Formatted JSON response
Raises:
HTTPExceptionApi: If result is invalid or empty
"""
cls._validate_data(result, PostgresResponse, cls_object)
if not result.first:
raise HTTPExceptionApi(
lang=cls_object.lang,
error_code="HTTP_400_BAD_REQUEST",
)
instance = super().__new__(cls)
instance.__init__(
message=message,
result=result,
response_model=response_model,
status_code=status_code,
completed=completed,
cls_object=cls_object,
filter_attributes=filter_attributes,
)
pagination = instance._create_pagination()
data = instance._transform_data(result.data.get_dict())
return instance._format_response(pagination, data)
class AlchemyJsonResponse(BaseJsonResponse[T]):
"""Handler for multiple record responses from SQLAlchemy queries."""
def __new__(
cls,
message: str,
result: PostgresResponse,
response_model: Optional[Type[T]] = None,
status_code: str = "HTTP_200_OK",
completed: bool = True,
cls_object: Optional[Any] = None,
filter_attributes: Optional[Any] = None,
) -> JSONResponse:
"""Create response for multiple SQLAlchemy records.
Args:
message: Response message
result: PostgreSQL query result
response_model: Optional model for data transformation
status_code: HTTP status code
completed: Operation completion status
cls_object: Class object for error context
filter_attributes: Optional pagination and filtering attributes
Returns:
Formatted JSON response
Raises:
HTTPExceptionApi: If result is invalid
"""
cls._validate_data(result, PostgresResponse, cls_object)
if result.first:
@@ -272,40 +255,26 @@ class AlchemyJsonResponse(BaseJsonResponse):
error_code="HTTP_400_BAD_REQUEST",
)
instance = cls()
instance = super().__new__(cls)
instance.__init__(
ResponseConfig(
status_code=status_code,
message=message,
completed=completed,
cls_object=cls_object,
),
message=message,
result=result,
response_model=response_model,
status_code=status_code,
completed=completed,
cls_object=cls_object,
filter_attributes=filter_attributes,
)
pagination = instance._create_pagination()
data = [
(
response_model(**item.get_dict()).dump()
if response_model
else item.get_dict()
)
for item in result.data
]
data = [instance._transform_data(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.
"""
class ListJsonResponse(BaseJsonResponse[T]):
"""Handler for list data responses."""
def __new__(
cls,
@@ -317,37 +286,42 @@ class ListJsonResponse(BaseJsonResponse):
cls_object: Optional[Any] = None,
filter_attributes: Optional[Any] = None,
) -> JSONResponse:
"""Create response for list data.
Args:
message: Response message
result: List of data items
response_model: Optional model for data transformation
status_code: HTTP status code
completed: Operation completion status
cls_object: Class object for error context
filter_attributes: Optional pagination and filtering attributes
Returns:
Formatted JSON response
"""
cls._validate_data(result, list, cls_object)
instance = cls()
instance = super().__new__(cls)
instance.__init__(
ResponseConfig(
status_code=status_code,
message=message,
completed=completed,
cls_object=cls_object,
),
message=message,
result=result,
response_model=response_model,
status_code=status_code,
completed=completed,
cls_object=cls_object,
filter_attributes=filter_attributes,
)
pagination = instance._create_pagination()
data = [
response_model(**item).dump() if response_model else item for item in result
]
data = [instance._transform_data(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.
"""
class DictJsonResponse(BaseJsonResponse[T]):
"""Handler for dictionary data responses."""
def __new__(
cls,
@@ -359,21 +333,34 @@ class DictJsonResponse(BaseJsonResponse):
cls_object: Optional[Any] = None,
filter_attributes: Optional[Any] = None,
) -> JSONResponse:
"""Create response for dictionary data.
Args:
message: Response message
result: Dictionary data
response_model: Optional model for data transformation
status_code: HTTP status code
completed: Operation completion status
cls_object: Class object for error context
filter_attributes: Optional pagination and filtering attributes
Returns:
Formatted JSON response
"""
cls._validate_data(result, dict, cls_object)
instance = cls()
instance = super().__new__(cls)
instance.__init__(
ResponseConfig(
status_code=status_code,
message=message,
completed=completed,
cls_object=cls_object,
),
message=message,
result=result,
response_model=response_model,
status_code=status_code,
completed=completed,
cls_object=cls_object,
filter_attributes=filter_attributes,
)
pagination = instance._create_pagination()
data = response_model(**result).dump() if response_model else result
data = instance._transform_data(result)
return instance._format_response(pagination, data)

View File

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

View File

@@ -500,27 +500,27 @@ class FilterAttributes:
return PostgresResponse(query=query, first=True)
@classmethod
def raise_http_exception(
cls,
status_code: str,
error_case: str,
data: Dict[str, Any],
message: str,
) -> None:
"""
Raise HTTP exception with formatted error details.
# @classmethod
# def 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
# 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",
)
# Raises:
# HTTPException: With formatted error details
# """
# raise HTTPExceptionApi(
# error_code="HTTP_304_NOT_MODIFIED",
# lang=cls.lang or "tr",
# )

View File

@@ -1,190 +0,0 @@
"""
How to Use PostgreSQL Models
This module provides examples of how to use the base models and database sessions
effectively in your application.
"""
from datetime import datetime
from typing import Optional, List
from sqlalchemy import String, Integer
from sqlalchemy.orm import Mapped, mapped_column
from Services.PostgresDb import CrudCollection
from Services.PostgresDb.database import get_db
# Example Model Definition
class User(CrudCollection):
"""Example user model demonstrating CrudCollection usage."""
__tablename__ = "users"
# Additional fields (id and other common fields come from CrudCollection)
username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
email: Mapped[str] = mapped_column(String(100), unique=True)
age: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
# Example Usage
def example_create():
"""Example of creating a new record."""
with get_db() as db:
# Create a new user
user = User.find_or_create(
db, username="john_doe", email="john@example.com", age=30
)
db.commit()
return user
def example_batch_create():
"""Example of creating multiple records in a single transaction."""
with get_db() as db:
try:
# Create multiple users in one transaction
users = []
for i in range(3):
user = User.find_or_create(
db, username=f"user_{i}", email=f"user_{i}@example.com"
)
users.append(user)
db.commit()
return users
except Exception:
db.rollback()
raise
def example_update():
"""Example of updating a record."""
with get_db() as db:
# Find user and update
user = db.query(User).filter(User.username == "john_doe").first()
if user:
user.update(db, email="john.doe@newdomain.com", age=31)
db.commit()
return user
def example_soft_delete():
"""Example of soft deleting a record."""
with get_db() as db:
user = db.query(User).filter(User.username == "john_doe").first()
if user:
# This will set deleted=True instead of actually deleting the record
user.update(db, deleted=True)
db.commit()
def example_query():
"""Example of querying records."""
with get_db() as db:
# Get active (non-deleted) users
active_users = (
db.query(User)
.filter(User.active == True, User.deleted == False, User.age >= 18)
.order_by(User.created_at.desc())
.all()
)
return active_users
def example_complex_transaction():
"""Example of a complex transaction with multiple operations."""
with get_db() as db:
try:
# Multiple operations in single transaction
user = User.find_or_create(db, username="new_user", email="new@example.com")
# Update existing user
other_user = db.query(User).filter(User.username == "old_user").first()
if other_user:
other_user.update(db, email="updated@example.com")
# Soft delete another user
deleted_user = db.query(User).filter(User.username == "to_delete").first()
if deleted_user:
deleted_user.update(db, deleted=True)
# Commit all changes at once
db.commit()
except Exception:
# Rollback all changes if any operation fails
db.rollback()
raise
def example_serialization():
"""Example of serializing records to dictionaries."""
with get_db() as db:
user = db.query(User).first()
if user:
# Get all fields except specified ones
dict_with_excludes = user.get_dict(exclude=["created_at", "updated_at"])
# Get only specified fields
dict_with_includes = user.get_dict(include=["id", "username", "email"])
return {"excluded": dict_with_excludes, "included": dict_with_includes}
def example_confirmation():
"""Example of confirming a record."""
with get_db() as db:
user = db.query(User).filter(User.username == "pending_user").first()
if user:
# Only update confirmation status
user.update(db, is_confirmed=True)
db.commit()
return user
# Example of error handling
def example_error_handling():
"""Example of proper error handling."""
with get_db() as db:
try:
# Attempt to create user
user = User.find_or_create(
db,
username="existing_user", # This might cause unique constraint violation
email="exists@example.com",
)
db.commit()
return {"status": "success", "user": user.get_dict()}
except Exception as e:
db.rollback()
return {
"status": "error",
"message": str(e),
"error_type": e.__class__.__name__,
}
# Example of working with dates
def example_date_handling():
"""Example of working with dates and expiry."""
with get_db() as db:
# Find records valid at current time
current_users = (
db.query(User)
.filter(
User.expiry_starts <= datetime.utcnow(),
User.expiry_ends > datetime.utcnow(),
)
.all()
)
# Set expiry for a user
user = db.query(User).first()
if user:
user.update(db, expiry_ends=datetime(2024, 12, 31, 23, 59, 59))
db.commit()
return current_users

View File

@@ -31,14 +31,16 @@ from sqlalchemy_mixins.repr import ReprMixin
from sqlalchemy_mixins.smartquery import SmartQueryMixin
from ApiLibrary import DateTimeLocal, system_arrow
from Services.PostgresDb.database import Base
from Services.PostgresDb.Models.base_model import BaseModel
from Services.PostgresDb.Models.filter_functions import FilterAttributes
# Type variable for class methods returning self
T = TypeVar("T", bound="CrudMixin")
class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
class CrudMixin(
BaseModel, SmartQueryMixin, SerializeMixin, ReprMixin, FilterAttributes
):
"""
Base mixin providing CRUD operations and common fields for PostgreSQL models.
@@ -426,7 +428,7 @@ class CrudMixin(Base, SmartQueryMixin, SerializeMixin):
return return_dict
class BaseCollection(CrudMixin, ReprMixin):
class BaseCollection(CrudMixin):
"""Base model class with minimal fields."""
__abstract__ = True
@@ -435,7 +437,7 @@ class BaseCollection(CrudMixin, ReprMixin):
id: Mapped[int] = mapped_column(primary_key=True)
class CrudCollection(CrudMixin, SmartQueryMixin, FilterAttributes):
class CrudCollection(CrudMixin):
"""
Full-featured model class with all common fields.

View File

@@ -70,7 +70,7 @@ class PostgresResponse(Generic[T]):
)
@property
def first_item(self) -> Optional[T]:
def first(self) -> Optional[T]:
"""Get first result only."""
return self.data if self._first else (self.data[0] if self.data else None)

View File

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

View File