From 2125506f6704d888a9beecad5cc41b543d8e7314 Mon Sep 17 00:00:00 2001 From: andwati Date: Sun, 28 Sep 2025 18:51:07 +0300 Subject: [PATCH] antigravity --- app.py | 132 +++++- architecture_diagrams/00_system_overview.py | 92 ++++ .../01_data_flow_architecture.py | 63 +++ architecture_diagrams/02_eda_process_flow.py | 50 +++ .../03_ml_pipeline_architecture.py | 63 +++ .../04_application_architecture.py | 71 +++ .../05_deployment_architecture.py | 87 ++++ .../06_feature_engineering_pipeline.py | 70 +++ architecture_diagrams/07_user_journey_flow.py | 79 ++++ .../08_security_architecture.py | 93 ++++ .../generate_all_diagrams.py | 82 ++++ pyproject.toml | 1 + src/components/form_components.py | 371 ++++++++++++++++ src/components/prediction_components.py | 274 ++++++++++++ src/utils/auth.py | 308 +++++++++++++ src/utils/models_utils.py | 112 +++++ users.json | 12 + uv.lock | 420 ++++++++++++++++++ 18 files changed, 2378 insertions(+), 2 deletions(-) create mode 100644 architecture_diagrams/00_system_overview.py create mode 100644 architecture_diagrams/01_data_flow_architecture.py create mode 100644 architecture_diagrams/02_eda_process_flow.py create mode 100644 architecture_diagrams/03_ml_pipeline_architecture.py create mode 100644 architecture_diagrams/04_application_architecture.py create mode 100644 architecture_diagrams/05_deployment_architecture.py create mode 100644 architecture_diagrams/06_feature_engineering_pipeline.py create mode 100644 architecture_diagrams/07_user_journey_flow.py create mode 100644 architecture_diagrams/08_security_architecture.py create mode 100644 architecture_diagrams/generate_all_diagrams.py create mode 100644 src/components/form_components.py create mode 100644 src/components/prediction_components.py create mode 100644 src/utils/models_utils.py create mode 100644 users.json diff --git a/app.py b/app.py index c487adf..d97dca2 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,134 @@ +""" +Main Streamlit application for Student Success Predictor +""" + +import streamlit as st + +from src.components.form_components import ( + render_preset_selector, + render_personal_info_section, + render_academic_background_section, + render_family_background_section, + render_financial_social_section, + render_academic_performance_section, + render_economic_indicators_section +) +from src.components.prediction_components import ( + render_prediction_section, + render_model_info_sidebar, + render_export_section, + render_footer +) +from src.config.mappings import create_feature_mappings +from src.config.theme import ROSE_PINE_THEME, PAGE_CONFIG, APP_TITLE, APP_SUBTITLE +from src.utils.auth import require_auth, check_role_permission, render_admin_panel, check_session_timeout +from src.utils.models_utils import load_model_and_info + + +def apply_theme(): + """Apply the Rose Pine dark theme""" + st.markdown(ROSE_PINE_THEME, unsafe_allow_html=True) + + +def render_header(): + """Render the main application header""" + st.markdown(f'

{APP_TITLE}

', unsafe_allow_html=True) + st.markdown(f'

{APP_SUBTITLE}

', unsafe_allow_html=True) + + +def collect_user_input(): + """Collect all user input from the sidebar form""" + feature_maps = create_feature_mappings() + + with st.sidebar: + # Preset selector + preset = render_preset_selector() + + # Personal information + personal_info = render_personal_info_section(preset, feature_maps) + + # Academic background + academic_bg = render_academic_background_section(preset, feature_maps) + + # Family background + family_bg = render_family_background_section(feature_maps) + + # Financial and social factors + financial_social = render_financial_social_section() + + # Academic performance + academic_performance = render_academic_performance_section(preset) + + # Economic indicators + economic_indicators = render_economic_indicators_section() + + # Combine all input data + user_input = { + **personal_info, + **academic_bg, + **family_bg, + **financial_social, + **academic_performance, + **economic_indicators + } + + return user_input + + +@require_auth def main(): - print("Hello from student-retention!") + """Main Streamlit application""" + # Check session timeout (30 minutes) + check_session_timeout(30) + + # Apply theme + apply_theme() + + # Load model and preprocessing info + model, preprocessing_info, feature_names = load_model_and_info() + + # Render header with user context + render_header() + + # Add admin panel if user is admin + if check_role_permission("admin"): + with st.sidebar: + render_admin_panel() + + # Collect user input + user_input = collect_user_input() + + # Main content area + col1, col2 = st.columns([2, 1]) + + with col1: + st.markdown('

Prediction Results

', unsafe_allow_html=True) + + if check_role_permission("educator"): + st.markdown( + '
🎓 Educator Access: You have full access to prediction features and student data analysis.
', + unsafe_allow_html=True) + + # Render prediction section + render_prediction_section(model, preprocessing_info, user_input) + + with col2: + # Render model info and validation + render_model_info_sidebar(user_input) + + # Render export section (role-based access) + if check_role_permission("educator"): + render_export_section(user_input) + else: + st.markdown( + '
📊 Export Feature: Available for educators and administrators only.
', + unsafe_allow_html=True) + + # Render footer + render_footer() if __name__ == "__main__": - main() + # Configure page before authentication + st.set_page_config(**PAGE_CONFIG) + main() \ No newline at end of file diff --git a/architecture_diagrams/00_system_overview.py b/architecture_diagrams/00_system_overview.py new file mode 100644 index 0000000..4edeed6 --- /dev/null +++ b/architecture_diagrams/00_system_overview.py @@ -0,0 +1,92 @@ +""" +System Overview Diagram +High-level view of the entire Student Success Prediction system +""" + +from diagrams import Diagram, Cluster, Edge +from diagrams.onprem.client import Users +from diagrams.generic.storage import Storage +from diagrams.programming.language import Python +from diagrams.programming.framework import Django +from diagrams.onprem.analytics import Tableau +from diagrams.onprem.inmemory import Redis +from diagrams.aws.ml import SagemakerModel +from diagrams.generic.compute import Rack +from diagrams.onprem.monitoring import Grafana +from diagrams.onprem.network import Nginx + +with Diagram("Student Success Prediction - System Overview", + filename="diagrams/00_system_overview", + show=False, + direction="TB"): + + # System Title and Context + title = Storage("Student Success Prediction System\n• 93.84% Accuracy Random Forest Model\n• 36 Features, 3 Outcomes\n• 4,424 Student Dataset") + + # Phase 1: Data Analysis & Model Development + with Cluster("Phase 1: Data Analysis & Model Development"): + raw_data = Storage("Raw Dataset\ndata.csv") + eda_process = Tableau("Exploratory Data Analysis\n• Data quality assessment\n• Statistical analysis\n• Feature insights") + ml_pipeline = Python("ML Pipeline\n• Feature engineering\n• Model training\n• Performance validation") + model_artifacts = SagemakerModel("Model Artifacts\n• Random Forest (93.84%)\n• Preprocessing info\n• Feature mappings") + + # Phase 2: Application Development + with Cluster("Phase 2: Application Development"): + modular_architecture = Rack("Modular Architecture\n• Component separation\n• Configuration management\n• Utility services") + + auth_system = Redis("Authentication System\n• Role-based access\n• Session management\n• Security controls") + + ui_components = Django("UI Components\n• Rose Pine theme\n• Interactive forms\n• Responsive design") + + streamlit_app = Django("Streamlit Application\n• Main dashboard\n• User interface\n• API integration") + + # Phase 3: Production Deployment + with Cluster("Phase 3: Production Deployment"): + deployment_options = Rack("Deployment Options\n• Streamlit Cloud\n• Docker containers\n• Traditional servers") + + monitoring = Grafana("Monitoring & Analytics\n• Performance metrics\n• User analytics\n• System health") + + security = Redis("Security & Compliance\n• Data protection\n• Access control\n• Audit logging") + + # End Users and Outcomes + with Cluster("Stakeholders & Users"): + students = Users("Students\n• Academic planning\n• Risk assessment\n• Personal insights") + educators = Users("Educators\n• Student analytics\n• Intervention planning\n• Performance tracking") + administrators = Users("Administrators\n• System management\n• User oversight\n• Strategic planning") + + # Global Internet Access + internet = Nginx("Global Access\nSecure HTTPS") + + # System Flow + title >> raw_data >> eda_process >> ml_pipeline >> model_artifacts + + model_artifacts >> modular_architecture + modular_architecture >> auth_system + modular_architecture >> ui_components + auth_system >> streamlit_app + ui_components >> streamlit_app + + streamlit_app >> deployment_options + deployment_options >> monitoring + deployment_options >> security + + internet >> streamlit_app + streamlit_app >> students + streamlit_app >> educators + streamlit_app >> administrators + + # Key Metrics and Features + with Cluster("Key System Metrics"): + accuracy_metric = Storage("Model Performance\n• 93.84% Test Accuracy\n• Balanced class performance\n• Robust generalization") + + feature_count = Storage("Feature Engineering\n• 36 input features\n• 105+ encoded features\n• Comprehensive preprocessing") + + user_experience = Storage("User Experience\n• 3 user roles\n• Intuitive interface\n• Real-time predictions") + + scalability = Storage("Scalability\n• Cloud deployment\n• Modular architecture\n• Production-ready") + + # Connect metrics to main system + streamlit_app >> accuracy_metric + ml_pipeline >> feature_count + ui_components >> user_experience + deployment_options >> scalability \ No newline at end of file diff --git a/architecture_diagrams/01_data_flow_architecture.py b/architecture_diagrams/01_data_flow_architecture.py new file mode 100644 index 0000000..70796ba --- /dev/null +++ b/architecture_diagrams/01_data_flow_architecture.py @@ -0,0 +1,63 @@ +""" +Data Flow Architecture Diagram +Shows the complete data pipeline from raw data to model predictions +""" + +from diagrams import Diagram, Cluster, Edge +from diagrams.onprem.database import PostgreSQL +from diagrams.onprem.inmemory import Redis +from diagrams.programming.language import Python +from diagrams.onprem.analytics import Tableau +from diagrams.onprem.client import Users +from diagrams.onprem.workflow import Airflow +from diagrams.generic.storage import Storage +from diagrams.generic.compute import Rack +from diagrams.aws.ml import SagemakerModel +from diagrams.programming.framework import Django +from diagrams.onprem.monitoring import Grafana + +with Diagram("Student Success Prediction - Data Flow Architecture", + filename="diagrams/01_data_flow_architecture", + show=False, + direction="TB"): + + # Data Sources + with Cluster("Data Sources"): + raw_data = Storage("Raw Dataset\n(data.csv)\n4,424 students\n37 features") + + # Data Processing Pipeline + with Cluster("Data Processing Pipeline"): + eda_notebook = Python("Exploratory Data Analysis\n(predictive_analysis.ipynb)") + data_cleaning = Rack("Data Cleaning\n• Remove apostrophes\n• Fix typos\n• Handle duplicates") + feature_eng = Rack("Feature Engineering\n• One-hot encoding\n• Label encoding\n• Scaling") + + # Model Development + with Cluster("Model Development"): + model_training = Python("Model Training\n(train_and_save_model.py)") + model_validation = Grafana("Model Validation\n• Cross-validation\n• Performance metrics") + model_storage = Storage("Model Artifacts\n• random_forest_model.pkl\n• preprocessing_info.json") + + # Production System + with Cluster("Production Dashboard"): + auth_system = Redis("Authentication\n• Session management\n• Role-based access") + streamlit_app = Django("Streamlit App\n(app.py)") + + with Cluster("Modular Components"): + theme_config = Python("Theme Config\n• Rose Pine CSS\n• UI constants") + form_components = Python("Form Components\n• Input validation\n• User interface") + model_utils = Python("Model Utils\n• Preprocessing\n• Inference") + + # End Users + users = Users("End Users\n• Students\n• Educators\n• Administrators") + + # Data Flow + raw_data >> eda_notebook >> data_cleaning >> feature_eng + feature_eng >> model_training >> model_validation >> model_storage + + model_storage >> model_utils + auth_system >> streamlit_app + theme_config >> streamlit_app + form_components >> streamlit_app + model_utils >> streamlit_app + + streamlit_app >> users \ No newline at end of file diff --git a/architecture_diagrams/02_eda_process_flow.py b/architecture_diagrams/02_eda_process_flow.py new file mode 100644 index 0000000..c7a10c4 --- /dev/null +++ b/architecture_diagrams/02_eda_process_flow.py @@ -0,0 +1,50 @@ +""" +EDA Process Flow Diagram +Shows the exploratory data analysis workflow and key insights +""" + +from diagrams import Diagram, Cluster, Edge +from diagrams.generic.storage import Storage +from diagrams.generic.compute import Rack +from diagrams.programming.language import Python +from diagrams.onprem.analytics import Tableau +from diagrams.onprem.monitoring import Grafana + +with Diagram("Exploratory Data Analysis Process Flow", + filename="diagrams/02_eda_process_flow", + show=False, + direction="TB"): + + # Data Input + raw_dataset = Storage("Raw Dataset\ndata.csv\n4,424 rows × 37 columns") + + # EDA Steps + with Cluster("Data Quality Assessment"): + data_info = Rack("Data Info\n• No missing values\n• No duplicates\n• Mixed data types") + column_analysis = Rack("Column Analysis\n• 36 predictors\n• 1 target variable\n• Column name cleanup") + + with Cluster("Statistical Analysis"): + descriptive_stats = Grafana("Descriptive Statistics\n• Central tendencies\n• Distributions\n• Outlier detection") + correlation_analysis = Grafana("Correlation Analysis\n• Feature relationships\n• Multicollinearity check") + + with Cluster("Visualization & Insights"): + target_distribution = Tableau("Target Distribution\n• Graduate: 50%\n• Dropout: 32%\n• Enrolled: 17%") + feature_plots = Tableau("Feature Analysis\n• Histograms\n• Box plots\n• Categorical counts") + pattern_discovery = Tableau("Pattern Discovery\n• Key predictors\n• Risk factors\n• Success indicators") + + # Key Findings + with Cluster("Key Findings"): + class_imbalance = Rack("Class Imbalance\nIdentified for modeling") + high_cardinality = Rack("High Cardinality\nCourse (51 values)\nApplication order (9 values)") + data_quality = Rack("Data Quality\nClean dataset\nReady for modeling") + + # Data Flow + raw_dataset >> data_info >> descriptive_stats + raw_dataset >> column_analysis >> correlation_analysis + descriptive_stats >> target_distribution + correlation_analysis >> feature_plots + target_distribution >> pattern_discovery + + pattern_discovery >> class_imbalance + pattern_discovery >> high_cardinality + pattern_discovery >> data_quality \ No newline at end of file diff --git a/architecture_diagrams/03_ml_pipeline_architecture.py b/architecture_diagrams/03_ml_pipeline_architecture.py new file mode 100644 index 0000000..614a456 --- /dev/null +++ b/architecture_diagrams/03_ml_pipeline_architecture.py @@ -0,0 +1,63 @@ +""" +Machine Learning Pipeline Architecture +Shows the complete ML workflow from preprocessing to model deployment +""" + +from diagrams import Diagram, Cluster, Edge +from diagrams.generic.storage import Storage +from diagrams.generic.compute import Rack +from diagrams.programming.language import Python +from diagrams.aws.ml import SagemakerModel +from diagrams.onprem.monitoring import Grafana +from diagrams.generic.database import SQL + +with Diagram("Machine Learning Pipeline Architecture", + filename="diagrams/03_ml_pipeline_architecture", + show=False, + direction="TB"): + + # Input Data + clean_data = Storage("Cleaned Dataset\n• 4,424 samples\n• 36 features\n• 3 target classes") + + # Preprocessing Pipeline + with Cluster("Data Preprocessing"): + feature_encoding = Rack("Feature Encoding\n• Label encoding (target)\n• One-hot encoding (categorical)") + data_splitting = Rack("Data Splitting\n• Train: 75%\n• Test: 25%\n• Stratified split") + feature_alignment = Rack("Feature Alignment\n• Consistent column order\n• Missing feature handling") + + # Model Training + with Cluster("Model Training & Validation"): + model_training = Python("Random Forest Training\n• n_estimators=100\n• random_state=42") + cross_validation = Grafana("Cross Validation\n• 5-fold CV\n• Performance metrics") + hyperparameter_tuning = Grafana("Hyperparameter Tuning\n• Grid search\n• Best parameters") + + # Model Evaluation + with Cluster("Model Evaluation"): + performance_metrics = Grafana("Performance Metrics\n• Accuracy: 93.84%\n• Precision/Recall\n• F1-Score") + confusion_matrix = Grafana("Confusion Matrix\n• Class-wise performance\n• Error analysis") + feature_importance = Grafana("Feature Importance\n• Top predictors\n• Impact analysis") + + # Model Persistence + with Cluster("Model Artifacts"): + model_pickle = Storage("Model Pickle\nrandom_forest_model.pkl") + preprocessing_info = SQL("Preprocessing Info\npreprocessing_info.json\n• Feature names\n• Encodings\n• Mappings") + + # Model Validation Results + with Cluster("Validation Results"): + accuracy_report = Rack("Accuracy Report\n• Training: 100%\n• Testing: 93.84%\n• Generalization: Good") + class_performance = Rack("Class Performance\n• Graduate: 95% F1\n• Dropout: 92% F1\n• Enrolled: 88% F1") + + # Data Flow + clean_data >> feature_encoding >> data_splitting + data_splitting >> model_training >> cross_validation + cross_validation >> hyperparameter_tuning >> performance_metrics + + performance_metrics >> confusion_matrix + performance_metrics >> feature_importance + + model_training >> model_pickle + feature_encoding >> preprocessing_info + feature_alignment >> preprocessing_info + + performance_metrics >> accuracy_report + confusion_matrix >> class_performance \ No newline at end of file diff --git a/architecture_diagrams/04_application_architecture.py b/architecture_diagrams/04_application_architecture.py new file mode 100644 index 0000000..dd10919 --- /dev/null +++ b/architecture_diagrams/04_application_architecture.py @@ -0,0 +1,71 @@ +""" +Application Architecture Diagram +Shows the modular Streamlit application structure and components +""" + +from diagrams import Diagram, Cluster, Edge +from diagrams.onprem.client import Users +from diagrams.onprem.inmemory import Redis +from diagrams.programming.framework import Django, React +from diagrams.programming.language import Python +from diagrams.generic.storage import Storage +from diagrams.generic.compute import Rack +from diagrams.onprem.network import Nginx + +with Diagram("Streamlit Application Architecture", + filename="diagrams/04_application_architecture", + show=False, + direction="TB"): + + # Users + users = Users("End Users\n• Students\n• Educators\n• Administrators") + + # Web Interface + streamlit_frontend = Django("Streamlit Frontend\n• Rose Pine Theme\n• Responsive UI\n• Interactive Forms") + + # Authentication Layer + with Cluster("Authentication System"): + auth_service = Redis("Authentication Service\n• Session management\n• Role-based access\n• Security validation") + user_roles = Rack("User Roles\n• Admin (full access)\n• Educator (analytics)\n• User (predictions)") + + # Application Core + with Cluster("Application Core (app.py)"): + main_app = Python("Main Application\n• Request routing\n• Session handling\n• Error management") + auth_decorator = Python("Auth Decorator\n• @require_auth\n• Permission checks\n• Session validation") + + # Configuration Layer + with Cluster("Configuration (src/config/)"): + theme_config = React("Theme Configuration\n• Rose Pine CSS\n• Color constants\n• UI styling") + mappings_config = Storage("Feature Mappings\n• Human-readable labels\n• Preset templates\n• Validation rules") + + # Utility Services + with Cluster("Utility Services (src/utils/)"): + model_utils = Python("Model Utilities\n• Model loading\n• Preprocessing\n• Prediction logic") + auth_utils = Python("Auth Utilities\n• User validation\n• Session management\n• Password hashing") + + # UI Components + with Cluster("UI Components (src/components/)"): + form_components = React("Form Components\n• Input sections\n• Validation feedback\n• Preset selection") + prediction_display = React("Prediction Display\n• Results visualization\n• Confidence scores\n• Recommendations") + + # Model Layer + with Cluster("Model Layer"): + trained_model = Storage("Trained Model\nrandom_forest_model.pkl\n93.84% accuracy") + preprocessing_pipeline = Storage("Preprocessing Pipeline\npreprocessing_info.json\nFeature transformations") + + # Data Flow + users >> streamlit_frontend + streamlit_frontend >> auth_service >> user_roles + auth_service >> main_app >> auth_decorator + + main_app >> theme_config + main_app >> mappings_config + main_app >> form_components + main_app >> prediction_display + + form_components >> model_utils + model_utils >> trained_model + model_utils >> preprocessing_pipeline + model_utils >> prediction_display + + auth_decorator >> auth_utils \ No newline at end of file diff --git a/architecture_diagrams/05_deployment_architecture.py b/architecture_diagrams/05_deployment_architecture.py new file mode 100644 index 0000000..05cc80e --- /dev/null +++ b/architecture_diagrams/05_deployment_architecture.py @@ -0,0 +1,87 @@ +""" +Deployment Architecture Diagram +Shows the deployment options and infrastructure for the application +""" + +from diagrams import Diagram, Cluster, Edge +from diagrams.onprem.client import Users +from diagrams.saas.cdn import Cloudflare +from diagrams.programming.framework import Django +from diagrams.onprem.container import Docker +from diagrams.k8s.compute import Pod +from diagrams.generic.storage import Storage +from diagrams.generic.network import Internet +from diagrams.aws.compute import EC2 +from diagrams.gcp.compute import GKE + +with Diagram("Deployment Architecture Options", + filename="diagrams/05_deployment_architecture", + show=False, + direction="TB"): + + # Users + users = Users("Global Users") + internet = Internet("Internet") + + # Deployment Option 1: Streamlit Cloud + with Cluster("Option 1: Streamlit Cloud (Recommended)"): + streamlit_cloud = Django("Streamlit Cloud\n• Automatic deployment\n• GitHub integration\n• SSL/HTTPS\n• Global CDN") + github_repo = Storage("GitHub Repository\n• Source code\n• Model artifacts\n• Configuration") + + # Cloud features + cloud_features = [ + Storage("Automatic Scaling"), + Storage("Monitoring & Logs"), + Storage("Custom Domain Support") + ] + + # Deployment Option 2: Docker Container + with Cluster("Option 2: Docker Deployment"): + docker_container = Docker("Docker Container\n• Streamlit app\n• Python runtime\n• Dependencies") + container_registry = Storage("Container Registry\n• Docker Hub\n• AWS ECR\n• GCR") + + # Container orchestration options + with Cluster("Orchestration Options"): + kubernetes = Pod("Kubernetes\n• Pod management\n• Auto-scaling\n• Load balancing") + cloud_run = GKE("Cloud Run/Fargate\n• Serverless containers\n• Pay-per-use\n• Auto-scaling") + + # Deployment Option 3: Traditional Server + with Cluster("Option 3: Traditional Server"): + web_server = EC2("Web Server\n• EC2/Compute Engine\n• Nginx reverse proxy\n• SSL certificate") + app_server = Django("Application Server\n• Streamlit process\n• PM2/Supervisor\n• Process management") + + # Shared Resources + with Cluster("Shared Resources"): + model_storage = Storage("Model Storage\n• Trained models\n• Preprocessing info\n• Version control") + monitoring = Storage("Monitoring\n• Application logs\n• Performance metrics\n• Error tracking") + backup = Storage("Backup & Recovery\n• Model versioning\n• Configuration backup\n• Disaster recovery") + + # Data Flow + users >> internet + + # Streamlit Cloud flow + internet >> streamlit_cloud + github_repo >> streamlit_cloud + streamlit_cloud >> cloud_features[0] + streamlit_cloud >> cloud_features[1] + streamlit_cloud >> cloud_features[2] + + # Docker flow + internet >> docker_container + container_registry >> docker_container + docker_container >> kubernetes + docker_container >> cloud_run + + # Traditional server flow + internet >> web_server >> app_server + + # Shared resources connections + streamlit_cloud >> model_storage + docker_container >> model_storage + app_server >> model_storage + + streamlit_cloud >> monitoring + kubernetes >> monitoring + app_server >> monitoring + + model_storage >> backup \ No newline at end of file diff --git a/architecture_diagrams/06_feature_engineering_pipeline.py b/architecture_diagrams/06_feature_engineering_pipeline.py new file mode 100644 index 0000000..708365d --- /dev/null +++ b/architecture_diagrams/06_feature_engineering_pipeline.py @@ -0,0 +1,70 @@ +""" +Feature Engineering Pipeline Diagram +Shows the detailed feature transformation and encoding process +""" + +from diagrams import Diagram, Cluster, Edge +from diagrams.generic.storage import Storage +from diagrams.generic.compute import Rack +from diagrams.programming.language import Python +from diagrams.onprem.analytics import Tableau +from diagrams.generic.database import SQL + +with Diagram("Feature Engineering Pipeline", + filename="diagrams/06_feature_engineering_pipeline", + show=False, + direction="TB"): + + # Raw Features Input + raw_features = Storage("Raw Features (37)\n• 36 predictors\n• 1 target variable\n• Mixed data types") + + # Categorical Features Processing + with Cluster("Categorical Feature Processing"): + categorical_features = Rack("Categorical Features\n• Marital status\n• Application mode\n• Course\n• Nationality\n• Mother's/Father's qualification\n• Mother's/Father's occupation\n• Displaced\n• Educational special needs\n• Debtor\n• Tuition fees up to date\n• Gender\n• Scholarship holder\n• International") + + label_encoding = Python("Label Encoding\n• Target variable\n• Dropout = 0\n• Enrolled = 1\n• Graduate = 2") + + one_hot_encoding = Python("One-Hot Encoding\n• Categorical predictors\n• Binary columns creation\n• Feature expansion") + + # Numerical Features Processing + with Cluster("Numerical Feature Processing"): + numerical_features = Rack("Numerical Features\n• Application order\n• Previous qualification grade\n• Admission grade\n• Age at enrollment\n• Curricular units (1st & 2nd sem)\n• Unemployment rate\n• Inflation rate\n• GDP") + + feature_validation = Python("Feature Validation\n• Range checks\n• Outlier detection\n• Data type validation") + + feature_scaling = Python("Feature Scaling\n• Optional for tree models\n• Standardization available\n• Normalization available") + + # Feature Engineering Output + with Cluster("Engineered Features"): + encoded_features = Storage("Encoded Features (~105)\n• Original numerical (14)\n• One-hot encoded categorical (90+)\n• Label encoded target (1)") + + feature_mapping = SQL("Feature Mapping\n• Original → Encoded names\n• Category mappings\n• Reverse mappings") + + preprocessing_metadata = SQL("Preprocessing Metadata\n• Encoding schemes\n• Feature names\n• Transformation info") + + # Quality Assurance + with Cluster("Quality Assurance"): + feature_consistency = Rack("Feature Consistency\n• Column order preservation\n• Missing feature handling\n• Shape validation") + + data_validation = Rack("Data Validation\n• No missing values\n• Consistent data types\n• Feature alignment") + + pipeline_testing = Tableau("Pipeline Testing\n• Train/test consistency\n• Transformation verification\n• Edge case handling") + + # Data Flow + raw_features >> categorical_features >> one_hot_encoding + raw_features >> numerical_features >> feature_validation + + categorical_features >> label_encoding + numerical_features >> feature_scaling + + one_hot_encoding >> encoded_features + label_encoding >> encoded_features + feature_validation >> encoded_features + feature_scaling >> encoded_features + + encoded_features >> feature_mapping + encoded_features >> preprocessing_metadata + + encoded_features >> feature_consistency + feature_consistency >> data_validation + data_validation >> pipeline_testing \ No newline at end of file diff --git a/architecture_diagrams/07_user_journey_flow.py b/architecture_diagrams/07_user_journey_flow.py new file mode 100644 index 0000000..aa530f8 --- /dev/null +++ b/architecture_diagrams/07_user_journey_flow.py @@ -0,0 +1,79 @@ +""" +User Journey and Interaction Flow Diagram +Shows how different user types interact with the application +""" + +from diagrams import Diagram, Cluster, Edge +from diagrams.onprem.client import Users, User +from diagrams.programming.framework import Django +from diagrams.onprem.inmemory import Redis +from diagrams.generic.compute import Rack +from diagrams.onprem.analytics import Tableau +from diagrams.generic.storage import Storage + +with Diagram("User Journey and Interaction Flow", + filename="diagrams/07_user_journey_flow", + show=False, + direction="TB"): + + # User Types + with Cluster("User Types"): + student_user = User("Student\n• Personal predictions\n• Academic planning\n• Risk assessment") + educator_user = User("Educator\n• Student analytics\n• Intervention planning\n• Batch predictions") + admin_user = User("Administrator\n• System management\n• User oversight\n• Full access") + + # Authentication Flow + with Cluster("Authentication Flow"): + login_page = Django("Login Page\n• Credential input\n• Role selection\n• Security validation") + auth_service = Redis("Authentication Service\n• Session creation\n• Role assignment\n• Access control") + session_management = Redis("Session Management\n• Timeout handling\n• Security checks\n• State persistence") + + # Application Interface + with Cluster("Application Interface"): + main_dashboard = Django("Main Dashboard\n• Role-based UI\n• Feature access\n• Navigation menu") + + # Student workflow + with Cluster("Student Workflow"): + personal_form = Rack("Personal Information Form\n• Basic demographics\n• Academic history\n• Family background") + academic_form = Rack("Academic Performance Form\n• Current grades\n• Curricular units\n• Progress tracking") + prediction_results = Tableau("Prediction Results\n• Outcome probability\n• Confidence scores\n• Recommendations") + + # Educator workflow + with Cluster("Educator Workflow"): + student_management = Rack("Student Management\n• Batch input\n• Template selection\n• Data validation") + analytics_dashboard = Tableau("Analytics Dashboard\n• Class performance\n• Risk indicators\n• Intervention alerts") + reporting_tools = Tableau("Reporting Tools\n• Performance reports\n• Trend analysis\n• Export capabilities") + + # Admin workflow + with Cluster("Admin Workflow"): + user_management = Rack("User Management\n• Account creation\n• Role assignment\n• Access control") + system_monitoring = Tableau("System Monitoring\n• Usage statistics\n• Performance metrics\n• Error logs") + model_management = Storage("Model Management\n• Version control\n• Performance tracking\n• Updates") + + # Prediction Engine + with Cluster("Prediction Engine"): + input_validation = Rack("Input Validation\n• Data quality checks\n• Range validation\n• Required fields") + feature_processing = Rack("Feature Processing\n• Encoding\n• Transformation\n• Alignment") + model_inference = Storage("Model Inference\n• Random Forest prediction\n• Confidence calculation\n• Result formatting") + + # User Journey Flows + + # Student journey + student_user >> login_page >> auth_service >> main_dashboard + main_dashboard >> personal_form >> academic_form + academic_form >> input_validation >> feature_processing >> model_inference + model_inference >> prediction_results + + # Educator journey + educator_user >> login_page >> auth_service >> main_dashboard + main_dashboard >> student_management >> analytics_dashboard + student_management >> input_validation >> feature_processing >> model_inference + model_inference >> reporting_tools + + # Admin journey + admin_user >> login_page >> auth_service >> main_dashboard + main_dashboard >> user_management >> system_monitoring >> model_management + + # Session management connections + auth_service >> session_management + session_management >> main_dashboard \ No newline at end of file diff --git a/architecture_diagrams/08_security_architecture.py b/architecture_diagrams/08_security_architecture.py new file mode 100644 index 0000000..6059508 --- /dev/null +++ b/architecture_diagrams/08_security_architecture.py @@ -0,0 +1,93 @@ +""" +Security and Authentication Architecture Diagram +Shows the comprehensive security model and authentication flow +""" + +from diagrams import Diagram, Cluster, Edge +from diagrams.onprem.client import Users +from diagrams.onprem.security import Oauth +from diagrams.onprem.inmemory import Redis +from diagrams.programming.language import Python +from diagrams.generic.compute import Rack +from diagrams.generic.storage import Storage +from diagrams.onprem.network import Nginx +from diagrams.generic.database import SQL + +with Diagram("Security and Authentication Architecture", + filename="diagrams/08_security_architecture", + show=False, + direction="TB"): + + # External Users + users = Users("External Users\n• Students\n• Educators\n• Administrators") + + # Security Perimeter + with Cluster("Security Perimeter"): + https_gateway = Nginx("HTTPS Gateway\n• SSL/TLS encryption\n• Certificate management\n• Secure headers") + rate_limiting = Rack("Rate Limiting\n• Request throttling\n• DDoS protection\n• Abuse prevention") + + # Authentication Layer + with Cluster("Authentication System"): + login_handler = Oauth("Login Handler\n• Credential validation\n• Multi-factor support\n• Brute force protection") + + password_security = Python("Password Security\n• Hashing (bcrypt/scrypt)\n• Salt generation\n• Strength validation") + + session_store = Redis("Session Store\n• Secure session tokens\n• Expiration management\n• Cross-site protection") + + # Authorization Layer + with Cluster("Authorization System"): + role_manager = Python("Role Manager\n• Role definitions\n• Permission mapping\n• Access control") + + permission_engine = Rack("Permission Engine\n• Route protection\n• Resource access\n• Action authorization") + + with Cluster("User Roles & Permissions"): + admin_permissions = Storage("Admin Permissions\n• Full system access\n• User management\n• Model updates\n• System configuration") + + educator_permissions = Storage("Educator Permissions\n• Student analytics\n• Batch predictions\n• Reporting tools\n• Limited admin") + + user_permissions = Storage("User Permissions\n• Personal predictions\n• Profile management\n• Basic analytics\n• Read-only access") + + # Security Middleware + with Cluster("Security Middleware"): + auth_decorator = Python("Auth Decorator\n• @require_auth\n• Route protection\n• Session validation") + + input_sanitization = Rack("Input Sanitization\n• XSS prevention\n• SQL injection protection\n• Data validation") + + csrf_protection = Rack("CSRF Protection\n• Token validation\n• State verification\n• Request authentication") + + # Application Security + with Cluster("Application Security"): + secure_storage = Storage("Secure Storage\n• Encrypted data\n• Access logs\n• Audit trails") + + error_handling = Rack("Error Handling\n• Secure error messages\n• Log sanitization\n• Information disclosure prevention") + + monitoring = SQL("Security Monitoring\n• Failed login attempts\n• Suspicious activity\n• Access pattern analysis") + + # Data Protection + with Cluster("Data Protection"): + data_encryption = Storage("Data Encryption\n• At-rest encryption\n• In-transit encryption\n• Key management") + + privacy_controls = Rack("Privacy Controls\n• Data anonymization\n• PII protection\n• Consent management") + + backup_security = Storage("Backup Security\n• Encrypted backups\n• Access controls\n• Recovery procedures") + + # Security Flow + users >> https_gateway >> rate_limiting >> login_handler + login_handler >> password_security >> session_store + + session_store >> role_manager >> permission_engine + + role_manager >> admin_permissions + role_manager >> educator_permissions + role_manager >> user_permissions + + permission_engine >> auth_decorator >> input_sanitization + input_sanitization >> csrf_protection + + auth_decorator >> secure_storage + csrf_protection >> error_handling + error_handling >> monitoring + + secure_storage >> data_encryption + data_encryption >> privacy_controls + privacy_controls >> backup_security \ No newline at end of file diff --git a/architecture_diagrams/generate_all_diagrams.py b/architecture_diagrams/generate_all_diagrams.py new file mode 100644 index 0000000..c85493e --- /dev/null +++ b/architecture_diagrams/generate_all_diagrams.py @@ -0,0 +1,82 @@ +""" +Master Script to Generate All Architecture Diagrams +Run this script to generate all diagram images at once +""" + +import os +import sys +import subprocess +from pathlib import Path + +def generate_diagram(script_name): + """Generate a single diagram by running its Python script""" + try: + print(f"Generating {script_name}...") + result = subprocess.run([sys.executable, script_name], + capture_output=True, text=True, cwd="diagrams") + + if result.returncode == 0: + print(f"✅ Successfully generated {script_name}") + else: + print(f"❌ Error generating {script_name}:") + print(result.stderr) + + except Exception as e: + print(f"❌ Exception while generating {script_name}: {e}") + +def main(): + """Generate all architecture diagrams""" + + # Check if diagrams library is installed + try: + import diagrams + print("✅ Diagrams library found") + except ImportError: + print("❌ Diagrams library not found. Installing...") + subprocess.run([sys.executable, "-m", "pip", "install", "-r", "diagrams/requirements.txt"]) + print("✅ Diagrams library installed") + + # List of diagram scripts to generate + diagram_scripts = [ + "00_system_overview.py", + "01_data_flow_architecture.py", + "02_eda_process_flow.py", + "03_ml_pipeline_architecture.py", + "04_application_architecture.py", + "05_deployment_architecture.py", + "06_feature_engineering_pipeline.py", + "07_user_journey_flow.py", + "08_security_architecture.py" + ] + + print("🚀 Starting diagram generation...") + print("=" * 50) + + # Create diagrams directory if it doesn't exist + Path("diagrams").mkdir(exist_ok=True) + + # Generate each diagram + for script in diagram_scripts: + if os.path.exists(f"diagrams/{script}"): + generate_diagram(script) + else: + print(f"⚠️ Script not found: {script}") + + print("=" * 50) + print("🎉 Diagram generation complete!") + print("\nGenerated diagrams:") + + # List generated PNG files + diagrams_dir = Path("diagrams") + png_files = list(diagrams_dir.glob("*.png")) + + if png_files: + for png_file in sorted(png_files): + print(f" 📊 {png_file.name}") + else: + print(" ⚠️ No PNG files found. Check for errors above.") + + print(f"\nTotal diagrams generated: {len(png_files)}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e79d816..3a067b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,4 +12,5 @@ dependencies = [ "plotly>=6.3.0", "ruff>=0.13.2", "scikit-learn>=1.7.2", + "streamlit>=1.50.0", ] diff --git a/src/components/form_components.py b/src/components/form_components.py new file mode 100644 index 0000000..bbc6074 --- /dev/null +++ b/src/components/form_components.py @@ -0,0 +1,371 @@ +""" +Form input components for the Student Success Predictor +""" + +import streamlit as st +from typing import Dict, Any, Tuple +from ..config.mappings import create_feature_mappings, PRESET_CONFIGURATIONS, ACADEMIC_DEFAULTS + + +def render_preset_selector() -> str: + """Render the preset selector section""" + st.markdown('', unsafe_allow_html=True) + + if preset != "Custom Entry": + st.markdown( + f'
✅ Loading {preset} preset values. You can modify any field below.
', + unsafe_allow_html=True) + + return preset + + +def render_personal_info_section(preset: str, feature_maps: Dict) -> Dict[str, Any]: + """Render personal information input section""" + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 👤 Personal Information") + st.markdown('
Basic demographic information about the student
', unsafe_allow_html=True) + + defaults = PRESET_CONFIGURATIONS[preset] + + marital_status = st.selectbox( + "Marital Status", + options=list(feature_maps['marital_status'].keys()), + format_func=lambda x: feature_maps['marital_status'][x], + index=list(feature_maps['marital_status'].keys()).index(defaults["marital"]) + ) + + gender = st.selectbox( + "Gender", + [0, 1], + format_func=lambda x: "Female" if x == 0 else "Male", + index=defaults["gender"] + ) + + age_at_enrollment = st.number_input( + "Age at Enrollment", + min_value=17, + max_value=70, + value=defaults["age"], + step=1 + ) + + nationality = st.selectbox( + "Nationality", + [1, 2, 6, 11, 13, 14, 17, 21, 22, 24, 25, 26, 32, 41, 62, 100, 101, 103, 105, 108, 109], + index=0 + ) + + return { + 'Marital status': marital_status, + 'Gender': gender, + 'Age at enrollment': age_at_enrollment, + 'Nationality': nationality + } + + +def render_academic_background_section(preset: str, feature_maps: Dict) -> Dict[str, Any]: + """Render academic background input section""" + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 🎓 Academic Background") + st.markdown('
Information about the student\'s application and academic history
', + unsafe_allow_html=True) + + defaults = PRESET_CONFIGURATIONS[preset] + + application_mode = st.selectbox( + "Application Mode", + options=list(feature_maps['application_mode'].keys()), + format_func=lambda x: feature_maps['application_mode'].get(x, f"Mode {x}"), + index=list(feature_maps['application_mode'].keys()).index(defaults["app_mode"]) if defaults["app_mode"] in + feature_maps[ + 'application_mode'] else 0 + ) + + application_order = st.selectbox( + "Application Order (1st choice, 2nd choice, etc.)", + [1, 2, 3, 4, 5, 6, 7, 8, 9], + index=0 + ) + + course = st.selectbox( + "Course", + options=list(feature_maps['course'].keys()), + format_func=lambda x: feature_maps['course'].get(x, f"Course {x}"), + index=list(feature_maps['course'].keys()).index(defaults["course"]) if defaults["course"] in feature_maps[ + 'course'] else 0 + ) + + daytime_evening = st.selectbox( + "Attendance", + [0, 1], + format_func=lambda x: "Evening" if x == 0 else "Daytime" + ) + + previous_qualification = st.selectbox( + "Previous Qualification", + options=list(feature_maps['qualification'].keys()), + format_func=lambda x: feature_maps['qualification'].get(x, f"Qualification {x}") + ) + + col1, col2 = st.columns(2) + with col1: + previous_qualification_grade = st.number_input( + "Previous Qualification Grade", + min_value=95, + max_value=190, + value=defaults["prev_grade"], + step=1 + ) + with col2: + admission_grade = st.number_input( + "Admission Grade", + min_value=95, + max_value=190, + value=defaults["adm_grade"], + step=1 + ) + + return { + 'Application mode': application_mode, + 'Application order': application_order, + 'Course': course, + 'Daytime/evening attendance': daytime_evening, + 'Previous qualification': previous_qualification, + 'Previous qualification (grade)': previous_qualification_grade, + 'Admission grade': admission_grade + } + + +def render_family_background_section(feature_maps: Dict) -> Dict[str, Any]: + """Render family background input section""" + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 👨‍👩‍👧‍👦 Family Background") + st.markdown('
Parent education and occupation information
', unsafe_allow_html=True) + + mothers_qualification = st.selectbox( + "Mother's Qualification", + options=list(feature_maps['qualification'].keys()), + format_func=lambda x: feature_maps['qualification'].get(x, f"Qualification {x}") + ) + + fathers_qualification = st.selectbox( + "Father's Qualification", + options=list(feature_maps['qualification'].keys()), + format_func=lambda x: feature_maps['qualification'].get(x, f"Qualification {x}") + ) + + mothers_occupation = st.selectbox( + "Mother's Occupation", + options=list(feature_maps['occupation'].keys()), + format_func=lambda x: feature_maps['occupation'].get(x, f"Occupation {x}") + ) + + fathers_occupation = st.selectbox( + "Father's Occupation", + options=list(feature_maps['occupation'].keys()), + format_func=lambda x: feature_maps['occupation'].get(x, f"Occupation {x}") + ) + + return { + 'Mothers qualification': mothers_qualification, + 'Fathers qualification': fathers_qualification, + 'Mothers occupation': mothers_occupation, + 'Fathers occupation': fathers_occupation + } + + +def render_financial_social_section() -> Dict[str, Any]: + """Render financial and social factors input section""" + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 💰 Financial & Social") + st.markdown('
Student financial status and special circumstances
', + unsafe_allow_html=True) + + col1, col2 = st.columns(2) + with col1: + displaced = st.radio( + "Displaced Student", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + educational_special_needs = st.radio( + "Educational Special Needs", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + debtor = st.radio( + "Has Outstanding Debts", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + with col2: + tuition_fees_up_to_date = st.radio( + "Tuition Fees Up to Date", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + scholarship_holder = st.radio( + "Scholarship Holder", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + international = st.radio( + "International Student", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + + return { + 'Displaced': displaced, + 'Educational special needs': educational_special_needs, + 'Debtor': debtor, + 'Tuition fees up to date': tuition_fees_up_to_date, + 'Scholarship holder': scholarship_holder, + 'International': international + } + + +def render_academic_performance_section(preset: str) -> Dict[str, Any]: + """Render academic performance input sections""" + academic_defaults = ACADEMIC_DEFAULTS[preset] + + # 1st Semester + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 📚 1st Semester Performance") + st.markdown( + '
⚠️ Important: Academic performance is a key predictor of student success
', + unsafe_allow_html=True) + + col1, col2 = st.columns(2) + with col1: + curricular_units_1st_sem_enrolled = st.number_input( + "Enrolled Units", min_value=0, max_value=26, + value=academic_defaults["1st_sem"]["enrolled"], step=1, + key="1st_enrolled", help="Number of courses registered for" + ) + curricular_units_1st_sem_approved = st.number_input( + "Approved Units", min_value=0, max_value=26, + value=academic_defaults["1st_sem"]["approved"], step=1, + key="1st_approved", help="Number of courses passed" + ) + curricular_units_1st_sem_credited = st.number_input( + "Credited Units", min_value=0, max_value=26, + value=academic_defaults["1st_sem"]["credited"], step=1, + key="1st_credited", help="Credits from previous education" + ) + with col2: + curricular_units_1st_sem_evaluations = st.number_input( + "Evaluations", min_value=0, max_value=45, + value=academic_defaults["1st_sem"]["evaluations"], step=1, + key="1st_evaluations", help="Number of exams taken" + ) + curricular_units_1st_sem_grade = st.number_input( + "Average Grade (0-20)", min_value=0, max_value=20, + value=academic_defaults["1st_sem"]["grade"], step=1, + key="1st_grade", help="Average grade on 0-20 scale" + ) + curricular_units_1st_sem_without_evaluations = st.number_input( + "Without Evaluations", min_value=0, max_value=14, + value=academic_defaults["1st_sem"]["without"], step=1, + key="1st_without", help="Courses registered but not evaluated" + ) + + # 2nd Semester + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 📖 2nd Semester Performance") + st.markdown('
Second semester data helps improve prediction accuracy
', + unsafe_allow_html=True) + + col1, col2 = st.columns(2) + with col1: + curricular_units_2nd_sem_enrolled = st.number_input( + "Enrolled Units", min_value=0, max_value=23, + value=academic_defaults["2nd_sem"]["enrolled"], step=1, + key="2nd_enrolled", help="Number of courses registered for" + ) + curricular_units_2nd_sem_approved = st.number_input( + "Approved Units", min_value=0, max_value=20, + value=academic_defaults["2nd_sem"]["approved"], step=1, + key="2nd_approved", help="Number of courses passed" + ) + curricular_units_2nd_sem_credited = st.number_input( + "Credited Units", min_value=0, max_value=20, + value=academic_defaults["2nd_sem"]["credited"], step=1, + key="2nd_credited", help="Credits from previous education" + ) + with col2: + curricular_units_2nd_sem_evaluations = st.number_input( + "Evaluations", min_value=0, max_value=33, + value=academic_defaults["2nd_sem"]["evaluations"], step=1, + key="2nd_evaluations", help="Number of exams taken" + ) + curricular_units_2nd_sem_grade = st.number_input( + "Average Grade (0-20)", min_value=0, max_value=20, + value=academic_defaults["2nd_sem"]["grade"], step=1, + key="2nd_grade", help="Average grade on 0-20 scale" + ) + curricular_units_2nd_sem_without_evaluations = st.number_input( + "Without Evaluations", min_value=0, max_value=12, + value=academic_defaults["2nd_sem"]["without"], step=1, + key="2nd_without", help="Courses registered but not evaluated" + ) + + return { + 'Curricular units 1st sem (credited)': curricular_units_1st_sem_credited, + 'Curricular units 1st sem (enrolled)': curricular_units_1st_sem_enrolled, + 'Curricular units 1st sem (evaluations)': curricular_units_1st_sem_evaluations, + 'Curricular units 1st sem (approved)': curricular_units_1st_sem_approved, + 'Curricular units 1st sem (grade)': curricular_units_1st_sem_grade, + 'Curricular units 1st sem (without evaluations)': curricular_units_1st_sem_without_evaluations, + 'Curricular units 2nd sem (credited)': curricular_units_2nd_sem_credited, + 'Curricular units 2nd sem (enrolled)': curricular_units_2nd_sem_enrolled, + 'Curricular units 2nd sem (evaluations)': curricular_units_2nd_sem_evaluations, + 'Curricular units 2nd sem (approved)': curricular_units_2nd_sem_approved, + 'Curricular units 2nd sem (grade)': curricular_units_2nd_sem_grade, + 'Curricular units 2nd sem (without evaluations)': curricular_units_2nd_sem_without_evaluations + } + + +def render_economic_indicators_section() -> Dict[str, Any]: + """Render economic indicators input section""" + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 📊 Economic Indicators") + st.markdown( + '
📈 Economic context data - typically set by institution based on current conditions
', + unsafe_allow_html=True) + + col1, col2, col3 = st.columns(3) + with col1: + unemployment_rate = st.number_input( + "Unemployment Rate (%)", + min_value=7.0, max_value=16.2, value=10.8, step=0.1, format="%.1f" + ) + with col2: + inflation_rate = st.number_input( + "Inflation Rate (%)", + min_value=-0.8, max_value=3.7, value=1.4, step=0.1, format="%.1f" + ) + with col3: + gdp = st.number_input( + "GDP Growth", + min_value=-4.06, max_value=3.51, value=1.74, step=0.1, format="%.2f" + ) + + return { + 'Unemployment rate': unemployment_rate, + 'Inflation rate': inflation_rate, + 'GDP': gdp + } \ No newline at end of file diff --git a/src/components/prediction_components.py b/src/components/prediction_components.py new file mode 100644 index 0000000..2c23057 --- /dev/null +++ b/src/components/prediction_components.py @@ -0,0 +1,274 @@ +""" +Prediction and results display components +""" + +import streamlit as st +import plotly.express as px +import plotly.graph_objects as go +import time +import json +from typing import Dict, Any, Tuple, List + + +def render_prediction_section(model, preprocessing_info, user_input: Dict[str, Any]) -> None: + """Render the prediction section with enhanced animations""" + from ..utils.model_utils import preprocess_input, generate_recommendations + + # Prediction button with enhanced styling + predict_button = st.button("🔬 Analyze Student Data", type="primary", use_container_width=True) + + if predict_button: + # Enhanced loading animation + progress_bar = st.progress(0) + status_text = st.empty() + + # Simulate processing steps + steps = [ + ("Loading student data...", 20), + ("Preprocessing features...", 40), + ("Running machine learning model...", 70), + ("Calculating prediction confidence...", 90), + ("Finalizing results...", 100) + ] + + for step, progress in steps: + status_text.text(step) + progress_bar.progress(progress) + time.sleep(0.3) + + # Clear progress indicators + progress_bar.empty() + status_text.empty() + + # Preprocess input + processed_input = preprocess_input(user_input, preprocessing_info) + + # Make prediction + prediction = model.predict(processed_input)[0] + prediction_proba = model.predict_proba(processed_input)[0] + + # Get prediction label + target_reverse_mapping = preprocessing_info['target_reverse_mapping'] + predicted_outcome = target_reverse_mapping[prediction] + + # Store in session state for export + st.session_state['last_prediction'] = { + 'outcome': predicted_outcome, + 'probabilities': prediction_proba, + 'timestamp': time.time() + } + + # Enhanced prediction display with confidence indicators + confidence_level = max(prediction_proba) + confidence_text = "High" if confidence_level > 0.7 else "Medium" if confidence_level > 0.5 else "Low" + + # Display prediction with enhanced styling and confidence + if predicted_outcome == "Graduate": + st.markdown(f'''
+ 🎓 GRADUATE +
Confidence: {confidence_level:.1%} ({confidence_text}) +
''', unsafe_allow_html=True) + st.balloons() + elif predicted_outcome == "Dropout": + st.markdown(f'''
+ ⚠️ AT RISK OF DROPOUT +
Confidence: {confidence_level:.1%} ({confidence_text}) +
''', unsafe_allow_html=True) + else: + st.markdown(f'''
+ 📚 LIKELY TO STAY ENROLLED +
Confidence: {confidence_level:.1%} ({confidence_text}) +
''', unsafe_allow_html=True) + + # Add personalized recommendations + render_recommendations(predicted_outcome, user_input) + + # Display probability charts + render_probability_charts(prediction_proba) + + +def render_recommendations(predicted_outcome: str, user_input: Dict[str, Any]) -> None: + """Render personalized recommendations section""" + from ..utils.model_utils import generate_recommendations + + st.markdown("### 💡 Personalized Recommendations") + + recommendations = generate_recommendations(predicted_outcome, user_input) + + if predicted_outcome == "Graduate": + st.markdown( + '
🎯 Success Indicators: This student shows strong potential for graduation. Continue supporting their academic journey.
', + unsafe_allow_html=True) + elif predicted_outcome == "Dropout": + if recommendations: + rec_text = '
'.join(recommendations) + st.markdown(f'
🎯 Intervention Strategies:
{rec_text}
', + unsafe_allow_html=True) + else: + st.markdown( + '
📈 Monitoring Needed: This student may benefit from regular check-ins and academic support.
', + unsafe_allow_html=True) + + +def render_probability_charts(prediction_proba: List[float]) -> None: + """Render prediction probability charts and metrics""" + st.markdown("### 📊 Prediction Confidence") + + # Create probability dataframe + import pandas as pd + prob_df = pd.DataFrame({ + 'Outcome': ['Dropout', 'Enrolled', 'Graduate'], + 'Probability': prediction_proba + }) + + # Create probability chart + fig = px.bar( + prob_df, + x='Outcome', + y='Probability', + color='Outcome', + color_discrete_map={ + 'Graduate': '#9ccfd8', # Rose Pine foam + 'Dropout': '#eb6f92', # Rose Pine love + 'Enrolled': '#f6c177' # Rose Pine gold + }, + title="Prediction Probabilities" + ) + fig.update_layout( + showlegend=False, + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)' + ) + st.plotly_chart(fig, use_container_width=True) + + # Display detailed probabilities in cards + col_a, col_b, col_c = st.columns(3) + + with col_a: + st.markdown(f""" +
+

Graduate

+

{prediction_proba[2]:.1%}

+
+ """, unsafe_allow_html=True) + + with col_b: + st.markdown(f""" +
+

Dropout

+

{prediction_proba[0]:.1%}

+
+ """, unsafe_allow_html=True) + + with col_c: + st.markdown(f""" +
+

Enrolled

+

{prediction_proba[1]:.1%}

+
+ """, unsafe_allow_html=True) + + +def render_model_info_sidebar(user_input: Dict[str, Any]) -> None: + """Render model information and validation in sidebar""" + from ..utils.model_utils import validate_input + from ..config.theme import MODEL_METRICS + + st.markdown('

📊 Model Insights

', unsafe_allow_html=True) + + # Model information + st.markdown('
', unsafe_allow_html=True) + st.markdown(f""" + **🤖 Model Details:** + - **Algorithm:** Random Forest Classifier + - **Features:** 36 student characteristics + - **Training Accuracy:** 100% + - **Test Accuracy:** {MODEL_METRICS['accuracy']}% + - **Classes:** Graduate, Dropout, Enrolled + """) + st.markdown('
', unsafe_allow_html=True) + + # Input validation feedback + st.markdown("### 🔍 Input Validation") + validation_issues = validate_input(user_input) + + if validation_issues: + for issue in validation_issues: + st.warning(issue) + else: + st.success("✅ All inputs look valid!") + + # Model performance visualization + st.markdown("### Model Performance") + + metrics = ['Accuracy', 'Precision', 'Recall', 'F1-Score'] + values = [MODEL_METRICS['accuracy'], MODEL_METRICS['precision'], + MODEL_METRICS['recall'], MODEL_METRICS['f1_score']] + + fig_perf = go.Figure(data=go.Scatterpolar( + r=values, + theta=metrics, + fill='toself', + line_color='#c4a7e7' # Rose Pine iris + )) + + fig_perf.update_layout( + polar=dict( + radialaxis=dict( + visible=True, + range=[0, 100] + )), + showlegend=False, + title="Model Performance Metrics", + height=300, + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)' + ) + + st.plotly_chart(fig_perf, use_container_width=True) + + +def render_export_section(user_input: Dict[str, Any]) -> None: + """Render export functionality""" + st.markdown("### 📥 Export Results") + + if st.session_state.get('last_prediction'): + export_data = { + 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), + 'prediction': st.session_state['last_prediction']['outcome'], + 'confidence': f"{max(st.session_state['last_prediction']['probabilities']):.1%}", + 'student_data': user_input + } + + # Convert to JSON for download + json_str = json.dumps(export_data, indent=2, ensure_ascii=False) + + st.download_button( + label="📁 Download Prediction Report", + data=json_str, + file_name=f"student_prediction_{time.strftime('%Y%m%d_%H%M%S')}.json", + mime="application/json" + ) + + +def render_footer() -> None: + """Render enhanced footer with project information""" + st.markdown('
', unsafe_allow_html=True) + col1, col2, col3 = st.columns(3) + + with col1: + st.markdown("**📊 Data Source**") + st.caption("UCI ML Repository - Student Academic Success") + + with col2: + st.markdown("**🔬 Model Info**") + st.caption("Random Forest with 93.84% accuracy") + + with col3: + st.markdown("**🛠️ Built With**") + st.caption("Streamlit • scikit-learn • Plotly") + + st.markdown("---") + st.markdown( + '

🎓 Empowering educational institutions with data-driven insights for student success

', + unsafe_allow_html=True) \ No newline at end of file diff --git a/src/utils/auth.py b/src/utils/auth.py index e69de29..92f7261 100644 --- a/src/utils/auth.py +++ b/src/utils/auth.py @@ -0,0 +1,308 @@ +""" +Authentication module for Student Success Predictor +Provides secure login/logout functionality with session management +""" + +import streamlit as st +import hashlib +import json +import os +from typing import Dict, Optional, Tuple +from pathlib import Path + +# Path to store user credentials (in production, use a proper database) +USERS_FILE = Path("users.json") + +# Default admin user (change password in production!) +DEFAULT_USERS = { + "admin": { + "password_hash": "240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9", # "admin123" + "role": "admin", + "name": "Administrator" + }, +} + + +def hash_password(password: str) -> str: + """Hash a password using SHA-256""" + return hashlib.sha256(password.encode()).hexdigest() + + +def load_users() -> Dict: + """Load users from file or create default users""" + if USERS_FILE.exists(): + try: + with open(USERS_FILE, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + pass + + # Create default users file + save_users(DEFAULT_USERS) + return DEFAULT_USERS + + +def save_users(users: Dict) -> None: + """Save users to file""" + with open(USERS_FILE, 'w') as f: + json.dump(users, f, indent=2) + + +def verify_credentials(username: str, password: str) -> Tuple[bool, Optional[Dict]]: + """Verify user credentials""" + users = load_users() + + if username in users: + stored_hash = users[username]["password_hash"] + input_hash = hash_password(password) + + if stored_hash == input_hash: + return True, users[username] + + return False, None + + +def init_session_state(): + """Initialize session state for authentication""" + if 'authenticated' not in st.session_state: + st.session_state.authenticated = False + if 'username' not in st.session_state: + st.session_state.username = None + if 'user_role' not in st.session_state: + st.session_state.user_role = None + if 'user_name' not in st.session_state: + st.session_state.user_name = None + + +def login_form(): + """Render login form with Rose Pine styling""" + st.markdown(""" + + """, unsafe_allow_html=True) + + with st.container(): + st.markdown('
', unsafe_allow_html=True) + + st.markdown('

🔐 Student Success Predictor

', unsafe_allow_html=True) + + # Demo credentials info + st.markdown(""" + + """, unsafe_allow_html=True) + + with st.form("login_form"): + username = st.text_input("Username", placeholder="Enter your username") + password = st.text_input("Password", type="password", placeholder="Enter your password") + + col1, col2, col3 = st.columns([1, 2, 1]) + with col2: + login_button = st.form_submit_button("🚀 Login", use_container_width=True) + + if login_button: + if username and password: + is_valid, user_info = verify_credentials(username, password) + + if is_valid: + st.session_state.authenticated = True + st.session_state.username = username + st.session_state.user_role = user_info["role"] + st.session_state.user_name = user_info["name"] + st.success(f"Welcome back, {user_info['name']}!") + st.rerun() + else: + st.error("❌ Invalid username or password") + else: + st.warning("⚠️ Please enter both username and password") + + st.markdown('
', unsafe_allow_html=True) + + +def logout(): + """Logout user and clear session""" + st.session_state.authenticated = False + st.session_state.username = None + st.session_state.user_role = None + st.session_state.user_name = None + st.rerun() + + +def render_user_info(): + """Render user information in sidebar""" + if st.session_state.authenticated: + with st.sidebar: + st.markdown("---") + st.markdown("### 👤 User Information") + + # User info card + st.markdown(f""" +
+ 👋 {st.session_state.user_name}
+ Role: {st.session_state.user_role.title()}
+ User: @{st.session_state.username} +
+ """, unsafe_allow_html=True) + + # Logout button + if st.button("🚪 Logout", type="secondary", use_container_width=True): + logout() + + +def require_auth(func): + """Decorator to require authentication for functions""" + + def wrapper(*args, **kwargs): + init_session_state() + + if not st.session_state.authenticated: + login_form() + return None + + # Add user info to sidebar + render_user_info() + + return func(*args, **kwargs) + + return wrapper + + +def check_role_permission(required_role: str = None) -> bool: + """Check if current user has required role""" + if not st.session_state.authenticated: + return False + + if required_role is None: + return True + + user_role = st.session_state.user_role + + # Define role hierarchy (admin > educator > user) + role_hierarchy = { + "admin": 3, + "educator": 2, + "user": 1 + } + + current_level = role_hierarchy.get(user_role, 0) + required_level = role_hierarchy.get(required_role, 0) + + return current_level >= required_level + + +def add_user(username: str, password: str, name: str, role: str = "user") -> bool: + """Add a new user (admin only)""" + if not check_role_permission("admin"): + return False + + users = load_users() + + if username in users: + return False # User already exists + + users[username] = { + "password_hash": hash_password(password), + "role": role, + "name": name + } + + save_users(users) + return True + + +def render_admin_panel(): + """Render admin panel for user management""" + if not check_role_permission("admin"): + st.error("🚫 Access denied. Admin privileges required.") + return + + st.markdown("### 👨‍💼 Admin Panel") + + with st.expander("👥 User Management"): + st.markdown("#### Add New User") + + with st.form("add_user_form"): + new_username = st.text_input("Username") + new_password = st.text_input("Password", type="password") + new_name = st.text_input("Full Name") + new_role = st.selectbox("Role", ["user", "educator", "admin"]) + + if st.form_submit_button("➕ Add User"): + if all([new_username, new_password, new_name]): + if add_user(new_username, new_password, new_name, new_role): + st.success(f"✅ User '{new_username}' added successfully!") + else: + st.error("❌ User already exists or operation failed") + else: + st.warning("⚠️ Please fill all fields") + + # Display existing users + st.markdown("#### Existing Users") + users = load_users() + + for username, info in users.items(): + col1, col2, col3 = st.columns([2, 1, 1]) + with col1: + st.text(f"👤 {info['name']} (@{username})") + with col2: + st.text(f"🏷️ {info['role']}") + with col3: + if username != st.session_state.username: # Can't delete self + if st.button("🗑️", key=f"del_{username}", help="Delete user"): + del users[username] + save_users(users) + st.rerun() + + +# Session timeout functionality +def check_session_timeout(timeout_minutes: int = 30): + """Check if session has timed out""" + import time + + if 'last_activity' not in st.session_state: + st.session_state.last_activity = time.time() + + current_time = time.time() + time_diff = (current_time - st.session_state.last_activity) / 60 # Convert to minutes + + if time_diff > timeout_minutes and st.session_state.authenticated: + st.warning(f"⏰ Session expired after {timeout_minutes} minutes of inactivity") + logout() + else: + st.session_state.last_activity = current_time \ No newline at end of file diff --git a/src/utils/models_utils.py b/src/utils/models_utils.py new file mode 100644 index 0000000..e3cca8b --- /dev/null +++ b/src/utils/models_utils.py @@ -0,0 +1,112 @@ +""" +Model loading and data preprocessing utilities +""" + +import streamlit as st +import pandas as pd +import pickle +from typing import Dict, Any, List + + +@st.cache_data +def load_model_and_info(): + """Load the trained model and preprocessing information""" + try: + with open('random_forest_model.pkl', 'rb') as f: + model = pickle.load(f) + + with open('preprocessing_info.pkl', 'rb') as f: + preprocessing_info = pickle.load(f) + + with open('feature_names.pkl', 'rb') as f: + feature_names = pickle.load(f) + + return model, preprocessing_info, feature_names + except FileNotFoundError: + st.error("❌ Model files not found! Please run 'train_and_save_model.py' first.") + st.stop() + + +def preprocess_input(user_input: Dict[str, Any], preprocessing_info: Dict[str, Any]) -> pd.DataFrame: + """Preprocess user input to match the training data format""" + + # Create a DataFrame with the user input + input_df = pd.DataFrame([user_input]) + + # Get categorical columns that need one-hot encoding + categorical_columns = [ + 'Marital status', 'Application mode', 'Course', 'Daytime/evening attendance', + 'Previous qualification', 'Nationality', 'Mothers qualification', + 'Fathers qualification', 'Mothers occupation', 'Fathers occupation', + 'Gender', 'Displaced', 'Educational special needs', 'Debtor', + 'Tuition fees up to date', 'Scholarship holder', 'International', + 'Application order' + ] + + # Perform one-hot encoding for categorical columns + for column in categorical_columns: + if column in input_df.columns: + # Get dummies with prefix + dummies = pd.get_dummies(input_df[column], prefix=column) + # Concatenate with original data + input_df = pd.concat([input_df, dummies], axis=1) + # Drop original column + input_df.drop(columns=[column], inplace=True) + + # Ensure all training features are present + feature_names = preprocessing_info['feature_names'] + + # Add missing columns with 0 values + for feature in feature_names: + if feature not in input_df.columns: + input_df[feature] = 0 + + # Select only the features used in training and in the same order + input_df = input_df[feature_names] + + return input_df + + +def validate_input(user_input: Dict[str, Any]) -> List[str]: + """Validate user input and return list of issues""" + validation_issues = [] + + # Check for unrealistic combinations + if user_input['Age at enrollment'] < 17: + validation_issues.append("⚠️ Age seems too low for enrollment") + + if user_input['Curricular units 1st sem (approved)'] > user_input['Curricular units 1st sem (enrolled)']: + validation_issues.append("⚠️ Approved units exceed enrolled units (1st sem)") + + if user_input['Curricular units 2nd sem (approved)'] > user_input['Curricular units 2nd sem (enrolled)']: + validation_issues.append("⚠️ Approved units exceed enrolled units (2nd sem)") + + return validation_issues + + +def generate_recommendations(predicted_outcome: str, user_input: Dict[str, Any]) -> List[str]: + """Generate personalized recommendations based on prediction and input""" + recommendations = [] + + if predicted_outcome == "Dropout": + if user_input['Curricular units 1st sem (approved)'] == 0: + recommendations.append("📚 **Academic Support:** Provide tutoring for struggling courses") + + if user_input['Scholarship holder'] == 0: + recommendations.append("💰 **Financial Aid:** Consider scholarship opportunities") + + if user_input['Age at enrollment'] > 25: + recommendations.append("🕐 **Flexible Scheduling:** Offer evening or part-time options") + + if user_input['Debtor'] == 1: + recommendations.append("💳 **Financial Counseling:** Address outstanding debts") + + elif predicted_outcome == "Graduate": + recommendations.append("🎯 **Success Indicators:** This student shows strong potential for graduation") + recommendations.append("🔄 **Continue Support:** Maintain current academic support systems") + + else: # Enrolled + recommendations.append("📈 **Regular Monitoring:** Schedule periodic academic check-ins") + recommendations.append("🎯 **Goal Setting:** Help establish clear academic objectives") + + return recommendations \ No newline at end of file diff --git a/users.json b/users.json new file mode 100644 index 0000000..ac9b040 --- /dev/null +++ b/users.json @@ -0,0 +1,12 @@ +{ + "admin": { + "password_hash": "240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9", + "role": "admin", + "name": "Administrator" + }, + "educator": { + "password_hash": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", + "role": "educator", + "name": "Educator" + } +} \ No newline at end of file diff --git a/uv.lock b/uv.lock index 38cc87f..ab75165 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,58 @@ version = 1 revision = 3 requires-python = ">=3.13" +[[package]] +name = "altair" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305, upload-time = "2024-11-23T23:39:58.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200, upload-time = "2024-11-23T23:39:56.4Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/61/e4fad8155db4a04bfb4734c7c8ff0882f078f24294d42798b3568eb63bff/cachetools-6.2.0.tar.gz", hash = "sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32", size = 30988, upload-time = "2025-08-25T18:57:30.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl", hash = "sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6", size = 11276, upload-time = "2025-08-25T18:57:29.684Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + [[package]] name = "cfgv" version = "3.4.0" @@ -11,6 +63,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + [[package]] name = "contourpy" version = "1.3.3" @@ -140,6 +244,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/a4/247d3e54eb5ed59e94e09866cfc4f9567e274fbf310ba390711851f63b3b/fonttools-4.60.0-py3-none-any.whl", hash = "sha256:496d26e4d14dcccdd6ada2e937e4d174d3138e3d73f5c9b6ec6eb2fd1dab4f66", size = 1142186, upload-time = "2025-09-17T11:33:59.287Z" }, ] +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, +] + [[package]] name = "graphviz" version = "0.20.3" @@ -158,6 +286,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" }, ] +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -179,6 +316,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, ] +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.9" @@ -536,6 +700,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, ] +[[package]] +name = "protobuf" +version = "6.32.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/a4/cc17347aa2897568beece2e674674359f911d6fe21b0b8d6268cd42727ac/protobuf-6.32.1.tar.gz", hash = "sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d", size = 440635, upload-time = "2025-09-11T21:38:42.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/98/645183ea03ab3995d29086b8bf4f7562ebd3d10c9a4b14ee3f20d47cfe50/protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085", size = 424411, upload-time = "2025-09-11T21:38:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f3/6f58f841f6ebafe076cebeae33fc336e900619d34b1c93e4b5c97a81fdfa/protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1", size = 435738, upload-time = "2025-09-11T21:38:30.959Z" }, + { url = "https://files.pythonhosted.org/packages/10/56/a8a3f4e7190837139e68c7002ec749190a163af3e330f65d90309145a210/protobuf-6.32.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281", size = 426454, upload-time = "2025-09-11T21:38:34.076Z" }, + { url = "https://files.pythonhosted.org/packages/3f/be/8dd0a927c559b37d7a6c8ab79034fd167dcc1f851595f2e641ad62be8643/protobuf-6.32.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4", size = 322874, upload-time = "2025-09-11T21:38:35.509Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f6/88d77011b605ef979aace37b7703e4eefad066f7e84d935e5a696515c2dd/protobuf-6.32.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710", size = 322013, upload-time = "2025-09-11T21:38:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/97/b7/15cc7d93443d6c6a84626ae3258a91f4c6ac8c0edd5df35ea7658f71b79c/protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346", size = 169289, upload-time = "2025-09-11T21:38:41.234Z" }, +] + +[[package]] +name = "pyarrow" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306, upload-time = "2025-07-18T00:56:04.42Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622, upload-time = "2025-07-18T00:56:07.505Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094, upload-time = "2025-07-18T00:56:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576, upload-time = "2025-07-18T00:56:15.569Z" }, + { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342, upload-time = "2025-07-18T00:56:19.531Z" }, + { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218, upload-time = "2025-07-18T00:56:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551, upload-time = "2025-07-18T00:56:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064, upload-time = "2025-07-18T00:56:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837, upload-time = "2025-07-18T00:56:33.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158, upload-time = "2025-07-18T00:56:37.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885, upload-time = "2025-07-18T00:56:41.483Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625, upload-time = "2025-07-18T00:56:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890, upload-time = "2025-07-18T00:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006, upload-time = "2025-07-18T00:56:56.379Z" }, +] + +[[package]] +name = "pydeck" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240, upload-time = "2024-05-10T15:36:21.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403, upload-time = "2024-05-10T15:36:17.36Z" }, +] + [[package]] name = "pyparsing" version = "3.2.5" @@ -602,6 +815,100 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, +] + [[package]] name = "ruff" version = "0.13.2" @@ -717,6 +1024,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "streamlit" +version = "1.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altair" }, + { name = "blinker" }, + { name = "cachetools" }, + { name = "click" }, + { name = "gitpython" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "pyarrow" }, + { name = "pydeck" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "toml" }, + { name = "tornado" }, + { name = "typing-extensions" }, + { name = "watchdog", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/f6/f7d3a0146577c1918439d3163707040f7111a7d2e7e2c73fa7adeb169c06/streamlit-1.50.0.tar.gz", hash = "sha256:87221d568aac585274a05ef18a378b03df332b93e08103fffcf3cd84d852af46", size = 9664808, upload-time = "2025-09-23T19:24:00.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/38/991bbf9fa3ed3d9c8e69265fc449bdaade8131c7f0f750dbd388c3c477dc/streamlit-1.50.0-py3-none-any.whl", hash = "sha256:9403b8f94c0a89f80cf679c2fcc803d9a6951e0fba542e7611995de3f67b4bb3", size = 10068477, upload-time = "2025-09-23T19:23:57.245Z" }, +] + [[package]] name = "student-retention" version = "0.1.0" @@ -729,6 +1074,7 @@ dependencies = [ { name = "plotly" }, { name = "ruff" }, { name = "scikit-learn" }, + { name = "streamlit" }, ] [package.metadata] @@ -740,6 +1086,16 @@ requires-dist = [ { name = "plotly", specifier = ">=6.3.0" }, { name = "ruff", specifier = ">=0.13.2" }, { name = "scikit-learn", specifier = ">=1.7.2" }, + { name = "streamlit", specifier = ">=1.50.0" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] [[package]] @@ -751,6 +1107,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, + { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + [[package]] name = "tzdata" version = "2025.2" @@ -760,6 +1153,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + [[package]] name = "virtualenv" version = "20.34.0" @@ -773,3 +1175,21 @@ sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615 wheels = [ { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, ] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +]