first commit
This commit is contained in:
209
calorie_tracker_app/api_client.py
Normal file
209
calorie_tracker_app/api_client.py
Normal file
@@ -0,0 +1,209 @@
|
||||
import requests
|
||||
import json
|
||||
from models import db, APICache, FoodItem
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class NutritionAPI:
|
||||
"""API client for nutrition data with caching"""
|
||||
|
||||
def __init__(self, api_key):
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://api.api-ninjas.com/v1/nutrition"
|
||||
self.headers = {'X-Api-Key': api_key}
|
||||
self.cache_duration_days = 30
|
||||
|
||||
def search_food(self, query):
|
||||
"""
|
||||
Search for food nutrition data
|
||||
Returns list of food items with nutrition info
|
||||
"""
|
||||
# Check cache first
|
||||
cached = self._get_from_cache(query)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
# Make API request
|
||||
try:
|
||||
response = requests.get(
|
||||
self.base_url,
|
||||
headers=self.headers,
|
||||
params={'query': query},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
# Cache the response
|
||||
self._save_to_cache(query, 'api_ninjas', data)
|
||||
|
||||
# Parse and return standardized format
|
||||
return self._parse_api_response(data)
|
||||
else:
|
||||
print(f"API Error: {response.status_code}")
|
||||
return []
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"API Request failed: {e}")
|
||||
return []
|
||||
|
||||
def _get_from_cache(self, query):
|
||||
"""Check if query exists in cache and is not expired"""
|
||||
cache_entry = APICache.query.filter_by(query=query.lower()).first()
|
||||
|
||||
if cache_entry:
|
||||
# Check if cache is still valid (30 days)
|
||||
age = datetime.utcnow() - cache_entry.cached_at
|
||||
if age.days < self.cache_duration_days:
|
||||
data = json.loads(cache_entry.response_json)
|
||||
return self._parse_api_response(data)
|
||||
|
||||
return None
|
||||
|
||||
def _save_to_cache(self, query, source, data):
|
||||
"""Save API response to cache"""
|
||||
try:
|
||||
cache_entry = APICache.query.filter_by(query=query.lower()).first()
|
||||
|
||||
if cache_entry:
|
||||
# Update existing cache
|
||||
cache_entry.response_json = json.dumps(data)
|
||||
cache_entry.cached_at = datetime.utcnow()
|
||||
else:
|
||||
# Create new cache entry
|
||||
cache_entry = APICache(
|
||||
query=query.lower(),
|
||||
api_source=source,
|
||||
response_json=json.dumps(data),
|
||||
cached_at=datetime.utcnow()
|
||||
)
|
||||
db.session.add(cache_entry)
|
||||
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
print(f"Cache save failed: {e}")
|
||||
db.session.rollback()
|
||||
|
||||
def _parse_api_response(self, data):
|
||||
"""Parse API response into standardized format"""
|
||||
foods = []
|
||||
|
||||
for item in data:
|
||||
food = {
|
||||
'name': item.get('name', '').title(),
|
||||
'calories': item.get('calories', 0),
|
||||
'protein_g': item.get('protein_g', 0),
|
||||
'carbs_g': item.get('carbohydrates_total_g', 0),
|
||||
'fat_g': item.get('fat_total_g', 0),
|
||||
'fiber_g': item.get('fiber_g', 0),
|
||||
'sugar_g': item.get('sugar_g', 0),
|
||||
'sodium_mg': item.get('sodium_mg', 0),
|
||||
'serving_size_g': item.get('serving_size_g', 100),
|
||||
'source': 'api_ninjas'
|
||||
}
|
||||
foods.append(food)
|
||||
|
||||
return foods
|
||||
|
||||
def save_food_to_db(self, food_data):
|
||||
"""Save a food item to the database"""
|
||||
try:
|
||||
# Check if food already exists
|
||||
existing = FoodItem.query.filter_by(
|
||||
name=food_data['name'],
|
||||
source=food_data.get('source', 'api')
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
# Create new food item
|
||||
food = FoodItem(
|
||||
name=food_data['name'],
|
||||
calories=food_data['calories'],
|
||||
protein_g=food_data.get('protein_g', 0),
|
||||
carbs_g=food_data.get('carbs_g', 0),
|
||||
fat_g=food_data.get('fat_g', 0),
|
||||
fiber_g=food_data.get('fiber_g', 0),
|
||||
sugar_g=food_data.get('sugar_g', 0),
|
||||
sodium_mg=food_data.get('sodium_mg', 0),
|
||||
serving_size_g=food_data.get('serving_size_g', 100),
|
||||
serving_description=food_data.get('serving_description', '1 serving'),
|
||||
source=food_data.get('source', 'api'),
|
||||
api_data=json.dumps(food_data)
|
||||
)
|
||||
|
||||
db.session.add(food)
|
||||
db.session.commit()
|
||||
|
||||
return food
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error saving food to DB: {e}")
|
||||
db.session.rollback()
|
||||
return None
|
||||
|
||||
def search_all_sources(query, api_client):
|
||||
"""
|
||||
Search for food in all available sources
|
||||
Priority: Local DB (Filipino) -> Local DB (cached) -> API
|
||||
"""
|
||||
results = []
|
||||
|
||||
# 1. Search Filipino foods first
|
||||
filipino_foods = FoodItem.query.filter(
|
||||
FoodItem.is_filipino == True,
|
||||
db.or_(
|
||||
FoodItem.name.ilike(f'%{query}%'),
|
||||
FoodItem.name_tagalog.ilike(f'%{query}%')
|
||||
)
|
||||
).limit(5).all()
|
||||
|
||||
for food in filipino_foods:
|
||||
results.append({
|
||||
'id': food.id,
|
||||
'name': food.name,
|
||||
'name_tagalog': food.name_tagalog,
|
||||
'calories': food.calories,
|
||||
'protein_g': food.protein_g,
|
||||
'carbs_g': food.carbs_g,
|
||||
'fat_g': food.fat_g,
|
||||
'serving_description': food.serving_description,
|
||||
'source': 'filipino',
|
||||
'category': food.category
|
||||
})
|
||||
|
||||
# 2. Search other local foods
|
||||
other_foods = FoodItem.query.filter(
|
||||
FoodItem.is_filipino == False,
|
||||
FoodItem.name.ilike(f'%{query}%')
|
||||
).limit(5).all()
|
||||
|
||||
for food in other_foods:
|
||||
results.append({
|
||||
'id': food.id,
|
||||
'name': food.name,
|
||||
'calories': food.calories,
|
||||
'protein_g': food.protein_g,
|
||||
'carbs_g': food.carbs_g,
|
||||
'fat_g': food.fat_g,
|
||||
'serving_description': food.serving_description,
|
||||
'source': food.source
|
||||
})
|
||||
|
||||
# 3. If not enough results, search API
|
||||
if len(results) < 3 and api_client.api_key:
|
||||
api_results = api_client.search_food(query)
|
||||
for food_data in api_results[:5]:
|
||||
results.append({
|
||||
'name': food_data['name'],
|
||||
'calories': food_data['calories'],
|
||||
'protein_g': food_data['protein_g'],
|
||||
'carbs_g': food_data['carbs_g'],
|
||||
'fat_g': food_data['fat_g'],
|
||||
'serving_size_g': food_data['serving_size_g'],
|
||||
'source': 'api',
|
||||
'api_data': food_data
|
||||
})
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user