Spaces:
Running
Running
| import streamlit as st | |
| import requests | |
| import pandas as pd | |
| import json | |
| from datetime import datetime | |
| import time | |
| import os | |
| # Set page configuration | |
| st.set_page_config( | |
| page_title="PetMatch - Find Your Perfect Pet", | |
| page_icon="🐾", | |
| layout="wide" | |
| ) | |
| # 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: #f0f0f0; | |
| border-radius: 20px; | |
| padding: 0.2rem 0.6rem; | |
| margin-right: 0.3rem; | |
| font-size: 0.8rem; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Initialize session state variables | |
| if 'access_token' not in st.session_state: | |
| st.session_state.access_token = None | |
| if 'token_expires' not in st.session_state: | |
| st.session_state.token_expires = 0 | |
| 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 'page' not in st.session_state: | |
| st.session_state.page = 1 | |
| if 'favorites' not in st.session_state: | |
| st.session_state.favorites = [] | |
| # Function to get access token | |
| def get_access_token(): | |
| # Check if token is still valid | |
| if st.session_state.access_token and time.time() < st.session_state.token_expires: | |
| return st.session_state.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.error("⚠️ Petfinder API credentials are missing. Please set them in your environment variables or Streamlit secrets.") | |
| 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.access_token = token_data['access_token'] | |
| st.session_state.token_expires = time.time() + token_data['expires_in'] - 60 # Buffer of 60 seconds | |
| return st.session_state.access_token | |
| except requests.exceptions.RequestException as e: | |
| st.error(f"⚠️ Error getting access token: {str(e)}") | |
| return None | |
| # Function to search pets | |
| def search_pets(params): | |
| token = get_access_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() | |
| return response.json() | |
| except requests.exceptions.RequestException as e: | |
| st.error(f"⚠️ Error searching pets: {str(e)}") | |
| return None | |
| # Function to get breeds | |
| def get_breeds(animal_type): | |
| token = get_access_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.error(f"⚠️ Error getting breeds: {str(e)}") | |
| return [] | |
| # Function to get organizations | |
| def get_organizations(location): | |
| token = get_access_token() | |
| if not token: | |
| return [] | |
| url = "https://api.petfinder.com/v2/organizations" | |
| headers = {"Authorization": f"Bearer {token}"} | |
| params = {"location": location, "distance": 100, "limit": 100} | |
| try: | |
| response = requests.get(url, headers=headers, params=params) | |
| response.raise_for_status() | |
| return [(org['id'], org['name']) for org in response.json()['organizations']] | |
| except requests.exceptions.RequestException as e: | |
| st.error(f"⚠️ Error getting organizations: {str(e)}") | |
| return [] | |
| # Function to get pet details | |
| def get_pet_details(pet_id): | |
| token = get_access_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() | |
| return response.json()['animal'] | |
| except requests.exceptions.RequestException as e: | |
| st.error(f"⚠️ Error getting pet details: {str(e)}") | |
| return None | |
| # Function to format pet card | |
| def display_pet_card(pet, is_favorite=False): | |
| col1, col2 = st.columns([1, 2]) | |
| with col1: | |
| if pet['photos'] and len(pet['photos']) > 0: | |
| st.image(pet['photos'][0]['medium'], use_column_width=True) | |
| else: | |
| st.image("https://via.placeholder.com/300x300?text=No+Image", use_column_width=True) | |
| with col2: | |
| st.markdown(f"<div class='pet-name'>{pet['name']}</div>", unsafe_allow_html=True) | |
| # Tags | |
| tags_html = "" | |
| if pet['status'] == 'adoptable': | |
| tags_html += "<span class='tag' style='background-color: #c8e6c9;'>Adoptable</span> " | |
| else: | |
| tags_html += f"<span class='tag' style='background-color: #ffcdd2;'>{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'][:150]}{'...' if len(pet['description']) > 150 else ''}</div>", unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("View Details", key=f"details_{pet['id']}"): | |
| st.session_state.selected_pet = pet['id'] | |
| with col2: | |
| if not is_favorite: | |
| if st.button("Add to Favorites", key=f"fav_{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.experimental_rerun() | |
| else: | |
| if st.button("Remove from Favorites", key=f"unfav_{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.experimental_rerun() | |
| # Function to generate pet compatibility message | |
| def get_compatibility_message(pet): | |
| messages = [] | |
| # Check for kids | |
| if '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 '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 '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 pet['attributes']: | |
| 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 details page | |
| def display_pet_details(pet_id): | |
| pet = get_pet_details(pet_id) | |
| if not pet: | |
| st.error("Unable to retrieve pet details. Please try again.") | |
| return | |
| # Back button | |
| if st.button("← Back to Search Results"): | |
| st.session_state.selected_pet = None | |
| st.experimental_rerun() | |
| # Pet name and status | |
| st.markdown(f"<h1 class='main-header'>{pet['name']}</h1>", unsafe_allow_html=True) | |
| 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_column_width=True) | |
| else: | |
| st.image("https://via.placeholder.com/500x300?text=No+Image", use_column_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(pet['description']) | |
| # Contact information | |
| st.markdown("### Adoption Information") | |
| # Organization info | |
| if pet['organization_id']: | |
| st.markdown(f"**Organization:** {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 Petfinder | |
| if pet['url']: | |
| st.markdown(f"[View on Petfinder]({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"): | |
| st.session_state.favorites.append(pet) | |
| st.success(f"Added {pet['name']} to favorites!") | |
| st.experimental_rerun() | |
| else: | |
| if st.button("Remove from Favorites"): | |
| 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.experimental_rerun() | |
| # Main app | |
| def main(): | |
| # Title | |
| st.markdown("<h1 class='main-header'>🐾 PetMatch</h1>", unsafe_allow_html=True) | |
| st.markdown("<p class='sub-header'>Find your perfect pet companion</p>", unsafe_allow_html=True) | |
| # Create tabs | |
| tab1, tab2, tab3 = st.tabs(["Search", "Favorites", "About"]) | |
| with tab1: | |
| # If a pet is selected, show details | |
| if st.session_state.selected_pet: | |
| display_pet_details(st.session_state.selected_pet) | |
| else: | |
| # Search form | |
| with st.expander("Search Options", expanded=True): | |
| with st.form("pet_search_form"): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| animal_type = st.selectbox( | |
| "Animal Type", | |
| ["Dog", "Cat", "Rabbit", "Small & Furry", "Horse", "Bird", "Scales, Fins & Other", "Barnyard"] | |
| ) | |
| location = st.text_input("Location (ZIP code or City, State)", "") | |
| distance = st.slider("Distance (miles)", min_value=10, max_value=500, value=50, step=10) | |
| with col2: | |
| age_options = ["", "Baby", "Young", "Adult", "Senior"] | |
| age = st.selectbox("Age", age_options) | |
| size_options = ["", "Small", "Medium", "Large", "XLarge"] | |
| size = st.selectbox("Size", size_options) | |
| gender_options = ["", "Male", "Female"] | |
| gender = st.selectbox("Gender", gender_options) | |
| good_with_children = st.checkbox("Good with children") | |
| good_with_dogs = st.checkbox("Good with dogs") | |
| good_with_cats = st.checkbox("Good with cats") | |
| house_trained = st.checkbox("House-trained") | |
| special_needs = st.checkbox("Special needs") | |
| submitted = st.form_submit_button("Search") | |
| if submitted: | |
| # Build search parameters | |
| params = { | |
| "type": animal_type.split(" ")[0], # Take first word for types like "Small & Furry" | |
| "location": location, | |
| "distance": distance, | |
| "status": "adoptable", | |
| "sort": "distance", | |
| "limit": 100 | |
| } | |
| if age and age != "": | |
| params["age"] = age | |
| if size and size != "": | |
| params["size"] = size | |
| if gender and gender != "": | |
| params["gender"] = gender | |
| # Add advanced filters | |
| if good_with_children: | |
| params["good_with_children"] = 1 | |
| if good_with_dogs: | |
| params["good_with_dogs"] = 1 | |
| if good_with_cats: | |
| params["good_with_cats"] = 1 | |
| if house_trained: | |
| params["house_trained"] = 1 | |
| if special_needs: | |
| params["special_needs"] = 1 | |
| # Perform search | |
| results = search_pets(params) | |
| if results and 'animals' in results: | |
| st.session_state.search_results = results | |
| st.session_state.page = 1 | |
| st.success(f"Found {len(results['animals'])} pets!") | |
| else: | |
| st.error("No pets found with those criteria. Try expanding your search.") | |
| # Display search results | |
| if st.session_state.search_results and 'animals' in st.session_state.search_results: | |
| st.markdown("### Search Results") | |
| # Pagination | |
| results = st.session_state.search_results['animals'] | |
| total_pages = (len(results) + 9) // 10 # 10 items per page | |
| # Display page selector | |
| if total_pages > 1: | |
| col1, col2, col3 = st.columns([1, 3, 1]) | |
| with col2: | |
| page = st.slider("Page", 1, total_pages, st.session_state.page) | |
| if page != st.session_state.page: | |
| st.session_state.page = page | |
| # Display pets for current page | |
| start_idx = (st.session_state.page - 1) * 10 | |
| end_idx = min(start_idx + 10, len(results)) | |
| for pet in results[start_idx:end_idx]: | |
| st.markdown("---") | |
| display_pet_card(pet) | |
| with tab2: | |
| st.markdown("### Your Favorite Pets") | |
| if not st.session_state.favorites: | |
| st.info("You haven't added any pets to your favorites yet. Start searching to find your perfect match!") | |
| else: | |
| for pet in st.session_state.favorites: | |
| st.markdown("---") | |
| display_pet_card(pet, is_favorite=True) | |
| with tab3: | |
| st.markdown("### About PetMatch") | |
| st.markdown(""" | |
| PetMatch helps you find your perfect pet companion from thousands of adoptable animals across the country. | |
| **How to use PetMatch:** | |
| 1. Search for pets based on your preferences and location | |
| 2. Browse through the results and click "View Details" to learn more about each pet | |
| 3. Add pets to your favorites to keep track of the ones you're interested in | |
| 4. Contact the shelter or rescue organization directly using the provided information | |
| **Data Source:** | |
| PetMatch uses the Petfinder API to provide up-to-date information on adoptable pets. Petfinder is North America's largest adoption website with hundreds of thousands of adoptable pets listed by more than 11,500 animal shelters and rescue organizations. | |
| **Privacy:** | |
| PetMatch does not store any personal information or search history. Your favorites are stored locally in your browser and are not shared with any third parties. | |
| """) | |
| st.markdown("### Made with ❤️ by Claude") | |
| if __name__ == "__main__": | |
| main() |