Spaces:
Running
Running
import streamlit as st | |
import requests | |
import pandas as pd | |
import json | |
from datetime import datetime | |
import time | |
import os | |
import functools | |
import asyncio | |
import aiohttp | |
from concurrent.futures import ThreadPoolExecutor | |
# Set page configuration | |
st.set_page_config( | |
page_title="PetMatch - Find Your Perfect Pet", | |
page_icon="🐾", | |
layout="centered" | |
) | |
# Custom CSS | |
st.markdown(""" | |
<style> | |
.main-header { | |
font-size: 2.5rem; | |
color: #ff6b6c; | |
text-align: center; | |
margin-bottom: 1rem; | |
} | |
.sub-header { | |
font-size: 1.5rem; | |
color: #4a4a4a; | |
text-align: center; | |
margin-bottom: 2rem; | |
} | |
.pet-card { | |
border-radius: 10px; | |
border: 1px solid #e0e0e0; | |
padding: 1rem; | |
margin-bottom: 1rem; | |
} | |
.pet-name { | |
font-size: 1.3rem; | |
font-weight: bold; | |
color: #ff6b6b; | |
} | |
.pet-details { | |
margin-top: 0.5rem; | |
} | |
.pet-description { | |
margin-top: 1rem; | |
font-style: italic; | |
} | |
.tag { | |
background-color: #808080; | |
border-radius: 20px; | |
padding: 0.2rem 0.6rem; | |
margin-right: 0.3rem; | |
font-size: 0.8rem; | |
} | |
.source-tag { | |
background-color: #6c5ce7; | |
color: white; | |
border-radius: 20px; | |
padding: 0.2rem 0.6rem; | |
margin-right: 0.3rem; | |
font-size: 0.8rem; | |
} | |
.api-status { | |
padding: 5px; | |
border-radius: 5px; | |
margin-bottom: 10px; | |
font-size: 0.8em; | |
text-align: center; | |
} | |
.api-success { | |
background-color: #d4edda; | |
color: #155724; | |
} | |
.api-error { | |
background-color: #f8d7da; | |
color: #721c24; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# Initialize session state variables | |
if 'petfinder_access_token' not in st.session_state: | |
st.session_state.petfinder_access_token = None | |
if 'petfinder_token_expires' not in st.session_state: | |
st.session_state.petfinder_token_expires = 0 | |
if 'rescuegroups_access_token' not in st.session_state: | |
st.session_state.rescuegroups_access_token = None | |
if 'search_results' not in st.session_state: | |
st.session_state.search_results = None | |
if 'selected_pet' not in st.session_state: | |
st.session_state.selected_pet = None | |
if 'selected_pet_source' not in st.session_state: | |
st.session_state.selected_pet_source = None | |
if 'page' not in st.session_state: | |
st.session_state.page = 1 | |
if 'favorites' not in st.session_state: | |
st.session_state.favorites = [] | |
if 'petfinder_status' not in st.session_state: | |
st.session_state.petfinder_status = {'success': False, 'message': 'Not initialized'} | |
if 'rescuegroups_status' not in st.session_state: | |
st.session_state.rescuegroups_status = {'success': False, 'message': 'Not initialized'} | |
# Function to get Petfinder access token | |
def get_petfinder_token(): | |
# Check if token is still valid | |
if st.session_state.petfinder_access_token and time.time() < st.session_state.petfinder_token_expires: | |
return st.session_state.petfinder_access_token | |
# Get API credentials from environment variables or secrets | |
api_key = os.environ.get('PETFINDER_API_KEY') or st.secrets.get('PETFINDER_API_KEY') | |
api_secret = os.environ.get('PETFINDER_API_SECRET') or st.secrets.get('PETFINDER_API_SECRET') | |
if not api_key or not api_secret: | |
st.session_state.petfinder_status = {'success': False, 'message': 'API credentials missing'} | |
return None | |
# Get new token | |
url = "https://api.petfinder.com/v2/oauth2/token" | |
data = { | |
"grant_type": "client_credentials", | |
"client_id": api_key, | |
"client_secret": api_secret | |
} | |
try: | |
response = requests.post(url, data=data) | |
response.raise_for_status() | |
token_data = response.json() | |
st.session_state.petfinder_access_token = token_data['access_token'] | |
st.session_state.petfinder_token_expires = time.time() + token_data['expires_in'] - 60 # Buffer of 60 seconds | |
st.session_state.petfinder_status = {'success': True, 'message': 'API connected'} | |
return st.session_state.petfinder_access_token | |
except requests.exceptions.RequestException as e: | |
st.session_state.petfinder_status = {'success': False, 'message': f'Error: {str(e)}'} | |
return None | |
# Function to get RescueGroups.org API key | |
def get_rescuegroups_token(): | |
# RescueGroups.org uses API key directly, no OAuth flow needed | |
api_key = os.environ.get('RESCUEGROUPS_API_KEY') or st.secrets.get('RESCUEGROUPS_API_KEY') | |
if not api_key: | |
st.session_state.rescuegroups_status = {'success': False, 'message': 'API key missing'} | |
return None | |
# Just set the status and return the key | |
st.session_state.rescuegroups_status = {'success': True, 'message': 'API connected'} | |
st.session_state.rescuegroups_access_token = api_key | |
return api_key | |
# Function to search pets on Petfinder | |
def search_petfinder(params): | |
token = get_petfinder_token() | |
if not token: | |
return None | |
url = "https://api.petfinder.com/v2/animals" | |
headers = {"Authorization": f"Bearer {token}"} | |
try: | |
response = requests.get(url, headers=headers, params=params) | |
response.raise_for_status() | |
results = response.json() | |
# Add source information to each pet | |
for pet in results.get('animals', []): | |
pet['source'] = 'petfinder' | |
return results | |
except requests.exceptions.RequestException as e: | |
st.session_state.petfinder_status = {'success': False, 'message': f'Search error: {str(e)}'} | |
return None | |
# Function to search pets on RescueGroups.org | |
def search_rescuegroups(params): | |
api_key = get_rescuegroups_token() | |
if not api_key: | |
return None | |
url = "https://api.rescuegroups.org/v5/public/animals/search/available" | |
headers = { | |
"Authorization": api_key, | |
"Content-Type": "application/vnd.api+json" | |
} | |
# Convert params to RescueGroups.org format | |
rg_params = { | |
"data": { | |
"filters": [], | |
"filterProcessing": "AND", | |
"filterRadius": { | |
"miles": params.get('distance', 50), | |
"postalcode": params.get('location', '') | |
} if params.get('location') else None | |
} | |
} | |
# Add animal type filter | |
if params.get('type'): | |
animal_type = params['type'].lower() | |
# Map Petfinder types to RescueGroups types | |
type_mapping = { | |
'dog': ['Dog'], | |
'cat': ['Cat'], | |
'rabbit': ['Rabbit'], | |
'small': ['Guinea Pig', 'Hamster', 'Gerbil', 'Mouse', 'Rat', 'Degu', 'Chinchilla', 'Hedgehog', 'Sugar Glider'], | |
'horse': ['Horse'], | |
'bird': ['Bird'], | |
'scales': ['Reptile', 'Amphibian', 'Fish'], | |
'barnyard': ['Farm'] | |
} | |
# Find the matching RescueGroups animal types | |
rg_types = [] | |
for key, values in type_mapping.items(): | |
if key in animal_type.lower(): | |
rg_types.extend(values) | |
if rg_types: | |
rg_params['data']['filters'].append({ | |
"fieldName": "species.singular", | |
"operation": "in", | |
"criteria": rg_types | |
}) | |
# Add age filter | |
if params.get('age'): | |
age_mapping = { | |
'Baby': 'Baby', | |
'Young': 'Young', | |
'Adult': 'Adult', | |
'Senior': 'Senior' | |
} | |
if params['age'] in age_mapping: | |
rg_params['data']['filters'].append({ | |
"fieldName": "ageGroup", | |
"operation": "equals", | |
"criteria": age_mapping[params['age']] | |
}) | |
# Add gender filter | |
if params.get('gender'): | |
gender_mapping = { | |
'Male': 'Male', | |
'Female': 'Female' | |
} | |
if params['gender'] in gender_mapping: | |
rg_params['data']['filters'].append({ | |
"fieldName": "sex", | |
"operation": "equals", | |
"criteria": gender_mapping[params['gender']] | |
}) | |
# Add compatibility filters | |
if params.get('good_with_children'): | |
rg_params['data']['filters'].append({ | |
"fieldName": "isKidFriendly", | |
"operation": "equals", | |
"criteria": True | |
}) | |
if params.get('good_with_dogs'): | |
rg_params['data']['filters'].append({ | |
"fieldName": "isDogFriendly", | |
"operation": "equals", | |
"criteria": True | |
}) | |
if params.get('good_with_cats'): | |
rg_params['data']['filters'].append({ | |
"fieldName": "isCatFriendly", | |
"operation": "equals", | |
"criteria": True | |
}) | |
# Size filter - mapping is approximate | |
if params.get('size'): | |
size_mapping = { | |
'Small': ['Small'], | |
'Medium': ['Medium'], | |
'Large': ['Large', 'Extra Large'], | |
'XLarge': ['Extra Large'] | |
} | |
if params['size'] in size_mapping: | |
rg_params['data']['filters'].append({ | |
"fieldName": "sizeGroup", | |
"operation": "in", | |
"criteria": size_mapping[params['size']] | |
}) | |
try: | |
response = requests.post(url, headers=headers, json=rg_params) | |
response.raise_for_status() | |
results = response.json() | |
# Normalize to match Petfinder format and add source | |
normalized_results = normalize_rescuegroups_results(results) | |
return normalized_results | |
except requests.exceptions.RequestException as e: | |
st.session_state.rescuegroups_status = {'success': False, 'message': f'Search error: {str(e)}'} | |
return None | |
# Function to normalize RescueGroups.org results to match Petfinder format | |
def normalize_rescuegroups_results(rg_results): | |
normalized = {"animals": []} | |
if 'data' not in rg_results: | |
return normalized | |
for pet in rg_results['data']: | |
attributes = pet.get('attributes', {}) | |
relationships = pet.get('relationships', {}) | |
# Map breed | |
breeds = { | |
'primary': attributes.get('breedPrimary', {}).get('name', ''), | |
'secondary': attributes.get('breedSecondary', {}).get('name', ''), | |
'mixed': attributes.get('isMixedBreed', False) | |
} | |
# Map colors | |
colors = { | |
'primary': attributes.get('colorPrimary', {}).get('name', ''), | |
'secondary': attributes.get('colorSecondary', {}).get('name', ''), | |
'tertiary': attributes.get('colorTertiary', {}).get('name', '') | |
} | |
# Map contact info | |
contact = { | |
'email': attributes.get('contactEmail', ''), | |
'phone': attributes.get('contactPhone', ''), | |
'address': { | |
'address1': attributes.get('locationAddress', ''), | |
'address2': '', | |
'city': attributes.get('locationCity', ''), | |
'state': attributes.get('locationState', ''), | |
'postcode': attributes.get('locationPostalcode', ''), | |
'country': attributes.get('locationCountry', 'US') | |
} | |
} | |
# Map environment compatibility | |
environment = { | |
'children': attributes.get('isKidFriendly', None), | |
'dogs': attributes.get('isDogFriendly', None), | |
'cats': attributes.get('isCatFriendly', None) | |
} | |
# Map attributes | |
pet_attributes = { | |
'spayed_neutered': attributes.get('isAltered', False), | |
'house_trained': attributes.get('isHousetrained', False), | |
'special_needs': attributes.get('hasSpecialNeeds', False), | |
'shots_current': attributes.get('isVaccinated', False) | |
} | |
# Map photos | |
photos = [] | |
if 'pictures' in relationships and relationships['pictures'].get('data'): | |
for i, pic in enumerate(relationships['pictures']['data']): | |
pic_id = pic.get('id') | |
if pic_id and 'included' in rg_results: | |
for included in rg_results['included']: | |
if included.get('id') == pic_id and included.get('type') == 'pictures': | |
pic_url = included.get('attributes', {}).get('original', '') | |
if pic_url: | |
photos.append({ | |
'small': pic_url, | |
'medium': pic_url, | |
'large': pic_url, | |
'full': pic_url | |
}) | |
# Build the normalized pet object | |
normalized_pet = { | |
'id': pet['id'], | |
'source': 'rescuegroups', | |
'organization_id': attributes.get('orgId', ''), | |
'url': attributes.get('url', ''), | |
'type': attributes.get('species', {}).get('singular', ''), | |
'species': attributes.get('species', {}).get('singular', ''), | |
'age': attributes.get('ageGroup', ''), | |
'gender': attributes.get('sex', ''), | |
'size': attributes.get('sizeGroup', ''), | |
'name': attributes.get('name', ''), | |
'description': attributes.get('descriptionText', ''), | |
'status': 'adoptable', | |
'breeds': breeds, | |
'colors': colors, | |
'contact': contact, | |
'photos': photos, | |
'environment': environment, | |
'attributes': pet_attributes, | |
'published_at': attributes.get('createdDate', ''), | |
'distance': attributes.get('distance', None) | |
} | |
normalized['animals'].append(normalized_pet) | |
return normalized | |
# Function to search pets on both APIs and combine results | |
def search_all_apis(params): | |
# Initialize results containers | |
results = {"animals": []} | |
petfinder_count = 0 | |
rescuegroups_count = 0 | |
# Search Petfinder | |
petfinder_results = search_petfinder(params) | |
if petfinder_results and 'animals' in petfinder_results: | |
results['animals'].extend(petfinder_results['animals']) | |
petfinder_count = len(petfinder_results['animals']) | |
# Search RescueGroups.org | |
rescuegroups_results = search_rescuegroups(params) | |
if rescuegroups_results and 'animals' in rescuegroups_results: | |
results['animals'].extend(rescuegroups_results['animals']) | |
rescuegroups_count = len(rescuegroups_results['animals']) | |
# Sort by distance if location was provided | |
if params.get('location') and results['animals']: | |
results['animals'] = sorted( | |
results['animals'], | |
key=lambda x: x.get('distance', float('inf')) if x.get('distance') is not None else float('inf') | |
) | |
return results, petfinder_count, rescuegroups_count | |
# Function to get pet details from Petfinder | |
def get_petfinder_details(pet_id): | |
token = get_petfinder_token() | |
if not token: | |
return None | |
url = f"https://api.petfinder.com/v2/animals/{pet_id}" | |
headers = {"Authorization": f"Bearer {token}"} | |
try: | |
response = requests.get(url, headers=headers) | |
response.raise_for_status() | |
pet = response.json()['animal'] | |
pet['source'] = 'petfinder' | |
return pet | |
except requests.exceptions.RequestException as e: | |
st.session_state.petfinder_status = {'success': False, 'message': f'Details error: {str(e)}'} | |
return None | |
# Function to get pet details from RescueGroups.org | |
def get_rescuegroups_details(pet_id): | |
api_key = get_rescuegroups_token() | |
if not api_key: | |
return None | |
url = f"https://api.rescuegroups.org/v5/public/animals/{pet_id}" | |
headers = { | |
"Authorization": api_key, | |
"Content-Type": "application/vnd.api+json" | |
} | |
try: | |
response = requests.get(url, headers=headers) | |
response.raise_for_status() | |
result = response.json() | |
if 'data' not in result: | |
return None | |
# Normalize to match Petfinder format | |
normalized_results = {"animals": []} | |
normalized = normalize_rescuegroups_results(result) | |
if normalized['animals']: | |
pet = normalized['animals'][0] | |
return pet | |
return None | |
except requests.exceptions.RequestException as e: | |
st.session_state.rescuegroups_status = {'success': False, 'message': f'Details error: {str(e)}'} | |
return None | |
# Function to get pet details from either API based on source | |
def get_pet_details(pet_id, source): | |
if source == 'petfinder': | |
return get_petfinder_details(pet_id) | |
elif source == 'rescuegroups': | |
return get_rescuegroups_details(pet_id) | |
return None | |
# Function to get breeds from Petfinder | |
def get_breeds(animal_type): | |
token = get_petfinder_token() | |
if not token: | |
return [] | |
url = f"https://api.petfinder.com/v2/types/{animal_type}/breeds" | |
headers = {"Authorization": f"Bearer {token}"} | |
try: | |
response = requests.get(url, headers=headers) | |
response.raise_for_status() | |
return [breed['name'] for breed in response.json()['breeds']] | |
except requests.exceptions.RequestException as e: | |
st.session_state.petfinder_status = {'success': False, 'message': f'Breeds error: {str(e)}'} | |
return [] | |
# Function to generate pet compatibility message | |
def get_compatibility_message(pet): | |
messages = [] | |
# Check for kids | |
if 'environment' in pet and 'children' in pet['environment'] and pet['environment']['children'] is not None: | |
if pet['environment']['children']: | |
messages.append("✅ Good with children") | |
else: | |
messages.append("❌ Not recommended for homes with children") | |
# Check for dogs | |
if 'environment' in pet and 'dogs' in pet['environment'] and pet['environment']['dogs'] is not None: | |
if pet['environment']['dogs']: | |
messages.append("✅ Good with dogs") | |
else: | |
messages.append("❌ Not recommended for homes with dogs") | |
# Check for cats | |
if 'environment' in pet and 'cats' in pet['environment'] and pet['environment']['cats'] is not None: | |
if pet['environment']['cats']: | |
messages.append("✅ Good with cats") | |
else: | |
messages.append("❌ Not recommended for homes with cats") | |
# Handling care needs | |
if 'attributes' in pet: | |
if 'special_needs' in pet['attributes'] and pet['attributes']['special_needs']: | |
messages.append("⚠️ Has special needs") | |
if 'house_trained' in pet['attributes'] and pet['attributes']['house_trained']: | |
messages.append("✅ House-trained") | |
elif 'house_trained' in pet['attributes']: | |
messages.append("❌ Not house-trained") | |
if 'shots_current' in pet['attributes'] and pet['attributes']['shots_current']: | |
messages.append("✅ Vaccinations up to date") | |
if 'spayed_neutered' in pet['attributes'] and pet['attributes']['spayed_neutered']: | |
messages.append("✅ Spayed/neutered") | |
return messages | |
# Function to display pet card with source indicator | |
def display_pet_card(pet, is_favorite=False, context="search", tab_id="tab1"): | |
col1, col2 = st.columns([1, 2]) | |
with col1: | |
if pet['photos'] and len(pet['photos']) > 0: | |
st.image(pet['photos'][0]['medium'], use_container_width=True) | |
else: | |
st.image("https://via.placeholder.com/300x300?text=No+Image", use_container_width=True) | |
with col2: | |
# Pet name and source indicator | |
source_tag = '<span class="source-tag">Petfinder</span>' if pet['source'] == 'petfinder' else '<span class="source-tag">RescueGroups</span>' | |
st.markdown(f"<div class='pet-name'>{pet['name']} {source_tag}</div>", unsafe_allow_html=True) | |
# Tags | |
tags_html = "" | |
if pet['status'] == 'adoptable': | |
tags_html += "<span class='tag' style='background-color: #808080;'>Adoptable</span> " | |
else: | |
tags_html += f"<span class='tag' style='background-color: #808080;'>{pet['status'].title()}</span> " | |
if pet['age']: | |
tags_html += f"<span class='tag'>{pet['age']}</span> " | |
if pet['gender']: | |
tags_html += f"<span class='tag'>{pet['gender']}</span> " | |
if pet['size']: | |
tags_html += f"<span class='tag'>{pet['size']}</span> " | |
st.markdown(f"<div>{tags_html}</div>", unsafe_allow_html=True) | |
st.markdown("<div class='pet-details'>", unsafe_allow_html=True) | |
if pet['breeds']['primary']: | |
breed_text = pet['breeds']['primary'] | |
if pet['breeds']['secondary']: | |
breed_text += f" & {pet['breeds']['secondary']}" | |
if pet['breeds']['mixed']: | |
breed_text += " (Mixed)" | |
st.markdown(f"<strong>Breed:</strong> {breed_text}", unsafe_allow_html=True) | |
if pet['colors']['primary'] or pet['colors']['secondary'] or pet['colors']['tertiary']: | |
colors = [c for c in [pet['colors']['primary'], pet['colors']['secondary'], pet['colors']['tertiary']] if c] | |
st.markdown(f"<strong>Colors:</strong> {', '.join(colors)}", unsafe_allow_html=True) | |
if 'location' in pet and pet['contact']['address']['city'] and pet['contact']['address']['state']: | |
st.markdown(f"<strong>Location:</strong> {pet['contact']['address']['city']}, {pet['contact']['address']['state']}", unsafe_allow_html=True) | |
st.markdown("</div>", unsafe_allow_html=True) | |
if pet['description']: | |
st.markdown(f"<div class='pet-description'>{pet['description'][:300]}{'...' if len(pet['description']) > 300 else ''}</div>", unsafe_allow_html=True) | |
col1, col2 = st.columns(2) | |
with col1: | |
if st.button("View Details", key=f"details_{tab_id}_{context}_{pet['source']}_{pet['id']}"): | |
st.session_state.selected_pet = pet['id'] | |
st.session_state.selected_pet_source = pet['source'] | |
st.rerun() | |
with col2: | |
if not is_favorite: | |
if st.button("Add to Favorites", key=f"fav_{tab_id}_{context}_{pet['source']}_{pet['id']}"): | |
if pet['id'] not in [p['id'] for p in st.session_state.favorites]: | |
st.session_state.favorites.append(pet) | |
st.success(f"Added {pet['name']} to favorites!") | |
st.rerun() | |
else: | |
if st.button("Remove from Favorites", key=f"unfav_{tab_id}_{context}_{pet['source']}_{pet['id']}"): | |
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']] | |
st.success(f"Removed {pet['name']} from favorites!") | |
st.rerun() | |
# Function to display pet details page | |
def display_pet_details(pet_id, pet_source, context="search", tab_id="tab1"): | |
pet = get_pet_details(pet_id, pet_source) | |
if not pet: | |
st.error("Unable to retrieve pet details. Please try again.") | |
return | |
# Back button | |
if st.button("← Back to Search Results", key=f"back_{tab_id}_{context}_{pet_source}_{pet_id}"): | |
st.session_state.selected_pet = None | |
st.session_state.selected_pet_source = None | |
st.rerun() | |
# Pet name and source indicator | |
source_tag = '<span class="source-tag">Petfinder</span>' if pet_source == 'petfinder' else '<span class="source-tag">RescueGroups</span>' | |
st.markdown(f"<h1 class='main-header'>{pet['name']} {source_tag}</h1>", unsafe_allow_html=True) | |
# Pet status | |
status_color = "#c8e6c9" if pet['status'] == 'adoptable' else "#ffcdd2" | |
st.markdown(f"<div style='text-align: center;'><span class='tag' style='background-color: {status_color}; font-size: 1rem;'>{pet['status'].title()}</span></div>", unsafe_allow_html=True) | |
# Pet photos | |
if pet['photos'] and len(pet['photos']) > 0: | |
photo_cols = st.columns(min(3, len(pet['photos']))) | |
for i, col in enumerate(photo_cols): | |
if i < len(pet['photos']): | |
col.image(pet['photos'][i]['large'], use_container_width=True) | |
else: | |
st.image("https://via.placeholder.com/500x300?text=No+Image", use_container_width=True) | |
# Pet details | |
col1, col2 = st.columns(2) | |
with col1: | |
st.markdown("### Details") | |
# Fix the breed line | |
breed_text = pet['breeds']['primary'] | |
if pet['breeds']['secondary']: | |
breed_text += f" & {pet['breeds']['secondary']}" | |
if pet['breeds']['mixed']: | |
breed_text += " (Mixed)" | |
details = [ | |
f"**Type:** {pet['type']}", | |
f"**Breed:** {breed_text}", | |
f"**Age:** {pet['age']}", | |
f"**Gender:** {pet['gender']}", | |
f"**Size:** {pet['size']}" | |
] | |
# Fix the colors line as well, to be safe | |
colors = [c for c in [pet['colors']['primary'], pet['colors']['secondary'], pet['colors']['tertiary']] if c] | |
if colors: | |
details.append(f"**Colors:** {', '.join(colors)}") | |
for detail in details: | |
st.markdown(detail) | |
with col2: | |
st.markdown("### Compatibility") | |
compatibility = get_compatibility_message(pet) | |
for msg in compatibility: | |
st.markdown(msg) | |
# Description | |
if pet['description']: | |
st.markdown("### About") | |
st.markdown(f"<div class='pet-description'>{pet['description']}</div>", unsafe_allow_html=True) | |
# Contact information | |
st.markdown("### Adoption Information") | |
# Organization info | |
if pet['organization_id']: | |
st.markdown(f"**Organization ID:** {pet['organization_id']}") | |
# Contact details | |
contact_info = [] | |
if pet['contact']['email']: | |
contact_info.append(f"**Email:** {pet['contact']['email']}") | |
if pet['contact']['phone']: | |
contact_info.append(f"**Phone:** {pet['contact']['phone']}") | |
if pet['contact']['address']['city'] and pet['contact']['address']['state']: | |
contact_info.append(f"**Location:** {pet['contact']['address']['city']}, {pet['contact']['address']['state']} {pet['contact']['address']['postcode'] or ''}") | |
for info in contact_info: | |
st.markdown(info) | |
# URL to pet on source website | |
if pet['url']: | |
st.markdown(f"[View on {'Petfinder' if pet_source == 'petfinder' else 'RescueGroups'}]({pet['url']})") | |
# Add to favorites | |
is_favorite = pet['id'] in [p['id'] for p in st.session_state.favorites] | |
if not is_favorite: | |
if st.button("Add to Favorites", key=f"add_fav_{tab_id}_{context}_{pet_source}_{pet['id']}"): | |
if pet['id'] not in [p['id'] for p in st.session_state.favorites]: | |
st.session_state.favorites.append(pet) | |
st.success(f"Added {pet['name']} to favorites!") | |
st.rerun() | |
else: | |
if st.button("Remove from Favorites", key=f"remove_fav_{tab_id}_{context}_{pet_source}_{pet['id']}"): | |
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']] | |
st.success(f"Removed {pet['name']} from favorites!") | |
st.rerun() | |
# Main interface components | |
st.markdown("<h1 class='main-header'>PetMatch 🐾</h1>", unsafe_allow_html=True) | |
st.markdown("<h2 class='sub-header'>Find Your Perfect Pet Companion</h2>", unsafe_allow_html=True) | |
# Display API connection status | |
col1, col2 = st.columns(2) | |
with col1: | |
status_class = "api-success" if st.session_state.petfinder_status['success'] else "api-error" | |
st.markdown(f"<div class='api-status {status_class}'>Petfinder API: {st.session_state.petfinder_status['message']}</div>", unsafe_allow_html=True) | |
with col2: | |
status_class = "api-success" if st.session_state.rescuegroups_status['success'] else "api-error" | |
st.markdown(f"<div class='api-status {status_class}'>RescueGroups API: {st.session_state.rescuegroups_status['message']}</div>", unsafe_allow_html=True) | |
# Create tabs for search and favorites | |
tab1, tab2 = st.tabs(["Search", "My Favorites"]) | |
# Search tab | |
with tab1: | |
if st.session_state.selected_pet: | |
display_pet_details(st.session_state.selected_pet, st.session_state.selected_pet_source, context="search", tab_id="tab1") | |
else: | |
# Search form | |
with st.expander("Search Options", expanded=True): | |
search_col1, search_col2 = st.columns(2) | |
with search_col1: | |
location = st.text_input("Location (ZIP code or City, State)", key="location") | |
distance = st.slider("Distance (miles)", 10, 500, 100, 10, key="distance") | |
pet_type = st.selectbox( | |
"Animal Type", | |
["Dog", "Cat", "Rabbit", "Small & Furry", "Horse", "Bird", "Scales, Fins & Other", "Barnyard"], | |
key="type" | |
) | |
breed_list = get_breeds(pet_type.lower().split(" ")[0]) if pet_type else [] | |
breed = st.selectbox("Breed", ["Any"] + breed_list, key="breed") | |
with search_col2: | |
age = st.multiselect( | |
"Age", | |
["Baby", "Young", "Adult", "Senior"], | |
key="ages" | |
) | |
gender = st.multiselect( | |
"Gender", | |
["Male", "Female"], | |
key="genders" | |
) | |
size = st.multiselect( | |
"Size", | |
["Small", "Medium", "Large", "XLarge"], | |
key="sizes" | |
) | |
st.markdown("### Pet Compatibility") | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
good_with_children = st.checkbox("Good with children", key="children") | |
with col2: | |
good_with_dogs = st.checkbox("Good with dogs", key="dogs") | |
with col3: | |
good_with_cats = st.checkbox("Good with cats", key="cats") | |
# Search button | |
if st.button("Search for Pets"): | |
# Build search parameters | |
params = { | |
"type": pet_type.lower().split(" ")[0], | |
"location": location, | |
"distance": distance, | |
"page": st.session_state.page, | |
"limit": 100, # Maximum allowed by Petfinder | |
"sort": "distance" if location else "recent" | |
} | |
# Add optional parameters | |
if breed and breed != "Any": | |
params["breed"] = breed | |
if age: | |
params["age"] = ",".join(age) | |
if gender: | |
params["gender"] = ",".join(gender) | |
if size: | |
params["size"] = ",".join(size) | |
if good_with_children: | |
params["good_with_children"] = True | |
if good_with_dogs: | |
params["good_with_dogs"] = True | |
if good_with_cats: | |
params["good_with_cats"] = True | |
# Perform search | |
with st.spinner("Searching for pets..."): | |
results, petfinder_count, rescuegroups_count = search_all_apis(params) | |
st.session_state.search_results = results | |
# Show source counts | |
st.markdown(f"Found {len(results['animals'])} pets ({petfinder_count} from Petfinder, {rescuegroups_count} from RescueGroups)") | |
# Display search results | |
if st.session_state.search_results: | |
# Display results | |
for pet in st.session_state.search_results['animals']: | |
st.markdown("<hr>", unsafe_allow_html=True) | |
display_pet_card(pet, context="search", tab_id="tab1") | |
# Pagination is not currently implemented | |
# Future enhancement: Add pagination controls | |
# Favorites tab | |
with tab2: | |
if st.session_state.selected_pet and st.session_state.selected_pet_source: | |
display_pet_details(st.session_state.selected_pet, st.session_state.selected_pet_source, context="favorites", tab_id="tab2") | |
else: | |
if not st.session_state.favorites: | |
st.info("You haven't added any pets to your favorites yet.") | |
else: | |
st.markdown(f"### Your Favorites ({len(st.session_state.favorites)} pets)") | |
for pet in st.session_state.favorites: | |
st.markdown("<hr>", unsafe_allow_html=True) | |
display_pet_card(pet, is_favorite=True, context="favorites", tab_id="tab2") | |
# Footer | |
st.markdown(""" | |
<div style="text-align: center; margin-top: 3rem; opacity: 0.7; font-size: 0.8rem;"> | |
<p>PetMatch connects to Petfinder and RescueGroups.org APIs to help you find adoptable pets.</p> | |
<p>© 2025 PetMatch. Not affiliated with Petfinder or RescueGroups.</p> | |
</div> | |
""", unsafe_allow_html=True) | |
# Initialize API tokens on app start | |
if not st.session_state.petfinder_access_token: | |
get_petfinder_token() | |
if not st.session_state.rescuegroups_access_token: | |
get_rescuegroups_token() |