Files
marquiz-metrics/app/api/v1/endpoints/counters.py
13orlov b05a6165aa
All checks were successful
continuous-integration/drone/push Build is passing
good release 2.0 code name: chiki-puki
2025-08-31 22:28:24 +01:00

246 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
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>'.
"""
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",
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)