event decarotor checked & event 2 endpoint dynmc create is tested

This commit is contained in:
berkay 2025-01-15 23:40:55 +03:00
parent 76d286b519
commit 049a7c1e11
12 changed files with 369 additions and 173 deletions

View File

@ -1,4 +1,9 @@
import typing import typing
from collections.abc import Callable
from fastapi import Request
from typing import Dict, Any
from ApiValidations.Custom.token_objects import ( from ApiValidations.Custom.token_objects import (
OccupantTokenObject, OccupantTokenObject,
EmployeeTokenObject, EmployeeTokenObject,
@ -17,12 +22,14 @@ from ApiValidations.Request import (
ListOptions, ListOptions,
) )
from Services.PostgresDb.Models.alchemy_response import AlchemyJsonResponse from Services.PostgresDb.Models.alchemy_response import AlchemyJsonResponse
from ApiEvents.abstract_class import ( from ApiValidations.Response import AccountRecordResponse
from events.abstract_class import (
MethodToEvent, MethodToEvent,
RouteFactoryConfig, RouteFactoryConfig,
EndpointFactoryConfig, EndpointFactoryConfig,
) )
from ApiValidations.Response import AccountRecordResponse
# from events.utils import with_token_event
class AccountRecordsListEventMethods(MethodToEvent): class AccountRecordsListEventMethods(MethodToEvent):
@ -355,3 +362,108 @@ class AccountRecordsPatchEventMethods(MethodToEvent):
message="Account record patched successfully", message="Account record patched successfully",
result=account_record, result=account_record,
) )
def address_list(request: Request, data: dict) -> Dict[str, Any]:
"""Handle address list endpoint."""
# Access context through the handler
handler = address_list.handler
handler_context = address_list.handler.context
function_name = AccountRecordsListEventMethods.__event_keys__.get(handler.function_code)
original_function = getattr(AccountRecordsListEventMethods, function_name)
# original_function(data, request)
return {
"data": data,
"function_code": handler.function_code, # This will be the URL
"token_dict": handler_context.get('token_dict'),
"url_of_endpoint": handler_context.get('url_of_endpoint'),
"request": str(request.headers),
}
def address_create(request: Request, data: dict):
"""Handle address creation endpoint."""
return {
"data": data,
"request": str(request.headers),
"request_url": str(request.url),
"request_base_url": str(request.base_url),
}
def address_search(request: Request, data: dict):
"""Handle address search endpoint."""
# Get function_code from the wrapper's closure
function_code = address_search.function_code
return {
"data": data,
"function_code": function_code
}
def address_update(request: Request, address_uu_id: str, data: dict):
"""Handle address update endpoint."""
# Get function_code from the wrapper's closure
function_code = address_update.function_code
return {
"address_uu_id": address_uu_id,
"data": data,
"function_code": function_code
}
# Account Records Router Configuration
ACCOUNT_RECORDS_CONFIG = RouteFactoryConfig(
name='account_records',
prefix='/account/records',
tags=['Account Records'],
include_in_schema=True,
endpoints=[
EndpointFactoryConfig(
url_prefix = "/account/records",
url_endpoint="/address/list",
url_of_endpoint = "/account/records/address/list",
endpoint="/address/list",
method="POST",
summary="List Active/Delete/Confirm Address",
description="List Active/Delete/Confirm Address",
is_auth_required=True,
is_event_required=True,
endpoint_function=address_list
),
EndpointFactoryConfig(
url_prefix = "/account/records",
url_endpoint="/address/create",
url_of_endpoint = "/account/records/address/create",
endpoint="/address/create",
method="POST",
summary="Create Address with given auth levels",
description="Create Address with given auth levels",
is_auth_required=False,
is_event_required=False,
endpoint_function=address_create
),
EndpointFactoryConfig(
url_prefix = "/account/records",
url_endpoint="/address/search",
url_of_endpoint = "/account/records/address/search",
endpoint="/address/search",
method="POST",
summary="Search Address with given auth levels",
description="Search Address with given auth levels",
is_auth_required=True,
is_event_required=True,
endpoint_function=address_search
),
EndpointFactoryConfig(
url_prefix = "/account/records",
url_endpoint="/address/update/{address_uu_id}",
url_of_endpoint="/account/records/address/update/{address_uu_id}",
endpoint="/address/update/{address_uu_id}",
method="PUT",
summary="Update Address with given auth levels",
description="Update Address with given auth levels",
is_auth_required=True,
is_event_required=True,
endpoint_function=address_update
)
]
).as_dict()

