CCockrum commited on
Commit
bdad786
·
verified ·
1 Parent(s): f8a2290

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +320 -592
app.py CHANGED
@@ -6,9 +6,6 @@ from datetime import datetime
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,66 +54,35 @@ st.markdown("""
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,31 +97,16 @@ def get_petfinder_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,394 +116,65 @@ def search_petfinder(params):
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,9 +184,7 @@ def display_pet_card(pet, is_favorite=False, context="search", tab_id="tab1"):
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,45 +221,87 @@ def display_pet_card(pet, is_favorite=False, context="search", tab_id="tab1"):
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,15 +350,17 @@ def display_pet_details(pet_id, pet_source, context="search", tab_id="tab1"):
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,168 +374,232 @@ def display_pet_details(pet_id, pet_source, context="search", tab_id="tab1"):
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()
 
6
  import time
7
  import os
8
  import functools
 
 
 
9
 
10
  # Set page configuration
11
  st.set_page_config(
 
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
  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
  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
  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
  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
 
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
  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()