new api service and logic implemented
This commit is contained in:
115
Services/MongoDb/Models/action_models/base.py
Normal file
115
Services/MongoDb/Models/action_models/base.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Base models for MongoDB documents."""
|
||||
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from bson import ObjectId
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic.json_schema import JsonSchemaValue
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
|
||||
from ApiLibrary import system_arrow
|
||||
|
||||
|
||||
class PyObjectId(ObjectId):
|
||||
"""Custom type for handling MongoDB ObjectId in Pydantic models."""
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls,
|
||||
_source_type: Any,
|
||||
_handler: Any,
|
||||
) -> CoreSchema:
|
||||
"""Define the core schema for PyObjectId."""
|
||||
return core_schema.json_or_python_schema(
|
||||
json_schema=core_schema.str_schema(),
|
||||
python_schema=core_schema.union_schema(
|
||||
[
|
||||
core_schema.is_instance_schema(ObjectId),
|
||||
core_schema.chain_schema(
|
||||
[
|
||||
core_schema.str_schema(),
|
||||
core_schema.no_info_plain_validator_function(cls.validate),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
serialization=core_schema.plain_serializer_function_ser_schema(
|
||||
lambda x: str(x),
|
||||
return_schema=core_schema.str_schema(),
|
||||
when_used="json",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, value: Any) -> ObjectId:
|
||||
"""Validate and convert the value to ObjectId."""
|
||||
if not ObjectId.is_valid(value):
|
||||
raise ValueError("Invalid ObjectId")
|
||||
return ObjectId(value)
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(
|
||||
cls,
|
||||
_core_schema: CoreSchema,
|
||||
_handler: Any,
|
||||
) -> JsonSchemaValue:
|
||||
"""Define the JSON schema for PyObjectId."""
|
||||
return {"type": "string"}
|
||||
|
||||
|
||||
class MongoBaseModel(BaseModel):
|
||||
"""Base model for all MongoDB documents."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
json_encoders={ObjectId: str},
|
||||
populate_by_name=True,
|
||||
from_attributes=True,
|
||||
validate_assignment=True,
|
||||
extra="allow",
|
||||
)
|
||||
|
||||
# Optional _id field that will be ignored in create operations
|
||||
id: Optional[PyObjectId] = Field(None, alias="_id")
|
||||
|
||||
def get_extra(self, field_name: str, default: Any = None) -> Any:
|
||||
"""Safely get extra field value.
|
||||
|
||||
Args:
|
||||
field_name: Name of the extra field to retrieve
|
||||
default: Default value to return if field doesn't exist
|
||||
|
||||
Returns:
|
||||
Value of the extra field if it exists, otherwise the default value
|
||||
"""
|
||||
return getattr(self, field_name, default)
|
||||
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
"""Convert model to dictionary including all fields and extra fields.
|
||||
|
||||
Returns:
|
||||
Dict containing all model fields and extra fields with proper type conversion
|
||||
"""
|
||||
return self.model_dump(by_alias=True)
|
||||
|
||||
|
||||
class MongoDocument(MongoBaseModel):
|
||||
"""Base document model with timestamps."""
|
||||
|
||||
created_at: float = Field(default_factory=lambda: system_arrow.now().timestamp())
|
||||
updated_at: float = Field(default_factory=lambda: system_arrow.now().timestamp())
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def prevent_protected_fields(cls, data: Any) -> Any:
|
||||
"""Prevent user from setting protected fields like _id and timestamps."""
|
||||
if isinstance(data, dict):
|
||||
# Remove protected fields from input
|
||||
data.pop("_id", None)
|
||||
data.pop("created_at", None)
|
||||
data.pop("updated_at", None)
|
||||
|
||||
# Set timestamps
|
||||
data["created_at"] = system_arrow.now().timestamp()
|
||||
data["updated_at"] = system_arrow.now().timestamp()
|
||||
|
||||
return data
|
||||
76
Services/MongoDb/Models/action_models/domain.py
Normal file
76
Services/MongoDb/Models/action_models/domain.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
MongoDB Domain Models.
|
||||
|
||||
This module provides Pydantic models for domain management,
|
||||
including domain history and access details.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
||||
|
||||
from ApiLibrary import system_arrow
|
||||
from Services.MongoDb.Models.action_models.base import MongoBaseModel, MongoDocument
|
||||
|
||||
|
||||
class DomainData(MongoBaseModel):
|
||||
"""Model for domain data.
|
||||
|
||||
Attributes:
|
||||
user_uu_id: Unique identifier of the user
|
||||
main_domain: Primary domain
|
||||
other_domains_list: List of additional domains
|
||||
extra_data: Additional domain-related data
|
||||
"""
|
||||
|
||||
user_uu_id: str = Field(..., description="User's unique identifier")
|
||||
main_domain: str = Field(..., description="Primary domain")
|
||||
other_domains_list: List[str] = Field(
|
||||
default_factory=list, description="List of additional domains"
|
||||
)
|
||||
extra_data: Optional[Dict[str, Any]] = Field(
|
||||
default_factory=dict,
|
||||
alias="extraData",
|
||||
description="Additional domain-related data",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True, populate_by_name=True, validate_assignment=True
|
||||
)
|
||||
|
||||
|
||||
class DomainDocument(MongoDocument):
|
||||
"""Model for domain-related documents."""
|
||||
|
||||
data: DomainData = Field(..., description="Domain data")
|
||||
|
||||
def update_main_domain(self, new_domain: str) -> None:
|
||||
"""Update the main domain and move current to history.
|
||||
|
||||
Args:
|
||||
new_domain: New main domain to set
|
||||
"""
|
||||
if self.data.main_domain and self.data.main_domain != new_domain:
|
||||
if self.data.main_domain not in self.data.other_domains_list:
|
||||
self.data.other_domains_list.append(self.data.main_domain)
|
||||
self.data.main_domain = new_domain
|
||||
|
||||
|
||||
class DomainDocumentCreate(MongoDocument):
|
||||
"""Model for creating new domain documents."""
|
||||
|
||||
data: DomainData = Field(..., description="Initial domain data")
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True, populate_by_name=True, validate_assignment=True
|
||||
)
|
||||
|
||||
|
||||
class DomainDocumentUpdate(MongoDocument):
|
||||
"""Model for updating existing domain documents."""
|
||||
|
||||
data: DomainData = Field(..., description="Updated domain data")
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True, populate_by_name=True, validate_assignment=True
|
||||
)
|
||||
49
Services/MongoDb/Models/action_models/password.py
Normal file
49
Services/MongoDb/Models/action_models/password.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
MongoDB Password Models.
|
||||
|
||||
This module provides Pydantic models for password management,
|
||||
including password history and access details.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from pydantic import Field
|
||||
|
||||
from ApiLibrary import system_arrow
|
||||
from Services.MongoDb.Models.action_models.base import MongoBaseModel, MongoDocument
|
||||
|
||||
|
||||
class PasswordHistoryDetail(MongoBaseModel):
|
||||
"""Model for password history details."""
|
||||
|
||||
timestamp: datetime
|
||||
ip_address: Optional[str] = Field(None, alias="ipAddress")
|
||||
user_agent: Optional[str] = Field(None, alias="userAgent")
|
||||
location: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class PasswordHistoryData(MongoBaseModel):
|
||||
"""Model for password history data."""
|
||||
|
||||
password_history: List[str] = Field([], alias="passwordHistory")
|
||||
access_history_detail: Dict[str, PasswordHistoryDetail] = Field(
|
||||
default_factory=dict, alias="accessHistoryDetail"
|
||||
)
|
||||
|
||||
|
||||
class PasswordDocument(MongoDocument):
|
||||
"""Model for password-related documents."""
|
||||
|
||||
data: PasswordHistoryData
|
||||
|
||||
|
||||
class PasswordDocumentCreate(MongoBaseModel):
|
||||
"""Model for creating new password documents."""
|
||||
|
||||
data: PasswordHistoryData = Field(..., description="Initial password data")
|
||||
|
||||
|
||||
class PasswordDocumentUpdate(MongoBaseModel):
|
||||
"""Model for updating existing password documents."""
|
||||
|
||||
data: PasswordHistoryData
|
||||
128
Services/MongoDb/Models/actions.py
Normal file
128
Services/MongoDb/Models/actions.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
This module contains the MongoActions class, which provides methods for
|
||||
performing actions on the MongoDB database.
|
||||
Api Mongo functions in general retrieves 2 params which are
|
||||
companyUUID and Storage Reason
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
from pymongo import MongoClient
|
||||
from pymongo.collection import Collection
|
||||
|
||||
from Services.MongoDb.Models.mixins import (
|
||||
MongoUpdateMixin,
|
||||
MongoInsertMixin,
|
||||
MongoFindMixin,
|
||||
MongoDeleteMixin,
|
||||
MongoAggregateMixin,
|
||||
)
|
||||
from Services.MongoDb.Models.exceptions import (
|
||||
MongoDocumentNotFoundError,
|
||||
MongoDuplicateKeyError,
|
||||
MongoValidationError,
|
||||
MongoConnectionError,
|
||||
)
|
||||
|
||||
|
||||
class MongoActions(
|
||||
MongoUpdateMixin,
|
||||
MongoInsertMixin,
|
||||
MongoFindMixin,
|
||||
MongoDeleteMixin,
|
||||
MongoAggregateMixin,
|
||||
):
|
||||
"""Main MongoDB actions class that inherits all CRUD operation mixins.
|
||||
|
||||
This class provides a unified interface for all MongoDB operations while
|
||||
managing collections based on company UUID and storage reason.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, client: MongoClient, database: str, company_uuid: str, storage_reason: str
|
||||
):
|
||||
"""Initialize MongoDB actions with client and collection info.
|
||||
|
||||
Args:
|
||||
client: MongoDB client
|
||||
database: Database name to use
|
||||
company_uuid: Company UUID for collection naming
|
||||
storage_reason: Storage reason for collection naming
|
||||
"""
|
||||
self._client = client
|
||||
self._database = database
|
||||
self._company_uuid = company_uuid
|
||||
self._storage_reason = storage_reason
|
||||
self._collection = None
|
||||
self.use_collection(storage_reason)
|
||||
|
||||
def use_collection(self, storage_reason: str) -> None:
|
||||
"""Switch to a different collection.
|
||||
|
||||
Args:
|
||||
storage_reason: New storage reason for collection naming
|
||||
"""
|
||||
collection_name = f"{self._company_uuid}*{storage_reason}"
|
||||
self._collection = self._client[self._database][collection_name]
|
||||
|
||||
@property
|
||||
def collection(self) -> Collection:
|
||||
"""Get current MongoDB collection."""
|
||||
return self._collection
|
||||
|
||||
def insert_one(self, document: Dict[str, Any]):
|
||||
"""Insert a single document."""
|
||||
return super().insert_one(self.collection, document)
|
||||
|
||||
def insert_many(self, documents: List[Dict[str, Any]]):
|
||||
"""Insert multiple documents."""
|
||||
return super().insert_many(self.collection, documents)
|
||||
|
||||
def find_one(
|
||||
self, filter_query: Dict[str, Any], projection: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""Find a single document."""
|
||||
return super().find_one(self.collection, filter_query, projection)
|
||||
|
||||
def find_many(
|
||||
self,
|
||||
filter_query: Dict[str, Any],
|
||||
projection: Optional[Dict[str, Any]] = None,
|
||||
sort: Optional[List[tuple]] = None,
|
||||
limit: Optional[int] = None,
|
||||
skip: Optional[int] = None,
|
||||
):
|
||||
"""Find multiple documents."""
|
||||
return super().find_many(
|
||||
self.collection, filter_query, projection, sort, limit, skip
|
||||
)
|
||||
|
||||
def update_one(
|
||||
self,
|
||||
filter_query: Dict[str, Any],
|
||||
update_data: Dict[str, Any],
|
||||
upsert: bool = False,
|
||||
):
|
||||
"""Update a single document."""
|
||||
return super().update_one(self.collection, filter_query, update_data, upsert)
|
||||
|
||||
def update_many(
|
||||
self,
|
||||
filter_query: Dict[str, Any],
|
||||
update_data: Dict[str, Any],
|
||||
upsert: bool = False,
|
||||
):
|
||||
"""Update multiple documents."""
|
||||
return super().update_many(self.collection, filter_query, update_data, upsert)
|
||||
|
||||
def delete_one(self, filter_query: Dict[str, Any]):
|
||||
"""Delete a single document."""
|
||||
return super().delete_one(self.collection, filter_query)
|
||||
|
||||
def delete_many(self, filter_query: Dict[str, Any]):
|
||||
"""Delete multiple documents."""
|
||||
return super().delete_many(self.collection, filter_query)
|
||||
|
||||
def aggregate(self, pipeline: List[Dict[str, Any]]):
|
||||
"""Execute an aggregation pipeline."""
|
||||
return super().aggregate(self.collection, pipeline)
|
||||
188
Services/MongoDb/Models/exception_handlers.py
Normal file
188
Services/MongoDb/Models/exception_handlers.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
Exception handlers for MongoDB operations.
|
||||
|
||||
This module provides exception handlers for MongoDB-related errors,
|
||||
converting them to appropriate HTTP responses.
|
||||
"""
|
||||
|
||||
from typing import Callable, Any
|
||||
from fastapi import Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from pymongo.errors import PyMongoError, DuplicateKeyError, ConnectionFailure
|
||||
|
||||
from ApiLibrary.common.line_number import get_line_number_for_error
|
||||
from Services.MongoDb.Models.exceptions import (
|
||||
MongoBaseException,
|
||||
MongoConnectionError,
|
||||
MongoDocumentNotFoundError,
|
||||
MongoValidationError,
|
||||
MongoDuplicateKeyError,
|
||||
PasswordHistoryError,
|
||||
PasswordReuseError,
|
||||
PasswordHistoryLimitError,
|
||||
InvalidPasswordDetailError,
|
||||
)
|
||||
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
|
||||
|
||||
|
||||
def handle_mongo_errors(func: Callable) -> Callable:
|
||||
"""Decorator to handle MongoDB operation errors.
|
||||
|
||||
Args:
|
||||
func: Function to wrap with error handling
|
||||
|
||||
Returns:
|
||||
Wrapped function with error handling
|
||||
"""
|
||||
|
||||
async def wrapper(*args, **kwargs) -> Any:
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except ConnectionFailure as e:
|
||||
raise MongoConnectionError(
|
||||
message=str(e), details={"error_type": "connection_failure"}
|
||||
).to_http_exception()
|
||||
except DuplicateKeyError as e:
|
||||
raise MongoDuplicateKeyError(
|
||||
collection=e.details.get("namespace", "unknown"),
|
||||
key_pattern=e.details.get("keyPattern", {}),
|
||||
).to_http_exception()
|
||||
except PyMongoError as e:
|
||||
raise MongoBaseException(
|
||||
message=str(e), details={"error_type": "pymongo_error"}
|
||||
).to_http_exception()
|
||||
except Exception as e:
|
||||
raise HTTPExceptionApi(
|
||||
lang="en",
|
||||
error_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
loc=get_line_number_for_error(),
|
||||
sys_msg=str(e),
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
async def mongo_base_exception_handler(
|
||||
request: Request, exc: MongoBaseException
|
||||
) -> JSONResponse:
|
||||
"""Handle base MongoDB exceptions.
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: MongoDB base exception
|
||||
|
||||
Returns:
|
||||
JSON response with error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code, content={"error": exc.to_http_exception()}
|
||||
)
|
||||
|
||||
|
||||
async def mongo_connection_error_handler(
|
||||
request: Request, exc: MongoConnectionError
|
||||
) -> JSONResponse:
|
||||
"""Handle MongoDB connection errors.
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: MongoDB connection error
|
||||
|
||||
Returns:
|
||||
JSON response with connection error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
content={"error": exc.to_http_exception()},
|
||||
)
|
||||
|
||||
|
||||
async def mongo_document_not_found_handler(
|
||||
request: Request, exc: MongoDocumentNotFoundError
|
||||
) -> JSONResponse:
|
||||
"""Handle document not found errors.
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: Document not found error
|
||||
|
||||
Returns:
|
||||
JSON response with not found error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
content={"error": exc.to_http_exception()},
|
||||
)
|
||||
|
||||
|
||||
async def mongo_validation_error_handler(
|
||||
request: Request, exc: MongoValidationError
|
||||
) -> JSONResponse:
|
||||
"""Handle validation errors.
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: Validation error
|
||||
|
||||
Returns:
|
||||
JSON response with validation error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={"error": exc.to_http_exception()},
|
||||
)
|
||||
|
||||
|
||||
async def mongo_duplicate_key_error_handler(
|
||||
request: Request, exc: MongoDuplicateKeyError
|
||||
) -> JSONResponse:
|
||||
"""Handle duplicate key errors.
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: Duplicate key error
|
||||
|
||||
Returns:
|
||||
JSON response with duplicate key error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_409_CONFLICT, content={"error": exc.to_http_exception()}
|
||||
)
|
||||
|
||||
|
||||
async def password_history_error_handler(
|
||||
request: Request, exc: PasswordHistoryError
|
||||
) -> JSONResponse:
|
||||
"""Handle password history errors.
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: Password history error
|
||||
|
||||
Returns:
|
||||
JSON response with password history error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code, content={"error": exc.to_http_exception()}
|
||||
)
|
||||
|
||||
|
||||
def register_exception_handlers(app: Any) -> None:
|
||||
"""Register all MongoDB exception handlers with FastAPI app.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
"""
|
||||
app.add_exception_handler(MongoBaseException, mongo_base_exception_handler)
|
||||
app.add_exception_handler(MongoConnectionError, mongo_connection_error_handler)
|
||||
app.add_exception_handler(
|
||||
MongoDocumentNotFoundError, mongo_document_not_found_handler
|
||||
)
|
||||
app.add_exception_handler(MongoValidationError, mongo_validation_error_handler)
|
||||
app.add_exception_handler(MongoDuplicateKeyError, mongo_duplicate_key_error_handler)
|
||||
app.add_exception_handler(PasswordHistoryError, password_history_error_handler)
|
||||
app.add_exception_handler(PasswordReuseError, password_history_error_handler)
|
||||
app.add_exception_handler(PasswordHistoryLimitError, password_history_error_handler)
|
||||
app.add_exception_handler(
|
||||
InvalidPasswordDetailError, password_history_error_handler
|
||||
)
|
||||
146
Services/MongoDb/Models/exceptions.py
Normal file
146
Services/MongoDb/Models/exceptions.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Custom exceptions for MongoDB operations and password management.
|
||||
|
||||
This module defines custom exceptions for handling various error cases in MongoDB
|
||||
operations and password-related functionality.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from fastapi import HTTPException, status
|
||||
from ApiLibrary.common.line_number import get_line_number_for_error
|
||||
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
|
||||
|
||||
|
||||
class MongoBaseException(Exception):
|
||||
"""Base exception for MongoDB-related errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
self.details = details or {}
|
||||
super().__init__(self.message)
|
||||
|
||||
def to_http_exception(self) -> HTTPException:
|
||||
"""Convert to FastAPI HTTPException."""
|
||||
raise HTTPExceptionApi(
|
||||
lang="en",
|
||||
error_code=self.status_code,
|
||||
loc=get_line_number_for_error(),
|
||||
sys_msg=self.message,
|
||||
)
|
||||
|
||||
|
||||
class MongoConnectionError(MongoBaseException):
|
||||
"""Raised when there's an error connecting to MongoDB."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Failed to connect to MongoDB",
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
class MongoDocumentNotFoundError(MongoBaseException):
|
||||
"""Raised when a document is not found in MongoDB."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
collection: str,
|
||||
filter_query: Dict[str, Any],
|
||||
message: Optional[str] = None,
|
||||
):
|
||||
message = message or f"Document not found in collection '{collection}'"
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
details={"collection": collection, "filter": filter_query},
|
||||
)
|
||||
|
||||
|
||||
class MongoValidationError(MongoBaseException):
|
||||
"""Raised when document validation fails."""
|
||||
|
||||
def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None):
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
details={"field_errors": field_errors or {}},
|
||||
)
|
||||
|
||||
|
||||
class MongoDuplicateKeyError(MongoBaseException):
|
||||
"""Raised when trying to insert a document with a duplicate key."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
collection: str,
|
||||
key_pattern: Dict[str, Any],
|
||||
message: Optional[str] = None,
|
||||
):
|
||||
message = message or f"Duplicate key error in collection '{collection}'"
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
details={"collection": collection, "key_pattern": key_pattern},
|
||||
)
|
||||
|
||||
|
||||
class PasswordHistoryError(MongoBaseException):
|
||||
"""Base exception for password history-related errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
status_code: int = status.HTTP_400_BAD_REQUEST,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
super().__init__(message, status_code, details)
|
||||
|
||||
|
||||
class PasswordReuseError(PasswordHistoryError):
|
||||
"""Raised when attempting to reuse a recent password."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Password was used recently",
|
||||
history_limit: Optional[int] = None,
|
||||
):
|
||||
details = {"history_limit": history_limit} if history_limit else None
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
class PasswordHistoryLimitError(PasswordHistoryError):
|
||||
"""Raised when password history limit is reached."""
|
||||
|
||||
def __init__(self, limit: int, message: Optional[str] = None):
|
||||
message = message or f"Password history limit of {limit} reached"
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
details={"limit": limit},
|
||||
)
|
||||
|
||||
|
||||
class InvalidPasswordDetailError(PasswordHistoryError):
|
||||
"""Raised when password history detail is invalid."""
|
||||
|
||||
def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None):
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
details={"field_errors": field_errors or {}},
|
||||
)
|
||||
171
Services/MongoDb/Models/mixins.py
Normal file
171
Services/MongoDb/Models/mixins.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
MongoDB CRUD Operation Mixins.
|
||||
|
||||
This module provides mixins for common MongoDB operations:
|
||||
1. Document creation (insert)
|
||||
2. Document retrieval (find)
|
||||
3. Document updates
|
||||
4. Document deletion
|
||||
5. Aggregation operations
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from functools import wraps
|
||||
|
||||
from pymongo.collection import Collection
|
||||
from pymongo.errors import (
|
||||
ConnectionFailure,
|
||||
OperationFailure,
|
||||
ServerSelectionTimeoutError,
|
||||
PyMongoError,
|
||||
)
|
||||
|
||||
from ApiLibrary.common.line_number import get_line_number_for_error
|
||||
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
|
||||
|
||||
|
||||
def handle_mongo_errors(func):
|
||||
"""Decorator to handle MongoDB operation errors.
|
||||
|
||||
Catches MongoDB-specific errors and converts them to HTTPExceptionApi.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except ConnectionFailure:
|
||||
raise HTTPExceptionApi(
|
||||
error_code="HTTP_503_SERVICE_UNAVAILABLE",
|
||||
lang="en",
|
||||
loc=get_line_number_for_error(),
|
||||
sys_msg="MongoDB connection failed",
|
||||
)
|
||||
except ServerSelectionTimeoutError:
|
||||
raise HTTPExceptionApi(
|
||||
error_code="HTTP_504_GATEWAY_TIMEOUT",
|
||||
lang="en",
|
||||
loc=get_line_number_for_error(),
|
||||
sys_msg="MongoDB connection timed out",
|
||||
)
|
||||
except OperationFailure as e:
|
||||
raise HTTPExceptionApi(
|
||||
error_code="HTTP_400_BAD_REQUEST",
|
||||
lang="en",
|
||||
loc=get_line_number_for_error(),
|
||||
sys_msg=str(e),
|
||||
)
|
||||
except PyMongoError as e:
|
||||
raise HTTPExceptionApi(
|
||||
error_code="HTTP_500_INTERNAL_SERVER_ERROR",
|
||||
lang="en",
|
||||
loc=get_line_number_for_error(),
|
||||
sys_msg=str(e),
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class MongoInsertMixin:
|
||||
"""Mixin for MongoDB insert operations."""
|
||||
|
||||
@handle_mongo_errors
|
||||
def insert_one(self, collection: Collection, document: Dict[str, Any]):
|
||||
"""Insert a single document into the collection."""
|
||||
result = collection.insert_one(document)
|
||||
return result
|
||||
|
||||
@handle_mongo_errors
|
||||
def insert_many(self, collection: Collection, documents: List[Dict[str, Any]]):
|
||||
"""Insert multiple documents into the collection."""
|
||||
result = collection.insert_many(documents)
|
||||
return result
|
||||
|
||||
|
||||
class MongoFindMixin:
|
||||
"""Mixin for MongoDB find operations."""
|
||||
|
||||
@handle_mongo_errors
|
||||
def find_one(
|
||||
self,
|
||||
collection: Collection,
|
||||
filter_query: Dict[str, Any],
|
||||
projection: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""Find a single document in the collection."""
|
||||
result = collection.find_one(filter_query, projection)
|
||||
return result
|
||||
|
||||
@handle_mongo_errors
|
||||
def find_many(
|
||||
self,
|
||||
collection: Collection,
|
||||
filter_query: Dict[str, Any],
|
||||
projection: Optional[Dict[str, Any]] = None,
|
||||
sort: Optional[List[tuple]] = None,
|
||||
limit: Optional[int] = None,
|
||||
skip: Optional[int] = None,
|
||||
):
|
||||
"""Find multiple documents in the collection with pagination support."""
|
||||
cursor = collection.find(filter_query, projection)
|
||||
if sort:
|
||||
cursor = cursor.sort(sort)
|
||||
if skip:
|
||||
cursor = cursor.skip(skip)
|
||||
if limit:
|
||||
cursor = cursor.limit(limit)
|
||||
return list(cursor)
|
||||
|
||||
|
||||
class MongoUpdateMixin:
|
||||
"""Mixin for MongoDB update operations."""
|
||||
|
||||
@handle_mongo_errors
|
||||
def update_one(
|
||||
self,
|
||||
collection: Collection,
|
||||
filter_query: Dict[str, Any],
|
||||
update_data: Dict[str, Any],
|
||||
upsert: bool = False,
|
||||
):
|
||||
"""Update a single document in the collection."""
|
||||
result = collection.update_one(filter_query, update_data, upsert=upsert)
|
||||
return result
|
||||
|
||||
@handle_mongo_errors
|
||||
def update_many(
|
||||
self,
|
||||
collection: Collection,
|
||||
filter_query: Dict[str, Any],
|
||||
update_data: Dict[str, Any],
|
||||
upsert: bool = False,
|
||||
):
|
||||
"""Update multiple documents in the collection."""
|
||||
result = collection.update_many(filter_query, update_data, upsert=upsert)
|
||||
return result
|
||||
|
||||
|
||||
class MongoDeleteMixin:
|
||||
"""Mixin for MongoDB delete operations."""
|
||||
|
||||
@handle_mongo_errors
|
||||
def delete_one(self, collection: Collection, filter_query: Dict[str, Any]):
|
||||
"""Delete a single document from the collection."""
|
||||
result = collection.delete_one(filter_query)
|
||||
return result
|
||||
|
||||
@handle_mongo_errors
|
||||
def delete_many(self, collection: Collection, filter_query: Dict[str, Any]):
|
||||
"""Delete multiple documents from the collection."""
|
||||
result = collection.delete_many(filter_query)
|
||||
return result
|
||||
|
||||
|
||||
class MongoAggregateMixin:
|
||||
"""Mixin for MongoDB aggregation operations."""
|
||||
|
||||
@handle_mongo_errors
|
||||
def aggregate(self, collection: Collection, pipeline: List[Dict[str, Any]]):
|
||||
"""Execute an aggregation pipeline on the collection."""
|
||||
result = collection.aggregate(pipeline)
|
||||
return result
|
||||
85
Services/MongoDb/Models/response.py
Normal file
85
Services/MongoDb/Models/response.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Response handler for MongoDB query results.
|
||||
|
||||
This module provides a wrapper class for MongoDB query results,
|
||||
adding convenience methods for accessing data and managing query state.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, TypeVar, Generic, Union
|
||||
from pymongo.cursor import Cursor
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class MongoResponse(Generic[T]):
|
||||
"""
|
||||
Wrapper for MongoDB query results.
|
||||
|
||||
Attributes:
|
||||
cursor: MongoDB cursor object
|
||||
first: Whether to return first result only
|
||||
data: Query results (lazy loaded)
|
||||
count: Total count of results
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cursor: Optional[Cursor] = None,
|
||||
first: bool = False,
|
||||
status: bool = True,
|
||||
message: str = "",
|
||||
error: Optional[str] = None,
|
||||
data: Optional[Union[List[T], T]] = None,
|
||||
):
|
||||
self._cursor = cursor
|
||||
self._first = first
|
||||
self.status = status
|
||||
self.message = message
|
||||
self.error = error
|
||||
self._data: Optional[Union[List[T], T]] = data
|
||||
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 and self._cursor is not None:
|
||||
results = list(self._cursor)
|
||||
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:
|
||||
if self._cursor is not None:
|
||||
self._count = self._cursor.count()
|
||||
else:
|
||||
self._count = len(self.all)
|
||||
return self._count
|
||||
|
||||
@property
|
||||
def all(self) -> List[T]:
|
||||
"""Get all results as list."""
|
||||
return (
|
||||
self.data
|
||||
if isinstance(self.data, list)
|
||||
else [self.data] if self.data else []
|
||||
)
|
||||
|
||||
@property
|
||||
def first(self) -> Optional[T]:
|
||||
"""Get first result only."""
|
||||
return self.data if self._first else (self.data[0] if self.data else None)
|
||||
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
"""Convert response to dictionary format."""
|
||||
return {
|
||||
"status": self.status,
|
||||
"message": self.message,
|
||||
"data": self.data,
|
||||
"count": self.count,
|
||||
"error": self.error,
|
||||
}
|
||||
Reference in New Issue
Block a user