View File

@ -5,93 +5,13 @@ This module collects and registers all route configurations from different modul
to be used by the dynamic route creation system. to be used by the dynamic route creation system.
""" """
from typing import Dict, List, Any, Callable from typing import Dict, List, Any
from fastapi import Request from events.account.account_records import ACCOUNT_RECORDS_CONFIG
from ApiEvents.abstract_class import RouteFactoryConfig, EndpointFactoryConfig
from ApiEvents.EventServiceApi.utils import with_token_event
from ApiEvents.EventServiceApi.account.account_records import (
AccountRecordsListEventMethods,
ListOptions,
InsertAccountRecord,
SearchAddress,
UpdateAccountRecord,
)
@with_token_event
def address_list(request: Request, list_options: ListOptions):
"""Handle address list endpoint."""
pass
@with_token_event
def address_create(request: Request, data: InsertAccountRecord):
"""Handle address creation endpoint."""
pass
@with_token_event
def address_search(request: Request, data: SearchAddress):
"""Handle address search endpoint."""
pass
@with_token_event
def address_update(request: Request, address_uu_id: str, data: UpdateAccountRecord):
"""Handle address update endpoint."""
pass
# Account Records Router Configuration
ACCOUNT_RECORDS_CONFIG = {
'name': 'account_records',
'prefix': '/account/records',
'tags': ['Account Records'],
'include_in_schema': True,
'endpoints': [
EndpointFactoryConfig(
endpoint="/list",
method="POST",
summary="List Active/Delete/Confirm Address",
description="List Active/Delete/Confirm Address",
is_auth_required=True,
is_event_required=True,
request_model=ListOptions,
endpoint_function=address_list
),
EndpointFactoryConfig(
endpoint="/create",
method="POST",
summary="Create Address with given auth levels",
description="Create Address with given auth levels",
is_auth_required=True,
is_event_required=True,
request_model=InsertAccountRecord,
endpoint_function=address_create
),
EndpointFactoryConfig(
endpoint="/search",
method="POST",
summary="Search Address with given auth levels",
description="Search Address with given auth levels",
is_auth_required=True,
is_event_required=True,
request_model=SearchAddress,
endpoint_function=address_search
),
EndpointFactoryConfig(
endpoint="/update/{address_uu_id}",
method="POST",
summary="Update Address with given auth levels",
description="Update Address with given auth levels",
is_auth_required=True,
is_event_required=True,
request_model=UpdateAccountRecord,
endpoint_function=address_update
)
]
}
# Registry of all route configurations # Registry of all route configurations
ROUTE_CONFIGS = [ ROUTE_CONFIGS = [
ACCOUNT_RECORDS_CONFIG, ACCOUNT_RECORDS_CONFIG,
# Add other route configurations here
] ]
def get_route_configs() -> List[Dict[str, Any]]: def get_route_configs() -> List[Dict[str, Any]]:

View File

