This commit is contained in:
199
services/nutrition_service/fatsecret_client.py
Normal file
199
services/nutrition_service/fatsecret_client.py
Normal file
@@ -0,0 +1,199 @@
|
||||
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
|
||||
Reference in New Issue
Block a user