diff --git a/app/api/v1/endpoints/counters.py b/app/api/v1/endpoints/counters.py index c074531..f5068a1 100644 --- a/app/api/v1/endpoints/counters.py +++ b/app/api/v1/endpoints/counters.py @@ -1,28 +1,24 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from typing import Annotated, Dict, List +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 GoalCreateRequest, GoalCreateResponse, Goal +# Обновляем импорты для схем целей +from app.api.v1.schemas.goal import GoalListResponse, Goal, GoalCreateRequest, GoalCreateResponse, CreatedGoal router = APIRouter() bearer_scheme = HTTPBearer() -# ФИНАЛЬНАЯ ВЕРСИЯ: Ключи используют правильное написание "Marquiz" (в нижнем регистре для сравнения). -PREDEFINED_GOALS: Dict[str, str] = { - "marquiz-start": "Посетитель открыл квиз", - "marquiz-startquiz": "Посетитель нажал на кнопку стартовой страницы", - "marquiz-form": "Посетитель дошёл до формы контактов", - "marquiz-result": "Посетитель увидел результат", - "marquiz-contacts1": "Посетитель заполнил и отправил форму контактов", -} - -# --- КОД ЭНДПОИНТОВ get_counters и create_counter ОСТАЕТСЯ БЕЗ ИЗМЕНЕНИЙ --- +# --- Эндпоинт для получения списка счетчиков --- @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.") @@ -57,6 +53,7 @@ async def get_counters(credentials: Annotated[HTTPAuthorizationCredentials, Depe detail="An internal server error occurred." ) +# --- Эндпоинт для создания нового счетчика --- @router.post( "/", response_model=CounterCreateResponse, @@ -67,6 +64,10 @@ 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://", "") @@ -90,11 +91,7 @@ async def create_counter( created_counter_data = response.json().get("counter", {}) logger.success(f"Successfully created counter ID: {created_counter_data.get('id')}") - return CounterCreateResponse( - id=created_counter_data.get('id'), - name=created_counter_data.get('name'), - code_status=created_counter_data.get('code_status') - ) + return CounterCreateResponse(**created_counter_data) except httpx.HTTPStatusError as e: error_details = e.response.json() @@ -111,61 +108,96 @@ async def create_counter( ) +# --- НОВЫЙ ЭНДПОИНТ: Получение списка целей --- +@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="Создание стандартного набора целей в счетчике" + summary="Создание кастомных целей в счетчике" ) -async def create_goals_for_counter( +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 goals for counter ID: {counter_id}") + 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[Goal] = [] + created_goals_list: List[CreatedGoal] = [] async with httpx.AsyncClient() as client: - for identifier in request_body.goal_identifiers: - # Приводим входящий идентификатор к нижнему регистру для надежного поиска - goal_name = PREDEFINED_GOALS.get(identifier.lower()) - - if not goal_name: - logger.warning(f"Unknown goal identifier '{identifier}' skipped.") - continue - + # Итерируемся по списку целей из запроса + for goal_in in request_body.goals: goal_payload = { - "name": goal_name, - "type": "action", - "conditions": [{"type": "exact", "url": identifier}] + "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_name}': {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( - Goal(id=created_goal_data['id'], name=created_goal_data['name']) - ) + 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_name}'. Reason: {error_details.get('message', 'Unknown error')}") - except Exception: - logger.opt(exception=True).error(f"An unexpected error occurred while creating goal '{goal_name}'.") + 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: - logger.warning("No goals were created.") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Could not create any of the requested goals. Check logs for details." diff --git a/app/api/v1/schemas/goal.py b/app/api/v1/schemas/goal.py index 1ab6ab6..6fb27a9 100644 --- a/app/api/v1/schemas/goal.py +++ b/app/api/v1/schemas/goal.py @@ -1,26 +1,31 @@ from pydantic import BaseModel, Field from typing import List -class GoalCreateRequest(BaseModel): - """Схема запроса на создание стандартных целей.""" - goal_identifiers: List[str] = Field( - ..., - description="Список идентификаторов стандартных целей для квиза.", - # --- ИЗМЕНЕНИЕ ЗДЕСЬ: Обновляем пример на правильный и полный --- - examples=[[ - "Marquiz-start", - "Marquiz-startquiz", - "Marquiz-form", - "Marquiz-result", - "Marquiz-contacts1" - ]] - ) - +# Схема для ОДНОЙ цели в списке (для GET запроса) class Goal(BaseModel): - """Схема для одной созданной цели (в ответе).""" + id: int + name: str + type: str + +# Схема для ответа со списком целей +class GoalListResponse(BaseModel): + goals: List[Goal] + +# --- ИЗМЕНЕНИЯ ЗДЕСЬ --- +# Схема для ОДНОЙ кастомной цели в запросе на создание (для POST) +class CustomGoalIn(BaseModel): + identifier: str = Field(..., description="Уникальный идентификатор JS-события") + name: str = Field(..., max_length=255, description="Название цели, которое будет видно в Метрике") + +# Обновленная схема запроса на создание целей +class GoalCreateRequest(BaseModel): + goals: List[CustomGoalIn] + +# Схема для ОДНОЙ созданной цели в ответе +class CreatedGoal(BaseModel): id: int name: str +# Схема ответа после создания целей class GoalCreateResponse(BaseModel): - """Схема ответа после создания целей.""" - created_goals: List[Goal] \ No newline at end of file + created_goals: List[CreatedGoal] \ No newline at end of file