@ -1,51 +0,0 @@
"""
Utility functions for API event handling.
"""
from functools import wraps
from inspect import signature
from typing import Callable, TypeVar, ParamSpec, Any
from fastapi import Request
from Services.PostgresDb.Models.token_models import parse_token_object_to_dict
P = ParamSpec('P')
R = TypeVar('R')
def with_token_event(func: Callable[P, R]) -> Callable[P, R]:
"""
Decorator that handles token parsing and event execution.
This decorator:
1. Parses the token from the request
2. Calls the appropriate event with the token and other arguments
Args:
func: The endpoint function to wrap
Returns:
Wrapped function that handles token parsing and event execution
"""
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
# Extract request from args or kwargs
request = next(
(arg for arg in args if isinstance(arg, Request)),
kwargs.get('request')
)
if not request:
raise ValueError("Request object not found in arguments")
# Parse token
token_dict = parse_token_object_to_dict(request=request)
# Add token_dict to kwargs
kwargs['token_dict'] = token_dict
# Call the original function
return token_dict.available_event(**{
k: v for k, v in kwargs.items()
if k in signature(token_dict.available_event).parameters
})
return wrapper

View File

@ -5,10 +5,11 @@ This module provides core abstractions for route configuration and factory,
with support for authentication and event handling. with support for authentication and event handling.
""" """
from typing import Optional, Dict, Any, List, Type, Union, ClassVar, Tuple, TypeVar from typing import Optional, Dict, Any, List, Type, Union, ClassVar, Tuple, TypeVar, Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pydantic import BaseModel from pydantic import BaseModel
ResponseModel = TypeVar('ResponseModel', bound=BaseModel) ResponseModel = TypeVar('ResponseModel', bound=BaseModel)
@ -17,25 +18,44 @@ class EndpointFactoryConfig:
"""Configuration class for API endpoints. """Configuration class for API endpoints.
Attributes: Attributes:
url_of_endpoint: Full URL path for this endpoint
endpoint: URL path for this endpoint endpoint: URL path for this endpoint
method: HTTP method (GET, POST, etc.) method: HTTP method (GET, POST, etc.)
summary: Short description for API documentation summary: Short description for API documentation
description: Detailed description for API documentation description: Detailed description for API documentation
endpoint_function: Function to handle the endpoint
is_auth_required: Whether authentication is required is_auth_required: Whether authentication is required
is_event_required: Whether event handling is required is_event_required: Whether event handling is required
request_model: Expected request model type
extra_options: Additional endpoint options extra_options: Additional endpoint options
response_model: Expected response model type
""" """
url_prefix :str
url_endpoint: str
url_of_endpoint: str
endpoint: str endpoint: str
method: str method: str
summary: str summary: str
description: str description: str
is_auth_required: bool endpoint_function: Callable
is_event_required: bool is_auth_required: bool = True
request_model: Type[BaseModel] is_event_required: bool = False
extra_options: Dict[str, Any] = field(default_factory=dict) extra_options: Dict[str, Any] = field(default_factory=dict)
response_model: Optional[Type[BaseModel]] = None
def __post_init__(self):
"""Post-initialization processing.
Apply appropriate wrappers based on auth and event requirements:
- If both auth and event required -> wrap with with_token_event
- If only auth required -> wrap with MiddlewareModule.auth_required
"""
# Store url_of_endpoint for the handler
self.endpoint_function.url_of_endpoint = self.url_of_endpoint
if self.is_auth_required and self.is_event_required:
from events.utils import with_token_event
self.endpoint_function = with_token_event(self.endpoint_function)
elif self.is_auth_required:
from DockerApiServices.AllApiNeeds.middleware.auth_middleware import MiddlewareModule
self.endpoint_function = MiddlewareModule.auth_required(self.endpoint_function)
@dataclass @dataclass
@ -75,10 +95,9 @@ class RouteFactoryConfig:
"method": ep.method, "method": ep.method,
"summary": ep.summary, "summary": ep.summary,
"description": ep.description, "description": ep.description,
"endpoint_function": ep.endpoint_function,
"is_auth_required": ep.is_auth_required, "is_auth_required": ep.is_auth_required,
"is_event_required": ep.is_event_required, "is_event_required": ep.is_event_required,
"response_model": ep.response_model.__name__ if ep.response_model else None,
"request_model": ep.request_model.__name__,
"extra_options": ep.extra_options "extra_options": ep.extra_options
} }
for ep in self.endpoints for ep in self.endpoints

