Files
marquiz-metrics/app/api/v1/endpoints/counters.py
13orlov e76c917f8d
All checks were successful
continuous-integration/drone/push Build is passing
feat(goals): Add deleting goals
2025-08-25 19:07:33 +01:00

249 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
router = APIRouter()
bearer_scheme = HTTPBearer()
# --- Эндпоинт для получения списка счетчиков ---
@router.get("/", response_model=CounterListResponse, summary="Получение списка счетчиков")
async def get_counters(credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)]):
"""
Получает список счетчиков Яндекс.Метрики, доступных для данного токена.
Токен должен быть передан в заголовке 'Authorization: Bearer <token>'.
"""
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>' заголовок.
"""
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/{goal_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Удаление цели в счетчике"
)
async def delete_goal_in_counter(
counter_id: int,
goal_id: int,
credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)]
):
"""
Удаляет указанную цель в указанном счетчике.
В случае успеха возвращает пустой ответ со статусом 204.
"""
token = credentials.credentials
logger.info(f"Attempting to delete goal ID: {goal_id} from counter ID: {counter_id}")
url = f"{settings.YANDEX_METRIKA_API_URL}/management/v1/counter/{counter_id}/goal/{goal_id}"
headers = {'Authorization': f'OAuth {token}'}
try:
async with httpx.AsyncClient() as client:
response = await client.delete(url, headers=headers)
response.raise_for_status()
logger.success(f"Successfully deleted goal ID: {goal_id} from counter {counter_id}.")
# При успехе стандартно возвращается пустой ответ
return
except httpx.HTTPStatusError as e:
error_details = e.response.json()
logger.error(f"Yandex Metrika API error during goal deletion: {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 goal deletion.")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An internal server error occurred."
)