test file added & mongo tested
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
# )
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user