246 lines
11 KiB
Python
246 lines
11 KiB
Python
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) |