Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -6,6 +6,9 @@ from datetime import datetime
|
|
6 |
import time
|
7 |
import os
|
8 |
import functools
|
|
|
|
|
|
|
9 |
|
10 |
# Set page configuration
|
11 |
st.set_page_config(
|
@@ -54,35 +57,66 @@ st.markdown("""
|
|
54 |
margin-right: 0.3rem;
|
55 |
font-size: 0.8rem;
|
56 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
</style>
|
58 |
""", unsafe_allow_html=True)
|
59 |
|
60 |
# Initialize session state variables
|
61 |
-
if '
|
62 |
-
st.session_state.
|
63 |
-
if '
|
64 |
-
st.session_state.
|
|
|
|
|
65 |
if 'search_results' not in st.session_state:
|
66 |
st.session_state.search_results = None
|
67 |
if 'selected_pet' not in st.session_state:
|
68 |
st.session_state.selected_pet = None
|
|
|
|
|
69 |
if 'page' not in st.session_state:
|
70 |
st.session_state.page = 1
|
71 |
if 'favorites' not in st.session_state:
|
72 |
st.session_state.favorites = []
|
|
|
|
|
|
|
|
|
73 |
|
74 |
-
# Function to get access token
|
75 |
-
def
|
76 |
# Check if token is still valid
|
77 |
-
if st.session_state.
|
78 |
-
return st.session_state.
|
79 |
|
80 |
# Get API credentials from environment variables or secrets
|
81 |
api_key = os.environ.get('PETFINDER_API_KEY') or st.secrets.get('PETFINDER_API_KEY')
|
82 |
api_secret = os.environ.get('PETFINDER_API_SECRET') or st.secrets.get('PETFINDER_API_SECRET')
|
83 |
|
84 |
if not api_key or not api_secret:
|
85 |
-
st.
|
86 |
return None
|
87 |
|
88 |
# Get new token
|
@@ -97,16 +131,31 @@ def get_access_token():
|
|
97 |
response = requests.post(url, data=data)
|
98 |
response.raise_for_status()
|
99 |
token_data = response.json()
|
100 |
-
st.session_state.
|
101 |
-
st.session_state.
|
102 |
-
|
|
|
103 |
except requests.exceptions.RequestException as e:
|
104 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
return None
|
|
|
|
|
|
|
|
|
|
|
106 |
|
107 |
-
# Function to search pets
|
108 |
-
def
|
109 |
-
token =
|
110 |
if not token:
|
111 |
return None
|
112 |
|
@@ -116,65 +165,394 @@ def search_pets(params):
|
|
116 |
try:
|
117 |
response = requests.get(url, headers=headers, params=params)
|
118 |
response.raise_for_status()
|
119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
except requests.exceptions.RequestException as e:
|
121 |
-
st.
|
122 |
return None
|
123 |
|
124 |
-
# Function to
|
125 |
-
def
|
126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
if not token:
|
128 |
-
return
|
129 |
|
130 |
-
url = f"https://api.petfinder.com/v2/
|
131 |
headers = {"Authorization": f"Bearer {token}"}
|
132 |
|
133 |
try:
|
134 |
response = requests.get(url, headers=headers)
|
135 |
response.raise_for_status()
|
136 |
-
|
|
|
|
|
137 |
except requests.exceptions.RequestException as e:
|
138 |
-
st.
|
139 |
-
return
|
140 |
|
141 |
-
# Function to get
|
142 |
-
def
|
143 |
-
|
144 |
-
if not
|
145 |
-
return
|
146 |
|
147 |
-
url = "https://api.
|
148 |
-
headers = {
|
149 |
-
|
|
|
|
|
150 |
|
151 |
try:
|
152 |
-
response = requests.get(url, headers=headers
|
153 |
response.raise_for_status()
|
154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
except requests.exceptions.RequestException as e:
|
156 |
-
st.
|
157 |
-
return
|
158 |
|
159 |
-
# Function to get pet details
|
160 |
-
def get_pet_details(pet_id):
|
161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
162 |
if not token:
|
163 |
-
return
|
164 |
|
165 |
-
url = f"https://api.petfinder.com/v2/
|
166 |
headers = {"Authorization": f"Bearer {token}"}
|
167 |
|
168 |
try:
|
169 |
response = requests.get(url, headers=headers)
|
170 |
response.raise_for_status()
|
171 |
-
return response.json()['
|
172 |
except requests.exceptions.RequestException as e:
|
173 |
-
st.
|
174 |
-
return
|
175 |
|
176 |
-
# Function to
|
177 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
178 |
col1, col2 = st.columns([1, 2])
|
179 |
|
180 |
with col1:
|
@@ -184,7 +562,9 @@ def display_pet_card(pet, is_favorite=False, context="search"):
|
|
184 |
st.image("https://via.placeholder.com/300x300?text=No+Image", use_container_width=True)
|
185 |
|
186 |
with col2:
|
187 |
-
|
|
|
|
|
188 |
|
189 |
# Tags
|
190 |
tags_html = ""
|
@@ -221,87 +601,45 @@ def display_pet_card(pet, is_favorite=False, context="search"):
|
|
221 |
st.markdown("</div>", unsafe_allow_html=True)
|
222 |
|
223 |
if pet['description']:
|
224 |
-
st.markdown(f"<div class='pet-description'>{pet['description'][:
|
225 |
|
226 |
col1, col2 = st.columns(2)
|
227 |
with col1:
|
228 |
-
if st.button("View Details", key=f"details_{context}_{pet['id']}"):
|
229 |
st.session_state.selected_pet = pet['id']
|
|
|
230 |
st.rerun()
|
231 |
with col2:
|
232 |
if not is_favorite:
|
233 |
-
if st.button("Add to Favorites", key=f"fav_{context}_{pet['id']}"):
|
234 |
if pet['id'] not in [p['id'] for p in st.session_state.favorites]:
|
235 |
st.session_state.favorites.append(pet)
|
236 |
st.success(f"Added {pet['name']} to favorites!")
|
237 |
st.rerun()
|
238 |
else:
|
239 |
-
if st.button("Remove from Favorites", key=f"unfav_{context}_{pet['id']}"):
|
240 |
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']]
|
241 |
st.success(f"Removed {pet['name']} from favorites!")
|
242 |
st.rerun()
|
243 |
|
244 |
-
# Function to generate pet compatibility message
|
245 |
-
def get_compatibility_message(pet):
|
246 |
-
messages = []
|
247 |
-
|
248 |
-
# Check for kids
|
249 |
-
if 'children' in pet['environment'] and pet['environment']['children'] is not None:
|
250 |
-
if pet['environment']['children']:
|
251 |
-
messages.append("✅ Good with children")
|
252 |
-
else:
|
253 |
-
messages.append("❌ Not recommended for homes with children")
|
254 |
-
|
255 |
-
# Check for dogs
|
256 |
-
if 'dogs' in pet['environment'] and pet['environment']['dogs'] is not None:
|
257 |
-
if pet['environment']['dogs']:
|
258 |
-
messages.append("✅ Good with dogs")
|
259 |
-
else:
|
260 |
-
messages.append("❌ Not recommended for homes with dogs")
|
261 |
-
|
262 |
-
# Check for cats
|
263 |
-
if 'cats' in pet['environment'] and pet['environment']['cats'] is not None:
|
264 |
-
if pet['environment']['cats']:
|
265 |
-
messages.append("✅ Good with cats")
|
266 |
-
else:
|
267 |
-
messages.append("❌ Not recommended for homes with cats")
|
268 |
-
|
269 |
-
# Handling care needs
|
270 |
-
if pet['attributes']:
|
271 |
-
if 'special_needs' in pet['attributes'] and pet['attributes']['special_needs']:
|
272 |
-
messages.append("⚠️ Has special needs")
|
273 |
-
|
274 |
-
if 'house_trained' in pet['attributes'] and pet['attributes']['house_trained']:
|
275 |
-
messages.append("✅ House-trained")
|
276 |
-
elif 'house_trained' in pet['attributes']:
|
277 |
-
messages.append("❌ Not house-trained")
|
278 |
-
|
279 |
-
if 'shots_current' in pet['attributes'] and pet['attributes']['shots_current']:
|
280 |
-
messages.append("✅ Vaccinations up to date")
|
281 |
-
|
282 |
-
if 'spayed_neutered' in pet['attributes'] and pet['attributes']['spayed_neutered']:
|
283 |
-
messages.append("✅ Spayed/neutered")
|
284 |
-
|
285 |
-
return messages
|
286 |
-
|
287 |
# Function to display pet details page
|
288 |
-
|
289 |
-
|
290 |
-
# Function to display pet details page with unique tab identifier
|
291 |
-
def display_pet_details(pet_id, context="search", tab_id="tab1"):
|
292 |
-
pet = get_pet_details(pet_id)
|
293 |
if not pet:
|
294 |
st.error("Unable to retrieve pet details. Please try again.")
|
295 |
return
|
296 |
|
297 |
-
# Back button
|
298 |
-
if st.button("← Back to Search Results", key=f"back_{tab_id}_{context}_{pet_id}"):
|
299 |
st.session_state.selected_pet = None
|
300 |
-
st.
|
|
|
301 |
|
302 |
-
# Pet name and
|
303 |
-
|
|
|
304 |
|
|
|
305 |
status_color = "#c8e6c9" if pet['status'] == 'adoptable' else "#ffcdd2"
|
306 |
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)
|
307 |
|
@@ -350,17 +688,15 @@ def display_pet_details(pet_id, context="search", tab_id="tab1"):
|
|
350 |
|
351 |
# Description
|
352 |
if pet['description']:
|
353 |
-
|
354 |
-
|
355 |
-
#st.markdown(pet['description'])
|
356 |
-
st.markdown(f"<div class='pet-description'>{pet['description'][:500]}{'...' if len(pet['description']) > 500 else ''}</div>", unsafe_allow_html=True)
|
357 |
|
358 |
# Contact information
|
359 |
st.markdown("### Adoption Information")
|
360 |
|
361 |
# Organization info
|
362 |
if pet['organization_id']:
|
363 |
-
st.markdown(f"**Organization:** {pet['organization_id']}")
|
364 |
|
365 |
# Contact details
|
366 |
contact_info = []
|
@@ -374,232 +710,168 @@ def display_pet_details(pet_id, context="search", tab_id="tab1"):
|
|
374 |
for info in contact_info:
|
375 |
st.markdown(info)
|
376 |
|
377 |
-
# URL to pet on
|
378 |
if pet['url']:
|
379 |
-
st.markdown(f"[View on Petfinder]({pet['url']})")
|
380 |
|
381 |
-
# Add to favorites
|
382 |
is_favorite = pet['id'] in [p['id'] for p in st.session_state.favorites]
|
383 |
if not is_favorite:
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
if st.button("Remove from Favorites", key=f"rem_fav_{tab_id}_{context}_{pet_id}"):
|
390 |
-
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']]
|
391 |
-
st.success(f"Removed {pet['name']} from favorites!")
|
392 |
-
st.rerun()
|
393 |
-
|
394 |
-
# Function to format pet card with unique tab identifier
|
395 |
-
def display_pet_card(pet, is_favorite=False, context="search", tab_id="tab1"):
|
396 |
-
col1, col2 = st.columns([1, 2])
|
397 |
-
|
398 |
-
with col1:
|
399 |
-
if pet['photos'] and len(pet['photos']) > 0:
|
400 |
-
st.image(pet['photos'][0]['medium'], use_container_width=True)
|
401 |
-
else:
|
402 |
-
st.image("https://via.placeholder.com/300x300?text=No+Image", use_container_width=True)
|
403 |
-
|
404 |
-
with col2:
|
405 |
-
st.markdown(f"<div class='pet-name'>{pet['name']}</div>", unsafe_allow_html=True)
|
406 |
-
|
407 |
-
# Tags
|
408 |
-
tags_html = ""
|
409 |
-
if pet['status'] == 'adoptable':
|
410 |
-
tags_html += "<span class='tag' style='background-color: #808080;'>Adoptable</span> "
|
411 |
else:
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
tags_html += f"<span class='tag'>{pet['age']}</span> "
|
416 |
-
if pet['gender']:
|
417 |
-
tags_html += f"<span class='tag'>{pet['gender']}</span> "
|
418 |
-
if pet['size']:
|
419 |
-
tags_html += f"<span class='tag'>{pet['size']}</span> "
|
420 |
-
|
421 |
-
st.markdown(f"<div>{tags_html}</div>", unsafe_allow_html=True)
|
422 |
-
|
423 |
-
st.markdown("<div class='pet-details'>", unsafe_allow_html=True)
|
424 |
-
if pet['breeds']['primary']:
|
425 |
-
breed_text = pet['breeds']['primary']
|
426 |
-
if pet['breeds']['secondary']:
|
427 |
-
breed_text += f" & {pet['breeds']['secondary']}"
|
428 |
-
if pet['breeds']['mixed']:
|
429 |
-
breed_text += " (Mixed)"
|
430 |
-
st.markdown(f"<strong>Breed:</strong> {breed_text}", unsafe_allow_html=True)
|
431 |
-
|
432 |
-
if pet['colors']['primary'] or pet['colors']['secondary'] or pet['colors']['tertiary']:
|
433 |
-
colors = [c for c in [pet['colors']['primary'], pet['colors']['secondary'], pet['colors']['tertiary']] if c]
|
434 |
-
st.markdown(f"<strong>Colors:</strong> {', '.join(colors)}", unsafe_allow_html=True)
|
435 |
-
|
436 |
-
if 'location' in pet and pet['contact']['address']['city'] and pet['contact']['address']['state']:
|
437 |
-
st.markdown(f"<strong>Location:</strong> {pet['contact']['address']['city']}, {pet['contact']['address']['state']}", unsafe_allow_html=True)
|
438 |
-
|
439 |
-
st.markdown("</div>", unsafe_allow_html=True)
|
440 |
-
|
441 |
-
if pet['description']:
|
442 |
-
st.markdown(f"<div class='pet-description'>{pet['description'][:300]}{'...' if len(pet['description']) > 300 else ''}</div>", unsafe_allow_html=True)
|
443 |
-
|
444 |
-
col1, col2 = st.columns(2)
|
445 |
-
with col1:
|
446 |
-
if st.button("View Details", key=f"details_{tab_id}_{context}_{pet['id']}"):
|
447 |
-
st.session_state.selected_pet = pet['id']
|
448 |
st.rerun()
|
449 |
-
with col2:
|
450 |
-
if not is_favorite:
|
451 |
-
if st.button("Add to Favorites", key=f"fav_{tab_id}_{context}_{pet['id']}"):
|
452 |
-
if pet['id'] not in [p['id'] for p in st.session_state.favorites]:
|
453 |
-
st.session_state.favorites.append(pet)
|
454 |
-
st.success(f"Added {pet['name']} to favorites!")
|
455 |
-
st.rerun()
|
456 |
-
else:
|
457 |
-
if st.button("Remove from Favorites", key=f"unfav_{tab_id}_{context}_{pet['id']}"):
|
458 |
-
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']]
|
459 |
-
st.success(f"Removed {pet['name']} from favorites!")
|
460 |
-
st.rerun()
|
461 |
|
462 |
-
# Main
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
location = st.text_input("Location (ZIP code or City, State)", "")
|
488 |
-
|
489 |
-
distance = st.slider("Distance (miles)", min_value=10, max_value=500, value=50, step=10)
|
490 |
-
|
491 |
-
with col2:
|
492 |
-
age_options = ["", "Baby", "Young", "Adult", "Senior"]
|
493 |
-
age = st.selectbox("Age", age_options)
|
494 |
-
|
495 |
-
size_options = ["", "Small", "Medium", "Large", "XLarge"]
|
496 |
-
size = st.selectbox("Size", size_options)
|
497 |
-
|
498 |
-
gender_options = ["", "Male", "Female"]
|
499 |
-
gender = st.selectbox("Gender", gender_options)
|
500 |
-
good_with_children = st.checkbox("Good with children")
|
501 |
-
good_with_dogs = st.checkbox("Good with dogs")
|
502 |
-
good_with_cats = st.checkbox("Good with cats")
|
503 |
-
house_trained = st.checkbox("House-trained")
|
504 |
-
special_needs = st.checkbox("Special needs")
|
505 |
-
|
506 |
-
submitted = st.form_submit_button("Search")
|
507 |
-
|
508 |
-
if submitted:
|
509 |
-
# Build search parameters
|
510 |
-
params = {
|
511 |
-
"type": animal_type.split(" ")[0], # Take first word for types like "Small & Furry"
|
512 |
-
"location": location,
|
513 |
-
"distance": distance,
|
514 |
-
"status": "adoptable",
|
515 |
-
"sort": "distance",
|
516 |
-
"limit": 100
|
517 |
-
}
|
518 |
-
|
519 |
-
if age and age != "":
|
520 |
-
params["age"] = age
|
521 |
-
if size and size != "":
|
522 |
-
params["size"] = size
|
523 |
-
if gender and gender != "":
|
524 |
-
params["gender"] = gender
|
525 |
-
|
526 |
-
# Add advanced filters
|
527 |
-
if good_with_children:
|
528 |
-
params["good_with_children"] = 1
|
529 |
-
if good_with_dogs:
|
530 |
-
params["good_with_dogs"] = 1
|
531 |
-
if good_with_cats:
|
532 |
-
params["good_with_cats"] = 1
|
533 |
-
if house_trained:
|
534 |
-
params["house_trained"] = 1
|
535 |
-
if special_needs:
|
536 |
-
params["special_needs"] = 1
|
537 |
-
|
538 |
-
# Perform search
|
539 |
-
results = search_pets(params)
|
540 |
-
if results and 'animals' in results:
|
541 |
-
st.session_state.search_results = results
|
542 |
-
st.session_state.page = 1
|
543 |
-
st.success(f"Found {len(results['animals'])} pets!")
|
544 |
-
else:
|
545 |
-
st.error("No pets found with those criteria. Try expanding your search.")
|
546 |
|
547 |
-
|
548 |
-
|
549 |
-
st.
|
550 |
|
551 |
-
|
552 |
-
|
553 |
-
|
|
|
|
|
554 |
|
555 |
-
|
556 |
-
|
557 |
-
col1, col2, col3 = st.columns([1, 3, 1])
|
558 |
-
with col2:
|
559 |
-
page = st.slider("Page", 1, total_pages, st.session_state.page)
|
560 |
-
if page != st.session_state.page:
|
561 |
-
st.session_state.page = page
|
562 |
|
563 |
-
|
564 |
-
|
565 |
-
|
|
|
|
|
|
|
566 |
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
573 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
574 |
if not st.session_state.favorites:
|
575 |
-
st.info("You haven't added any pets to your favorites yet.
|
576 |
else:
|
577 |
-
|
578 |
-
|
579 |
-
|
580 |
-
|
581 |
-
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
|
587 |
-
|
588 |
-
|
589 |
-
|
590 |
-
**How to use PetMatch:**
|
591 |
-
1. Search for pets based on your preferences and location
|
592 |
-
2. Browse through the results and click "View Details" to learn more about each pet
|
593 |
-
3. Add pets to your favorites to keep track of the ones you're interested in
|
594 |
-
4. Contact the shelter or rescue organization directly using the provided information
|
595 |
-
|
596 |
-
**Data Source:**
|
597 |
-
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.
|
598 |
-
|
599 |
-
**Privacy:**
|
600 |
-
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.
|
601 |
-
""")
|
602 |
|
|
|
|
|
|
|
603 |
|
604 |
-
if
|
605 |
-
|
|
|
6 |
import time
|
7 |
import os
|
8 |
import functools
|
9 |
+
import asyncio
|
10 |
+
import aiohttp
|
11 |
+
from concurrent.futures import ThreadPoolExecutor
|
12 |
|
13 |
# Set page configuration
|
14 |
st.set_page_config(
|
|
|
57 |
margin-right: 0.3rem;
|
58 |
font-size: 0.8rem;
|
59 |
}
|
60 |
+
.source-tag {
|
61 |
+
background-color: #6c5ce7;
|
62 |
+
color: white;
|
63 |
+
border-radius: 20px;
|
64 |
+
padding: 0.2rem 0.6rem;
|
65 |
+
margin-right: 0.3rem;
|
66 |
+
font-size: 0.8rem;
|
67 |
+
}
|
68 |
+
.api-status {
|
69 |
+
padding: 5px;
|
70 |
+
border-radius: 5px;
|
71 |
+
margin-bottom: 10px;
|
72 |
+
font-size: 0.8em;
|
73 |
+
text-align: center;
|
74 |
+
}
|
75 |
+
.api-success {
|
76 |
+
background-color: #d4edda;
|
77 |
+
color: #155724;
|
78 |
+
}
|
79 |
+
.api-error {
|
80 |
+
background-color: #f8d7da;
|
81 |
+
color: #721c24;
|
82 |
+
}
|
83 |
</style>
|
84 |
""", unsafe_allow_html=True)
|
85 |
|
86 |
# Initialize session state variables
|
87 |
+
if 'petfinder_access_token' not in st.session_state:
|
88 |
+
st.session_state.petfinder_access_token = None
|
89 |
+
if 'petfinder_token_expires' not in st.session_state:
|
90 |
+
st.session_state.petfinder_token_expires = 0
|
91 |
+
if 'rescuegroups_access_token' not in st.session_state:
|
92 |
+
st.session_state.rescuegroups_access_token = None
|
93 |
if 'search_results' not in st.session_state:
|
94 |
st.session_state.search_results = None
|
95 |
if 'selected_pet' not in st.session_state:
|
96 |
st.session_state.selected_pet = None
|
97 |
+
if 'selected_pet_source' not in st.session_state:
|
98 |
+
st.session_state.selected_pet_source = None
|
99 |
if 'page' not in st.session_state:
|
100 |
st.session_state.page = 1
|
101 |
if 'favorites' not in st.session_state:
|
102 |
st.session_state.favorites = []
|
103 |
+
if 'petfinder_status' not in st.session_state:
|
104 |
+
st.session_state.petfinder_status = {'success': False, 'message': 'Not initialized'}
|
105 |
+
if 'rescuegroups_status' not in st.session_state:
|
106 |
+
st.session_state.rescuegroups_status = {'success': False, 'message': 'Not initialized'}
|
107 |
|
108 |
+
# Function to get Petfinder access token
|
109 |
+
def get_petfinder_token():
|
110 |
# Check if token is still valid
|
111 |
+
if st.session_state.petfinder_access_token and time.time() < st.session_state.petfinder_token_expires:
|
112 |
+
return st.session_state.petfinder_access_token
|
113 |
|
114 |
# Get API credentials from environment variables or secrets
|
115 |
api_key = os.environ.get('PETFINDER_API_KEY') or st.secrets.get('PETFINDER_API_KEY')
|
116 |
api_secret = os.environ.get('PETFINDER_API_SECRET') or st.secrets.get('PETFINDER_API_SECRET')
|
117 |
|
118 |
if not api_key or not api_secret:
|
119 |
+
st.session_state.petfinder_status = {'success': False, 'message': 'API credentials missing'}
|
120 |
return None
|
121 |
|
122 |
# Get new token
|
|
|
131 |
response = requests.post(url, data=data)
|
132 |
response.raise_for_status()
|
133 |
token_data = response.json()
|
134 |
+
st.session_state.petfinder_access_token = token_data['access_token']
|
135 |
+
st.session_state.petfinder_token_expires = time.time() + token_data['expires_in'] - 60 # Buffer of 60 seconds
|
136 |
+
st.session_state.petfinder_status = {'success': True, 'message': 'API connected'}
|
137 |
+
return st.session_state.petfinder_access_token
|
138 |
except requests.exceptions.RequestException as e:
|
139 |
+
st.session_state.petfinder_status = {'success': False, 'message': f'Error: {str(e)}'}
|
140 |
+
return None
|
141 |
+
|
142 |
+
# Function to get RescueGroups.org API key
|
143 |
+
def get_rescuegroups_token():
|
144 |
+
# RescueGroups.org uses API key directly, no OAuth flow needed
|
145 |
+
api_key = os.environ.get('RESCUEGROUPS_API_KEY') or st.secrets.get('RESCUEGROUPS_API_KEY')
|
146 |
+
|
147 |
+
if not api_key:
|
148 |
+
st.session_state.rescuegroups_status = {'success': False, 'message': 'API key missing'}
|
149 |
return None
|
150 |
+
|
151 |
+
# Just set the status and return the key
|
152 |
+
st.session_state.rescuegroups_status = {'success': True, 'message': 'API connected'}
|
153 |
+
st.session_state.rescuegroups_access_token = api_key
|
154 |
+
return api_key
|
155 |
|
156 |
+
# Function to search pets on Petfinder
|
157 |
+
def search_petfinder(params):
|
158 |
+
token = get_petfinder_token()
|
159 |
if not token:
|
160 |
return None
|
161 |
|
|
|
165 |
try:
|
166 |
response = requests.get(url, headers=headers, params=params)
|
167 |
response.raise_for_status()
|
168 |
+
results = response.json()
|
169 |
+
|
170 |
+
# Add source information to each pet
|
171 |
+
for pet in results.get('animals', []):
|
172 |
+
pet['source'] = 'petfinder'
|
173 |
+
|
174 |
+
return results
|
175 |
except requests.exceptions.RequestException as e:
|
176 |
+
st.session_state.petfinder_status = {'success': False, 'message': f'Search error: {str(e)}'}
|
177 |
return None
|
178 |
|
179 |
+
# Function to search pets on RescueGroups.org
|
180 |
+
def search_rescuegroups(params):
|
181 |
+
api_key = get_rescuegroups_token()
|
182 |
+
if not api_key:
|
183 |
+
return None
|
184 |
+
|
185 |
+
url = "https://api.rescuegroups.org/v5/public/animals/search/available"
|
186 |
+
headers = {
|
187 |
+
"Authorization": api_key,
|
188 |
+
"Content-Type": "application/vnd.api+json"
|
189 |
+
}
|
190 |
+
|
191 |
+
# Convert params to RescueGroups.org format
|
192 |
+
rg_params = {
|
193 |
+
"data": {
|
194 |
+
"filters": [],
|
195 |
+
"filterProcessing": "AND",
|
196 |
+
"filterRadius": {
|
197 |
+
"miles": params.get('distance', 50),
|
198 |
+
"postalcode": params.get('location', '')
|
199 |
+
} if params.get('location') else None
|
200 |
+
}
|
201 |
+
}
|
202 |
+
|
203 |
+
# Add animal type filter
|
204 |
+
if params.get('type'):
|
205 |
+
animal_type = params['type'].lower()
|
206 |
+
# Map Petfinder types to RescueGroups types
|
207 |
+
type_mapping = {
|
208 |
+
'dog': ['Dog'],
|
209 |
+
'cat': ['Cat'],
|
210 |
+
'rabbit': ['Rabbit'],
|
211 |
+
'small': ['Guinea Pig', 'Hamster', 'Gerbil', 'Mouse', 'Rat', 'Degu', 'Chinchilla', 'Hedgehog', 'Sugar Glider'],
|
212 |
+
'horse': ['Horse'],
|
213 |
+
'bird': ['Bird'],
|
214 |
+
'scales': ['Reptile', 'Amphibian', 'Fish'],
|
215 |
+
'barnyard': ['Farm']
|
216 |
+
}
|
217 |
+
|
218 |
+
# Find the matching RescueGroups animal types
|
219 |
+
rg_types = []
|
220 |
+
for key, values in type_mapping.items():
|
221 |
+
if key in animal_type.lower():
|
222 |
+
rg_types.extend(values)
|
223 |
+
|
224 |
+
if rg_types:
|
225 |
+
rg_params['data']['filters'].append({
|
226 |
+
"fieldName": "species.singular",
|
227 |
+
"operation": "in",
|
228 |
+
"criteria": rg_types
|
229 |
+
})
|
230 |
+
|
231 |
+
# Add age filter
|
232 |
+
if params.get('age'):
|
233 |
+
age_mapping = {
|
234 |
+
'Baby': 'Baby',
|
235 |
+
'Young': 'Young',
|
236 |
+
'Adult': 'Adult',
|
237 |
+
'Senior': 'Senior'
|
238 |
+
}
|
239 |
+
if params['age'] in age_mapping:
|
240 |
+
rg_params['data']['filters'].append({
|
241 |
+
"fieldName": "ageGroup",
|
242 |
+
"operation": "equals",
|
243 |
+
"criteria": age_mapping[params['age']]
|
244 |
+
})
|
245 |
+
|
246 |
+
# Add gender filter
|
247 |
+
if params.get('gender'):
|
248 |
+
gender_mapping = {
|
249 |
+
'Male': 'Male',
|
250 |
+
'Female': 'Female'
|
251 |
+
}
|
252 |
+
if params['gender'] in gender_mapping:
|
253 |
+
rg_params['data']['filters'].append({
|
254 |
+
"fieldName": "sex",
|
255 |
+
"operation": "equals",
|
256 |
+
"criteria": gender_mapping[params['gender']]
|
257 |
+
})
|
258 |
+
|
259 |
+
# Add compatibility filters
|
260 |
+
if params.get('good_with_children'):
|
261 |
+
rg_params['data']['filters'].append({
|
262 |
+
"fieldName": "isKidFriendly",
|
263 |
+
"operation": "equals",
|
264 |
+
"criteria": True
|
265 |
+
})
|
266 |
+
|
267 |
+
if params.get('good_with_dogs'):
|
268 |
+
rg_params['data']['filters'].append({
|
269 |
+
"fieldName": "isDogFriendly",
|
270 |
+
"operation": "equals",
|
271 |
+
"criteria": True
|
272 |
+
})
|
273 |
+
|
274 |
+
if params.get('good_with_cats'):
|
275 |
+
rg_params['data']['filters'].append({
|
276 |
+
"fieldName": "isCatFriendly",
|
277 |
+
"operation": "equals",
|
278 |
+
"criteria": True
|
279 |
+
})
|
280 |
+
|
281 |
+
# Size filter - mapping is approximate
|
282 |
+
if params.get('size'):
|
283 |
+
size_mapping = {
|
284 |
+
'Small': ['Small'],
|
285 |
+
'Medium': ['Medium'],
|
286 |
+
'Large': ['Large', 'Extra Large'],
|
287 |
+
'XLarge': ['Extra Large']
|
288 |
+
}
|
289 |
+
if params['size'] in size_mapping:
|
290 |
+
rg_params['data']['filters'].append({
|
291 |
+
"fieldName": "sizeGroup",
|
292 |
+
"operation": "in",
|
293 |
+
"criteria": size_mapping[params['size']]
|
294 |
+
})
|
295 |
+
|
296 |
+
try:
|
297 |
+
response = requests.post(url, headers=headers, json=rg_params)
|
298 |
+
response.raise_for_status()
|
299 |
+
results = response.json()
|
300 |
+
|
301 |
+
# Normalize to match Petfinder format and add source
|
302 |
+
normalized_results = normalize_rescuegroups_results(results)
|
303 |
+
return normalized_results
|
304 |
+
except requests.exceptions.RequestException as e:
|
305 |
+
st.session_state.rescuegroups_status = {'success': False, 'message': f'Search error: {str(e)}'}
|
306 |
+
return None
|
307 |
+
|
308 |
+
# Function to normalize RescueGroups.org results to match Petfinder format
|
309 |
+
def normalize_rescuegroups_results(rg_results):
|
310 |
+
normalized = {"animals": []}
|
311 |
+
|
312 |
+
if 'data' not in rg_results:
|
313 |
+
return normalized
|
314 |
+
|
315 |
+
for pet in rg_results['data']:
|
316 |
+
attributes = pet.get('attributes', {})
|
317 |
+
relationships = pet.get('relationships', {})
|
318 |
+
|
319 |
+
# Map breed
|
320 |
+
breeds = {
|
321 |
+
'primary': attributes.get('breedPrimary', {}).get('name', ''),
|
322 |
+
'secondary': attributes.get('breedSecondary', {}).get('name', ''),
|
323 |
+
'mixed': attributes.get('isMixedBreed', False)
|
324 |
+
}
|
325 |
+
|
326 |
+
# Map colors
|
327 |
+
colors = {
|
328 |
+
'primary': attributes.get('colorPrimary', {}).get('name', ''),
|
329 |
+
'secondary': attributes.get('colorSecondary', {}).get('name', ''),
|
330 |
+
'tertiary': attributes.get('colorTertiary', {}).get('name', '')
|
331 |
+
}
|
332 |
+
|
333 |
+
# Map contact info
|
334 |
+
contact = {
|
335 |
+
'email': attributes.get('contactEmail', ''),
|
336 |
+
'phone': attributes.get('contactPhone', ''),
|
337 |
+
'address': {
|
338 |
+
'address1': attributes.get('locationAddress', ''),
|
339 |
+
'address2': '',
|
340 |
+
'city': attributes.get('locationCity', ''),
|
341 |
+
'state': attributes.get('locationState', ''),
|
342 |
+
'postcode': attributes.get('locationPostalcode', ''),
|
343 |
+
'country': attributes.get('locationCountry', 'US')
|
344 |
+
}
|
345 |
+
}
|
346 |
+
|
347 |
+
# Map environment compatibility
|
348 |
+
environment = {
|
349 |
+
'children': attributes.get('isKidFriendly', None),
|
350 |
+
'dogs': attributes.get('isDogFriendly', None),
|
351 |
+
'cats': attributes.get('isCatFriendly', None)
|
352 |
+
}
|
353 |
+
|
354 |
+
# Map attributes
|
355 |
+
pet_attributes = {
|
356 |
+
'spayed_neutered': attributes.get('isAltered', False),
|
357 |
+
'house_trained': attributes.get('isHousetrained', False),
|
358 |
+
'special_needs': attributes.get('hasSpecialNeeds', False),
|
359 |
+
'shots_current': attributes.get('isVaccinated', False)
|
360 |
+
}
|
361 |
+
|
362 |
+
# Map photos
|
363 |
+
photos = []
|
364 |
+
if 'pictures' in relationships and relationships['pictures'].get('data'):
|
365 |
+
for i, pic in enumerate(relationships['pictures']['data']):
|
366 |
+
pic_id = pic.get('id')
|
367 |
+
if pic_id and 'included' in rg_results:
|
368 |
+
for included in rg_results['included']:
|
369 |
+
if included.get('id') == pic_id and included.get('type') == 'pictures':
|
370 |
+
pic_url = included.get('attributes', {}).get('original', '')
|
371 |
+
if pic_url:
|
372 |
+
photos.append({
|
373 |
+
'small': pic_url,
|
374 |
+
'medium': pic_url,
|
375 |
+
'large': pic_url,
|
376 |
+
'full': pic_url
|
377 |
+
})
|
378 |
+
|
379 |
+
# Build the normalized pet object
|
380 |
+
normalized_pet = {
|
381 |
+
'id': pet['id'],
|
382 |
+
'source': 'rescuegroups',
|
383 |
+
'organization_id': attributes.get('orgId', ''),
|
384 |
+
'url': attributes.get('url', ''),
|
385 |
+
'type': attributes.get('species', {}).get('singular', ''),
|
386 |
+
'species': attributes.get('species', {}).get('singular', ''),
|
387 |
+
'age': attributes.get('ageGroup', ''),
|
388 |
+
'gender': attributes.get('sex', ''),
|
389 |
+
'size': attributes.get('sizeGroup', ''),
|
390 |
+
'name': attributes.get('name', ''),
|
391 |
+
'description': attributes.get('descriptionText', ''),
|
392 |
+
'status': 'adoptable',
|
393 |
+
'breeds': breeds,
|
394 |
+
'colors': colors,
|
395 |
+
'contact': contact,
|
396 |
+
'photos': photos,
|
397 |
+
'environment': environment,
|
398 |
+
'attributes': pet_attributes,
|
399 |
+
'published_at': attributes.get('createdDate', ''),
|
400 |
+
'distance': attributes.get('distance', None)
|
401 |
+
}
|
402 |
+
|
403 |
+
normalized['animals'].append(normalized_pet)
|
404 |
+
|
405 |
+
return normalized
|
406 |
+
|
407 |
+
# Function to search pets on both APIs and combine results
|
408 |
+
def search_all_apis(params):
|
409 |
+
# Initialize results containers
|
410 |
+
results = {"animals": []}
|
411 |
+
petfinder_count = 0
|
412 |
+
rescuegroups_count = 0
|
413 |
+
|
414 |
+
# Search Petfinder
|
415 |
+
petfinder_results = search_petfinder(params)
|
416 |
+
if petfinder_results and 'animals' in petfinder_results:
|
417 |
+
results['animals'].extend(petfinder_results['animals'])
|
418 |
+
petfinder_count = len(petfinder_results['animals'])
|
419 |
+
|
420 |
+
# Search RescueGroups.org
|
421 |
+
rescuegroups_results = search_rescuegroups(params)
|
422 |
+
if rescuegroups_results and 'animals' in rescuegroups_results:
|
423 |
+
results['animals'].extend(rescuegroups_results['animals'])
|
424 |
+
rescuegroups_count = len(rescuegroups_results['animals'])
|
425 |
+
|
426 |
+
# Sort by distance if location was provided
|
427 |
+
if params.get('location') and results['animals']:
|
428 |
+
results['animals'] = sorted(
|
429 |
+
results['animals'],
|
430 |
+
key=lambda x: x.get('distance', float('inf')) if x.get('distance') is not None else float('inf')
|
431 |
+
)
|
432 |
+
|
433 |
+
return results, petfinder_count, rescuegroups_count
|
434 |
+
|
435 |
+
# Function to get pet details from Petfinder
|
436 |
+
def get_petfinder_details(pet_id):
|
437 |
+
token = get_petfinder_token()
|
438 |
if not token:
|
439 |
+
return None
|
440 |
|
441 |
+
url = f"https://api.petfinder.com/v2/animals/{pet_id}"
|
442 |
headers = {"Authorization": f"Bearer {token}"}
|
443 |
|
444 |
try:
|
445 |
response = requests.get(url, headers=headers)
|
446 |
response.raise_for_status()
|
447 |
+
pet = response.json()['animal']
|
448 |
+
pet['source'] = 'petfinder'
|
449 |
+
return pet
|
450 |
except requests.exceptions.RequestException as e:
|
451 |
+
st.session_state.petfinder_status = {'success': False, 'message': f'Details error: {str(e)}'}
|
452 |
+
return None
|
453 |
|
454 |
+
# Function to get pet details from RescueGroups.org
|
455 |
+
def get_rescuegroups_details(pet_id):
|
456 |
+
api_key = get_rescuegroups_token()
|
457 |
+
if not api_key:
|
458 |
+
return None
|
459 |
|
460 |
+
url = f"https://api.rescuegroups.org/v5/public/animals/{pet_id}"
|
461 |
+
headers = {
|
462 |
+
"Authorization": api_key,
|
463 |
+
"Content-Type": "application/vnd.api+json"
|
464 |
+
}
|
465 |
|
466 |
try:
|
467 |
+
response = requests.get(url, headers=headers)
|
468 |
response.raise_for_status()
|
469 |
+
result = response.json()
|
470 |
+
|
471 |
+
if 'data' not in result:
|
472 |
+
return None
|
473 |
+
|
474 |
+
# Normalize to match Petfinder format
|
475 |
+
normalized_results = {"animals": []}
|
476 |
+
normalized = normalize_rescuegroups_results(result)
|
477 |
+
|
478 |
+
if normalized['animals']:
|
479 |
+
pet = normalized['animals'][0]
|
480 |
+
return pet
|
481 |
+
return None
|
482 |
except requests.exceptions.RequestException as e:
|
483 |
+
st.session_state.rescuegroups_status = {'success': False, 'message': f'Details error: {str(e)}'}
|
484 |
+
return None
|
485 |
|
486 |
+
# Function to get pet details from either API based on source
|
487 |
+
def get_pet_details(pet_id, source):
|
488 |
+
if source == 'petfinder':
|
489 |
+
return get_petfinder_details(pet_id)
|
490 |
+
elif source == 'rescuegroups':
|
491 |
+
return get_rescuegroups_details(pet_id)
|
492 |
+
return None
|
493 |
+
|
494 |
+
# Function to get breeds from Petfinder
|
495 |
+
def get_breeds(animal_type):
|
496 |
+
token = get_petfinder_token()
|
497 |
if not token:
|
498 |
+
return []
|
499 |
|
500 |
+
url = f"https://api.petfinder.com/v2/types/{animal_type}/breeds"
|
501 |
headers = {"Authorization": f"Bearer {token}"}
|
502 |
|
503 |
try:
|
504 |
response = requests.get(url, headers=headers)
|
505 |
response.raise_for_status()
|
506 |
+
return [breed['name'] for breed in response.json()['breeds']]
|
507 |
except requests.exceptions.RequestException as e:
|
508 |
+
st.session_state.petfinder_status = {'success': False, 'message': f'Breeds error: {str(e)}'}
|
509 |
+
return []
|
510 |
|
511 |
+
# Function to generate pet compatibility message
|
512 |
+
def get_compatibility_message(pet):
|
513 |
+
messages = []
|
514 |
+
|
515 |
+
# Check for kids
|
516 |
+
if 'environment' in pet and 'children' in pet['environment'] and pet['environment']['children'] is not None:
|
517 |
+
if pet['environment']['children']:
|
518 |
+
messages.append("✅ Good with children")
|
519 |
+
else:
|
520 |
+
messages.append("❌ Not recommended for homes with children")
|
521 |
+
|
522 |
+
# Check for dogs
|
523 |
+
if 'environment' in pet and 'dogs' in pet['environment'] and pet['environment']['dogs'] is not None:
|
524 |
+
if pet['environment']['dogs']:
|
525 |
+
messages.append("✅ Good with dogs")
|
526 |
+
else:
|
527 |
+
messages.append("❌ Not recommended for homes with dogs")
|
528 |
+
|
529 |
+
# Check for cats
|
530 |
+
if 'environment' in pet and 'cats' in pet['environment'] and pet['environment']['cats'] is not None:
|
531 |
+
if pet['environment']['cats']:
|
532 |
+
messages.append("✅ Good with cats")
|
533 |
+
else:
|
534 |
+
messages.append("❌ Not recommended for homes with cats")
|
535 |
+
|
536 |
+
# Handling care needs
|
537 |
+
if 'attributes' in pet:
|
538 |
+
if 'special_needs' in pet['attributes'] and pet['attributes']['special_needs']:
|
539 |
+
messages.append("⚠️ Has special needs")
|
540 |
+
|
541 |
+
if 'house_trained' in pet['attributes'] and pet['attributes']['house_trained']:
|
542 |
+
messages.append("✅ House-trained")
|
543 |
+
elif 'house_trained' in pet['attributes']:
|
544 |
+
messages.append("❌ Not house-trained")
|
545 |
+
|
546 |
+
if 'shots_current' in pet['attributes'] and pet['attributes']['shots_current']:
|
547 |
+
messages.append("✅ Vaccinations up to date")
|
548 |
+
|
549 |
+
if 'spayed_neutered' in pet['attributes'] and pet['attributes']['spayed_neutered']:
|
550 |
+
messages.append("✅ Spayed/neutered")
|
551 |
+
|
552 |
+
return messages
|
553 |
+
|
554 |
+
# Function to display pet card with source indicator
|
555 |
+
def display_pet_card(pet, is_favorite=False, context="search", tab_id="tab1"):
|
556 |
col1, col2 = st.columns([1, 2])
|
557 |
|
558 |
with col1:
|
|
|
562 |
st.image("https://via.placeholder.com/300x300?text=No+Image", use_container_width=True)
|
563 |
|
564 |
with col2:
|
565 |
+
# Pet name and source indicator
|
566 |
+
source_tag = '<span class="source-tag">Petfinder</span>' if pet['source'] == 'petfinder' else '<span class="source-tag">RescueGroups</span>'
|
567 |
+
st.markdown(f"<div class='pet-name'>{pet['name']} {source_tag}</div>", unsafe_allow_html=True)
|
568 |
|
569 |
# Tags
|
570 |
tags_html = ""
|
|
|
601 |
st.markdown("</div>", unsafe_allow_html=True)
|
602 |
|
603 |
if pet['description']:
|
604 |
+
st.markdown(f"<div class='pet-description'>{pet['description'][:300]}{'...' if len(pet['description']) > 300 else ''}</div>", unsafe_allow_html=True)
|
605 |
|
606 |
col1, col2 = st.columns(2)
|
607 |
with col1:
|
608 |
+
if st.button("View Details", key=f"details_{tab_id}_{context}_{pet['source']}_{pet['id']}"):
|
609 |
st.session_state.selected_pet = pet['id']
|
610 |
+
st.session_state.selected_pet_source = pet['source']
|
611 |
st.rerun()
|
612 |
with col2:
|
613 |
if not is_favorite:
|
614 |
+
if st.button("Add to Favorites", key=f"fav_{tab_id}_{context}_{pet['source']}_{pet['id']}"):
|
615 |
if pet['id'] not in [p['id'] for p in st.session_state.favorites]:
|
616 |
st.session_state.favorites.append(pet)
|
617 |
st.success(f"Added {pet['name']} to favorites!")
|
618 |
st.rerun()
|
619 |
else:
|
620 |
+
if st.button("Remove from Favorites", key=f"unfav_{tab_id}_{context}_{pet['source']}_{pet['id']}"):
|
621 |
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']]
|
622 |
st.success(f"Removed {pet['name']} from favorites!")
|
623 |
st.rerun()
|
624 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
625 |
# Function to display pet details page
|
626 |
+
def display_pet_details(pet_id, pet_source, context="search", tab_id="tab1"):
|
627 |
+
pet = get_pet_details(pet_id, pet_source)
|
|
|
|
|
|
|
628 |
if not pet:
|
629 |
st.error("Unable to retrieve pet details. Please try again.")
|
630 |
return
|
631 |
|
632 |
+
# Back button
|
633 |
+
if st.button("← Back to Search Results", key=f"back_{tab_id}_{context}_{pet_source}_{pet_id}"):
|
634 |
st.session_state.selected_pet = None
|
635 |
+
st.session_state.selected_pet_source = None
|
636 |
+
st.rerun()
|
637 |
|
638 |
+
# Pet name and source indicator
|
639 |
+
source_tag = '<span class="source-tag">Petfinder</span>' if pet_source == 'petfinder' else '<span class="source-tag">RescueGroups</span>'
|
640 |
+
st.markdown(f"<h1 class='main-header'>{pet['name']} {source_tag}</h1>", unsafe_allow_html=True)
|
641 |
|
642 |
+
# Pet status
|
643 |
status_color = "#c8e6c9" if pet['status'] == 'adoptable' else "#ffcdd2"
|
644 |
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)
|
645 |
|
|
|
688 |
|
689 |
# Description
|
690 |
if pet['description']:
|
691 |
+
st.markdown("### About")
|
692 |
+
st.markdown(f"<div class='pet-description'>{pet['description']}</div>", unsafe_allow_html=True)
|
|
|
|
|
693 |
|
694 |
# Contact information
|
695 |
st.markdown("### Adoption Information")
|
696 |
|
697 |
# Organization info
|
698 |
if pet['organization_id']:
|
699 |
+
st.markdown(f"**Organization ID:** {pet['organization_id']}")
|
700 |
|
701 |
# Contact details
|
702 |
contact_info = []
|
|
|
710 |
for info in contact_info:
|
711 |
st.markdown(info)
|
712 |
|
713 |
+
# URL to pet on source website
|
714 |
if pet['url']:
|
715 |
+
st.markdown(f"[View on {'Petfinder' if pet_source == 'petfinder' else 'RescueGroups'}]({pet['url']})")
|
716 |
|
717 |
+
# Add to favorites
|
718 |
is_favorite = pet['id'] in [p['id'] for p in st.session_state.favorites]
|
719 |
if not is_favorite:
|
720 |
+
if st.button("Add to Favorites", key=f"add_fav_{tab_id}_{context}_{pet_source}_{pet['id']}"):
|
721 |
+
if pet['id'] not in [p['id'] for p in st.session_state.favorites]:
|
722 |
+
st.session_state.favorites.append(pet)
|
723 |
+
st.success(f"Added {pet['name']} to favorites!")
|
724 |
+
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
725 |
else:
|
726 |
+
if st.button("Remove from Favorites", key=f"remove_fav_{tab_id}_{context}_{pet_source}_{pet['id']}"):
|
727 |
+
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']]
|
728 |
+
st.success(f"Removed {pet['name']} from favorites!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
729 |
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
730 |
|
731 |
+
# Main interface components
|
732 |
+
st.markdown("<h1 class='main-header'>PetMatch 🐾</h1>", unsafe_allow_html=True)
|
733 |
+
st.markdown("<h2 class='sub-header'>Find Your Perfect Pet Companion</h2>", unsafe_allow_html=True)
|
734 |
+
|
735 |
+
# Display API connection status
|
736 |
+
col1, col2 = st.columns(2)
|
737 |
+
with col1:
|
738 |
+
status_class = "api-success" if st.session_state.petfinder_status['success'] else "api-error"
|
739 |
+
st.markdown(f"<div class='api-status {status_class}'>Petfinder API: {st.session_state.petfinder_status['message']}</div>", unsafe_allow_html=True)
|
740 |
+
|
741 |
+
with col2:
|
742 |
+
status_class = "api-success" if st.session_state.rescuegroups_status['success'] else "api-error"
|
743 |
+
st.markdown(f"<div class='api-status {status_class}'>RescueGroups API: {st.session_state.rescuegroups_status['message']}</div>", unsafe_allow_html=True)
|
744 |
+
|
745 |
+
# Create tabs for search and favorites
|
746 |
+
tab1, tab2 = st.tabs(["Search", "My Favorites"])
|
747 |
+
|
748 |
+
# Search tab
|
749 |
+
with tab1:
|
750 |
+
if st.session_state.selected_pet:
|
751 |
+
display_pet_details(st.session_state.selected_pet, st.session_state.selected_pet_source, context="search", tab_id="tab1")
|
752 |
+
else:
|
753 |
+
# Search form
|
754 |
+
with st.expander("Search Options", expanded=True):
|
755 |
+
search_col1, search_col2 = st.columns(2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
756 |
|
757 |
+
with search_col1:
|
758 |
+
location = st.text_input("Location (ZIP code or City, State)", key="location")
|
759 |
+
distance = st.slider("Distance (miles)", 10, 500, 100, 10, key="distance")
|
760 |
|
761 |
+
pet_type = st.selectbox(
|
762 |
+
"Animal Type",
|
763 |
+
["Dog", "Cat", "Rabbit", "Small & Furry", "Horse", "Bird", "Scales, Fins & Other", "Barnyard"],
|
764 |
+
key="type"
|
765 |
+
)
|
766 |
|
767 |
+
breed_list = get_breeds(pet_type.lower().split(" ")[0]) if pet_type else []
|
768 |
+
breed = st.selectbox("Breed", ["Any"] + breed_list, key="breed")
|
|
|
|
|
|
|
|
|
|
|
769 |
|
770 |
+
with search_col2:
|
771 |
+
age = st.multiselect(
|
772 |
+
"Age",
|
773 |
+
["Baby", "Young", "Adult", "Senior"],
|
774 |
+
key="ages"
|
775 |
+
)
|
776 |
|
777 |
+
gender = st.multiselect(
|
778 |
+
"Gender",
|
779 |
+
["Male", "Female"],
|
780 |
+
key="genders"
|
781 |
+
)
|
782 |
+
|
783 |
+
size = st.multiselect(
|
784 |
+
"Size",
|
785 |
+
["Small", "Medium", "Large", "XLarge"],
|
786 |
+
key="sizes"
|
787 |
+
)
|
788 |
+
|
789 |
+
st.markdown("### Pet Compatibility")
|
790 |
+
col1, col2, col3 = st.columns(3)
|
791 |
+
with col1:
|
792 |
+
good_with_children = st.checkbox("Good with children", key="children")
|
793 |
+
with col2:
|
794 |
+
good_with_dogs = st.checkbox("Good with dogs", key="dogs")
|
795 |
+
with col3:
|
796 |
+
good_with_cats = st.checkbox("Good with cats", key="cats")
|
797 |
|
798 |
+
# Search button
|
799 |
+
if st.button("Search for Pets"):
|
800 |
+
# Build search parameters
|
801 |
+
params = {
|
802 |
+
"type": pet_type.lower().split(" ")[0],
|
803 |
+
"location": location,
|
804 |
+
"distance": distance,
|
805 |
+
"page": st.session_state.page,
|
806 |
+
"limit": 100, # Maximum allowed by Petfinder
|
807 |
+
"sort": "distance" if location else "recent"
|
808 |
+
}
|
809 |
+
|
810 |
+
# Add optional parameters
|
811 |
+
if breed and breed != "Any":
|
812 |
+
params["breed"] = breed
|
813 |
+
|
814 |
+
if age:
|
815 |
+
params["age"] = ",".join(age)
|
816 |
+
|
817 |
+
if gender:
|
818 |
+
params["gender"] = ",".join(gender)
|
819 |
+
|
820 |
+
if size:
|
821 |
+
params["size"] = ",".join(size)
|
822 |
+
|
823 |
+
if good_with_children:
|
824 |
+
params["good_with_children"] = True
|
825 |
+
|
826 |
+
if good_with_dogs:
|
827 |
+
params["good_with_dogs"] = True
|
828 |
+
|
829 |
+
if good_with_cats:
|
830 |
+
params["good_with_cats"] = True
|
831 |
+
|
832 |
+
# Perform search
|
833 |
+
with st.spinner("Searching for pets..."):
|
834 |
+
results, petfinder_count, rescuegroups_count = search_all_apis(params)
|
835 |
+
st.session_state.search_results = results
|
836 |
+
|
837 |
+
# Show source counts
|
838 |
+
st.markdown(f"Found {len(results['animals'])} pets ({petfinder_count} from Petfinder, {rescuegroups_count} from RescueGroups)")
|
839 |
+
|
840 |
+
# Display search results
|
841 |
+
if st.session_state.search_results:
|
842 |
+
# Display results
|
843 |
+
for pet in st.session_state.search_results['animals']:
|
844 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
845 |
+
display_pet_card(pet, context="search", tab_id="tab1")
|
846 |
+
|
847 |
+
# Pagination is not currently implemented
|
848 |
+
# Future enhancement: Add pagination controls
|
849 |
+
|
850 |
+
# Favorites tab
|
851 |
+
with tab2:
|
852 |
+
if st.session_state.selected_pet and st.session_state.selected_pet_source:
|
853 |
+
display_pet_details(st.session_state.selected_pet, st.session_state.selected_pet_source, context="favorites", tab_id="tab2")
|
854 |
+
else:
|
855 |
if not st.session_state.favorites:
|
856 |
+
st.info("You haven't added any pets to your favorites yet.")
|
857 |
else:
|
858 |
+
st.markdown(f"### Your Favorites ({len(st.session_state.favorites)} pets)")
|
859 |
+
|
860 |
+
for pet in st.session_state.favorites:
|
861 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
862 |
+
display_pet_card(pet, is_favorite=True, context="favorites", tab_id="tab2")
|
863 |
+
|
864 |
+
# Footer
|
865 |
+
st.markdown("""
|
866 |
+
<div style="text-align: center; margin-top: 3rem; opacity: 0.7; font-size: 0.8rem;">
|
867 |
+
<p>PetMatch connects to Petfinder and RescueGroups.org APIs to help you find adoptable pets.</p>
|
868 |
+
<p>© 2025 PetMatch. Not affiliated with Petfinder or RescueGroups.</p>
|
869 |
+
</div>
|
870 |
+
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
871 |
|
872 |
+
# Initialize API tokens on app start
|
873 |
+
if not st.session_state.petfinder_access_token:
|
874 |
+
get_petfinder_token()
|
875 |
|
876 |
+
if not st.session_state.rescuegroups_access_token:
|
877 |
+
get_rescuegroups_token()
|