wag-managment-api-service-v.../ApiEvents/abstract_class.py

361 lines
12 KiB
Python

"""
Abstract base classes for API route and event handling.
This module provides core abstractions for route configuration and factory,
with support for authentication and event handling.
"""
from typing import (
Tuple,
TypeVar,
Optional,
Callable,
Dict,
Any,
List,
Type,
ClassVar,
Union,
Awaitable,
)
from dataclasses import dataclass, field
from pydantic import BaseModel
from fastapi import Request, Depends, APIRouter
from functools import wraps
import inspect
from ApiLibrary.common.line_number import get_line_number_for_error
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from Schemas.rules.rules import EndpointRestriction
ResponseModel = TypeVar("ResponseModel", bound=BaseModel)
def endpoint_wrapper(url_of_endpoint: Optional[str] = None):
"""Create a wrapper for endpoints that stores url_of_endpoint in closure.
Args:
url_of_endpoint: Optional URL path for the endpoint
"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
async def wrapper(
*args: Any, **kwargs: Any
) -> Union[Dict[str, Any], BaseModel]:
# Handle both async and sync functions
if inspect.iscoroutinefunction(func):
result = await func(*args, **kwargs)
else:
result = func(*args, **kwargs)
# If result is a coroutine, await it
if inspect.iscoroutine(result):
result = await result
# Add endpoint to the result
if isinstance(result, dict):
result["endpoint"] = url_of_endpoint
return result
elif isinstance(result, BaseModel):
# Convert Pydantic model to dict and add endpoint
result_dict = result.model_dump()
result_dict["endpoint"] = url_of_endpoint
return result_dict
return result
wrapper.url_of_endpoint = url_of_endpoint
return wrapper
return decorator
@dataclass
class EndpointFactoryConfig:
"""Configuration class for API endpoints.
Attributes:
url_of_endpoint: Full URL path for this endpoint
endpoint: URL path for this endpoint
method: HTTP method (GET, POST, etc.)
summary: Short description for API documentation
description: Detailed description for API documentation
endpoint_function: Function to handle the endpoint
is_auth_required: Whether authentication is required
response_model: Optional response model for OpenAPI schema
request_model: Optional request model for OpenAPI schema
is_event_required: Whether event handling is required
extra_options: Additional endpoint options
"""
url_prefix: str
url_endpoint: str
url_of_endpoint: str
endpoint: str
method: str
summary: str
description: str
endpoint_function: Callable[..., Any] # Now accepts any parameters and return type
response_model: Optional[type] = None
request_model: Optional[type] = None
is_auth_required: bool = True
is_event_required: bool = False
extra_options: Dict[str, Any] = field(default_factory=dict)
def __post_init__(self):
"""Post initialization hook.
Wraps endpoint function with appropriate middleware based on configuration:
- If auth and event required -> wrap with TokenEventMiddleware
- If only event required -> wrap with EventMiddleware
- If only auth required -> wrap with MiddlewareModule.auth_required
"""
# First apply auth/event middleware
if self.is_event_required:
from middleware import TokenEventMiddleware
self.endpoint_function = TokenEventMiddleware.event_required(
self.endpoint_function
)
elif self.is_auth_required:
from middleware import MiddlewareModule
self.endpoint_function = MiddlewareModule.auth_required(
self.endpoint_function
)
# Then wrap with endpoint_wrapper to store url_of_endpoint
self.endpoint_function = endpoint_wrapper(self.url_of_endpoint)(
self.endpoint_function
)
class RouteFactoryConfig:
"""Configuration class for API route factories.
Attributes:
name: Route name
tags: List of tags for API documentation
prefix: URL prefix for all endpoints in this route
include_in_schema: Whether to include in OpenAPI schema
endpoints: List of endpoint configurations
extra_options: Additional route options
"""
def __init__(
self,
name: str,
tags: List[str],
prefix: str,
include_in_schema: bool = True,
endpoints: List[EndpointFactoryConfig] = None,
extra_options: Dict[str, Any] = None,
):
self.name = name
self.tags = tags
self.prefix = prefix
self.include_in_schema = include_in_schema
self.endpoints = endpoints or []
self.extra_options = extra_options or {}
def __post_init__(self):
"""Validate and normalize configuration after initialization."""
if self.endpoints is None:
self.endpoints = []
if self.extra_options is None:
self.extra_options = {}
def as_dict(self) -> Dict[str, Any]:
"""Convert configuration to dictionary format."""
return {
"name": self.name,
"tags": self.tags,
"prefix": self.prefix,
"include_in_schema": self.include_in_schema,
"endpoints": [endpoint.__dict__ for endpoint in self.endpoints],
"extra_options": self.extra_options,
}
class ActionsSchema:
"""Base class for defining API action schemas.
This class handles endpoint registration and validation in the database.
Subclasses should implement specific validation logic.
"""
def __init__(self, endpoint: str):
"""Initialize with an API endpoint path.
Args:
endpoint: The API endpoint path (e.g. "/users/create")
"""
self.endpoint = endpoint
def retrieve_action_from_endpoint(self) -> Dict[str, Any]:
"""Retrieve the endpoint registration from the database.
Returns:
Dict containing the endpoint registration data
Raises:
HTTPException: If endpoint is not found in database
"""
raise NotImplementedError(
"Subclasses must implement retrieve_action_from_endpoint"
)
class ActionsSchemaFactory:
"""Factory class for creating and validating action schemas.
This class ensures proper initialization and validation of API endpoints
through their action schemas.
"""
def __init__(self, action: ActionsSchema):
"""Initialize with an action schema.
Args:
action: The action schema to initialize
Raises:
HTTPException: If action initialization fails
"""
self.action = action
self.action_match = self.action.retrieve_action_from_endpoint()
class MethodToEvent:
"""Base class for mapping methods to API events with type safety.
This class provides a framework for handling API events with proper
type checking for tokens and response models.
Type Parameters:
TokenType: Type of authentication token
ResponseModel: Type of response model
"""
action_key: ClassVar[Optional[str]] = None
event_type: ClassVar[Optional[str]] = None
event_description: ClassVar[str] = ""
event_category: ClassVar[str] = ""
__event_keys__: ClassVar[Dict[str, str]] = {}
__event_validation__: Dict[str, Tuple[Type, Union[List, tuple]]] = {}
@classmethod
def retrieve_event_response_model(cls, function_code: str) -> Tuple:
"""Retrieve event validation for a specific function.
Args:
function_code: Function identifier
Returns:
Tuple containing response model and language models
"""
event_validation_list = cls.__event_validation__.get(function_code, None)
if not event_validation_list:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg="Function not found",
)
return event_validation_list[0]
@classmethod
def retrieve_event_languages(cls, function_code: str) -> Union[List, tuple]:
"""Retrieve event description for a specific function.
Args:
function_code: Function identifier
Returns:
Event description
"""
event_keys_list = cls.__event_validation__.get(function_code, None)
print('event_keys_list', event_keys_list)
if not event_keys_list:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg="Function not found",
)
function_language_models: Union[List, tuple] = event_keys_list[1]
if not function_language_models:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg="Function not found",
)
return function_language_models
@staticmethod
def merge_models(language_model: List) -> Tuple:
merged_models = {"tr": {}, "en": {}}
for model in language_model:
for lang in dict(model).keys():
if lang not in merged_models:
merged_models[lang] = model[lang]
else:
merged_models[lang].update(model[lang])
return merged_models
@classmethod
def retrieve_event_function(cls, function_code: str) -> Dict[str, str]:
"""Retrieve event parameters for a specific function.
Args:
function_code: Function identifier
Returns:
Dictionary of event parameters
"""
function_event = cls.__event_keys__[function_code]
function_itself = getattr(cls, function_event, None)
if not function_itself:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg="Function not found",
)
return function_itself
@classmethod
def retrieve_language_parameters(cls, function_code: str, language: str = "tr") -> Dict[str, str]:
"""Retrieve language-specific parameters for an event.
Args:
language: Language code (e.g. 'tr', 'en')
function_code: Function identifier
Returns:
Dictionary of language-specific field mappings
"""
event_language_models = cls.retrieve_event_languages(function_code)
event_response_model = cls.retrieve_event_response_model(function_code)
event_response_model_merged = cls.merge_models(event_language_models)
event_response_model_merged_lang = event_response_model_merged[language]
# Map response model fields to language-specific values
print('event_response_model', dict(
event_response_model=event_response_model,
event_response_model_merged_lang=event_response_model_merged_lang,
event_response_model_merged=event_response_model_merged,
language=language,
function_code=function_code,
))
only_language_dict = {
field: event_response_model_merged_lang[field]
for field in event_response_model.model_fields
if field in event_response_model_merged_lang
}
return {
"language_model": only_language_dict,
"language_models": event_response_model_merged,
}