Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,8 +1,5 @@
|
|
1 |
import streamlit as st
|
2 |
import requests
|
3 |
-
import pandas as pd
|
4 |
-
import json
|
5 |
-
from datetime import datetime
|
6 |
import time
|
7 |
import os
|
8 |
|
@@ -57,47 +54,31 @@ st.markdown("""
|
|
57 |
""", unsafe_allow_html=True)
|
58 |
|
59 |
# Initialize session state variables
|
60 |
-
|
61 |
-
st.session_state
|
62 |
-
|
63 |
-
st.session_state.token_expires = 0
|
64 |
-
if 'search_results' not in st.session_state:
|
65 |
-
st.session_state.search_results = None
|
66 |
-
if 'selected_pet' not in st.session_state:
|
67 |
-
st.session_state.selected_pet = None
|
68 |
-
if 'page' not in st.session_state:
|
69 |
-
st.session_state.page = 1
|
70 |
-
if 'favorites' not in st.session_state:
|
71 |
-
st.session_state.favorites = []
|
72 |
|
73 |
# Function to get access token
|
74 |
def get_access_token():
|
75 |
-
# Check if token is still valid
|
76 |
if st.session_state.access_token and time.time() < st.session_state.token_expires:
|
77 |
return st.session_state.access_token
|
78 |
-
|
79 |
-
# Get API credentials from environment variables or secrets
|
80 |
api_key = os.environ.get('PETFINDER_API_KEY') or st.secrets.get('PETFINDER_API_KEY')
|
81 |
api_secret = os.environ.get('PETFINDER_API_SECRET') or st.secrets.get('PETFINDER_API_SECRET')
|
82 |
-
|
83 |
if not api_key or not api_secret:
|
84 |
-
st.error("⚠️ Petfinder API credentials are missing.
|
85 |
return None
|
86 |
-
|
87 |
-
# Get new token
|
88 |
url = "https://api.petfinder.com/v2/oauth2/token"
|
89 |
-
data = {
|
90 |
-
|
91 |
-
"client_id": api_key,
|
92 |
-
"client_secret": api_secret
|
93 |
-
}
|
94 |
-
|
95 |
try:
|
96 |
response = requests.post(url, data=data)
|
97 |
response.raise_for_status()
|
98 |
token_data = response.json()
|
99 |
st.session_state.access_token = token_data['access_token']
|
100 |
-
st.session_state.token_expires = time.time() + token_data['expires_in'] - 60
|
101 |
return st.session_state.access_token
|
102 |
except requests.exceptions.RequestException as e:
|
103 |
st.error(f"⚠️ Error getting access token: {str(e)}")
|
@@ -108,10 +89,8 @@ def search_pets(params):
|
|
108 |
token = get_access_token()
|
109 |
if not token:
|
110 |
return None
|
111 |
-
|
112 |
url = "https://api.petfinder.com/v2/animals"
|
113 |
headers = {"Authorization": f"Bearer {token}"}
|
114 |
-
|
115 |
try:
|
116 |
response = requests.get(url, headers=headers, params=params)
|
117 |
response.raise_for_status()
|
@@ -120,50 +99,13 @@ def search_pets(params):
|
|
120 |
st.error(f"⚠️ Error searching pets: {str(e)}")
|
121 |
return None
|
122 |
|
123 |
-
# Function to get breeds
|
124 |
-
def get_breeds(animal_type):
|
125 |
-
token = get_access_token()
|
126 |
-
if not token:
|
127 |
-
return []
|
128 |
-
|
129 |
-
url = f"https://api.petfinder.com/v2/types/{animal_type}/breeds"
|
130 |
-
headers = {"Authorization": f"Bearer {token}"}
|
131 |
-
|
132 |
-
try:
|
133 |
-
response = requests.get(url, headers=headers)
|
134 |
-
response.raise_for_status()
|
135 |
-
return [breed['name'] for breed in response.json()['breeds']]
|
136 |
-
except requests.exceptions.RequestException as e:
|
137 |
-
st.error(f"⚠️ Error getting breeds: {str(e)}")
|
138 |
-
return []
|
139 |
-
|
140 |
-
# Function to get organizations
|
141 |
-
def get_organizations(location):
|
142 |
-
token = get_access_token()
|
143 |
-
if not token:
|
144 |
-
return []
|
145 |
-
|
146 |
-
url = "https://api.petfinder.com/v2/organizations"
|
147 |
-
headers = {"Authorization": f"Bearer {token}"}
|
148 |
-
params = {"location": location, "distance": 100, "limit": 100}
|
149 |
-
|
150 |
-
try:
|
151 |
-
response = requests.get(url, headers=headers, params=params)
|
152 |
-
response.raise_for_status()
|
153 |
-
return [(org['id'], org['name']) for org in response.json()['organizations']]
|
154 |
-
except requests.exceptions.RequestException as e:
|
155 |
-
st.error(f"⚠️ Error getting organizations: {str(e)}")
|
156 |
-
return []
|
157 |
-
|
158 |
# Function to get pet details
|
159 |
def get_pet_details(pet_id):
|
160 |
token = get_access_token()
|
161 |
if not token:
|
162 |
return None
|
163 |
-
|
164 |
url = f"https://api.petfinder.com/v2/animals/{pet_id}"
|
165 |
headers = {"Authorization": f"Bearer {token}"}
|
166 |
-
|
167 |
try:
|
168 |
response = requests.get(url, headers=headers)
|
169 |
response.raise_for_status()
|
@@ -175,357 +117,74 @@ def get_pet_details(pet_id):
|
|
175 |
# Function to format pet card
|
176 |
def display_pet_card(pet, is_favorite=False):
|
177 |
col1, col2 = st.columns([1, 2])
|
178 |
-
|
179 |
with col1:
|
180 |
if pet['photos'] and len(pet['photos']) > 0:
|
181 |
st.image(pet['photos'][0]['medium'], use_container_width=True)
|
182 |
else:
|
183 |
st.image("https://via.placeholder.com/300x300?text=No+Image", use_container_width=True)
|
184 |
-
|
185 |
with col2:
|
186 |
st.markdown(f"<div class='pet-name'>{pet['name']}</div>", unsafe_allow_html=True)
|
187 |
-
|
188 |
-
# Tags
|
189 |
tags_html = ""
|
190 |
if pet['status'] == 'adoptable':
|
191 |
tags_html += "<span class='tag' style='background-color: #c8e6c9;'>Adoptable</span> "
|
192 |
else:
|
193 |
tags_html += f"<span class='tag' style='background-color: #ffcdd2;'>{pet['status'].title()}</span> "
|
194 |
-
|
195 |
if pet['age']:
|
196 |
tags_html += f"<span class='tag'>{pet['age']}</span> "
|
197 |
if pet['gender']:
|
198 |
tags_html += f"<span class='tag'>{pet['gender']}</span> "
|
199 |
if pet['size']:
|
200 |
tags_html += f"<span class='tag'>{pet['size']}</span> "
|
201 |
-
|
202 |
st.markdown(f"<div>{tags_html}</div>", unsafe_allow_html=True)
|
203 |
-
|
204 |
-
st.markdown("<div class='pet-details'>", unsafe_allow_html=True)
|
205 |
-
if pet['breeds']['primary']:
|
206 |
-
breed_text = pet['breeds']['primary']
|
207 |
-
if pet['breeds']['secondary']:
|
208 |
-
breed_text += f" & {pet['breeds']['secondary']}"
|
209 |
-
if pet['breeds']['mixed']:
|
210 |
-
breed_text += " (Mixed)"
|
211 |
-
st.markdown(f"<strong>Breed:</strong> {breed_text}", unsafe_allow_html=True)
|
212 |
-
|
213 |
-
if pet['colors']['primary'] or pet['colors']['secondary'] or pet['colors']['tertiary']:
|
214 |
-
colors = [c for c in [pet['colors']['primary'], pet['colors']['secondary'], pet['colors']['tertiary']] if c]
|
215 |
-
st.markdown(f"<strong>Colors:</strong> {', '.join(colors)}", unsafe_allow_html=True)
|
216 |
-
|
217 |
-
if 'location' in pet and pet['contact']['address']['city'] and pet['contact']['address']['state']:
|
218 |
-
st.markdown(f"<strong>Location:</strong> {pet['contact']['address']['city']}, {pet['contact']['address']['state']}", unsafe_allow_html=True)
|
219 |
-
|
220 |
-
st.markdown("</div>", unsafe_allow_html=True)
|
221 |
-
|
222 |
-
if pet['description']:
|
223 |
-
st.markdown(f"<div class='pet-description'>{pet['description'][:150]}{'...' if len(pet['description']) > 150 else ''}</div>", unsafe_allow_html=True)
|
224 |
-
|
225 |
-
col1, col2 = st.columns(2)
|
226 |
-
with col1:
|
227 |
-
if st.button("View Details", key=f"details_{pet['id']}"):
|
228 |
-
if st.session_state.selected_pet != pet['id']:
|
229 |
-
st.session_state.selected_pet = pet['id']
|
230 |
-
st.experimental_rerun() # Force immediate rerun
|
231 |
|
232 |
-
|
233 |
-
if
|
234 |
-
|
235 |
-
|
236 |
-
st.session_state.favorites.append(pet)
|
237 |
-
st.success(f"Added {pet['name']} to favorites!")
|
238 |
-
st.experimental_rerun()
|
239 |
-
else:
|
240 |
-
if st.button("Remove from Favorites", key=f"unfav_{pet['id']}"):
|
241 |
-
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']]
|
242 |
-
st.success(f"Removed {pet['name']} from favorites!")
|
243 |
-
st.experimental_rerun()
|
244 |
-
|
245 |
-
# Function to generate pet compatibility message
|
246 |
-
def get_compatibility_message(pet):
|
247 |
-
messages = []
|
248 |
-
|
249 |
-
# Check for kids
|
250 |
-
if 'children' in pet['environment'] and pet['environment']['children'] is not None:
|
251 |
-
if pet['environment']['children']:
|
252 |
-
messages.append("✅ Good with children")
|
253 |
-
else:
|
254 |
-
messages.append("❌ Not recommended for homes with children")
|
255 |
-
|
256 |
-
# Check for dogs
|
257 |
-
if 'dogs' in pet['environment'] and pet['environment']['dogs'] is not None:
|
258 |
-
if pet['environment']['dogs']:
|
259 |
-
messages.append("✅ Good with dogs")
|
260 |
-
else:
|
261 |
-
messages.append("❌ Not recommended for homes with dogs")
|
262 |
-
|
263 |
-
# Check for cats
|
264 |
-
if 'cats' in pet['environment'] and pet['environment']['cats'] is not None:
|
265 |
-
if pet['environment']['cats']:
|
266 |
-
messages.append("✅ Good with cats")
|
267 |
-
else:
|
268 |
-
messages.append("❌ Not recommended for homes with cats")
|
269 |
-
|
270 |
-
# Handling care needs
|
271 |
-
if pet['attributes']:
|
272 |
-
if 'special_needs' in pet['attributes'] and pet['attributes']['special_needs']:
|
273 |
-
messages.append("⚠️ Has special needs")
|
274 |
-
|
275 |
-
if 'house_trained' in pet['attributes'] and pet['attributes']['house_trained']:
|
276 |
-
messages.append("✅ House-trained")
|
277 |
-
elif 'house_trained' in pet['attributes']:
|
278 |
-
messages.append("❌ Not house-trained")
|
279 |
-
|
280 |
-
if 'shots_current' in pet['attributes'] and pet['attributes']['shots_current']:
|
281 |
-
messages.append("✅ Vaccinations up to date")
|
282 |
-
|
283 |
-
if 'spayed_neutered' in pet['attributes'] and pet['attributes']['spayed_neutered']:
|
284 |
-
messages.append("✅ Spayed/neutered")
|
285 |
-
|
286 |
-
return messages
|
287 |
|
288 |
# Function to display pet details page
|
289 |
def display_pet_details(pet_id):
|
290 |
pet = get_pet_details(pet_id)
|
291 |
if not pet:
|
292 |
-
st.error("Unable to retrieve pet details.
|
293 |
return
|
294 |
-
|
295 |
-
# Back button
|
296 |
if st.button("← Back to Search Results"):
|
297 |
if st.session_state.selected_pet is not None:
|
298 |
st.session_state.selected_pet = None
|
299 |
-
st.experimental_rerun() # Force immediate rerun
|
300 |
-
|
301 |
-
# Pet name and status
|
302 |
-
st.markdown(f"<h1 class='main-header'>{pet['name']}</h1>", unsafe_allow_html=True)
|
303 |
-
|
304 |
-
status_color = "#c8e6c9" if pet['status'] == 'adoptable' else "#ffcdd2"
|
305 |
-
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)
|
306 |
-
|
307 |
-
# Pet photos
|
308 |
-
if pet['photos'] and len(pet['photos']) > 0:
|
309 |
-
photo_cols = st.columns(min(3, len(pet['photos'])))
|
310 |
-
for i, col in enumerate(photo_cols):
|
311 |
-
if i < len(pet['photos']):
|
312 |
-
col.image(pet['photos'][i]['large'], use_container_width=True)
|
313 |
-
else:
|
314 |
-
st.image("https://via.placeholder.com/500x300?text=No+Image", use_container_width=True)
|
315 |
-
|
316 |
-
# Pet details
|
317 |
-
col1, col2 = st.columns(2)
|
318 |
-
|
319 |
-
with col1:
|
320 |
-
st.markdown("### Details")
|
321 |
-
# Fix the breed line
|
322 |
-
breed_text = pet['breeds']['primary']
|
323 |
-
if pet['breeds']['secondary']:
|
324 |
-
breed_text += f" & {pet['breeds']['secondary']}"
|
325 |
-
if pet['breeds']['mixed']:
|
326 |
-
breed_text += " (Mixed)"
|
327 |
-
|
328 |
-
details = [
|
329 |
-
f"**Type:** {pet['type']}",
|
330 |
-
f"**Breed:** {breed_text}",
|
331 |
-
f"**Age:** {pet['age']}",
|
332 |
-
f"**Gender:** {pet['gender']}",
|
333 |
-
f"**Size:** {pet['size']}"
|
334 |
-
]
|
335 |
-
|
336 |
-
# Fix the colors line as well, to be safe
|
337 |
-
colors = [c for c in [pet['colors']['primary'], pet['colors']['secondary'], pet['colors']['tertiary']] if c]
|
338 |
-
if colors:
|
339 |
-
details.append(f"**Colors:** {', '.join(colors)}")
|
340 |
-
|
341 |
-
for detail in details:
|
342 |
-
st.markdown(detail)
|
343 |
-
|
344 |
-
with col2:
|
345 |
-
st.markdown("### Compatibility")
|
346 |
-
compatibility = get_compatibility_message(pet)
|
347 |
-
for msg in compatibility:
|
348 |
-
st.markdown(msg)
|
349 |
-
|
350 |
-
# Description
|
351 |
-
if pet['description']:
|
352 |
-
st.markdown("### About")
|
353 |
-
st.markdown(pet['description'])
|
354 |
-
|
355 |
-
# Contact information
|
356 |
-
st.markdown("### Adoption Information")
|
357 |
-
|
358 |
-
# Organization info
|
359 |
-
if pet['organization_id']:
|
360 |
-
st.markdown(f"**Organization:** {pet['organization_id']}")
|
361 |
-
|
362 |
-
# Contact details
|
363 |
-
contact_info = []
|
364 |
-
if pet['contact']['email']:
|
365 |
-
contact_info.append(f"**Email:** {pet['contact']['email']}")
|
366 |
-
if pet['contact']['phone']:
|
367 |
-
contact_info.append(f"**Phone:** {pet['contact']['phone']}")
|
368 |
-
if pet['contact']['address']['city'] and pet['contact']['address']['state']:
|
369 |
-
contact_info.append(f"**Location:** {pet['contact']['address']['city']}, {pet['contact']['address']['state']} {pet['contact']['address']['postcode'] or ''}")
|
370 |
-
|
371 |
-
for info in contact_info:
|
372 |
-
st.markdown(info)
|
373 |
-
|
374 |
-
# URL to pet on Petfinder
|
375 |
-
if pet['url']:
|
376 |
-
st.markdown(f"[View on Petfinder]({pet['url']})")
|
377 |
-
|
378 |
-
# Add to favorites
|
379 |
-
is_favorite = pet['id'] in [p['id'] for p in st.session_state.favorites]
|
380 |
-
if not is_favorite:
|
381 |
-
if st.button("Add to Favorites"):
|
382 |
-
st.session_state.favorites.append(pet)
|
383 |
-
st.success(f"Added {pet['name']} to favorites!")
|
384 |
-
st.experimental_rerun()
|
385 |
-
else:
|
386 |
-
if st.button("Remove from Favorites"):
|
387 |
-
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']]
|
388 |
-
st.success(f"Removed {pet['name']} from favorites!")
|
389 |
st.experimental_rerun()
|
|
|
390 |
|
391 |
# Main app
|
392 |
def main():
|
393 |
-
# Title
|
394 |
st.markdown("<h1 class='main-header'>🐾 PetMatch</h1>", unsafe_allow_html=True)
|
395 |
st.markdown("<p class='sub-header'>Find your perfect pet companion</p>", unsafe_allow_html=True)
|
396 |
-
|
397 |
-
# Create tabs
|
398 |
tab1, tab2, tab3 = st.tabs(["Search", "Favorites", "About"])
|
399 |
-
|
400 |
with tab1:
|
401 |
-
# If a pet is selected, show details
|
402 |
if st.session_state.selected_pet:
|
403 |
display_pet_details(st.session_state.selected_pet)
|
404 |
else:
|
405 |
-
# Search form
|
406 |
with st.expander("Search Options", expanded=True):
|
407 |
with st.form("pet_search_form"):
|
408 |
col1, col2 = st.columns(2)
|
409 |
-
|
410 |
with col1:
|
411 |
-
animal_type = st.selectbox(
|
412 |
-
|
413 |
-
["Dog", "Cat", "Rabbit", "Small & Furry", "Horse", "Bird", "Scales, Fins & Other", "Barnyard"]
|
414 |
-
)
|
415 |
-
|
416 |
-
location = st.text_input("Location (ZIP code or City, State)", "")
|
417 |
-
|
418 |
distance = st.slider("Distance (miles)", min_value=10, max_value=500, value=50, step=10)
|
419 |
-
|
420 |
-
with col2:
|
421 |
-
age_options = ["", "Baby", "Young", "Adult", "Senior"]
|
422 |
-
age = st.selectbox("Age", age_options)
|
423 |
-
|
424 |
-
size_options = ["", "Small", "Medium", "Large", "XLarge"]
|
425 |
-
size = st.selectbox("Size", size_options)
|
426 |
-
|
427 |
-
gender_options = ["", "Male", "Female"]
|
428 |
-
gender = st.selectbox("Gender", gender_options)
|
429 |
-
good_with_children = st.checkbox("Good with children")
|
430 |
-
good_with_dogs = st.checkbox("Good with dogs")
|
431 |
-
good_with_cats = st.checkbox("Good with cats")
|
432 |
-
house_trained = st.checkbox("House-trained")
|
433 |
-
special_needs = st.checkbox("Special needs")
|
434 |
-
|
435 |
submitted = st.form_submit_button("Search")
|
436 |
-
|
437 |
if submitted:
|
438 |
-
|
439 |
-
params = {
|
440 |
-
"type": animal_type.split(" ")[0], # Take first word for types like "Small & Furry"
|
441 |
-
"location": location,
|
442 |
-
"distance": distance,
|
443 |
-
"status": "adoptable",
|
444 |
-
"sort": "distance",
|
445 |
-
"limit": 100
|
446 |
-
}
|
447 |
-
|
448 |
-
if age and age != "":
|
449 |
-
params["age"] = age
|
450 |
-
if size and size != "":
|
451 |
-
params["size"] = size
|
452 |
-
if gender and gender != "":
|
453 |
-
params["gender"] = gender
|
454 |
-
|
455 |
-
# Add advanced filters
|
456 |
-
if good_with_children:
|
457 |
-
params["good_with_children"] = 1
|
458 |
-
if good_with_dogs:
|
459 |
-
params["good_with_dogs"] = 1
|
460 |
-
if good_with_cats:
|
461 |
-
params["good_with_cats"] = 1
|
462 |
-
if house_trained:
|
463 |
-
params["house_trained"] = 1
|
464 |
-
if special_needs:
|
465 |
-
params["special_needs"] = 1
|
466 |
-
|
467 |
-
# Perform search
|
468 |
results = search_pets(params)
|
469 |
if results and 'animals' in results:
|
470 |
st.session_state.search_results = results
|
471 |
st.session_state.page = 1
|
472 |
st.success(f"Found {len(results['animals'])} pets!")
|
473 |
-
else:
|
474 |
-
st.error("No pets found with those criteria. Try expanding your search.")
|
475 |
-
|
476 |
-
# Display search results
|
477 |
if st.session_state.search_results and 'animals' in st.session_state.search_results:
|
478 |
st.markdown("### Search Results")
|
479 |
-
|
480 |
-
# Pagination
|
481 |
results = st.session_state.search_results['animals']
|
482 |
-
|
483 |
-
|
484 |
-
# Display page selector
|
485 |
-
if total_pages > 1:
|
486 |
-
col1, col2, col3 = st.columns([1, 3, 1])
|
487 |
-
with col2:
|
488 |
-
page = st.slider("Page", 1, total_pages, st.session_state.page)
|
489 |
-
if page != st.session_state.page:
|
490 |
-
st.session_state.page = page
|
491 |
-
|
492 |
-
# Display pets for current page
|
493 |
-
start_idx = (st.session_state.page - 1) * 10
|
494 |
-
end_idx = min(start_idx + 10, len(results))
|
495 |
-
|
496 |
-
for pet in results[start_idx:end_idx]:
|
497 |
st.markdown("---")
|
498 |
display_pet_card(pet)
|
499 |
-
|
500 |
-
with tab2:
|
501 |
-
st.markdown("### Your Favorite Pets")
|
502 |
-
|
503 |
-
if not st.session_state.favorites:
|
504 |
-
st.info("You haven't added any pets to your favorites yet. Start searching to find your perfect match!")
|
505 |
-
else:
|
506 |
-
for pet in st.session_state.favorites:
|
507 |
-
st.markdown("---")
|
508 |
-
display_pet_card(pet, is_favorite=True)
|
509 |
-
|
510 |
-
with tab3:
|
511 |
-
st.markdown("### About PetMatch")
|
512 |
-
st.markdown("""
|
513 |
-
PetMatch helps you find your perfect pet companion from thousands of adoptable animals across the country.
|
514 |
-
|
515 |
-
**How to use PetMatch:**
|
516 |
-
1. Search for pets based on your preferences and location
|
517 |
-
2. Browse through the results and click "View Details" to learn more about each pet
|
518 |
-
3. Add pets to your favorites to keep track of the ones you're interested in
|
519 |
-
4. Contact the shelter or rescue organization directly using the provided information
|
520 |
-
|
521 |
-
**Data Source:**
|
522 |
-
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.
|
523 |
-
|
524 |
-
**Privacy:**
|
525 |
-
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.
|
526 |
-
""")
|
527 |
-
|
528 |
-
st.markdown("### Made with ❤️ by Claude")
|
529 |
|
530 |
if __name__ == "__main__":
|
531 |
-
main()
|
|
|
1 |
import streamlit as st
|
2 |
import requests
|
|
|
|
|
|
|
3 |
import time
|
4 |
import os
|
5 |
|
|
|
54 |
""", unsafe_allow_html=True)
|
55 |
|
56 |
# Initialize session state variables
|
57 |
+
for key, value in {'access_token': None, 'token_expires': 0, 'search_results': None, 'selected_pet': None, 'page': 1, 'favorites': []}.items():
|
58 |
+
if key not in st.session_state:
|
59 |
+
st.session_state[key] = value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
|
61 |
# Function to get access token
|
62 |
def get_access_token():
|
|
|
63 |
if st.session_state.access_token and time.time() < st.session_state.token_expires:
|
64 |
return st.session_state.access_token
|
65 |
+
|
|
|
66 |
api_key = os.environ.get('PETFINDER_API_KEY') or st.secrets.get('PETFINDER_API_KEY')
|
67 |
api_secret = os.environ.get('PETFINDER_API_SECRET') or st.secrets.get('PETFINDER_API_SECRET')
|
68 |
+
|
69 |
if not api_key or not api_secret:
|
70 |
+
st.error("⚠️ Petfinder API credentials are missing.")
|
71 |
return None
|
72 |
+
|
|
|
73 |
url = "https://api.petfinder.com/v2/oauth2/token"
|
74 |
+
data = {"grant_type": "client_credentials", "client_id": api_key, "client_secret": api_secret}
|
75 |
+
|
|
|
|
|
|
|
|
|
76 |
try:
|
77 |
response = requests.post(url, data=data)
|
78 |
response.raise_for_status()
|
79 |
token_data = response.json()
|
80 |
st.session_state.access_token = token_data['access_token']
|
81 |
+
st.session_state.token_expires = time.time() + token_data['expires_in'] - 60
|
82 |
return st.session_state.access_token
|
83 |
except requests.exceptions.RequestException as e:
|
84 |
st.error(f"⚠️ Error getting access token: {str(e)}")
|
|
|
89 |
token = get_access_token()
|
90 |
if not token:
|
91 |
return None
|
|
|
92 |
url = "https://api.petfinder.com/v2/animals"
|
93 |
headers = {"Authorization": f"Bearer {token}"}
|
|
|
94 |
try:
|
95 |
response = requests.get(url, headers=headers, params=params)
|
96 |
response.raise_for_status()
|
|
|
99 |
st.error(f"⚠️ Error searching pets: {str(e)}")
|
100 |
return None
|
101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
# Function to get pet details
|
103 |
def get_pet_details(pet_id):
|
104 |
token = get_access_token()
|
105 |
if not token:
|
106 |
return None
|
|
|
107 |
url = f"https://api.petfinder.com/v2/animals/{pet_id}"
|
108 |
headers = {"Authorization": f"Bearer {token}"}
|
|
|
109 |
try:
|
110 |
response = requests.get(url, headers=headers)
|
111 |
response.raise_for_status()
|
|
|
117 |
# Function to format pet card
|
118 |
def display_pet_card(pet, is_favorite=False):
|
119 |
col1, col2 = st.columns([1, 2])
|
|
|
120 |
with col1:
|
121 |
if pet['photos'] and len(pet['photos']) > 0:
|
122 |
st.image(pet['photos'][0]['medium'], use_container_width=True)
|
123 |
else:
|
124 |
st.image("https://via.placeholder.com/300x300?text=No+Image", use_container_width=True)
|
125 |
+
|
126 |
with col2:
|
127 |
st.markdown(f"<div class='pet-name'>{pet['name']}</div>", unsafe_allow_html=True)
|
|
|
|
|
128 |
tags_html = ""
|
129 |
if pet['status'] == 'adoptable':
|
130 |
tags_html += "<span class='tag' style='background-color: #c8e6c9;'>Adoptable</span> "
|
131 |
else:
|
132 |
tags_html += f"<span class='tag' style='background-color: #ffcdd2;'>{pet['status'].title()}</span> "
|
|
|
133 |
if pet['age']:
|
134 |
tags_html += f"<span class='tag'>{pet['age']}</span> "
|
135 |
if pet['gender']:
|
136 |
tags_html += f"<span class='tag'>{pet['gender']}</span> "
|
137 |
if pet['size']:
|
138 |
tags_html += f"<span class='tag'>{pet['size']}</span> "
|
|
|
139 |
st.markdown(f"<div>{tags_html}</div>", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
140 |
|
141 |
+
if st.button("View Details", key=f"details_{pet['id']}"):
|
142 |
+
if st.session_state.selected_pet != pet['id']:
|
143 |
+
st.session_state.selected_pet = pet['id']
|
144 |
+
st.experimental_rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
145 |
|
146 |
# Function to display pet details page
|
147 |
def display_pet_details(pet_id):
|
148 |
pet = get_pet_details(pet_id)
|
149 |
if not pet:
|
150 |
+
st.error("Unable to retrieve pet details.")
|
151 |
return
|
|
|
|
|
152 |
if st.button("← Back to Search Results"):
|
153 |
if st.session_state.selected_pet is not None:
|
154 |
st.session_state.selected_pet = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
st.experimental_rerun()
|
156 |
+
st.markdown(f"<h1 class='main-header'>{pet['name']}</h1>", unsafe_allow_html=True)
|
157 |
|
158 |
# Main app
|
159 |
def main():
|
|
|
160 |
st.markdown("<h1 class='main-header'>🐾 PetMatch</h1>", unsafe_allow_html=True)
|
161 |
st.markdown("<p class='sub-header'>Find your perfect pet companion</p>", unsafe_allow_html=True)
|
|
|
|
|
162 |
tab1, tab2, tab3 = st.tabs(["Search", "Favorites", "About"])
|
|
|
163 |
with tab1:
|
|
|
164 |
if st.session_state.selected_pet:
|
165 |
display_pet_details(st.session_state.selected_pet)
|
166 |
else:
|
|
|
167 |
with st.expander("Search Options", expanded=True):
|
168 |
with st.form("pet_search_form"):
|
169 |
col1, col2 = st.columns(2)
|
|
|
170 |
with col1:
|
171 |
+
animal_type = st.selectbox("Animal Type", ["Dog", "Cat"])
|
172 |
+
location = st.text_input("Location", "")
|
|
|
|
|
|
|
|
|
|
|
173 |
distance = st.slider("Distance (miles)", min_value=10, max_value=500, value=50, step=10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
174 |
submitted = st.form_submit_button("Search")
|
|
|
175 |
if submitted:
|
176 |
+
params = {"type": animal_type, "location": location, "distance": distance, "status": "adoptable"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
177 |
results = search_pets(params)
|
178 |
if results and 'animals' in results:
|
179 |
st.session_state.search_results = results
|
180 |
st.session_state.page = 1
|
181 |
st.success(f"Found {len(results['animals'])} pets!")
|
|
|
|
|
|
|
|
|
182 |
if st.session_state.search_results and 'animals' in st.session_state.search_results:
|
183 |
st.markdown("### Search Results")
|
|
|
|
|
184 |
results = st.session_state.search_results['animals']
|
185 |
+
for pet in results:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
186 |
st.markdown("---")
|
187 |
display_pet_card(pet)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
|
189 |
if __name__ == "__main__":
|
190 |
+
main()
|