104
ApiEvents/utils.py Normal file
View File

@ -0,0 +1,104 @@
"""
Utility functions for API event handling.
"""
from typing import TypeVar, Callable, Dict, Any
from functools import wraps
from fastapi import Request
R = TypeVar('R')
class BaseEndpointHandler:
"""Base class for handling endpoint execution with context."""
def __init__(self, func: Callable, url_of_endpoint: str):
self.func = func
self.url_of_endpoint = url_of_endpoint
self.function_code = url_of_endpoint # Set initial function_code
self._context = {
'url_of_endpoint': url_of_endpoint,
'function_code': url_of_endpoint, # Initialize with URL
}
@property
def context(self) -> dict:
"""Get the endpoint context."""
return self._context
def update_context(self, **kwargs):
"""Update the endpoint context with new values."""
self._context.update(kwargs)
# Update function_code property if it's in the context
if 'function_code' in kwargs:
self.function_code = kwargs['function_code']
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
class TokenEventHandler(BaseEndpointHandler):
"""Handler for endpoints that require token and event tracking."""
def __init__(self, func: Callable, url_of_endpoint: str):
super().__init__(func, url_of_endpoint)
self.update_context(
token_dict={
'user_id': '1234567890',
'username': 'test_user',
'email': 'asda@email.com',
}
)
class AuthHandler(BaseEndpointHandler):
"""Handler for endpoints that require only authentication."""
def __init__(self, func: Callable, url_of_endpoint: str):
super().__init__(func, url_of_endpoint)
self.update_context(
auth_level="user",
permissions=["read", "write"]
)
def with_token_event(func: Callable[..., Dict[str, Any]]) -> Callable[..., Dict[str, Any]]:
"""Decorator for endpoints with token and event requirements."""
@wraps(func)
def wrapper(*args, **kwargs) -> Dict[str, Any]:
# Create handler with context
handler = TokenEventHandler(
func=func,
url_of_endpoint=func.url_of_endpoint
)
# Update event-specific context
handler.update_context(
function_code=f"7192c2aa-5352-4e36-98b3-dafb7d036a3d" # Keep function_code as URL
)
# Make context available to the function
func.handler = handler
# Call the original function
return func(*args, **kwargs)
return wrapper
def auth_required(func: Callable[..., Dict[str, Any]]) -> Callable[..., Dict[str, Any]]:
"""Decorator for endpoints with only auth requirements."""
@wraps(func)
def wrapper(*args, **kwargs) -> Dict[str, Any]:
# Create handler with context
handler = AuthHandler(
func=func,
url_of_endpoint=func.url_of_endpoint
)
# Make context available to the function
func.handler = handler
# Call the original function
return func(*args, **kwargs)
return wrapper

View File

