from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from typing import Annotated, List import httpx from loguru import logger from app.core.config import settings from app.api.v1.schemas.counter import CounterListResponse, Counter, CounterCreateRequest, CounterCreateResponse from app.api.v1.schemas.goal import GoalListResponse, Goal, GoalCreateRequest, GoalCreateResponse, CreatedGoal from app.api.v1.schemas.goal import GoalDeleteRequest, GoalDeleteResponse router = APIRouter() bearer_scheme = HTTPBearer() @router.get("/", response_model=CounterListResponse, summary="Получение списка счетчиков") async def get_counters(credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)]): """ Получает список счетчиков Яндекс.Метрики, доступных для данного токена. Токен должен быть передан в заголовке 'Authorization: Bearer '. """ token = credentials.credentials logger.info("Fetching list of counters from Yandex.Metrika API.") url = f"{settings.YANDEX_METRIKA_API_URL}/management/v1/counters" headers = {'Authorization': f'OAuth {token}', 'Content-Type': 'application/json'} try: async with httpx.AsyncClient() as client: response = await client.get(url, headers=headers) response.raise_for_status() data = response.json() counters_list = [ Counter(id=c['id'], name=c['name'], site=c['site']) for c in data.get('counters', []) ] logger.success(f"Successfully fetched {len(counters_list)} counters.") return CounterListResponse(counters=counters_list) except httpx.HTTPStatusError as e: error_details = e.response.json() logger.error(f"Yandex Metrika API error: {e.response.status_code} - {error_details}") raise HTTPException( status_code=e.response.status_code, detail=f"Yandex API Error: {error_details.get('message', 'Unknown error')}" ) except Exception as e: logger.opt(exception=True).error("An unexpected error occurred while fetching counters.") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An internal server error occurred." ) @router.post( "/", response_model=CounterCreateResponse, status_code=status.HTTP_201_CREATED, summary="Создание нового счетчика" ) async def create_counter( request_body: CounterCreateRequest, credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)] ): """ Создает новый счетчик Яндекс.Метрики с настройками по умолчанию. Требует 'Authorization: Bearer ' заголовок. """ token = credentials.credentials site_url = str(request_body.site_url).strip('/') site_name = site_url.replace("https://", "").replace("http://", "") logger.info(f"Attempting to create a new counter for site: {site_name}") url = f"{settings.YANDEX_METRIKA_API_URL}/management/v1/counters" headers = {'Authorization': f'OAuth {token}', 'Content-Type': 'application/json'} payload = { "counter": { "name": site_name, "site": site_name } } try: async with httpx.AsyncClient() as client: response = await client.post(url, headers=headers, json=payload) response.raise_for_status() created_counter_data = response.json().get("counter", {}) logger.success(f"Successfully created counter ID: {created_counter_data.get('id')}") return CounterCreateResponse(**created_counter_data) except httpx.HTTPStatusError as e: error_details = e.response.json() logger.error(f"Yandex Metrika API error during counter creation: {e.response.status_code} - {error_details}") raise HTTPException( status_code=e.response.status_code, detail=f"Yandex API Error: {error_details.get('message', 'Unknown error')}" ) except Exception as e: logger.opt(exception=True).error("An unexpected error occurred during counter creation.") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An internal server error occurred." ) @router.get( "/{counter_id}/goals", response_model=GoalListResponse, summary="Получение списка целей для счетчика" ) async def get_goals_for_counter( counter_id: int, credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)] ): """ Получает список всех целей для указанного счетчика. """ token = credentials.credentials logger.info(f"Fetching goals for counter ID: {counter_id}") url = f"{settings.YANDEX_METRIKA_API_URL}/management/v1/counter/{counter_id}/goals" headers = {'Authorization': f'OAuth {token}'} try: async with httpx.AsyncClient() as client: response = await client.get(url, headers=headers) response.raise_for_status() data = response.json() # Pydantic автоматически отфильтрует только нужные нам поля (id, name, type) goals_list = [Goal(**g) for g in data.get('goals', [])] logger.success(f"Successfully fetched {len(goals_list)} goals for counter {counter_id}.") return GoalListResponse(goals=goals_list) except httpx.HTTPStatusError as e: error_details = e.response.json() logger.error(f"Yandex Metrika API error during goals fetching: {e.response.status_code} - {error_details}") raise HTTPException( status_code=e.response.status_code, detail=f"Yandex API Error: {error_details.get('message', 'Unknown error')}" ) except Exception as e: logger.opt(exception=True).error("An unexpected error occurred while fetching goals.") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An internal server error occurred." ) @router.post( "/{counter_id}/goals", response_model=GoalCreateResponse, status_code=status.HTTP_201_CREATED, summary="Создание кастомных целей в счетчике" ) async def create_custom_goals_for_counter( counter_id: int, request_body: GoalCreateRequest, credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)] ): """ Создает в указанном счетчике цели типа 'javascript-событие' на основе переданного списка объектов (identifier, name). """ token = credentials.credentials logger.info(f"Attempting to create {len(request_body.goals)} custom goals for counter ID: {counter_id}") url = f"{settings.YANDEX_METRIKA_API_URL}/management/v1/counter/{counter_id}/goals" headers = {'Authorization': f'OAuth {token}', 'Content-Type': 'application/json'} created_goals_list: List[CreatedGoal] = [] async with httpx.AsyncClient() as client: # Итерируемся по списку целей из запроса for goal_in in request_body.goals: goal_payload = { "name": goal_in.name, "type": "action", # Создаем только цели типа "JavaScript-событие" "conditions": [{"type": "exact", "url": goal_in.identifier}] } payload = {"goal": goal_payload} logger.debug(f"Sending payload for goal '{goal_in.name}': {payload}") try: response = await client.post(url, headers=headers, json=payload) response.raise_for_status() created_goal_data = response.json().get("goal", {}) if created_goal_data: logger.success(f"Successfully created goal: {created_goal_data.get('name')} (ID: {created_goal_data.get('id')})") created_goals_list.append(CreatedGoal(**created_goal_data)) except httpx.HTTPStatusError as e: error_details = e.response.json() logger.error(f"Failed to create goal '{goal_in.name}'. Reason: {error_details.get('message', 'Unknown error')}") except Exception as e: logger.opt(exception=True).error(f"An unexpected error occurred while creating goal '{goal_in.name}'.") if not created_goals_list: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Could not create any of the requested goals. Check logs for details." ) return GoalCreateResponse(created_goals=created_goals_list) # Массовое удаление целей --- @router.delete( "/{counter_id}/goals", response_model=GoalDeleteResponse, summary="Массовое удаление целей в счетчике" ) async def delete_goals_in_counter( counter_id: int, request_body: GoalDeleteRequest, credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)] ): """ Удаляет цели в указанном счетчике по списку их ID. Возвращает отчет о выполненной операции. """ token = credentials.credentials goal_ids_to_delete = request_body.goal_ids logger.info(f"Attempting to delete {len(goal_ids_to_delete)} goals from counter ID: {counter_id}") headers = {'Authorization': f'OAuth {token}'} deleted_count = 0 failed_ids = {} async with httpx.AsyncClient() as client: for goal_id in goal_ids_to_delete: url = f"{settings.YANDEX_METRIKA_API_URL}/management/v1/counter/{counter_id}/goal/{goal_id}" try: response = await client.delete(url, headers=headers) response.raise_for_status() logger.success(f"Successfully deleted goal ID: {goal_id}") deleted_count += 1 except httpx.HTTPStatusError as e: error_details = e.response.json() error_message = error_details.get('message', 'Unknown error') logger.error(f"Failed to delete goal ID: {goal_id}. Reason: {error_message}") failed_ids[goal_id] = error_message except Exception as e: logger.error(f"An unexpected error occurred while deleting goal ID: {goal_id}.") failed_ids[goal_id] = "Unexpected server error" message = f"Operation completed. Deleted {deleted_count} out of {len(goal_ids_to_delete)} goals." return GoalDeleteResponse(deleted_count=deleted_count, failed_ids=failed_ids, message=message)