main commit
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-10-16 16:30:25 +09:00
parent 91c7e04474
commit 537e7b363f
1146 changed files with 45926 additions and 77196 deletions

View 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