@ -0,0 +1,78 @@
from typing import Union
from AllConfigs.Token.config import Auth
from ErrorHandlers import HTTPExceptionApi
from Services.Redis import RedisActions, AccessToken
from fastapi import Request
from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject
class TokenService:
@classmethod
def raise_error_if_request_has_no_token(cls, request: Request) -> None:
"""Get access token from request headers."""
if not hasattr(request, "headers"):
raise HTTPExceptionApi(
error_code="",
lang="en",
)
if not request.headers.get(Auth.ACCESS_TOKEN_TAG):
raise HTTPExceptionApi(
error_code="",
lang="en",
)
@classmethod
def get_access_token_from_request(cls, request: Request) -> str:
"""Get access token from request headers."""
cls.raise_error_if_request_has_no_token(request=request)
return request.headers.get(Auth.ACCESS_TOKEN_TAG)
@classmethod
def get_object_via_access_key(cls, access_token: str) -> Union[EmployeeTokenObject, OccupantTokenObject]:
"""Get access token from request headers."""
access_token = AccessToken(
accessToken=access_token,
userUUID="",
)
if redis_object := RedisActions.get_json(
list_keys=access_token.to_list()
).first.data:
access_token.userUUID = redis_object.get("user_uu_id")
if redis_object.get("user_type") == 1:
if not redis_object.get("selected_company", None):
redis_object["selected_company"] = None
return EmployeeTokenObject(**redis_object)
elif redis_object.get("user_type") == 2:
if not redis_object.get("selected_occupant", None):
redis_object["selected_occupant"] = None
return OccupantTokenObject(**redis_object)
raise HTTPExceptionApi(
error_code="",
lang="en",
)
@classmethod
def get_object_via_user_uu_id(cls, user_id: str) -> Union[EmployeeTokenObject, OccupantTokenObject]:
"""Get access token from user uuid."""
access_token = AccessToken(
accessToken="",
userUUID=user_id,
)
if redis_object := RedisActions.get_json(
list_keys=access_token.to_list()
).first.data:
access_token.userUUID = redis_object.get("user_uu_id")
if redis_object.get("user_type") == 1:
if not redis_object.get("selected_company", None):
redis_object["selected_company"] = None
return EmployeeTokenObject(**redis_object)
elif redis_object.get("user_type") == 2:
if not redis_object.get("selected_occupant", None):
redis_object["selected_occupant"] = None
return OccupantTokenObject(**redis_object)
raise HTTPExceptionApi(
error_code="",
lang="en",
)

5
ApiServices/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from ApiServices.Token.token_handler import TokenService
__all__ = [
'TokenService',
]

View File

@ -15,7 +15,7 @@ from fastapi.routing import APIRoute
from middleware.auth_middleware import MiddlewareModule from middleware.auth_middleware import MiddlewareModule
from pydantic import BaseModel from pydantic import BaseModel
from AllConfigs.main import MainConfig as Config from AllConfigs.main import MainConfig as Config
from ApiEvents.EventServiceApi.route_configs import get_route_configs
@dataclass @dataclass
@ -99,21 +99,35 @@ def get_all_routers() -> tuple[List[APIRouter], Dict[str, List[str]]]:
Returns: Returns:
tuple: (routers, protected_routes) tuple: (routers, protected_routes)
""" """
from events.route_configs import get_route_configs
routers = [] routers = []
all_protected_routes = {} all_protected_routes = {}
# Get route configurations from the registry # Get route configurations from the registry
route_configs = get_route_configs() route_configs = get_route_configs()
factory_all = []
for config in route_configs: for config in route_configs:
factory = EnhancedEndpointFactory(config) factory = EnhancedEndpointFactory(config)
# Create endpoints from configuration # Create endpoints from configuration
for endpoint_config in config['endpoints']: for endpoint_dict in config['endpoints']:
endpoint_config = EndpointFactoryConfig(
endpoint=endpoint_dict['endpoint'],
method=endpoint_dict['method'],
summary=endpoint_dict['summary'],
description=endpoint_dict['description'],
endpoint_function=endpoint_dict['endpoint_function'],
is_auth_required=endpoint_dict['is_auth_required'],
is_event_required=endpoint_dict['is_event_required'],
extra_options=endpoint_dict.get('extra_options', {})
)
factory.create_endpoint(endpoint_config) factory.create_endpoint(endpoint_config)
factory_all.append(
endpoint_config.__dict__
)
# Add router and protected routes # Add router and protected routes
routers.append(factory.get_router()) routers.append(factory.get_router())
all_protected_routes.update(factory.get_protected_routes()) all_protected_routes.update(factory.get_protected_routes())
return routers, all_protected_routes return routers, all_protected_routes

View File

