CCockrum commited on
Commit
2149db2
·
verified ·
1 Parent(s): 750e66f

Create app.py

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