Files
chat/services/nutrition_service/fatsecret_client.py
Andrew K. Choi 537e7b363f
All checks were successful
continuous-integration/drone/push Build is passing
main commit
2025-10-16 16:30:25 +09:00

199 lines
8.4 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.

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