From 049a7c1e1126acf2987e75881bbe1a8283c144c2 Mon Sep 17 00:00:00 2001 From: berkay Date: Wed, 15 Jan 2025 23:40:55 +0300 Subject: [PATCH] event decarotor checked & event 2 endpoint dynmc create is tested --- .../account/account_records.py | 116 +++++++++++++++++- ApiEvents/EventServiceApi/route_configs.py | 84 +------------ ApiEvents/EventServiceApi/utils.py | 51 -------- ApiEvents/abstract_class.py | 37 ++++-- ApiEvents/utils.py | 104 ++++++++++++++++ ApiServices/Token/token_handler.py | 78 ++++++++++++ ApiServices/__init__.py | 5 + .../AllApiNeeds/create_routes.py | 22 +++- .../AllApiNeeds/middleware/auth_middleware.py | 28 ++--- DockerApiServices/EventServiceApi/Dockerfile | 8 ++ .../ErrorHandlers/api_exc_handler.py | 5 +- docker-compose-services.yml | 4 +- 12 files changed, 369 insertions(+), 173 deletions(-) delete mode 100644 ApiEvents/EventServiceApi/utils.py create mode 100644 ApiEvents/utils.py create mode 100644 ApiServices/Token/token_handler.py create mode 100644 ApiServices/__init__.py diff --git a/ApiEvents/EventServiceApi/account/account_records.py b/ApiEvents/EventServiceApi/account/account_records.py index 26032b6..452a135 100644 --- a/ApiEvents/EventServiceApi/account/account_records.py +++ b/ApiEvents/EventServiceApi/account/account_records.py @@ -1,4 +1,9 @@ import typing +from collections.abc import Callable + +from fastapi import Request +from typing import Dict, Any + from ApiValidations.Custom.token_objects import ( OccupantTokenObject, EmployeeTokenObject, @@ -17,12 +22,14 @@ from ApiValidations.Request import ( ListOptions, ) from Services.PostgresDb.Models.alchemy_response import AlchemyJsonResponse -from ApiEvents.abstract_class import ( +from ApiValidations.Response import AccountRecordResponse +from events.abstract_class import ( MethodToEvent, RouteFactoryConfig, EndpointFactoryConfig, ) -from ApiValidations.Response import AccountRecordResponse + +# from events.utils import with_token_event class AccountRecordsListEventMethods(MethodToEvent): @@ -355,3 +362,108 @@ class AccountRecordsPatchEventMethods(MethodToEvent): message="Account record patched successfully", 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() diff --git a/ApiEvents/EventServiceApi/route_configs.py b/ApiEvents/EventServiceApi/route_configs.py index 98f87c4..d5540f7 100644 --- a/ApiEvents/EventServiceApi/route_configs.py +++ b/ApiEvents/EventServiceApi/route_configs.py @@ -5,93 +5,13 @@ This module collects and registers all route configurations from different modul to be used by the dynamic route creation system. """ -from typing import Dict, List, Any, Callable -from fastapi import Request +from typing import Dict, List, Any +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 ROUTE_CONFIGS = [ ACCOUNT_RECORDS_CONFIG, - # Add other route configurations here ] def get_route_configs() -> List[Dict[str, Any]]: diff --git a/ApiEvents/EventServiceApi/utils.py b/ApiEvents/EventServiceApi/utils.py deleted file mode 100644 index 5650872..0000000 --- a/ApiEvents/EventServiceApi/utils.py +++ /dev/null @@ -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 diff --git a/ApiEvents/abstract_class.py b/ApiEvents/abstract_class.py index 6292b94..f6de128 100644 --- a/ApiEvents/abstract_class.py +++ b/ApiEvents/abstract_class.py @@ -5,10 +5,11 @@ This module provides core abstractions for route configuration and factory, 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 pydantic import BaseModel + ResponseModel = TypeVar('ResponseModel', bound=BaseModel) @@ -17,25 +18,44 @@ 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 is_event_required: Whether event handling is required - request_model: Expected request model type extra_options: Additional endpoint options - response_model: Expected response model type """ + url_prefix :str + url_endpoint: str + url_of_endpoint: str endpoint: str method: str summary: str description: str - is_auth_required: bool - is_event_required: bool - request_model: Type[BaseModel] + endpoint_function: Callable + is_auth_required: bool = True + is_event_required: bool = False 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 @@ -75,10 +95,9 @@ class RouteFactoryConfig: "method": ep.method, "summary": ep.summary, "description": ep.description, + "endpoint_function": ep.endpoint_function, "is_auth_required": ep.is_auth_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 } for ep in self.endpoints diff --git a/ApiEvents/utils.py b/ApiEvents/utils.py new file mode 100644 index 0000000..a4f091e --- /dev/null +++ b/ApiEvents/utils.py @@ -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 diff --git a/ApiServices/Token/token_handler.py b/ApiServices/Token/token_handler.py new file mode 100644 index 0000000..526ac38 --- /dev/null +++ b/ApiServices/Token/token_handler.py @@ -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", + ) diff --git a/ApiServices/__init__.py b/ApiServices/__init__.py new file mode 100644 index 0000000..a4a9419 --- /dev/null +++ b/ApiServices/__init__.py @@ -0,0 +1,5 @@ +from ApiServices.Token.token_handler import TokenService + +__all__ = [ + 'TokenService', +] \ No newline at end of file diff --git a/DockerApiServices/AllApiNeeds/create_routes.py b/DockerApiServices/AllApiNeeds/create_routes.py index a3bee04..25b9772 100644 --- a/DockerApiServices/AllApiNeeds/create_routes.py +++ b/DockerApiServices/AllApiNeeds/create_routes.py @@ -15,7 +15,7 @@ from fastapi.routing import APIRoute from middleware.auth_middleware import MiddlewareModule from pydantic import BaseModel from AllConfigs.main import MainConfig as Config -from ApiEvents.EventServiceApi.route_configs import get_route_configs + @dataclass @@ -99,21 +99,35 @@ def get_all_routers() -> tuple[List[APIRouter], Dict[str, List[str]]]: Returns: tuple: (routers, protected_routes) """ + from events.route_configs import get_route_configs + routers = [] all_protected_routes = {} # Get route configurations from the registry route_configs = get_route_configs() - + factory_all = [] for config in route_configs: factory = EnhancedEndpointFactory(config) # 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_all.append( + endpoint_config.__dict__ + ) # Add router and protected routes routers.append(factory.get_router()) all_protected_routes.update(factory.get_protected_routes()) - return routers, all_protected_routes \ No newline at end of file diff --git a/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py b/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py index 6ee83f9..8b109cb 100644 --- a/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py +++ b/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py @@ -95,25 +95,15 @@ class MiddlewareModule: """ @wraps(func) - async def wrapper(request: Request, *args, **kwargs): - try: - # Get token from header - _, token = cls.get_access_token(request) - - # Validate token and get user data - token_data = await cls.validate_token(token) - - # Add user data to request state for use in endpoint - request.state.user = token_data - - # Call the original endpoint function - return await 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") - + def wrapper(request: Request, *args, **kwargs): + from ApiServices import TokenService + # Get token from header + # token = TokenService.get_access_token_from_request(request=request) + # print(token) + # if not token: + # raise HTTPExceptionApi(error_code="NOT_AUTHORIZED", lang="tr") + # Call the original endpoint function + return func(request, *args, **kwargs) return wrapper diff --git a/DockerApiServices/EventServiceApi/Dockerfile b/DockerApiServices/EventServiceApi/Dockerfile index d7414b0..a50da42 100644 --- a/DockerApiServices/EventServiceApi/Dockerfile +++ b/DockerApiServices/EventServiceApi/Dockerfile @@ -20,12 +20,20 @@ RUN poetry config virtualenvs.create false \ # Copy application code COPY DockerApiServices/AllApiNeeds /app/ +COPY ErrorHandlers /app/ErrorHandlers +COPY LanguageModels /app/LanguageModels COPY ApiLibrary /app/ApiLibrary COPY ApiValidations /app/ApiValidations COPY AllConfigs /app/AllConfigs COPY ErrorHandlers /app/ErrorHandlers COPY Schemas /app/Schemas 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 ENV PYTHONPATH=/app \ diff --git a/ErrorHandlers/ErrorHandlers/api_exc_handler.py b/ErrorHandlers/ErrorHandlers/api_exc_handler.py index 46cc4ed..05db22e 100644 --- a/ErrorHandlers/ErrorHandlers/api_exc_handler.py +++ b/ErrorHandlers/ErrorHandlers/api_exc_handler.py @@ -17,18 +17,15 @@ class HTTPExceptionApiHandler: @staticmethod def retrieve_error_status_code(exc: HTTPExceptionApi) -> int: - from ErrorHandlers import DEFAULT_ERROR - error_by_codes = BaseErrorModelClass.retrieve_error_by_codes() 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) @staticmethod def retrieve_error_message(exc: HTTPExceptionApi, error_languages) -> str: from ErrorHandlers import DEFAULT_ERROR - return error_languages.get(str(exc.error_code).upper(), DEFAULT_ERROR) async def handle_exception( diff --git a/docker-compose-services.yml b/docker-compose-services.yml index 52f9354..d2ba154 100644 --- a/docker-compose-services.yml +++ b/docker-compose-services.yml @@ -11,12 +11,12 @@ services: context: . dockerfile: DockerApiServices/EventServiceApi/Dockerfile ports: - - "8001:8000" + - "41576:41575" validation-service: build: context: . dockerfile: DockerApiServices/ValidationServiceApi/Dockerfile 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