Spaces:
Sleeping
Sleeping
Upload 11 files
Browse files- app.py +196 -307
- config_manager.py +43 -7
- requirements.txt +2 -2
app.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
import json
|
| 3 |
import os
|
|
|
|
| 4 |
from datetime import datetime, date, timedelta
|
| 5 |
import pandas as pd
|
|
|
|
|
|
|
| 6 |
from config_manager import ConfigManager
|
| 7 |
from dashboard import Dashboard
|
| 8 |
from tasks import TasksManager
|
|
@@ -190,22 +193,120 @@ st.markdown("""
|
|
| 190 |
</style>
|
| 191 |
""", unsafe_allow_html=True)
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
def main():
|
| 194 |
# Initialize session state
|
| 195 |
if 'config_manager' not in st.session_state:
|
| 196 |
st.session_state.config_manager = ConfigManager()
|
| 197 |
|
| 198 |
-
#
|
| 199 |
-
if '
|
| 200 |
-
st.session_state.
|
| 201 |
-
|
| 202 |
-
if
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
else:
|
| 205 |
-
|
|
|
|
| 206 |
|
| 207 |
-
def
|
| 208 |
-
"""Show the
|
| 209 |
|
| 210 |
# Hero Section
|
| 211 |
st.markdown("""
|
|
@@ -307,12 +408,12 @@ def show_landing_page():
|
|
| 307 |
margin-bottom: 2rem;
|
| 308 |
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 309 |
">
|
| 310 |
-
<h3 style="color: #2d5016; margin-bottom: 1rem;">
|
| 311 |
<ul style="color: #666; line-height: 1.8;">
|
| 312 |
-
<li>
|
| 313 |
-
<li>
|
| 314 |
-
<li>
|
| 315 |
-
<li>
|
| 316 |
</ul>
|
| 317 |
</div>
|
| 318 |
""", unsafe_allow_html=True)
|
|
@@ -327,12 +428,12 @@ def show_landing_page():
|
|
| 327 |
margin-bottom: 2rem;
|
| 328 |
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 329 |
">
|
| 330 |
-
<h3 style="color: #2d5016; margin-bottom: 1rem;">
|
| 331 |
<ul style="color: #666; line-height: 1.8;">
|
| 332 |
-
<li>
|
| 333 |
-
<li>
|
| 334 |
-
<li>
|
| 335 |
-
<li>
|
| 336 |
</ul>
|
| 337 |
</div>
|
| 338 |
""", unsafe_allow_html=True)
|
|
@@ -350,67 +451,32 @@ def show_landing_page():
|
|
| 350 |
<h3 style="color: #2d5016; margin-bottom: 1rem;">☁️ Cloud Sync</h3>
|
| 351 |
<ul style="color: #666; line-height: 1.8;">
|
| 352 |
<li>Google Drive integration</li>
|
| 353 |
-
<li>Access from anywhere</li>
|
| 354 |
<li>Automatic backups</li>
|
| 355 |
-
<li>Multi-device
|
|
|
|
| 356 |
</ul>
|
| 357 |
</div>
|
| 358 |
""", unsafe_allow_html=True)
|
| 359 |
|
| 360 |
st.markdown("---")
|
| 361 |
|
| 362 |
-
#
|
| 363 |
-
st.markdown("##
|
| 364 |
-
|
| 365 |
-
st.markdown("""
|
| 366 |
-
Ready to start planning your wedding? Choose one of the options below to get started.
|
| 367 |
-
""")
|
| 368 |
-
|
| 369 |
-
# Create two columns for the buttons
|
| 370 |
-
col1, col2 = st.columns(2)
|
| 371 |
-
|
| 372 |
-
with col1:
|
| 373 |
-
st.markdown("### 🎭 Try Demo Mode")
|
| 374 |
-
st.markdown("""
|
| 375 |
-
Experience the app with sample data including:
|
| 376 |
-
• Sample wedding (Emma & James)
|
| 377 |
-
• Demo guests with RSVPs
|
| 378 |
-
• Vendors with complex payment schedules
|
| 379 |
-
• Tasks in various stages
|
| 380 |
-
• Wedding party information
|
| 381 |
-
""")
|
| 382 |
-
|
| 383 |
-
if st.button("🎭 Start with Demo Data", type="primary", use_container_width=True):
|
| 384 |
-
with st.spinner("Loading demo data from Google Drive..."):
|
| 385 |
-
config_manager = st.session_state.config_manager
|
| 386 |
-
if config_manager.load_demo_data_from_drive():
|
| 387 |
-
st.session_state.app_initialized = True
|
| 388 |
-
st.success("✅ Demo data loaded successfully!")
|
| 389 |
-
st.rerun()
|
| 390 |
-
else:
|
| 391 |
-
st.error("❌ Failed to load demo data from Google Drive. Please check your connection and try again.")
|
| 392 |
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
""
|
| 402 |
-
|
| 403 |
-
st.
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
config_manager = st.session_state.config_manager
|
| 408 |
-
if config_manager.load_existing_data_from_drive():
|
| 409 |
-
st.session_state.app_initialized = True
|
| 410 |
-
st.success("✅ Wedding data loaded successfully!")
|
| 411 |
-
st.rerun()
|
| 412 |
-
else:
|
| 413 |
-
st.error("❌ Failed to load data from Google Drive. Please check your connection and try again.")
|
| 414 |
|
| 415 |
|
| 416 |
|
|
@@ -611,244 +677,48 @@ def show_wedding_setup_form():
|
|
| 611 |
else:
|
| 612 |
st.error("Please fill in at least the partner names and wedding date range in the form above.")
|
| 613 |
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
st.
|
| 618 |
-
|
| 619 |
-
# Show Google Drive status on setup page
|
| 620 |
-
show_google_drive_status_setup()
|
| 621 |
-
|
| 622 |
-
# Demo mode option
|
| 623 |
-
st.markdown("#### Choose Your Experience")
|
| 624 |
-
col1, col2 = st.columns(2)
|
| 625 |
-
|
| 626 |
-
with col1:
|
| 627 |
-
st.markdown("**🎭 Try Demo Mode**")
|
| 628 |
-
st.markdown("Experience the app with sample data including:")
|
| 629 |
-
st.markdown("• Sample wedding (Emma & James)")
|
| 630 |
-
st.markdown("• Demo guests with RSVPs")
|
| 631 |
-
st.markdown("• Vendors with complex payment schedules")
|
| 632 |
-
st.markdown("• Tasks in various stages")
|
| 633 |
-
st.markdown("• Wedding party information")
|
| 634 |
-
|
| 635 |
-
if st.button("Start with Demo Data", type="primary"):
|
| 636 |
-
if st.session_state.config_manager.set_demo_mode(True):
|
| 637 |
-
st.success("Demo mode enabled! Loading sample data...")
|
| 638 |
-
st.rerun()
|
| 639 |
-
else:
|
| 640 |
-
st.error("Failed to enable demo mode.")
|
| 641 |
-
|
| 642 |
-
with col2:
|
| 643 |
-
st.markdown("**📝 Start Fresh**")
|
| 644 |
-
st.markdown("Create your own wedding from scratch:")
|
| 645 |
-
st.markdown("• Enter your wedding details")
|
| 646 |
-
st.markdown("• Add your own events")
|
| 647 |
-
st.markdown("• Build your guest list")
|
| 648 |
-
st.markdown("• Manage your vendors")
|
| 649 |
-
st.markdown("• Track your tasks")
|
| 650 |
-
|
| 651 |
-
if st.button("Create My Wedding", type="secondary"):
|
| 652 |
-
if st.session_state.config_manager.set_demo_mode(False):
|
| 653 |
-
st.success("Demo mode disabled. Ready to create your wedding!")
|
| 654 |
-
st.rerun()
|
| 655 |
-
else:
|
| 656 |
-
st.error("Failed to disable demo mode.")
|
| 657 |
-
|
| 658 |
-
st.markdown("---")
|
| 659 |
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
if 'setup_form_data' not in st.session_state:
|
| 664 |
-
st.session_state.setup_form_data = {
|
| 665 |
-
'partner1_name': '',
|
| 666 |
-
'partner2_name': '',
|
| 667 |
-
'venue_city': '',
|
| 668 |
-
'wedding_start_date': date.today(),
|
| 669 |
-
'wedding_end_date': date.today(),
|
| 670 |
-
'custom_tags': '',
|
| 671 |
-
'task_assignees': ''
|
| 672 |
-
}
|
| 673 |
|
| 674 |
-
#
|
| 675 |
-
|
| 676 |
-
st.markdown("#### Basic Wedding Information")
|
| 677 |
-
|
| 678 |
-
col1, col2 = st.columns(2)
|
| 679 |
-
with col1:
|
| 680 |
-
partner1_name = st.text_input("Partner 1 Name", value=st.session_state.setup_form_data['partner1_name'], placeholder="Enter first partner's name")
|
| 681 |
-
partner2_name = st.text_input("Partner 2 Name", value=st.session_state.setup_form_data['partner2_name'], placeholder="Enter second partner's name")
|
| 682 |
-
venue_city = st.text_input("City", value=st.session_state.setup_form_data['venue_city'], placeholder="Enter city")
|
| 683 |
-
|
| 684 |
-
with col2:
|
| 685 |
-
st.markdown("**Wedding Date Range**")
|
| 686 |
-
wedding_start_date = st.date_input("Start Date", value=st.session_state.setup_form_data['wedding_start_date'])
|
| 687 |
-
wedding_end_date = st.date_input("End Date", value=st.session_state.setup_form_data['wedding_end_date'])
|
| 688 |
-
|
| 689 |
-
if wedding_end_date < wedding_start_date:
|
| 690 |
-
st.error("End date must be after start date")
|
| 691 |
-
wedding_end_date = wedding_start_date
|
| 692 |
-
|
| 693 |
-
st.markdown("#### Task Organization")
|
| 694 |
-
st.info("Tasks will be automatically grouped by your wedding events.")
|
| 695 |
-
|
| 696 |
-
st.markdown("#### Custom Tags")
|
| 697 |
-
st.markdown("Enter custom tags (one per line):")
|
| 698 |
-
custom_tags = st.text_area("Custom Tags", value=st.session_state.setup_form_data['custom_tags'], placeholder="e.g.,\nUrgent\nHigh Priority\nDeposit Required\nResearch Needed")
|
| 699 |
-
|
| 700 |
-
st.markdown("#### Task Assignees")
|
| 701 |
-
st.markdown("Enter people who will regularly be assigned tasks (one per line):")
|
| 702 |
-
task_assignees = st.text_area("Task Assignees", value=st.session_state.setup_form_data['task_assignees'], placeholder="e.g.,\nMom\nDad\nWedding Planner\nBest Friend\nCoordinator")
|
| 703 |
-
|
| 704 |
-
form_submitted = st.form_submit_button("Update Wedding Information")
|
| 705 |
-
|
| 706 |
-
if form_submitted:
|
| 707 |
-
# Update session state with form data
|
| 708 |
-
st.session_state.setup_form_data = {
|
| 709 |
-
'partner1_name': partner1_name,
|
| 710 |
-
'partner2_name': partner2_name,
|
| 711 |
-
'venue_city': venue_city,
|
| 712 |
-
'wedding_start_date': wedding_start_date,
|
| 713 |
-
'wedding_end_date': wedding_end_date,
|
| 714 |
-
'custom_tags': custom_tags,
|
| 715 |
-
'task_assignees': task_assignees
|
| 716 |
-
}
|
| 717 |
-
st.success("Wedding information updated!")
|
| 718 |
-
st.rerun()
|
| 719 |
|
| 720 |
-
#
|
| 721 |
-
|
| 722 |
-
st.markdown("Define all your wedding events with their details:")
|
| 723 |
|
| 724 |
-
#
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
if st.button("➕ Add Event"):
|
| 728 |
-
# Set default date to wedding start date
|
| 729 |
-
wedding_start = st.session_state.setup_form_data['wedding_start_date']
|
| 730 |
-
st.session_state.setup_events.append({
|
| 731 |
-
"name": "New Event",
|
| 732 |
-
"description": "",
|
| 733 |
-
"date_offset": 0,
|
| 734 |
-
"requires_meal_choice": False,
|
| 735 |
-
"meal_options": [],
|
| 736 |
-
"location": "",
|
| 737 |
-
"address": ""
|
| 738 |
-
})
|
| 739 |
-
st.rerun()
|
| 740 |
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
st.rerun()
|
| 745 |
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
current_event_date = wedding_start + timedelta(days=event['date_offset'])
|
| 764 |
-
|
| 765 |
-
# Use date input without constraints - allow any date
|
| 766 |
-
event_date = st.date_input(
|
| 767 |
-
"Event Date",
|
| 768 |
-
value=current_event_date,
|
| 769 |
-
key=f"event_date_{i}",
|
| 770 |
-
help="Select any date for this event"
|
| 771 |
-
)
|
| 772 |
-
|
| 773 |
-
# Show warning if date is outside wedding range
|
| 774 |
-
if event_date < wedding_start or event_date > wedding_end:
|
| 775 |
-
st.warning(f"⚠️ Selected date is outside your wedding date range ({wedding_start.strftime('%B %d, %Y')} - {wedding_end.strftime('%B %d, %Y')})")
|
| 776 |
-
|
| 777 |
-
requires_meal_choice = st.checkbox("Requires Meal Choice", value=event['requires_meal_choice'], key=f"event_meal_{i}")
|
| 778 |
-
|
| 779 |
-
# Meal options section (only show if meal choice is required)
|
| 780 |
-
if requires_meal_choice:
|
| 781 |
-
st.markdown("**Meal Options**")
|
| 782 |
-
st.markdown("Enter meal options (one per line):")
|
| 783 |
-
current_meal_options = event.get('meal_options', [])
|
| 784 |
-
meal_options_text = '\n'.join(current_meal_options) if current_meal_options else ''
|
| 785 |
-
meal_options = st.text_area("Meal Options", value=meal_options_text, placeholder="e.g.,\nDuck\nSurf & Turf\nRisotto (vegetarian)\nStuffed Squash (vegetarian)", key=f"event_meal_options_{i}", height=100)
|
| 786 |
-
else:
|
| 787 |
-
meal_options = ""
|
| 788 |
-
|
| 789 |
-
event_address = st.text_area("Address", value=event.get('address', ''), placeholder="Enter full address (street, city, state, zip code)", key=f"event_address_{i}", height=80)
|
| 790 |
-
|
| 791 |
-
# Calculate date_offset from the selected date
|
| 792 |
-
date_offset = (event_date - wedding_start).days
|
| 793 |
-
|
| 794 |
-
# Parse meal options
|
| 795 |
-
meal_options_list = []
|
| 796 |
-
if requires_meal_choice and meal_options:
|
| 797 |
-
meal_options_list = [option.strip() for option in meal_options.split('\n') if option.strip()]
|
| 798 |
-
|
| 799 |
-
# Update session state
|
| 800 |
-
st.session_state.setup_events[i] = {
|
| 801 |
-
"name": event_name,
|
| 802 |
-
"description": event_description,
|
| 803 |
-
"date_offset": date_offset,
|
| 804 |
-
"requires_meal_choice": requires_meal_choice,
|
| 805 |
-
"meal_options": meal_options_list,
|
| 806 |
-
"location": event_location,
|
| 807 |
-
"address": event_address
|
| 808 |
-
}
|
| 809 |
-
else:
|
| 810 |
-
st.info("No events added yet. Click 'Add Event' to get started!")
|
| 811 |
|
| 812 |
-
# Save configuration button (after event management)
|
| 813 |
-
st.markdown("---")
|
| 814 |
-
if st.button("Save Configuration", type="primary"):
|
| 815 |
-
# Get form values from session state
|
| 816 |
-
form_data = st.session_state.setup_form_data
|
| 817 |
-
|
| 818 |
-
if form_data['partner1_name'] and form_data['partner2_name'] and form_data['wedding_start_date'] and form_data['wedding_end_date']:
|
| 819 |
-
# Parse tags (task groups will be auto-generated from events)
|
| 820 |
-
custom_tags_list = [tag.strip() for tag in form_data['custom_tags'].split('\n') if tag.strip()]
|
| 821 |
-
task_assignees_list = [assignee.strip() for assignee in form_data['task_assignees'].split('\n') if assignee.strip()]
|
| 822 |
-
|
| 823 |
-
# Create configuration
|
| 824 |
-
config = {
|
| 825 |
-
'wedding_info': {
|
| 826 |
-
'partner1_name': form_data['partner1_name'],
|
| 827 |
-
'partner2_name': form_data['partner2_name'],
|
| 828 |
-
'wedding_start_date': form_data['wedding_start_date'].isoformat(),
|
| 829 |
-
'wedding_end_date': form_data['wedding_end_date'].isoformat(),
|
| 830 |
-
'venue_city': form_data['venue_city']
|
| 831 |
-
},
|
| 832 |
-
'custom_settings': {
|
| 833 |
-
'custom_tags': custom_tags_list,
|
| 834 |
-
'task_assignees': task_assignees_list
|
| 835 |
-
},
|
| 836 |
-
'wedding_events': st.session_state.setup_events
|
| 837 |
-
}
|
| 838 |
-
|
| 839 |
-
# Save configuration
|
| 840 |
-
st.session_state.config_manager.save_config(config)
|
| 841 |
-
# Clear setup session state
|
| 842 |
-
if 'setup_events' in st.session_state:
|
| 843 |
-
del st.session_state.setup_events
|
| 844 |
-
if 'setup_form_data' in st.session_state:
|
| 845 |
-
del st.session_state.setup_form_data
|
| 846 |
-
st.success("Configuration saved successfully!")
|
| 847 |
-
st.rerun()
|
| 848 |
-
else:
|
| 849 |
-
st.error("Please fill in at least the partner names and wedding date range in the form above.")
|
| 850 |
-
|
| 851 |
-
def show_main_app():
|
| 852 |
# Load config
|
| 853 |
config = st.session_state.config_manager.load_config()
|
| 854 |
wedding_info = config.get('wedding_info', {})
|
|
@@ -869,6 +739,11 @@ def show_main_app():
|
|
| 869 |
else:
|
| 870 |
header_text = f"{partner1} & {partner2}'s Wedding Planner \n"
|
| 871 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 872 |
# Add demo mode indicator
|
| 873 |
if is_demo_mode:
|
| 874 |
header_text += "🎭 DEMO MODE - Sample Data"
|
|
@@ -897,11 +772,25 @@ def show_main_app():
|
|
| 897 |
|
| 898 |
# Sidebar navigation
|
| 899 |
with st.sidebar:
|
| 900 |
-
#
|
| 901 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 902 |
st.session_state.app_initialized = False
|
| 903 |
-
|
| 904 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 905 |
st.rerun()
|
| 906 |
|
| 907 |
st.markdown("---")
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
import json
|
| 3 |
import os
|
| 4 |
+
import yaml
|
| 5 |
from datetime import datetime, date, timedelta
|
| 6 |
import pandas as pd
|
| 7 |
+
from yaml.loader import SafeLoader
|
| 8 |
+
import streamlit_authenticator as stauth
|
| 9 |
from config_manager import ConfigManager
|
| 10 |
from dashboard import Dashboard
|
| 11 |
from tasks import TasksManager
|
|
|
|
| 193 |
</style>
|
| 194 |
""", unsafe_allow_html=True)
|
| 195 |
|
| 196 |
+
def load_auth_config():
|
| 197 |
+
"""Load authentication configuration from Google Drive"""
|
| 198 |
+
try:
|
| 199 |
+
config_manager = st.session_state.config_manager
|
| 200 |
+
|
| 201 |
+
# Try to load config.yaml from Google Drive root
|
| 202 |
+
if config_manager.google_drive_enabled:
|
| 203 |
+
config_content = config_manager.drive_manager.download_file('config.yaml')
|
| 204 |
+
if config_content:
|
| 205 |
+
# Parse YAML content
|
| 206 |
+
config = yaml.load(config_content, Loader=SafeLoader)
|
| 207 |
+
return config
|
| 208 |
+
|
| 209 |
+
# Fallback to local config.yaml if Google Drive fails
|
| 210 |
+
if os.path.exists('config.yaml'):
|
| 211 |
+
with open('config.yaml') as file:
|
| 212 |
+
config = yaml.load(file, Loader=SafeLoader)
|
| 213 |
+
return config
|
| 214 |
+
|
| 215 |
+
st.error("❌ No auth config found")
|
| 216 |
+
return None
|
| 217 |
+
except Exception as e:
|
| 218 |
+
st.error(f"Error loading authentication config: {e}")
|
| 219 |
+
return None
|
| 220 |
+
|
| 221 |
+
def get_user_folder_from_username(username):
|
| 222 |
+
"""Get the user folder based on username using wedding mappings"""
|
| 223 |
+
try:
|
| 224 |
+
# Load auth config to get wedding mappings
|
| 225 |
+
auth_config = st.session_state.get('auth_config')
|
| 226 |
+
if not auth_config:
|
| 227 |
+
# Fallback to loading config directly
|
| 228 |
+
auth_config = load_auth_config()
|
| 229 |
+
|
| 230 |
+
if auth_config and 'wedding_mappings' in auth_config:
|
| 231 |
+
wedding_mappings = auth_config['wedding_mappings']
|
| 232 |
+
|
| 233 |
+
# Search through all wedding mappings to find the user
|
| 234 |
+
for wedding_name, wedding_info in wedding_mappings.items():
|
| 235 |
+
if 'users' in wedding_info and username in wedding_info['users']:
|
| 236 |
+
return wedding_info['folder']
|
| 237 |
+
|
| 238 |
+
# Fallback to old hardcoded logic for backward compatibility
|
| 239 |
+
if username == 'demo':
|
| 240 |
+
return 'demo_data'
|
| 241 |
+
elif username == 'laraandumang':
|
| 242 |
+
return 'laraandumang'
|
| 243 |
+
else:
|
| 244 |
+
return 'demo_data' # Default fallback
|
| 245 |
+
|
| 246 |
+
except Exception as e:
|
| 247 |
+
st.error(f"Error getting user folder for {username}: {e}")
|
| 248 |
+
# Fallback to demo_data on error
|
| 249 |
+
return 'demo_data'
|
| 250 |
+
|
| 251 |
+
def get_wedding_info_for_user(username):
|
| 252 |
+
"""Get wedding information for a specific user"""
|
| 253 |
+
try:
|
| 254 |
+
auth_config = st.session_state.get('auth_config')
|
| 255 |
+
if not auth_config:
|
| 256 |
+
auth_config = load_auth_config()
|
| 257 |
+
|
| 258 |
+
if auth_config and 'wedding_mappings' in auth_config:
|
| 259 |
+
wedding_mappings = auth_config['wedding_mappings']
|
| 260 |
+
|
| 261 |
+
for wedding_name, wedding_info in wedding_mappings.items():
|
| 262 |
+
if 'users' in wedding_info and username in wedding_info['users']:
|
| 263 |
+
return {
|
| 264 |
+
'wedding_name': wedding_name,
|
| 265 |
+
'folder': wedding_info['folder'],
|
| 266 |
+
'users': wedding_info['users'],
|
| 267 |
+
'display_name': wedding_info.get('wedding_name', wedding_name)
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
return None
|
| 271 |
+
except Exception as e:
|
| 272 |
+
st.error(f"Error getting wedding info for {username}: {e}")
|
| 273 |
+
return None
|
| 274 |
+
|
| 275 |
def main():
|
| 276 |
# Initialize session state
|
| 277 |
if 'config_manager' not in st.session_state:
|
| 278 |
st.session_state.config_manager = ConfigManager()
|
| 279 |
|
| 280 |
+
# Load authentication configuration
|
| 281 |
+
if 'auth_config' not in st.session_state:
|
| 282 |
+
st.session_state.auth_config = load_auth_config()
|
| 283 |
+
|
| 284 |
+
if st.session_state.auth_config is None:
|
| 285 |
+
st.error("Failed to load authentication configuration. Please check your Google Drive setup.")
|
| 286 |
+
st.stop()
|
| 287 |
+
|
| 288 |
+
# Create authenticator (auto_hash=True by default, so passwords will be hashed automatically)
|
| 289 |
+
authenticator = stauth.Authenticate(
|
| 290 |
+
st.session_state.auth_config['credentials'],
|
| 291 |
+
st.session_state.auth_config['cookie']['name'],
|
| 292 |
+
st.session_state.auth_config['cookie']['key'],
|
| 293 |
+
st.session_state.auth_config['cookie']['expiry_days']
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
# Check authentication status
|
| 297 |
+
if 'authentication_status' not in st.session_state:
|
| 298 |
+
st.session_state.authentication_status = None
|
| 299 |
+
|
| 300 |
+
# Check if user is already authenticated
|
| 301 |
+
if st.session_state.get('authentication_status'):
|
| 302 |
+
# User is authenticated, show main app
|
| 303 |
+
show_main_app(authenticator)
|
| 304 |
else:
|
| 305 |
+
# Show login page and handle authentication
|
| 306 |
+
show_login_page(authenticator)
|
| 307 |
|
| 308 |
+
def show_login_page(authenticator):
|
| 309 |
+
"""Show the login page"""
|
| 310 |
|
| 311 |
# Hero Section
|
| 312 |
st.markdown("""
|
|
|
|
| 408 |
margin-bottom: 2rem;
|
| 409 |
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 410 |
">
|
| 411 |
+
<h3 style="color: #2d5016; margin-bottom: 1rem;">👰 Wedding Party</h3>
|
| 412 |
<ul style="color: #666; line-height: 1.8;">
|
| 413 |
+
<li>Manage bridal party</li>
|
| 414 |
+
<li>Track responsibilities</li>
|
| 415 |
+
<li>Coordinate schedules</li>
|
| 416 |
+
<li>Store contact info</li>
|
| 417 |
</ul>
|
| 418 |
</div>
|
| 419 |
""", unsafe_allow_html=True)
|
|
|
|
| 428 |
margin-bottom: 2rem;
|
| 429 |
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 430 |
">
|
| 431 |
+
<h3 style="color: #2d5016; margin-bottom: 1rem;">📊 Dashboard</h3>
|
| 432 |
<ul style="color: #666; line-height: 1.8;">
|
| 433 |
+
<li>Visual progress tracking</li>
|
| 434 |
+
<li>Key metrics overview</li>
|
| 435 |
+
<li>Timeline management</li>
|
| 436 |
+
<li>Quick insights</li>
|
| 437 |
</ul>
|
| 438 |
</div>
|
| 439 |
""", unsafe_allow_html=True)
|
|
|
|
| 451 |
<h3 style="color: #2d5016; margin-bottom: 1rem;">☁️ Cloud Sync</h3>
|
| 452 |
<ul style="color: #666; line-height: 1.8;">
|
| 453 |
<li>Google Drive integration</li>
|
|
|
|
| 454 |
<li>Automatic backups</li>
|
| 455 |
+
<li>Multi-device access</li>
|
| 456 |
+
<li>Real-time updates</li>
|
| 457 |
</ul>
|
| 458 |
</div>
|
| 459 |
""", unsafe_allow_html=True)
|
| 460 |
|
| 461 |
st.markdown("---")
|
| 462 |
|
| 463 |
+
# Login Section
|
| 464 |
+
st.markdown("## 🔐 Login to Your Wedding Planner")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
|
| 466 |
+
# Login form
|
| 467 |
+
try:
|
| 468 |
+
authenticator.login(location='main')
|
| 469 |
+
except Exception as e:
|
| 470 |
+
st.error(f"Login error: {e}")
|
| 471 |
+
|
| 472 |
+
# Check authentication status and show appropriate message
|
| 473 |
+
if st.session_state.get('authentication_status') is False:
|
| 474 |
+
st.error("❌ Invalid username or password")
|
| 475 |
+
elif st.session_state.get('authentication_status') is None:
|
| 476 |
+
st.info("🔐 Please enter your username and password")
|
| 477 |
+
elif st.session_state.get('authentication_status'):
|
| 478 |
+
st.success(f"✅ Welcome, {st.session_state.get('name', 'User')}!")
|
| 479 |
+
st.rerun() # Refresh to show main app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
|
| 481 |
|
| 482 |
|
|
|
|
| 677 |
else:
|
| 678 |
st.error("Please fill in at least the partner names and wedding date range in the form above.")
|
| 679 |
|
| 680 |
+
|
| 681 |
+
def show_main_app(authenticator):
|
| 682 |
+
# Get current user from session state (set by authenticator.login)
|
| 683 |
+
username = st.session_state.get('username')
|
| 684 |
+
name = st.session_state.get('name')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
|
| 686 |
+
if not username or not name:
|
| 687 |
+
st.error("Authentication error: Missing user information")
|
| 688 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
|
| 690 |
+
# Set user folder based on username
|
| 691 |
+
user_folder = get_user_folder_from_username(username)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 692 |
|
| 693 |
+
# Get wedding info for the user (for future use)
|
| 694 |
+
wedding_info = get_wedding_info_for_user(username)
|
|
|
|
| 695 |
|
| 696 |
+
# Update config manager to use the correct user folder
|
| 697 |
+
config_manager = st.session_state.config_manager
|
| 698 |
+
config_manager.set_user_folder(user_folder)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
|
| 700 |
+
# Check if we need to load data (either not initialized or user changed)
|
| 701 |
+
current_user_folder = config_manager.get_current_user_folder()
|
| 702 |
+
user_changed = st.session_state.get('last_user_folder') != user_folder
|
|
|
|
| 703 |
|
| 704 |
+
if not st.session_state.get('app_initialized', False) or user_changed:
|
| 705 |
+
with st.spinner(f"Loading {name}'s wedding data..."):
|
| 706 |
+
if user_folder == 'demo_data':
|
| 707 |
+
if config_manager.load_demo_data_from_drive():
|
| 708 |
+
st.session_state.app_initialized = True
|
| 709 |
+
st.session_state.last_user_folder = user_folder
|
| 710 |
+
st.success("✅ Demo data loaded successfully!")
|
| 711 |
+
else:
|
| 712 |
+
st.error("Failed to load demo data.")
|
| 713 |
+
return
|
| 714 |
+
else:
|
| 715 |
+
if config_manager.load_existing_data_from_drive():
|
| 716 |
+
st.session_state.app_initialized = True
|
| 717 |
+
st.session_state.last_user_folder = user_folder
|
| 718 |
+
else:
|
| 719 |
+
st.error("Failed to load wedding data.")
|
| 720 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 721 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
# Load config
|
| 723 |
config = st.session_state.config_manager.load_config()
|
| 724 |
wedding_info = config.get('wedding_info', {})
|
|
|
|
| 739 |
else:
|
| 740 |
header_text = f"{partner1} & {partner2}'s Wedding Planner \n"
|
| 741 |
|
| 742 |
+
# Add wedding mapping info if available
|
| 743 |
+
user_wedding_info = get_wedding_info_for_user(username)
|
| 744 |
+
if user_wedding_info:
|
| 745 |
+
header_text += f"👥 Wedding: {user_wedding_info['display_name']} \n"
|
| 746 |
+
|
| 747 |
# Add demo mode indicator
|
| 748 |
if is_demo_mode:
|
| 749 |
header_text += "🎭 DEMO MODE - Sample Data"
|
|
|
|
| 772 |
|
| 773 |
# Sidebar navigation
|
| 774 |
with st.sidebar:
|
| 775 |
+
# User info and logout
|
| 776 |
+
# Extract first name for a more friendly greeting
|
| 777 |
+
first_name = name.split()[0] if name else "User"
|
| 778 |
+
st.markdown(f"**Welcome, {first_name}!**")
|
| 779 |
+
if authenticator.logout(location='sidebar', key='logout_button'):
|
| 780 |
+
# Clear session state on logout
|
| 781 |
+
for key in list(st.session_state.keys()):
|
| 782 |
+
if key not in ['config_manager', 'auth_config']:
|
| 783 |
+
del st.session_state[key]
|
| 784 |
+
|
| 785 |
+
# Reset app initialization state
|
| 786 |
st.session_state.app_initialized = False
|
| 787 |
+
st.session_state.last_user_folder = None
|
| 788 |
+
|
| 789 |
+
# Reset config manager state
|
| 790 |
+
if 'config_manager' in st.session_state:
|
| 791 |
+
st.session_state.config_manager.reset_app_state()
|
| 792 |
+
st.session_state.config_manager.user_folder = None
|
| 793 |
+
|
| 794 |
st.rerun()
|
| 795 |
|
| 796 |
st.markdown("---")
|
config_manager.py
CHANGED
|
@@ -48,6 +48,14 @@ class ConfigManager:
|
|
| 48 |
return "demo_data"
|
| 49 |
return "laraandumang"
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
def _initialize_google_drive(self):
|
| 52 |
"""Initialize Google Drive connection"""
|
| 53 |
try:
|
|
@@ -68,7 +76,7 @@ class ConfigManager:
|
|
| 68 |
|
| 69 |
try:
|
| 70 |
# Get user-specific folder
|
| 71 |
-
user_folder = self.
|
| 72 |
|
| 73 |
# List of data files to sync
|
| 74 |
data_files = [
|
|
@@ -128,6 +136,7 @@ class ConfigManager:
|
|
| 128 |
except Exception as e:
|
| 129 |
print(f"Error syncing from Google Drive: {e}")
|
| 130 |
# Don't fail the entire initialization if sync fails
|
|
|
|
| 131 |
|
| 132 |
def _sync_to_google_drive(self):
|
| 133 |
"""Sync local data files to Google Drive"""
|
|
@@ -162,6 +171,17 @@ class ConfigManager:
|
|
| 162 |
|
| 163 |
def load_app_config(self):
|
| 164 |
"""Load app configuration from file"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
if os.path.exists(self.app_config_file):
|
| 166 |
try:
|
| 167 |
with open(self.app_config_file, 'r') as f:
|
|
@@ -372,7 +392,13 @@ class ConfigManager:
|
|
| 372 |
"""Toggle demo mode on/off"""
|
| 373 |
self.app_config["demo_mode"] = not self.app_config.get("demo_mode", False)
|
| 374 |
try:
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
json.dump(self.app_config, f, indent=2)
|
| 377 |
return True
|
| 378 |
except Exception as e:
|
|
@@ -383,7 +409,13 @@ class ConfigManager:
|
|
| 383 |
"""Set demo mode to specific value"""
|
| 384 |
self.app_config["demo_mode"] = enabled
|
| 385 |
try:
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
json.dump(self.app_config, f, indent=2)
|
| 388 |
return True
|
| 389 |
except Exception as e:
|
|
@@ -487,7 +519,7 @@ class ConfigManager:
|
|
| 487 |
self.set_demo_mode(False)
|
| 488 |
|
| 489 |
# Get user-specific folder
|
| 490 |
-
user_folder = self.
|
| 491 |
|
| 492 |
# Check if wedding_config.json exists in Google Drive user folder
|
| 493 |
config_content = self.drive_manager.download_file(f'{user_folder}/wedding_config.json')
|
|
@@ -544,6 +576,8 @@ class ConfigManager:
|
|
| 544 |
return False
|
| 545 |
except Exception as e:
|
| 546 |
print(f"Error loading existing data from Google Drive: {e}")
|
|
|
|
|
|
|
| 547 |
return False
|
| 548 |
|
| 549 |
def load_demo_data_from_drive(self):
|
|
@@ -556,7 +590,7 @@ class ConfigManager:
|
|
| 556 |
self.set_demo_mode(True)
|
| 557 |
|
| 558 |
# Load demo data from demo folder in Google Drive
|
| 559 |
-
demo_folder = self.
|
| 560 |
|
| 561 |
# Check if wedding_config.json exists in demo folder
|
| 562 |
config_content = self.drive_manager.download_file(f'{demo_folder}/wedding_config.json')
|
|
@@ -613,6 +647,8 @@ class ConfigManager:
|
|
| 613 |
return False
|
| 614 |
except Exception as e:
|
| 615 |
print(f"Error loading demo data from Google Drive: {e}")
|
|
|
|
|
|
|
| 616 |
return False
|
| 617 |
|
| 618 |
def get_google_drive_status(self):
|
|
@@ -691,7 +727,7 @@ class ConfigManager:
|
|
| 691 |
|
| 692 |
try:
|
| 693 |
# Get user-specific folder
|
| 694 |
-
user_folder = self.
|
| 695 |
|
| 696 |
modified_files = self.get_modified_files()
|
| 697 |
|
|
@@ -750,4 +786,4 @@ class ConfigManager:
|
|
| 750 |
return True
|
| 751 |
except Exception as e:
|
| 752 |
print(f"Error resetting app state: {e}")
|
| 753 |
-
return False
|
|
|
|
| 48 |
return "demo_data"
|
| 49 |
return "laraandumang"
|
| 50 |
|
| 51 |
+
def set_user_folder(self, folder_name):
|
| 52 |
+
"""Set the user-specific folder name"""
|
| 53 |
+
self.user_folder = folder_name
|
| 54 |
+
|
| 55 |
+
def get_current_user_folder(self):
|
| 56 |
+
"""Get the currently set user folder"""
|
| 57 |
+
return getattr(self, 'user_folder', self.get_user_folder())
|
| 58 |
+
|
| 59 |
def _initialize_google_drive(self):
|
| 60 |
"""Initialize Google Drive connection"""
|
| 61 |
try:
|
|
|
|
| 76 |
|
| 77 |
try:
|
| 78 |
# Get user-specific folder
|
| 79 |
+
user_folder = self.get_current_user_folder()
|
| 80 |
|
| 81 |
# List of data files to sync
|
| 82 |
data_files = [
|
|
|
|
| 136 |
except Exception as e:
|
| 137 |
print(f"Error syncing from Google Drive: {e}")
|
| 138 |
# Don't fail the entire initialization if sync fails
|
| 139 |
+
# This is expected behavior - user can manually sync later
|
| 140 |
|
| 141 |
def _sync_to_google_drive(self):
|
| 142 |
"""Sync local data files to Google Drive"""
|
|
|
|
| 171 |
|
| 172 |
def load_app_config(self):
|
| 173 |
"""Load app configuration from file"""
|
| 174 |
+
# For Hugging Face Spaces, check /tmp directory first
|
| 175 |
+
if self.is_huggingface:
|
| 176 |
+
tmp_config_path = f"/tmp/{self.app_config_file}"
|
| 177 |
+
if os.path.exists(tmp_config_path):
|
| 178 |
+
try:
|
| 179 |
+
with open(tmp_config_path, 'r') as f:
|
| 180 |
+
return json.load(f)
|
| 181 |
+
except (json.JSONDecodeError, FileNotFoundError):
|
| 182 |
+
pass
|
| 183 |
+
|
| 184 |
+
# Check original location
|
| 185 |
if os.path.exists(self.app_config_file):
|
| 186 |
try:
|
| 187 |
with open(self.app_config_file, 'r') as f:
|
|
|
|
| 392 |
"""Toggle demo mode on/off"""
|
| 393 |
self.app_config["demo_mode"] = not self.app_config.get("demo_mode", False)
|
| 394 |
try:
|
| 395 |
+
# For Hugging Face Spaces, use /tmp directory to avoid permission issues
|
| 396 |
+
if self.is_huggingface:
|
| 397 |
+
config_path = f"/tmp/{self.app_config_file}"
|
| 398 |
+
else:
|
| 399 |
+
config_path = self.app_config_file
|
| 400 |
+
|
| 401 |
+
with open(config_path, 'w') as f:
|
| 402 |
json.dump(self.app_config, f, indent=2)
|
| 403 |
return True
|
| 404 |
except Exception as e:
|
|
|
|
| 409 |
"""Set demo mode to specific value"""
|
| 410 |
self.app_config["demo_mode"] = enabled
|
| 411 |
try:
|
| 412 |
+
# For Hugging Face Spaces, use /tmp directory to avoid permission issues
|
| 413 |
+
if self.is_huggingface:
|
| 414 |
+
config_path = f"/tmp/{self.app_config_file}"
|
| 415 |
+
else:
|
| 416 |
+
config_path = self.app_config_file
|
| 417 |
+
|
| 418 |
+
with open(config_path, 'w') as f:
|
| 419 |
json.dump(self.app_config, f, indent=2)
|
| 420 |
return True
|
| 421 |
except Exception as e:
|
|
|
|
| 519 |
self.set_demo_mode(False)
|
| 520 |
|
| 521 |
# Get user-specific folder
|
| 522 |
+
user_folder = self.get_current_user_folder()
|
| 523 |
|
| 524 |
# Check if wedding_config.json exists in Google Drive user folder
|
| 525 |
config_content = self.drive_manager.download_file(f'{user_folder}/wedding_config.json')
|
|
|
|
| 576 |
return False
|
| 577 |
except Exception as e:
|
| 578 |
print(f"Error loading existing data from Google Drive: {e}")
|
| 579 |
+
# This is a common issue on Hugging Face Spaces due to network/SSL issues
|
| 580 |
+
# The error is expected and the user can retry manually
|
| 581 |
return False
|
| 582 |
|
| 583 |
def load_demo_data_from_drive(self):
|
|
|
|
| 590 |
self.set_demo_mode(True)
|
| 591 |
|
| 592 |
# Load demo data from demo folder in Google Drive
|
| 593 |
+
demo_folder = self.get_current_user_folder() # Will return "demo_data" since demo mode is enabled
|
| 594 |
|
| 595 |
# Check if wedding_config.json exists in demo folder
|
| 596 |
config_content = self.drive_manager.download_file(f'{demo_folder}/wedding_config.json')
|
|
|
|
| 647 |
return False
|
| 648 |
except Exception as e:
|
| 649 |
print(f"Error loading demo data from Google Drive: {e}")
|
| 650 |
+
# This is a common issue on Hugging Face Spaces due to network/SSL issues
|
| 651 |
+
# The error is expected and the user can retry manually
|
| 652 |
return False
|
| 653 |
|
| 654 |
def get_google_drive_status(self):
|
|
|
|
| 727 |
|
| 728 |
try:
|
| 729 |
# Get user-specific folder
|
| 730 |
+
user_folder = self.get_current_user_folder()
|
| 731 |
|
| 732 |
modified_files = self.get_modified_files()
|
| 733 |
|
|
|
|
| 786 |
return True
|
| 787 |
except Exception as e:
|
| 788 |
print(f"Error resetting app state: {e}")
|
| 789 |
+
return False
|
requirements.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
streamlit==1.
|
| 2 |
pandas==2.1.3
|
| 3 |
plotly==5.17.0
|
| 4 |
datetime
|
|
@@ -8,5 +8,5 @@ google-api-python-client>=2.0.0
|
|
| 8 |
google-auth-httplib2>=0.1.0
|
| 9 |
google-auth-oauthlib>=0.5.0
|
| 10 |
google-auth>=2.0.0
|
| 11 |
-
streamlit-authenticator>=0.
|
| 12 |
PyYAML>=6.0
|
|
|
|
| 1 |
+
streamlit==1.28.1
|
| 2 |
pandas==2.1.3
|
| 3 |
plotly==5.17.0
|
| 4 |
datetime
|
|
|
|
| 8 |
google-auth-httplib2>=0.1.0
|
| 9 |
google-auth-oauthlib>=0.5.0
|
| 10 |
google-auth>=2.0.0
|
| 11 |
+
streamlit-authenticator>=0.2.3
|
| 12 |
PyYAML>=6.0
|