CCockrum commited on
Commit
c0dff24
·
verified ·
1 Parent(s): e294146

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +595 -323
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 'access_token' not in st.session_state:
62
- st.session_state.access_token = None
63
- if 'token_expires' not in st.session_state:
64
- st.session_state.token_expires = 0
 
 
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 get_access_token():
76
  # Check if token is still valid
77
- if st.session_state.access_token and time.time() < st.session_state.token_expires:
78
- return st.session_state.access_token
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.error("⚠️ Petfinder API credentials are missing. Please set them in your environment variables or Streamlit secrets.")
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.access_token = token_data['access_token']
101
- st.session_state.token_expires = time.time() + token_data['expires_in'] - 60 # Buffer of 60 seconds
102
- return st.session_state.access_token
 
103
  except requests.exceptions.RequestException as e:
104
- st.error(f"⚠️ Error getting access token: {str(e)}")
 
 
 
 
 
 
 
 
 
105
  return None
 
 
 
 
 
106
 
107
- # Function to search pets
108
- def search_pets(params):
109
- token = get_access_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
- return response.json()
 
 
 
 
 
 
120
  except requests.exceptions.RequestException as e:
121
- st.error(f"⚠️ Error searching pets: {str(e)}")
122
  return None
123
 
124
- # Function to get breeds
125
- def get_breeds(animal_type):
126
- token = get_access_token()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  if not token:
128
- return []
129
 
130
- url = f"https://api.petfinder.com/v2/types/{animal_type}/breeds"
131
  headers = {"Authorization": f"Bearer {token}"}
132
 
133
  try:
134
  response = requests.get(url, headers=headers)
135
  response.raise_for_status()
136
- return [breed['name'] for breed in response.json()['breeds']]
 
 
137
  except requests.exceptions.RequestException as e:
138
- st.error(f"⚠️ Error getting breeds: {str(e)}")
139
- return []
140
 
141
- # Function to get organizations
142
- def get_organizations(location):
143
- token = get_access_token()
144
- if not token:
145
- return []
146
 
147
- url = "https://api.petfinder.com/v2/organizations"
148
- headers = {"Authorization": f"Bearer {token}"}
149
- params = {"location": location, "distance": 100, "limit": 100}
 
 
150
 
151
  try:
152
- response = requests.get(url, headers=headers, params=params)
153
  response.raise_for_status()
154
- return [(org['id'], org['name']) for org in response.json()['organizations']]
 
 
 
 
 
 
 
 
 
 
 
 
155
  except requests.exceptions.RequestException as e:
156
- st.error(f"⚠️ Error getting organizations: {str(e)}")
157
- return []
158
 
159
- # Function to get pet details
160
- def get_pet_details(pet_id):
161
- token = get_access_token()
 
 
 
 
 
 
 
 
162
  if not token:
163
- return None
164
 
165
- url = f"https://api.petfinder.com/v2/animals/{pet_id}"
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()['animal']
172
  except requests.exceptions.RequestException as e:
173
- st.error(f"⚠️ Error getting pet details: {str(e)}")
174
- return None
175
 
176
- # Function to format pet card
177
- def display_pet_card(pet, is_favorite=False, context="search"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- st.markdown(f"<div class='pet-name'>{pet['name']}</div>", unsafe_allow_html=True)
 
 
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'][:500]}{'...' if len(pet['description']) > 500 else ''}</div>", unsafe_allow_html=True)
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
- # Changes to make keys unique across different tabs
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 with unique key that includes tab identifier
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.rerun() # Force immediate rerun
 
301
 
302
- # Pet name and status
303
- st.markdown(f"<h1 class='main-header'>{pet['name']}</h1>", unsafe_allow_html=True)
 
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
- if pet['description']:
354
- st.markdown("### About")
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 Petfinder
378
  if pet['url']:
379
- st.markdown(f"[View on Petfinder]({pet['url']})")
380
 
381
- # Add to favorites with unique key
382
  is_favorite = pet['id'] in [p['id'] for p in st.session_state.favorites]
383
  if not is_favorite:
384
- if st.button("Add to Favorites", key=f"add_fav_{tab_id}_{context}_{pet_id}"):
385
- st.session_state.favorites.append(pet)
386
- st.success(f"Added {pet['name']} to favorites!")
387
- st.rerun()
388
- else:
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
- tags_html += f"<span class='tag' style='background-color: #808080;'>{pet['status'].title()}</span> "
413
-
414
- if pet['age']:
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 app with updated function calls
463
- def main():
464
- # Title
465
- st.markdown("<h1 class='main-header'>🐾 PetMatch</h1>", unsafe_allow_html=True)
466
- st.markdown("<p class='sub-header'>Find your perfect pet companion</p>", unsafe_allow_html=True)
467
-
468
- # Create tabs
469
- tab1, tab2, tab3 = st.tabs(["Search", "Favorites", "About"])
470
-
471
- with tab1:
472
- # If a pet is selected, show details
473
- if st.session_state.selected_pet:
474
- display_pet_details(st.session_state.selected_pet, context="search", tab_id="tab1")
475
- else:
476
- # Search form
477
- with st.expander("Search Options", expanded=True):
478
- with st.form("pet_search_form"):
479
- col1, col2 = st.columns(2)
480
-
481
- with col1:
482
- animal_type = st.selectbox(
483
- "Animal Type",
484
- ["Dog", "Cat", "Rabbit", "Small & Furry", "Horse", "Bird", "Scales, Fins & Other", "Barnyard"]
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
- # Display search results
548
- if st.session_state.search_results and 'animals' in st.session_state.search_results:
549
- st.markdown("### Search Results")
550
 
551
- # Pagination
552
- results = st.session_state.search_results['animals']
553
- total_pages = (len(results) + 9) // 10 # 10 items per page
 
 
554
 
555
- # Display page selector
556
- if total_pages > 1:
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
- # Display pets for current page
564
- start_idx = (st.session_state.page - 1) * 10
565
- end_idx = min(start_idx + 10, len(results))
 
 
 
566
 
567
- for pet in results[start_idx:end_idx]:
568
- st.markdown("---")
569
- display_pet_card(pet, tab_id="tab1")
570
-
571
- with tab2:
572
- st.markdown("### Your Favorite Pets")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  if not st.session_state.favorites:
575
- st.info("You haven't added any pets to your favorites yet. Start searching to find your perfect match!")
576
  else:
577
- # Check if a pet is selected from favorites
578
- if st.session_state.selected_pet:
579
- display_pet_details(st.session_state.selected_pet, context="favorites", tab_id="tab2")
580
- else:
581
- for pet in st.session_state.favorites:
582
- st.markdown("---")
583
- display_pet_card(pet, is_favorite=True, context="favorites", tab_id="tab2")
584
-
585
- with tab3:
586
- st.markdown("### About PetMatch")
587
- st.markdown("""
588
- PetMatch helps you find your perfect pet companion from thousands of adoptable animals across the country.
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 __name__ == "__main__":
605
- main()
 
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()