@ -95,25 +95,15 @@ class MiddlewareModule:
""" """
@wraps(func) @wraps(func)
async def wrapper(request: Request, *args, **kwargs): def wrapper(request: Request, *args, **kwargs):
try: from ApiServices import TokenService
# Get token from header # Get token from header
_, token = cls.get_access_token(request) # token = TokenService.get_access_token_from_request(request=request)
# print(token)
# Validate token and get user data # if not token:
token_data = await cls.validate_token(token) # raise HTTPExceptionApi(error_code="NOT_AUTHORIZED", lang="tr")
# Add user data to request state for use in endpoint
request.state.user = token_data
# Call the original endpoint function # Call the original endpoint function
return await func(request, *args, **kwargs) return func(request, *args, **kwargs)
except HTTPExceptionApi:
raise HTTPExceptionApi(error_code="NOT_AUTHORIZED", lang="tr")
except Exception as e:
raise HTTPExceptionApi(error_code="NOT_AUTHORIZED", lang="tr")
return wrapper return wrapper

View File

@ -20,12 +20,20 @@ RUN poetry config virtualenvs.create false \
# Copy application code # Copy application code
COPY DockerApiServices/AllApiNeeds /app/ COPY DockerApiServices/AllApiNeeds /app/
COPY ErrorHandlers /app/ErrorHandlers
COPY LanguageModels /app/LanguageModels
COPY ApiLibrary /app/ApiLibrary COPY ApiLibrary /app/ApiLibrary
COPY ApiValidations /app/ApiValidations COPY ApiValidations /app/ApiValidations
COPY AllConfigs /app/AllConfigs COPY AllConfigs /app/AllConfigs
COPY ErrorHandlers /app/ErrorHandlers COPY ErrorHandlers /app/ErrorHandlers
COPY Schemas /app/Schemas COPY Schemas /app/Schemas
COPY Services /app/Services COPY Services /app/Services
COPY ApiServices /app/ApiServices
# Copy Events structure with consistent naming
COPY ApiEvents/EventServiceApi /app/events
COPY ApiEvents/utils.py /app/events/utils.py
COPY ApiEvents/abstract_class.py /app/events/abstract_class.py
# Set Python path to include app directory # Set Python path to include app directory
ENV PYTHONPATH=/app \ ENV PYTHONPATH=/app \

View File

@ -17,18 +17,15 @@ class HTTPExceptionApiHandler:
@staticmethod @staticmethod
def retrieve_error_status_code(exc: HTTPExceptionApi) -> int: def retrieve_error_status_code(exc: HTTPExceptionApi) -> int:
from ErrorHandlers import DEFAULT_ERROR
error_by_codes = BaseErrorModelClass.retrieve_error_by_codes() error_by_codes = BaseErrorModelClass.retrieve_error_by_codes()
grab_status_code = error_by_codes.get( grab_status_code = error_by_codes.get(
str(exc.error_code).upper(), DEFAULT_ERROR str(exc.error_code).upper(), 500
) )
return int(grab_status_code) return int(grab_status_code)
@staticmethod @staticmethod
def retrieve_error_message(exc: HTTPExceptionApi, error_languages) -> str: def retrieve_error_message(exc: HTTPExceptionApi, error_languages) -> str:
from ErrorHandlers import DEFAULT_ERROR from ErrorHandlers import DEFAULT_ERROR
return error_languages.get(str(exc.error_code).upper(), DEFAULT_ERROR) return error_languages.get(str(exc.error_code).upper(), DEFAULT_ERROR)
async def handle_exception( async def handle_exception(

View File

@ -11,12 +11,12 @@ services:
context: . context: .
dockerfile: DockerApiServices/EventServiceApi/Dockerfile dockerfile: DockerApiServices/EventServiceApi/Dockerfile
ports: ports:
- "8001:8000" - "41576:41575"
validation-service: validation-service:
build: build:
context: . context: .
dockerfile: DockerApiServices/ValidationServiceApi/Dockerfile dockerfile: DockerApiServices/ValidationServiceApi/Dockerfile
ports: ports:
- "8002:8000" - "41577:41575"
# and lets try to implement potry again in the dockerfile now we now that it is about copy of files # and lets try to implement potry again in the dockerfile now we now that it is about copy of files