new api service and logic implemented

This commit is contained in:
2025-01-23 22:27:25 +03:00
parent d91ecda9df
commit 32022ca521
245 changed files with 28004 additions and 0 deletions

View 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

View 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
)

View 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

View 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)

View 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
)

View 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 {}},
)

View 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

View 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,
}