""" This file contains general utility functions and helpers specifically for API v3. It aims to provide a clean slate for new methods and refactor existing ones from lib_general.py that are relevant to the v3 API, while removing unused or outdated functionalities. """ # Standard library imports import time import logging from typing import ( Any, Dict, List, Optional, Union, ) # Third-party imports from fastapi import ( APIRouter, Depends, Header, HTTPException, Query, Request, Response, status, ) from pydantic import ( BaseModel, Field, ValidationError, computed_field, model_validator, ) # Internal imports (from this project) from app.config import settings from app.db_sql import redis_lookup_id_random from app.log import get_logger logger = get_logger(__name__) # --- Pydantic Model for Account Context --- class AccountContext(BaseModel): account_id: Optional[int] account_id_random: Optional[str] administrator: bool = False manager: bool = False super: bool = False auth_method: str = 'legacy_header' # --- Dependency Function for Account Context --- def get_account_context( x_account_id: Optional[str] = Header(None, min_length=11, max_length=22), x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100), # Assuming 'bypass' or similar string x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22), ) -> AccountContext: """ Resolves the account context from headers/query parameters with defined precedence. Precedence: x_account_id (header) > x_no_account_id_token (query) > x_no_account_id (header flag) Raises HTTPException 403 if no valid account is found and no bypass is indicated. """ logger.setLevel(logging.DEBUG) # Adjust as needed logger.debug(locals()) resolved_account_id = None resolved_account_id_random = None if x_account_id: # Primary check: x_account_id header resolved_account_id_random = x_account_id if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_account_id): resolved_account_id = looked_up_id logger.info(f'Found account from x_account_id header: {resolved_account_id}') else: logger.warning(f'Invalid x_account_id header provided: {x_account_id}') raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Invalid X-Account-ID header.') elif x_no_account_id_token: # Secondary check: x_no_account_id_token query parameter resolved_account_id_random = x_no_account_id_token if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_no_account_id_token): resolved_account_id = looked_up_id logger.info(f'Found account from x_no_account_id_token query: {resolved_account_id}') else: logger.warning(f'Invalid x_no_account_id_token query provided: {x_no_account_id_token}') raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Invalid X-No-Account-ID-Token query parameter.') elif x_no_account_id: # Tertiary check: x_no_account_id header for bypass # For now, just presence indicates bypass. Can add a specific value check later if needed. logger.info(f'X-No-Account-ID header found: {x_no_account_id}. Proceeding without specific account context.') resolved_account_id = None # Explicitly None for "no specific account" resolved_account_id_random = '--- NO ACCOUNT ---' else: logger.warning('No valid account context provided via X-Account-ID, X-No-Account-ID-Token, or X-No-Account-ID.') raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Account context required. Please provide X-Account-ID, X-No-Account-ID-Token, or X-No-Account-ID.') return AccountContext(account_id=resolved_account_id, account_id_random=resolved_account_id_random) # --- Optional version to avoid breaking api_crud_v3 imports if they use it --- def get_account_context_optional( x_account_id: Optional[str] = Header(None, min_length=11, max_length=22), x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100), x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22), ) -> AccountContext: try: return get_account_context(x_account_id, x_no_account_id, x_no_account_id_token) except HTTPException: return AccountContext(account_id=None, account_id_random=None, auth_method='guest') # --- Pydantic Model for Pagination --- class PaginationParams(BaseModel): limit: int = 100 # Default limit offset: int = 0 # --- Dependency Function for Pagination --- def get_pagination_params( limit: int = Query(100, ge=0, description="Maximum number of items to return"), offset: int = Query(0, ge=0, description="Number of items to skip (for pagination)"), ) -> PaginationParams: return PaginationParams(limit=limit, offset=offset) # --- Pydantic Model for Status Filtering --- class StatusFilterParams(BaseModel): enabled: str = 'enabled' # 'enabled', 'disabled', 'all' hidden: str = 'not_hidden' # 'hidden', 'not_hidden', 'all' # --- Dependency Function for Status Filtering --- def get_status_filter_params( enabled: str = Query('enabled', description="Filter by object enabled status ('enabled', 'disabled', 'all')"), hidden: str = Query('not_hidden', description="Filter by object hidden status ('hidden', 'not_hidden', 'all')"), ) -> StatusFilterParams: allowed_enabled_values = {'enabled', 'disabled', 'all'} allowed_hidden_values = {'hidden', 'not_hidden', 'all'} if enabled not in allowed_enabled_values: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid value for 'enabled'. Must be one of {list(allowed_enabled_values)}." ) if hidden not in allowed_hidden_values: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid value for 'hidden'. Must be one of {list(allowed_hidden_values)}." ) return StatusFilterParams(enabled=enabled, hidden=hidden) # --- Pydantic Model for Serialization Options --- class SerializationParams(BaseModel): by_alias: bool = True exclude_unset: bool = False exclude_defaults: bool = False # Added based on common_route_params exclude_none: bool = False # Added based on common_route_params # --- Dependency Function for Serialization Options --- def get_serialization_params( by_alias: bool = Query(True, description="Whether to use field aliases for serialization"), exclude_unset: bool = Query(False, description="Whether to exclude unset fields from the response"), exclude_defaults: bool = Query(False, description="Whether to exclude fields with their default values from the response"), exclude_none: bool = Query(False, description="Whether to exclude fields that are None from the response"), ) -> SerializationParams: return SerializationParams( by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, ) # --- Pydantic Model for Delay --- class DelayParams(BaseModel): sleep_time_ms: int = 0 # Raw delay value in ms sleep_time_s: float = 0.0 # Converted to seconds for time.sleep() # --- Dependency Function for Delay --- def get_delay_params( x_delay_ms: Optional[int] = Header(0, alias='X-Delay-ms', description="Delay response for X milliseconds (header)"), delay_ms: Optional[int] = Query(0, description="Delay response for X milliseconds (query parameter)"), ) -> DelayParams: calculated_delay_ms = max(x_delay_ms or 0, delay_ms or 0) return DelayParams(sleep_time_ms=calculated_delay_ms, sleep_time_s=calculated_delay_ms / 1000.0)