All checks were successful
continuous-integration/drone/push Build is passing
199 lines
8.4 KiB
Python
199 lines
8.4 KiB
Python
import base64
|
||
import hashlib
|
||
import hmac
|
||
import json
|
||
import logging
|
||
import os
|
||
import random
|
||
import time
|
||
import urllib.parse
|
||
from datetime import datetime, timedelta
|
||
from typing import Dict, List, Optional, Any, Union
|
||
|
||
import httpx
|
||
|
||
from shared.config import settings
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class FatSecretClient:
|
||
"""Клиент для работы с API FatSecret"""
|
||
|
||
BASE_URL = "https://platform.fatsecret.com/rest/server.api"
|
||
|
||
def __init__(self):
|
||
"""Инициализация клиента для работы с API FatSecret"""
|
||
# Используем CUSTOMER_KEY для OAuth 1.0, если он доступен, иначе CLIENT_ID
|
||
self.api_key = settings.FATSECRET_CUSTOMER_KEY or settings.FATSECRET_CLIENT_ID
|
||
self.api_secret = settings.FATSECRET_CLIENT_SECRET
|
||
|
||
# Логируем информацию о ключах (без полного раскрытия)
|
||
logger.info(f"FatSecretClient initialized with key: {self.api_key[:8]}...")
|
||
|
||
def _generate_oauth_params(self, http_method: str, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Создание и подписание OAuth 1.0 параметров"""
|
||
# Текущее время в секундах
|
||
timestamp = str(int(time.time()))
|
||
# Случайная строка для nonce
|
||
nonce = ''.join([str(random.randint(0, 9)) for _ in range(8)])
|
||
|
||
# Базовый набор параметров OAuth
|
||
oauth_params = {
|
||
'oauth_consumer_key': self.api_key,
|
||
'oauth_nonce': nonce,
|
||
'oauth_signature_method': 'HMAC-SHA1',
|
||
'oauth_timestamp': timestamp,
|
||
'oauth_version': '1.0'
|
||
}
|
||
|
||
# Объединяем с параметрами запроса
|
||
all_params = {**params, **oauth_params}
|
||
|
||
# Сортируем параметры по ключу
|
||
sorted_params = sorted(all_params.items())
|
||
|
||
# Создаем строку параметров для подписи
|
||
param_string = "&".join([
|
||
f"{urllib.parse.quote(str(k), safe='')}={urllib.parse.quote(str(v), safe='')}"
|
||
for k, v in sorted_params
|
||
])
|
||
|
||
# Создаем строку для подписи
|
||
signature_base = f"{http_method}&{urllib.parse.quote(url, safe='')}&{urllib.parse.quote(param_string, safe='')}"
|
||
|
||
# Создаем ключ для подписи
|
||
signing_key = f"{urllib.parse.quote(str(self.api_secret), safe='')}&"
|
||
|
||
# Создаем HMAC-SHA1 подпись
|
||
signature = base64.b64encode(
|
||
hmac.new(
|
||
signing_key.encode(),
|
||
signature_base.encode(),
|
||
hashlib.sha1
|
||
).digest()
|
||
).decode()
|
||
|
||
# Добавляем подпись к параметрам OAuth
|
||
all_params['oauth_signature'] = signature
|
||
|
||
return all_params
|
||
|
||
async def search_foods(self, query: str, page_number: int = 0, max_results: int = 10) -> Dict[str, Any]:
|
||
"""Поиск продуктов по запросу"""
|
||
params = {
|
||
'method': 'foods.search',
|
||
'search_expression': query,
|
||
'page_number': str(page_number),
|
||
'max_results': str(max_results),
|
||
'format': 'json'
|
||
}
|
||
|
||
# Получаем подписанные OAuth параметры
|
||
oauth_params = self._generate_oauth_params("GET", self.BASE_URL, params)
|
||
|
||
try:
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(
|
||
self.BASE_URL,
|
||
params=oauth_params
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
except Exception as e:
|
||
logger.error(f"Error searching foods: {e}")
|
||
raise
|
||
|
||
async def get_food_details(self, food_id: Union[str, int]) -> Dict[str, Any]:
|
||
"""Получить детальную информацию о продукте по ID"""
|
||
params = {
|
||
'method': 'food.get.v2',
|
||
'food_id': str(food_id),
|
||
'format': 'json'
|
||
}
|
||
|
||
# Получаем подписанные OAuth параметры
|
||
oauth_params = self._generate_oauth_params("GET", self.BASE_URL, params)
|
||
|
||
try:
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(
|
||
self.BASE_URL,
|
||
params=oauth_params
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
except Exception as e:
|
||
logger.error(f"Error getting food details: {e}")
|
||
raise
|
||
|
||
async def parse_food_data(self, food_json: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Разбирает данные о продукте из API в более удобный формат"""
|
||
try:
|
||
food = food_json.get('food', {})
|
||
|
||
# Извлечение основной информации о продукте
|
||
food_id = food.get('food_id')
|
||
food_name = food.get('food_name', '')
|
||
food_type = food.get('food_type', '')
|
||
brand_name = food.get('brand_name', '')
|
||
|
||
# Обработка информации о питании
|
||
servings = food.get('servings', {}).get('serving', [])
|
||
|
||
# Если есть только одна порция, преобразуем ее в список
|
||
if isinstance(servings, dict):
|
||
servings = [servings]
|
||
|
||
# Берем первую порцию по умолчанию (обычно это 100г или стандартная порция)
|
||
serving_data = {}
|
||
for serving in servings:
|
||
if serving.get('is_default_serving', 0) == "1" or serving.get('serving_description', '').lower() == '100g':
|
||
serving_data = serving
|
||
break
|
||
|
||
# Если не нашли стандартную порцию, берем первую
|
||
if not serving_data and servings:
|
||
serving_data = servings[0]
|
||
|
||
# Извлечение данных о пищевой ценности
|
||
serving_description = serving_data.get('serving_description', '')
|
||
serving_amount = serving_data.get('metric_serving_amount', serving_data.get('serving_amount', ''))
|
||
serving_unit = serving_data.get('metric_serving_unit', serving_data.get('serving_unit', ''))
|
||
|
||
# Формирование читаемого текста размера порции
|
||
serving_size = f"{serving_amount} {serving_unit}" if serving_amount and serving_unit else serving_description
|
||
|
||
# Извлечение данных о пищевой ценности
|
||
calories = float(serving_data.get('calories', 0) or 0)
|
||
protein = float(serving_data.get('protein', 0) or 0)
|
||
fat = float(serving_data.get('fat', 0) or 0)
|
||
carbs = float(serving_data.get('carbohydrate', 0) or 0)
|
||
fiber = float(serving_data.get('fiber', 0) or 0)
|
||
sugar = float(serving_data.get('sugar', 0) or 0)
|
||
sodium = float(serving_data.get('sodium', 0) or 0)
|
||
cholesterol = float(serving_data.get('cholesterol', 0) or 0)
|
||
|
||
# Формирование результата
|
||
result = {
|
||
"fatsecret_id": food_id,
|
||
"name": food_name,
|
||
"brand": brand_name,
|
||
"food_type": food_type,
|
||
"serving_size": serving_size,
|
||
"serving_weight_grams": float(serving_amount) if serving_unit == 'g' else None,
|
||
"calories": calories,
|
||
"protein_grams": protein,
|
||
"fat_grams": fat,
|
||
"carbs_grams": carbs,
|
||
"fiber_grams": fiber,
|
||
"sugar_grams": sugar,
|
||
"sodium_mg": sodium,
|
||
"cholesterol_mg": cholesterol,
|
||
"is_verified": True
|
||
}
|
||
|
||
return result
|
||
except Exception as e:
|
||
logger.error(f"Error parsing food data: {e}")
|
||
raise |