antigravity

This commit is contained in:
2025-09-28 18:51:07 +03:00
parent 270bd071a8
commit 2125506f67
18 changed files with 2378 additions and 2 deletions
+129 -1
View File
@@ -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'<h1 class="main-header">{APP_TITLE}</h1>', unsafe_allow_html=True)
st.markdown(f'<p class="subtitle">{APP_SUBTITLE}</p>', 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('<h2 class="sub-header">Prediction Results</h2>', unsafe_allow_html=True)
if check_role_permission("educator"):
st.markdown(
'<div class="info-box">🎓 <strong>Educator Access:</strong> You have full access to prediction features and student data analysis.</div>',
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(
'<div class="warning-box">📊 <strong>Export Feature:</strong> Available for educators and administrators only.</div>',
unsafe_allow_html=True)
# Render footer
render_footer()
if __name__ == "__main__":
# Configure page before authentication
st.set_page_config(**PAGE_CONFIG)
main()
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
+1
View File
@@ -12,4 +12,5 @@ dependencies = [
"plotly>=6.3.0",
"ruff>=0.13.2",
"scikit-learn>=1.7.2",
"streamlit>=1.50.0",
]
+371
View File
@@ -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('<div class="sidebar-section">', unsafe_allow_html=True)
st.markdown("### 🚀 Quick Start")
preset = st.selectbox(
"Load Sample Data",
["Custom Entry", "Typical Graduate Student", "At-Risk Student", "International Student"],
help="Choose a preset to quickly fill common scenarios"
)
st.markdown('</div>', unsafe_allow_html=True)
if preset != "Custom Entry":
st.markdown(
f'<div class="success-box">✅ Loading <strong>{preset}</strong> preset values. You can modify any field below.</div>',
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('<div class="section-divider"></div>', unsafe_allow_html=True)
st.markdown("### 👤 Personal Information")
st.markdown('<div class="info-box">Basic demographic information about the student</div>', 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('<div class="section-divider"></div>', unsafe_allow_html=True)
st.markdown("### 🎓 Academic Background")
st.markdown('<div class="info-box">Information about the student\'s application and academic history</div>',
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('<div class="section-divider"></div>', unsafe_allow_html=True)
st.markdown("### 👨‍👩‍👧‍👦 Family Background")
st.markdown('<div class="info-box">Parent education and occupation information</div>', 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('<div class="section-divider"></div>', unsafe_allow_html=True)
st.markdown("### 💰 Financial & Social")
st.markdown('<div class="info-box">Student financial status and special circumstances</div>',
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('<div class="section-divider"></div>', unsafe_allow_html=True)
st.markdown("### 📚 1st Semester Performance")
st.markdown(
'<div class="warning-box">⚠️ <strong>Important:</strong> Academic performance is a key predictor of student success</div>',
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('<div class="section-divider"></div>', unsafe_allow_html=True)
st.markdown("### 📖 2nd Semester Performance")
st.markdown('<div class="info-box">Second semester data helps improve prediction accuracy</div>',
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('<div class="section-divider"></div>', unsafe_allow_html=True)
st.markdown("### 📊 Economic Indicators")
st.markdown(
'<div class="info-box">📈 Economic context data - typically set by institution based on current conditions</div>',
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
}
+274
View File
@@ -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'''<div class="prediction-box graduate-box fade-in">
🎓 GRADUATE
<br><small style="font-size: 1rem; opacity: 0.9;">Confidence: {confidence_level:.1%} ({confidence_text})</small>
</div>''', unsafe_allow_html=True)
st.balloons()
elif predicted_outcome == "Dropout":
st.markdown(f'''<div class="prediction-box dropout-box fade-in">
⚠️ AT RISK OF DROPOUT
<br><small style="font-size: 1rem; opacity: 0.9;">Confidence: {confidence_level:.1%} ({confidence_text})</small>
</div>''', unsafe_allow_html=True)
else:
st.markdown(f'''<div class="prediction-box enrolled-box fade-in">
📚 LIKELY TO STAY ENROLLED
<br><small style="font-size: 1rem; opacity: 0.9;">Confidence: {confidence_level:.1%} ({confidence_text})</small>
</div>''', 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(
'<div class="success-box">🎯 <strong>Success Indicators:</strong> This student shows strong potential for graduation. Continue supporting their academic journey.</div>',
unsafe_allow_html=True)
elif predicted_outcome == "Dropout":
if recommendations:
rec_text = '<br>'.join(recommendations)
st.markdown(f'<div class="warning-box">🎯 <strong>Intervention Strategies:</strong><br>{rec_text}</div>',
unsafe_allow_html=True)
else:
st.markdown(
'<div class="info-box">📈 <strong>Monitoring Needed:</strong> This student may benefit from regular check-ins and academic support.</div>',
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"""
<div class="metric-card">
<h4>Graduate</h4>
<h2>{prediction_proba[2]:.1%}</h2>
</div>
""", unsafe_allow_html=True)
with col_b:
st.markdown(f"""
<div class="metric-card">
<h4>Dropout</h4>
<h2>{prediction_proba[0]:.1%}</h2>
</div>
""", unsafe_allow_html=True)
with col_c:
st.markdown(f"""
<div class="metric-card">
<h4>Enrolled</h4>
<h2>{prediction_proba[1]:.1%}</h2>
</div>
""", 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('<h2 class="sub-header">📊 Model Insights</h2>', unsafe_allow_html=True)
# Model information
st.markdown('<div class="info-box">', 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('</div>', 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('<div class="section-divider"></div>', 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(
'<p style="text-align: center; color: #6c757d; font-size: 0.9rem;">🎓 Empowering educational institutions with data-driven insights for student success</p>',
unsafe_allow_html=True)
+308
View File
@@ -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("""
<style>
.login-container {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
background: linear-gradient(135deg, var(--rp-surface), var(--rp-overlay));
border-radius: 16px;
border: 2px solid var(--rp-iris);
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.login-header {
text-align: center;
color: var(--rp-text);
font-size: 2rem;
font-weight: 700;
margin-bottom: 2rem;
background: linear-gradient(135deg, var(--rp-iris), var(--rp-foam));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.login-info {
background: linear-gradient(135deg, var(--rp-overlay), var(--rp-surface));
padding: 1rem;
border-radius: 8px;
border-left: 4px solid var(--rp-gold);
margin-bottom: 1.5rem;
color: var(--rp-text);
font-size: 0.9rem;
}
</style>
""", unsafe_allow_html=True)
with st.container():
st.markdown('<div class="login-container">', unsafe_allow_html=True)
st.markdown('<h1 class="login-header">🔐 Student Success Predictor</h1>', unsafe_allow_html=True)
# Demo credentials info
st.markdown("""
<div class="login-info">
<strong>Demo Credentials:</strong><br>
👨‍💼 Admin: <code>admin</code> / <code>admin123</code><br>
</div>
""", 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('</div>', 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"""
<div style="
background: linear-gradient(135deg, var(--rp-surface), var(--rp-overlay));
padding: 1rem;
border-radius: 8px;
border-left: 3px solid var(--rp-iris);
margin-bottom: 1rem;
">
<strong>👋 {st.session_state.user_name}</strong><br>
<small>Role: {st.session_state.user_role.title()}</small><br>
<small>User: @{st.session_state.username}</small>
</div>
""", 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
+112
View File
@@ -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
+12
View File
@@ -0,0 +1,12 @@
{
"admin": {
"password_hash": "240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9",
"role": "admin",
"name": "Administrator"
},
"educator": {
"password_hash": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
"role": "educator",
"name": "Educator"
}
}
Generated
+420
View File
@@ -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" },
]