Compare commits

...

10 Commits

16 changed files with 365469 additions and 937 deletions

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
# Outputs and Databases
outputs/schedules/*.db
outputs/schedules/*.csv
outputs/schedules/*.txt
outputs/exports/
# Old local databases (just in case)
demands.db
optimized_cost.txt
resultado_otimizacao_alocacoes*.csv

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.12

50
APP.md Normal file
View File

@@ -0,0 +1,50 @@
Você é um especialista em UI/UX usando Streamlit.
[OBJETIVO]
- Construir uma interface gráfica para o arquivo main.py.
[REGRAS]
- A interface deve ser clean, moderna e intuitiva.
- Não crie gráficos que não sejam úteis.
[WORKSPACE]
- Você está em um ambiente Linux;
- O shell é fish;
- Coloque 'rtk' antes de todos os comandos;
- O ambiente venv foi criado usando 'uv';
- Rode scripts python com 'uv run ...';
- Adicione bibliotecas com 'uv add ...';
[PROJETO]
- Faça a interface toda em inglês;
- Crie uma aba chamada 'Data' com uma tabela do csv após filtragem e tratamento das variáveis;
- Crie uma aba chamada 'Fleet' com a tabela df_frota;
- Crie uma aba chamada 'Demand' com a tabela tabela_pax;
- Crie uma aba chamada 'Solver' com sliders para os parâmetros do problema:
- Frota de aeronaves;
- Período de operação;
- Local de origem;
- Local de destino;
- Número de aeronaves de cada tipo;
- Número de dias;
- Número de voos;
- Número de passageiros;
- Número de tripulantes;
- Número de bagagens;
- Número de aeronaves de cada tipo;
- Dentro da aba Solver coloque também, antes dos slides, o problema em 'matematiques' usando latex;
- No final da aba Solver, coloque um botão para resolver e uma caixa de texto com a saída do solver;
- Crie uma aba chamada 'Mapa' com um mapa da localização dos aeroportos na solução, com um slider por dia do ano;
- Crie uma aba chamada 'Resultados' com os resultados do solver, em tabelas. Coloque também o resultado não otimizado;
- Eu preciso saber também a quantiade de pernoites de uma aeronave em cada aeroporto.
- Desenhe a rota, ao selecionar a rota, um pop-up deve aparecer com informações sobre a rota (incluindo a aeronave e quantidade de pax e dias fora de sede);
- Mude a cor da rota caso seja uma rota de algum dia fora de sede.
- Crie um novo arquivo para este projeto, chamado main_app.py
- Utilize somente o Modelo VII
- Deixe o código bem documento usando doxygen
- Programe como um programado Senior usando Lean Code
- Antes de prosseguir, me mostre uma comparação para este projeto: Streamlit ou Shiny e eu decidirei qual o melhor.

Binary file not shown.

89
README.md Normal file
View File

@@ -0,0 +1,89 @@
# Fleet Assignment Optimization - Força Aérea Brasileira
## Integrantes do Grupo
- 1T Lago
- 1T Figueiredo
- 1T Fialho
## Sobre o Projeto
O presente trabalho aborda o problema clássico de **Fleet Assignment** (Alocação de Frota) aplicado ao contexto do transporte logístico aéreo militar.
O sistema foi desenvolvido como uma aplicação interativa (Dashboard Web) acoplada a um modelo matemático avançado. O objetivo é designar de forma ótima os diferentes modelos de aeronaves de transporte (C-97, C-95M, C-105, KC-390, KC-30, C-99A, C-98 e C-98A) de seus respectivos Esquadrões para atender à matriz de passageiros solicitada.
A arquitetura equilibra a maximização do atendimento de passageiros com a **minimização dos custos logísticos** (consumo de combustível em litros e penalidades de pernoite em bases externas), tudo isso balizado por limites de alcance, velocidades, tamanho físico das frotas disponíveis nas bases (Emissores) e capacidade de passageiros de cada aeronave.
### O Modelo Matemático
O núcleo de otimização foi construído utilizando Programação Linear Inteira Mista (MILP) processada pelo solver **SCIP** (implementado com *Google OR-Tools*). A modelagem contempla:
- **Função Objetivo:** $\min \sum_{t} \sum_{r} (c_{r} \cdot x_{r,t}) + \sum_{t} \sum_{d} (M \cdot s_{d,t})$
O algoritmo minimiza o custo total de combustível das missões ($c_r$), somado à grande penalidade ($M$) associada às variáveis de folga ($s_{d,t}$), forçando a malha a alocar missões para não deixar passageiros para trás, sempre que exequível.
- **Restrições Principais:**
- **Atendimento de Demanda:** $\sum (\text{cap}_{m} \cdot x) + s \ge \text{PAX}$
- **Bloqueio Temporal de Frota:** Se uma aeronave realizar uma missão que exige pernoite fora de sua base, o modelo garante o "bloqueio" desse ativo nos dias subsequentes da janela temporal ($T$), impossibilitando a clonagem da aeronave.
- **Limites de Estoque:** As decolagens originadas de uma localidade jamais podem exceder a propriedade física do $\text{MaxFleet}$ distribuída para o esquadrão emissor.
### Resultados Obtidos
A ferramenta é capaz de rodar a otimização global e extrair métricas gerenciais comparativas:
- **Baseline Fuel (Não-Otimizado):** O sistema isola qual seria o consumo nominal caso as demandas fossem atendidas de modo bruto.
- **Optimized Fuel Consumption:** Retorna o custo de combustível estrito gerado pelo motor SCIP.
- **Total Savings:** Revela à chefia o percentual quantitativo de economia gerada pela roteirização matemática contra o plano base.
- **Visão Tática:** A malha de voos diária pode ser "escrutinada" visualmente por meio de um Mapa Interativo renderizado sobre CartoDB / OpenStreetMap, evidenciando as decisões de despacho por código ICAO.
---
## 📂 Arquitetura do Projeto (Arara OARMP)
O projeto foi estruturado utilizando padrões modernos de software:
```text
/
├── app/
│ └── dashboard.py # Interface Streamlit Interativa
├── raw/ # Dados brutos fixos e mapas geo-referenciados
├── src/
│ └── fleet_assignment/ # Cérebro Matemático
│ ├── config.py # Constantes Globais
│ ├── ingest.py # Leitura, tratamento e merge
│ └── optimizer.py # Motor MILP (SCIP / OR-Tools)
├── outputs/ # Banco de dados temporário e CSVs finais
├── README.md
├── pyproject.toml
└── uv.lock
```
---
## 🛠️ Guia de Instalação e Execução
### 1. Instalar o Python (3.12+)
Abra seu terminal/prompt e utilize o gerenciador de pacotes nativo do seu Sistema Operacional:
* **Windows (Winget):** `winget install Python.Python.3.12`
* **Ubuntu/Debian (APT):** `sudo apt update && sudo apt install python3 python3-pip python3-venv`
* **Fedora (DNF):** `sudo dnf install python3`
* **Arch Linux (Pacman):** `sudo pacman -S python`
* **macOS (Homebrew):** `brew install python`
### 2. Instalar o Gerenciador `uv`
O projeto adota o `uv` (gerenciador de ambiente e dependências ultrarrápido em Rust):
* **macOS / Linux:**
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
* **Windows (PowerShell):**
```powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```
### 3. Configurar Ambiente e Executar
Abra o terminal na pasta raiz deste projeto (onde este `README.md` e o arquivo `pyproject.toml` se encontram) e siga os passos abaixo:
1. **Sincronizar Dependências (Instalar tudo e criar a VENV automaticamente):**
```bash
rtk uv sync
```
2. **Ligar o Servidor Web e a Interface:**
```bash
rtk uv run streamlit run app/dashboard.py
```
> **Acesso:** Assim que o servidor subir, o Python acionará automaticamente a abertura de uma nova guia no seu **navegador web padrão** acessando o painel de operações (normalmente via `http://localhost:8501`).

382
app/dashboard.py Normal file
View File

@@ -0,0 +1,382 @@
"""
Streamlit Dashboard for Brazilian Air Force Fleet Assignment.
Provides interactive mapping, scenario generation, and SCIP solver integration.
"""
import os
import json
import sqlite3
import subprocess
import datetime
import pandas as pd
import pydeck as pdk
import streamlit as st
from vincenty import vincenty
# ==========================================
# PAGE CONFIGURATION
# ==========================================
st.set_page_config(
page_title="Fleet Assignment",
page_icon="✈️",
layout="wide",
initial_sidebar_state="expanded"
)
# ==========================================
# CONSTANTS & CONFIGURATION
# ==========================================
import sys
# Ensure we can import from src
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.fleet_assignment.config import (
CSV_FILEPATH, JSON_AEROPORTOS, JSON_EMISSORES, DB_PATH,
RESULTS_FILE, COST_FILE, AIRCRAFT_CONFIG
)
from src.fleet_assignment.ingest import load_data
@st.cache_data
def get_cached_data():
return load_data()
demands_raw, df_fleet, valid_icaos, airports_geo = get_cached_data()
# ==========================================
# DATABASE MANAGEMENT
# ==========================================
def execute_sql(query, params=(), fetch=False):
"""Helper to execute SQL commands cleanly."""
with sqlite3.connect(DB_PATH) as conn:
if fetch:
return pd.read_sql_query(query, conn, params=params)
conn.execute(query, params)
conn.commit()
def init_db(demands_df, frota_df, force_reset=False):
"""Initializes SQLite Database as the single source of truth."""
if force_reset and os.path.exists(DB_PATH):
os.remove(DB_PATH)
with sqlite3.connect(DB_PATH) as conn:
conn.execute('''
CREATE TABLE IF NOT EXISTS demandas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data_apenas TEXT,
local_dec TEXT,
local_pou TEXT,
pax INTEGER
)
''')
count = conn.execute("SELECT COUNT(*) FROM demandas").fetchone()[0]
if count == 0:
df_insert = demands_df[['Data Apenas', 'Localidade Decolagem', 'Localidade Pouso', 'PAX']].copy()
df_insert.columns = ['data_apenas', 'local_dec', 'local_pou', 'pax']
df_insert.to_sql('demandas', conn, if_exists='append', index=False)
frota_df.to_sql('frota', conn, if_exists='replace', index=False)
def load_demands():
return execute_sql("SELECT id as ID, data_apenas as 'Date', local_dec as 'Dep', local_pou as 'Arr', pax as 'PAX' FROM demandas", fetch=True)
init_db(demands_raw, df_fleet)
@st.cache_data
def load_results():
try:
return pd.read_csv(RESULTS_FILE, sep=';')
except FileNotFoundError:
return pd.DataFrame()
df_results = load_results()
# ==========================================
# SOLVER INTEGRATION
# ==========================================
def run_solver_subprocess(time_limit_sec, output_placeholder):
"""Spawns the optimization worker and streams logs to the UI."""
process = subprocess.Popen(
["uv", "run", "python", "-u", "solver_worker.py", str(time_limit_sec)],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)
full_log = ""
for line in iter(process.stdout.readline, ''):
full_log += line
output_placeholder.code(full_log, language='text')
process.wait()
return process.returncode == 0
# ==========================================
# UI RENDER
# ==========================================
st.title("✈️ Fleet Assignment")
st.markdown("Brazilian Air Force Fleet Routing and Scheduling (Global Time-Space Network Flow)")
tab1, tab2, tab3, tab4, tab5 = st.tabs(["Demand Data", "Fleet", "Solver", "Map", "Results"])
# --- TAB 1: DEMAND DATA ---
with tab1:
col_title, col_reset = st.columns([0.8, 0.2])
with col_title:
st.header("Passenger Demands")
with col_reset:
if st.button("🔄 Reset DB to Raw CSV", type="secondary", use_container_width=True):
init_db(demands_raw, df_fleet, force_reset=True)
st.success("Database Reset!")
st.rerun()
df_display_demand = load_demands()
st.info("💡 You can edit, add, or delete rows directly in the table below. Click 'Save Changes' when done.")
edited_demand = st.data_editor(
df_display_demand,
num_rows="dynamic",
use_container_width=True,
hide_index=True,
key="demand_editor"
)
if st.button("💾 Save Demand Changes", type="primary", use_container_width=True):
with sqlite3.connect(DB_PATH) as conn:
conn.execute("DROP TABLE IF EXISTS demandas")
conn.execute('''
CREATE TABLE demandas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data_apenas TEXT,
local_dec TEXT,
local_pou TEXT,
pax INTEGER
)
''')
df_to_save = edited_demand.drop(columns=['ID'], errors='ignore')
df_to_save.columns = ['data_apenas', 'local_dec', 'local_pou', 'pax']
df_to_save.to_sql('demandas', conn, if_exists='append', index=False)
st.success("Database synchronized successfully!")
st.rerun()
# --- TAB 2: FLEET ---
with tab2:
st.header("Fleet Specifications")
# Check if we have modified fleet in DB, otherwise use default
with sqlite3.connect(DB_PATH) as conn:
try:
current_fleet = pd.read_sql_query("SELECT * FROM frota", conn)
except Exception:
current_fleet = df_fleet.copy()
df_display_fleet = current_fleet[['Modelo', 'Emissor', 'Tamanho da Frota', 'Consumo (km/l)', 'Velocidade Média (km/h)', 'Capacidade', 'Alcance']].copy()
df_display_fleet.columns = ['Modelo', 'Squadron', 'Fleet Size', 'Fuel Usage (km/l)', 'Mean Speed (km/h)', 'PAX Capacity', 'Max Range (km)']
st.info("💡 Edit specs directly below to simulate larger fleets, different fuel burn, or cargo capacity.")
edited_fleet = st.data_editor(
df_display_fleet,
use_container_width=True,
hide_index=True,
key="fleet_editor"
)
if st.button("💾 Save Fleet Changes", type="primary", use_container_width=True):
rename_back = {
'Modelo': 'Modelo', 'Squadron': 'Emissor', 'Fleet Size': 'Tamanho da Frota',
'Fuel Usage (km/l)': 'Consumo (km/l)', 'Mean Speed (km/h)': 'Velocidade Média (km/h)',
'PAX Capacity': 'Capacidade', 'Max Range (km)': 'Alcance'
}
df_save_fleet = edited_fleet.rename(columns=rename_back)
with sqlite3.connect(DB_PATH) as conn:
old_frota = pd.read_sql_query("SELECT * FROM frota", conn)
for idx, row in df_save_fleet.iterrows():
mask = (old_frota['Modelo'] == row['Modelo']) & (old_frota['Emissor'] == row['Emissor'])
old_frota.loc[mask, 'Tamanho da Frota'] = row['Tamanho da Frota']
old_frota.loc[mask, 'Consumo (km/l)'] = row['Consumo (km/l)']
old_frota.loc[mask, 'Velocidade Média (km/h)'] = row['Velocidade Média (km/h)']
old_frota.loc[mask, 'Capacidade'] = row['Capacidade']
old_frota.loc[mask, 'Alcance'] = row['Alcance']
old_frota.to_sql('frota', conn, if_exists='replace', index=False)
st.success("Fleet changes saved successfully!")
st.rerun()
# --- TAB 3: SOLVER ---
with tab3:
st.header("Mathematical Formulation")
st.latex(r"\min \sum_{t} \sum_{r} (c_{r} \cdot x_{r,t}) + \sum_{t} \sum_{d} (M \cdot s_{d,t})")
st.latex(r"\text{s.t.} \quad \sum_{m,e} (\text{cap}_{m} \cdot x_{m,e,r,t}) + s_{r,t} \ge \text{PAX}_{r,t} \quad \forall r, t")
st.latex(r"\sum_{t_{start} \le T \le t_{start} + \text{pernoites}} x_{m,e,r,t_{start}} \le \text{MaxFleet}_{m,e} \quad \forall m, e, T")
st.markdown(r"""
**Dictionary of Variables:**
- $x_{m,e,r,t}$: Decision Variable (Integer). Number of aircraft of model $m$, from squadron $e$, allocated to route $r$ on day $t$.
- $c_r$: Estimated total fuel cost for the route $r$ mission, including overnight stay penalties if applicable.
- $s_{r,t}$: Slack Variable. Represents the passenger demand that could **not** be met on that day due to fleet limitations.
- $M$: High Penalty (Big M). Imposed on the system for each unit of slack activated, forcing the solver to meet demand whenever possible.
- $\text{cap}_m$: Maximum passenger capacity (seats) of aircraft $m$.
- $\text{PAX}_{r,t}$: Actual number of Air Force passengers needing to fly on route $r$ on day $t$.
- $\text{MaxFleet}_{m,e}$: Total physical aircraft available in squadron $e$ for model $m$.
- $T$: Temporal inspection window. Ensures aircraft blockade during overnight stays, preventing fleet duplication.
""")
if st.button("🚀 Run Global Optimization", type="primary"):
st.info("Starting Worker... Follow the real-time SCIP log below:")
log_placeholder = st.empty()
if run_solver_subprocess(300, log_placeholder):
st.success("Solver Finished! Results saved to CSV.")
st.session_state['new_results'] = True
else:
st.error("Solver Failed or was aborted.")
# --- TAB 4: MAP ---
with tab4:
st.header("Interactive Flight Map")
if st.session_state.get('new_results'):
df_results = load_results()
view_state = pdk.ViewState(latitude=-15.78, longitude=-47.92, zoom=3.5, pitch=45)
layers = []
if df_results.empty:
st.info("No optimization results yet. Run the Global Optimization to see routes.")
else:
df_results['Data'] = pd.to_datetime(df_results['Data'])
dias_disponiveis = sorted(df_results['Data'].dt.date.unique())
# Safe initialization of map date
if 'current_map_date' not in st.session_state or st.session_state.current_map_date not in dias_disponiveis:
st.session_state.current_map_date = dias_disponiveis[0]
current_idx = dias_disponiveis.index(st.session_state.current_map_date)
# Navigation Controls
col_prev, col_drop, col_next = st.columns([1, 8, 1])
with col_prev:
st.markdown("<br/>", unsafe_allow_html=True)
if st.button("⬅️", disabled=current_idx==0, use_container_width=True):
st.session_state.current_map_date = dias_disponiveis[current_idx - 1]
st.rerun()
with col_drop:
selected_date = st.date_input("Select via Calendar", value=st.session_state.current_map_date)
if selected_date != st.session_state.current_map_date:
st.session_state.current_map_date = selected_date
st.rerun()
with col_next:
st.markdown("<br/>", unsafe_allow_html=True)
if st.button("➡️", disabled=current_idx>=len(dias_disponiveis)-1, use_container_width=True):
st.session_state.current_map_date = dias_disponiveis[current_idx + 1]
st.rerun()
slider_date = st.select_slider("Or scrub through scheduled flight days", options=dias_disponiveis, value=st.session_state.current_map_date)
if slider_date != st.session_state.current_map_date:
st.session_state.current_map_date = slider_date
st.rerun()
df_day = df_results[df_results['Data'].dt.date == st.session_state.current_map_date]
df_demands_map = load_demands()
# Map Data Parsing
map_data = []
for _, row in df_day.iterrows():
orig, dest = row['Local Decolagem'], row['Local Pouso']
if orig in airports_geo and dest in airports_geo:
lon1, lat1 = float(airports_geo[orig]['lon']), float(airports_geo[orig]['lat'])
lon2, lat2 = float(airports_geo[dest]['lon']), float(airports_geo[dest]['lat'])
pernoites = row.get('Pernoites', 0)
color = [255, 65, 54, 200] if pernoites > 0 else [0, 116, 217, 200]
# Calculate PAX sum for this route on this day
pax_mask = (df_demands_map['Dep'] == orig) & (df_demands_map['Arr'] == dest) & (df_demands_map['Date'] == str(st.session_state.current_map_date))
pax_count = df_demands_map[pax_mask]['PAX'].sum()
map_data.append({
"start": [lon1, lat1], "end": [lon2, lat2], "color": color,
"modelo": row['Modelo'], "orig": orig, "dest": dest,
"pax": int(pax_count), "pernoites": pernoites
})
df_map = pd.DataFrame(map_data)
if df_map.empty:
st.info("No flights scheduled for this day.")
else:
layers.append(pdk.Layer(
"ArcLayer", data=df_map,
get_source_position="start", get_target_position="end",
get_source_color="color", get_target_color="color",
get_width=3, pickable=True
))
st.pydeck_chart(pdk.Deck(
layers=layers,
initial_view_state=view_state,
tooltip={
"html": "<b>✈️ Aircraft:</b> {modelo}<br/><b>📍 Route:</b> {orig}{dest}<br/><b>👥 PAX:</b> {pax}<br/><b>🌙 Out of Base:</b> {pernoites} days",
"style": {"backgroundColor": "steelblue", "color": "white"}
} if layers else None,
map_style='road'
))
# Selected Day Results Table
if not df_results.empty:
df_day_display = df_day.copy()
# Bind PAX back into the table
df_day_display['PAX'] = df_day_display.apply(lambda r: df_demands_map[
(df_demands_map['Dep'] == r['Local Decolagem']) &
(df_demands_map['Arr'] == r['Local Pouso']) &
(df_demands_map['Date'] == str(st.session_state.current_map_date))
]['PAX'].sum(), axis=1)
df_day_display.rename(columns={
'Data': 'Date', 'Modelo': 'Model', 'Emissor': 'Squadron',
'Local Decolagem': 'Departure', 'Local Pouso': 'Arrival',
'Qtd Aeronaves': 'Allocated Fleet', 'Pernoites': 'Days Out of Base'
}, inplace=True)
st.subheader(f"Flight Schedule for {st.session_state.current_map_date}")
st.dataframe(df_day_display, use_container_width=True)
# --- TAB 5: RESULTS ---
with tab5:
st.header("Optimization Results")
try:
raw_fuel_df = execute_sql("SELECT SUM(\"Combustível\") as total FROM frota", fetch=True)
raw_fuel = raw_fuel_df['total'].iloc[0] if not raw_fuel_df.empty else 0
if pd.isna(raw_fuel): raw_fuel = 0
opt_fuel = None
if os.path.exists(COST_FILE):
with open(COST_FILE, "r") as f:
opt_fuel = float(f.read().strip())
colA, colB, colC = st.columns(3)
with colA:
st.metric(label="Baseline Fuel (Non-Optimized)", value=f"{raw_fuel:,.2f} L")
with colB:
if opt_fuel is not None:
st.metric(label="Optimized Fuel Consumption", value=f"{opt_fuel:,.2f} L", delta=f"{opt_fuel - raw_fuel:,.2f} L", delta_color="inverse")
with colC:
if opt_fuel is not None and raw_fuel > 0:
savings = (1 - (opt_fuel / raw_fuel)) * 100
st.metric(label="Total Fuel Savings", value=f"{savings:.2f}%")
except Exception:
pass
if not df_results.empty:
st.dataframe(df_results, use_container_width=True)
if 'Pernoites' in df_results.columns:
st.subheader("Days Out of Base (Pernoites) Breakdown")
df_pernoites = df_results[df_results['Pernoites'] > 0]
st.dataframe(df_pernoites[['Modelo', 'Local Decolagem', 'Local Pouso', 'Pernoites']].sort_values(by='Pernoites', ascending=False))
else:
st.warning("No results available.")

View File

@@ -1,286 +0,0 @@
import nbformat as nbf
nb = nbf.v4.new_notebook()
cells = []
# Cell 1: Markdown Introduction
cells.append(nbf.v4.new_markdown_cell("""# Alocação de Frotas (Fleet Scheduling) - Aeronaves C-97
Este projeto visa realizar a alocação de frota (Fleet Assignment) baseada nos princípios do livro referenciado *Airline Operations and Scheduling*.
Diferentemente do modelo anterior que utilizava aeronaves de carga, este foca exclusivamente na aeronave de passageiros **C-97**.
## Objetivos:
1. Remover cálculos de aeronaves de carga.
2. Analisar as demandas diárias para a aeronave C-97.
3. Identificar os esquadrões detentores dessas aeronaves e a quantidade de aeronaves (matrículas únicas).
4. Calcular os 5 trechos mais relevantes do ano de 2025 utilizando estatística e gráficos.
5. Realizar o Fleet Assignment para os 5 trechos, visando minimizar o custo total da operação (Modelo 1 proposto).
"""))
# Cell 2: Imports
cells.append(nbf.v4.new_code_cell("""# Importação de bibliotecas
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pulp
import pymap3d as pm
import math
import networkx as nx
# Configuração de estilo para os gráficos
sns.set_theme(style="whitegrid")
# Constantes globais
CSV_FILEPATH = "SISCO_AEREA_01-01-2025_A_31-12-2025_1911251714Z.csv"
JSON_AEROPORTOS = "airports.json"
CAPACIDADE_C97 = 30 # Capacidade média de passageiros do C-97 (Brasília)
"""))
# Cell 3: Markdown Load Data
cells.append(nbf.v4.new_markdown_cell("""## 1. Leitura e Limpeza dos Dados
Vamos carregar os dados, filtrar apenas para o modelo `C-97` e tratar os campos de localidades e datas.
"""))
# Cell 4: Code Load Data
cells.append(nbf.v4.new_code_cell("""# Carregamento do banco de dados
df = pd.read_csv(CSV_FILEPATH, low_memory=False)
df_aeroportos = pd.read_json(JSON_AEROPORTOS, orient='index')
# Tratamento básico de valores nulos expressos como 'NIL'
df.replace('NIL', 0, inplace=True)
# Filtro para a aeronave C-97 (Passageiros) e remoção de registros de carga
df_c97 = df[df['Modelo'] == 'C-97'].copy()
# Filtrando decolagens e pousos conhecidos
df_c97 = df_c97[df_c97['Localidade Decolagem'] != 0]
df_c97 = df_c97[df_c97['Localidade Pouso'] != 0]
# Criação da coluna Trecho
df_c97['Trecho'] = df_c97['Localidade Decolagem'] + '-' + df_c97['Localidade Pouso']
# Conversão da coluna PAX para numérico
df_c97['PAX'] = pd.to_numeric(df_c97['PAX']).fillna(0)
# Tratamento de datas
df_c97['Dep TimeStamp'] = pd.to_datetime(df_c97['Data de Decolagem'] + ' ' + df_c97['Hora de Decolagem'], format='mixed', dayfirst=True)
df_c97['Data Apenas'] = df_c97['Dep TimeStamp'].dt.date
"""))
# Cell 5: Markdown Squadrons
cells.append(nbf.v4.new_markdown_cell("""## 2. Análise dos Esquadrões e Matrículas (Aeronaves)
Identificaremos quais esquadrões operam o C-97 e qual a disponibilidade de aeronaves (número de matrículas únicas), pois isso definirá as restrições de frota.
"""))
# Cell 6: Code Squadrons
cells.append(nbf.v4.new_code_cell("""# Obtendo esquadrões e matrículas únicas
esquadroes = df_c97['Emissor'].unique()
matriculas_unicas = df_c97['Matrícula'].unique()
total_aeronaves = len(matriculas_unicas)
print(f"Esquadrões que operam o C-97: {', '.join(esquadroes)}")
print(f"Total de matrículas únicas (aeronaves disponíveis): {total_aeronaves}")
# Aeronaves por esquadrão
aeronaves_por_esquadrao = df_c97.groupby('Emissor')['Matrícula'].nunique().to_dict()
print("\\nAeronaves por esquadrão:")
for esq, qtd in aeronaves_por_esquadrao.items():
print(f" - {esq}: {qtd} aeronaves")
# Identificar a base principal de cada esquadrão (aeroporto com mais decolagens)
bases_esquadroes = {}
for esq in aeronaves_por_esquadrao.keys():
base = df_c97[df_c97['Emissor'] == esq]['Localidade Decolagem'].mode()[0]
bases_esquadroes[esq] = base
print("\\nBases inferidas por esquadrão (pela moda de decolagens):")
for esq, base in bases_esquadroes.items():
print(f" - {esq}: {base}")
"""))
# Cell 7: Markdown Stats
cells.append(nbf.v4.new_markdown_cell("""## 3. Análise Estatística das Demandas Diárias
Para definir os trechos mais relevantes do ano, calcularemos a demanda de passageiros (PAX) agregada diariamente. Em seguida, extraímos a **média de passageiros diários** e o **desvio padrão** para cada trecho. A relevância será determinada pela maior média de demanda diária de PAX.
Começaremos focando em 5 trechos (alvo variável).
"""))
# Cell 8: Code Stats
cells.append(nbf.v4.new_code_cell("""# Demanda diária por trecho
demanda_diaria = df_c97.groupby(['Trecho', 'Data Apenas'])['PAX'].sum().reset_index()
# Criar um range de datas para todo o ano de 2025 para contabilizar os dias sem voo
import itertools
datas_2025 = pd.date_range(start='2025-01-01', end='2025-12-31').date
todos_trechos = demanda_diaria['Trecho'].unique()
idx = pd.MultiIndex.from_product([todos_trechos, datas_2025], names=['Trecho', 'Data Apenas'])
demanda_diaria_completa = pd.DataFrame(index=idx).reset_index()
# Mesclar com os dados reais e preencher NaN com 0
demanda_diaria_completa = pd.merge(demanda_diaria_completa, demanda_diaria, on=['Trecho', 'Data Apenas'], how='left').fillna({'PAX': 0})
# Estatísticas (Média e Desvio Padrão) por trecho
estatisticas_trechos = demanda_diaria_completa.groupby('Trecho').agg(
Media_PAX_Diario=('PAX', 'mean'),
Desvio_Padrao_PAX=('PAX', 'std'),
Total_PAX=('PAX', 'sum'),
Dias_Operados=('PAX', lambda x: (x > 0).sum())
).reset_index()
# Média PAX por dias operados (usado para o Fleet Assignment Model)
estatisticas_trechos['Media_PAX_Dias_Operados'] = estatisticas_trechos.apply(
lambda row: row['Total_PAX'] / row['Dias_Operados'] if row['Dias_Operados'] > 0 else 0, axis=1
)
# Nova métrica solicitada: PAX * Dias_Operados
estatisticas_trechos['Score_Relevancia'] = estatisticas_trechos['Total_PAX'] * estatisticas_trechos['Dias_Operados']
# Ordenar pelas rotas com maior métrica (PAX * Dias_Operados)
estatisticas_trechos = estatisticas_trechos.sort_values(by='Score_Relevancia', ascending=False)
# Definir número de trechos alvo
NUM_TRECHOS = 10
top_trechos = estatisticas_trechos.head(NUM_TRECHOS)
display(top_trechos)
# Gráfico
plt.figure(figsize=(10, 6))
sns.barplot(data=top_trechos, x='Trecho', y='Score_Relevancia', hue='Trecho', palette='viridis', legend=False)
plt.title(f'Top {NUM_TRECHOS} Trechos Mais Relevantes (PAX * Dias Operados)')
plt.ylabel('Score de Relevância (PAX * Dias Operados)')
plt.xlabel('Trecho (Origem - Destino)')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
"""))
# Cell: Markdown Diagrama de Rede
cells.append(nbf.v4.new_markdown_cell("""## Diagrama de Rede dos Trechos
Para melhor visualização dos trechos mais relevantes, abaixo temos um diagrama de rede conectando as origens aos destinos.
"""))
# Cell: Code Diagrama de Rede
cells.append(nbf.v4.new_code_cell("""# Criação do grafo direcionado
G = nx.DiGraph()
# Adiciona arestas com peso baseado no Score de Relevância
for _, row in top_trechos.iterrows():
origem, destino = row['Trecho'].split('-')
peso = row['Score_Relevancia']
G.add_edge(origem, destino, weight=peso)
# Configuração do layout do grafo - k maior afasta mais os nós
pos = nx.spring_layout(G, k=2.5, iterations=100, seed=42)
plt.figure(figsize=(12, 8))
# Define o tamanho do nó
tamanho_no = 1200
# Desenha os nós
nx.draw_networkx_nodes(G, pos, node_size=tamanho_no, node_color='skyblue', edgecolors='black')
# Desenha as arestas, informando o node_size para que a seta não fique escondida sob o nó
nx.draw_networkx_edges(
G, pos,
edgelist=G.edges(),
arrows=True,
arrowstyle='-|>',
arrowsize=25,
edge_color='gray',
width=2,
node_size=tamanho_no,
connectionstyle='arc3,rad=0.15'
)
# Desenha os rótulos (nomes dos aeroportos)
nx.draw_networkx_labels(G, pos, font_size=12, font_family='sans-serif', font_weight='bold')
plt.title('Diagrama de Rede - Top Trechos Relevantes', fontsize=14)
plt.axis('off')
plt.tight_layout()
plt.show()
"""))
# Cell 9: Markdown FAM
cells.append(nbf.v4.new_markdown_cell("""## 4. Fleet Assignment Model (FAM)
Conforme o livro base *Airline Operations and Scheduling*, o problema de alocação de frotas busca atribuir os tipos de aeronaves às pernas de voo visando minimizar o custo e as perdas de receita (spill). Como temos apenas um tipo (C-97), nosso problema adapta-se a uma **alocação de esquadrões (e suas respectivas bases)** aos voos requeridos para minimizar o custo total de operação, que é proporcional à distância voada.
**Modelo Matemático:**
- **Variáveis de decisão:** $X_{s, r}$ (Quantidade de voos operados pelo esquadrão $s$ no trecho $r$)
- **Função Objetivo:** Minimizar a distância total (Distância de posicionamento da base + Distância do trecho + Retorno para a base).
- **Restrições:**
1. O número de voos alocados para o trecho deve suprir a demanda média (Média PAX / Capacidade do C-97).
2. O total de voos atribuídos a um esquadrão não pode exceder o número de aeronaves disponíveis naquele esquadrão no dia.
"""))
# Cell 10: Code FAM
cells.append(nbf.v4.new_code_cell("""# Preparação dos dados para o modelo
rotas_lista = top_trechos['Trecho'].tolist()
# Dicionário de distâncias
def calc_distancia(icao1, icao2):
try:
lat1, lon1, alt1 = df_aeroportos.loc[icao1, ['lat', 'lon', 'elevation']]
lat2, lon2, alt2 = df_aeroportos.loc[icao2, ['lat', 'lon', 'elevation']]
# Retorna a distância oblíqua em km
return pm.geodetic2aer(lat1, lon1, alt1, lat2, lon2, alt2)[2] / 1000.0
except KeyError:
return 9999.0 # Penalidade caso aeroporto não exista no json
distancias_voo = {}
for r in rotas_lista:
origem, destino = r.split('-')
dist_trecho = calc_distancia(origem, destino)
for esq, base in bases_esquadroes.items():
dist_ida = calc_distancia(base, origem) if base != origem else 0
dist_volta = calc_distancia(destino, base) if base != destino else 0
distancias_voo[(esq, r)] = dist_ida + dist_trecho + dist_volta
# Demanda de voos por rota (Média diária)
voos_requeridos = {}
for _, row in top_trechos.iterrows():
# Arredondamento para cima da (demanda / capacidade)
voos = math.ceil(row['Media_PAX_Dias_Operados'] / CAPACIDADE_C97)
# Pelo menos 1 voo para suprir a demanda da rota selecionada
voos_requeridos[row['Trecho']] = max(1, voos)
# MODELAGEM COM PULP
prob = pulp.LpProblem("Fleet_Assignment_C97", pulp.LpMinimize)
# Variáveis
X = pulp.LpVariable.dicts("X", [(esq, r) for esq in aeronaves_por_esquadrao.keys() for r in rotas_lista], lowBound=0, cat='Integer')
# Função Objetivo
prob += pulp.lpSum([distancias_voo[(esq, r)] * X[(esq, r)] for esq in aeronaves_por_esquadrao.keys() for r in rotas_lista])
# Restrição 1: Suprir a demanda da rota
for r in rotas_lista:
prob += pulp.lpSum([X[(esq, r)] for esq in aeronaves_por_esquadrao.keys()]) >= voos_requeridos[r]
# Restrição 2: Limite de aeronaves por esquadrão
for esq in aeronaves_por_esquadrao.keys():
prob += pulp.lpSum([X[(esq, r)] for r in rotas_lista]) <= aeronaves_por_esquadrao[esq]
# Solução
prob.solve(pulp.PULP_CBC_CMD(msg=False))
# Exibição dos resultados
print("== RESULTADO DO FLEET ASSIGNMENT ==")
print(f"Status: {pulp.LpStatus[prob.status]}")
print(f"Distância Total Minimizada: {pulp.value(prob.objective):.2f} km\\n")
print("Alocações (Voos Diários):")
for r in rotas_lista:
print(f"\\nTrecho: {r} (Voos necessários: {voos_requeridos[r]})")
for esq in aeronaves_por_esquadrao.keys():
qtd = int(X[(esq, r)].varValue)
if qtd > 0:
print(f" -> {esq} (Base {bases_esquadroes[esq]}): {qtd} voo(s)")
"""))
nb.cells = cells
with open('modelos.ipynb', 'w', encoding='utf-8') as f:
nbf.write(nb, f)
print("modelos.ipynb gerado com sucesso!")

File diff suppressed because one or more lines are too long

View File

@@ -13,5 +13,7 @@ dependencies = [
"pulp>=3.3.2",
"pymap3d>=3.2.0",
"seaborn>=0.13.2",
"streamlit>=1.58.0",
"tabulate>=0.10.0",
"vincenty>=0.1.4",
]

File diff suppressed because it is too large Load Diff

351662
raw/airports.json Normal file

File diff suppressed because it is too large Load Diff

46
raw/emissores.json Normal file
View File

@@ -0,0 +1,46 @@
[
{
"1GAV8": "SBNT",
"2/2 GT": "SBGL",
"BABV": "SBBV",
"CINDACTA I": "SBBS",
"ETA7": "SBMN",
"1 GTT": "BAAN",
"3GAV8": "SBSC",
"1GAV3": "SBBV",
"1GAV12": "SBSM",
"PAMA LS": "SBLS",
"ETA3": "SBGL",
"ETA1": "SBBE",
"2GAV3": "SBPV",
"1 GAVCA": "SBSC",
"1GAV9": "SBMN",
"ETA5": "SBCO",
"1/1 GT": "SBGL",
"2GAV6": "SBAN",
"CINDACTA IV": "SBMN",
"1 GDA": "SBAN",
"EPCAR": "SBBQ",
"1GAV14": "SBCO",
"1GAV15": "SBCG",
"BASM": "SBSM",
"BACG": "SBCG",
"EEAR": "SBGW",
"3GAV7": "SBBE",
"PAMA SP": "SBMT",
"ETA2": "SBNT",
"1/2 GT": "SBGL",
"3GAV3": "SBCG",
"5GAV8": "SBSM",
"ETA6": "SBBR",
"1GAV7": "SBSC",
"CINDACTA II": "SBBI",
"BAPV": "SBPV",
"7GAV8": "SBMN",
"1GAV10": "SBSM",
"2GAV5": "SBNT",
"2GAV10": "SBCG",
"2GAV7": "SBCO",
"CINDACTA III": "SBRF"
}
]

View File

@@ -0,0 +1,26 @@
import os
# Project Roots
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
RAW_DIR = os.path.join(ROOT_DIR, 'raw')
OUTPUTS_DIR = os.path.join(ROOT_DIR, 'outputs')
SCHEDULES_DIR = os.path.join(OUTPUTS_DIR, 'schedules')
# File Paths
CSV_FILEPATH = os.path.join(RAW_DIR, "SISCO_AEREA_01-01-2025_A_31-12-2025_1911251714Z.csv")
JSON_AEROPORTOS = os.path.join(RAW_DIR, "airports.json")
JSON_EMISSORES = os.path.join(RAW_DIR, "emissores.json")
DB_PATH = os.path.join(SCHEDULES_DIR, "demands.db")
RESULTS_FILE = os.path.join(SCHEDULES_DIR, "resultado_otimizacao_alocacoes.csv")
COST_FILE = os.path.join(SCHEDULES_DIR, "optimized_cost.txt")
AIRCRAFT_CONFIG = {
'C-97': {'cap': 30, 'range': 1600, 'daily_fuel': 240},
'C-95M': {'cap': 12, 'range': 1900, 'daily_fuel': 180},
'C-105': {'cap': 73, 'range': 5000, 'daily_fuel': 180},
'KC-390':{'cap': 80, 'range': 6000, 'daily_fuel': 180},
'KC-30': {'cap': 238, 'range': 14500, 'daily_fuel': 600},
'C-99A': {'cap': 50, 'range': 2200, 'daily_fuel': 180},
'C-98': {'cap': 10, 'range': 2400, 'daily_fuel': 180},
'C-98A': {'cap': 14, 'range': 2400, 'daily_fuel': 180}
}

View File

@@ -0,0 +1,77 @@
import json
import pandas as pd
from vincenty import vincenty
import sys
import os
# Ensure src module is visible if run standalone
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from src.fleet_assignment.config import (
CSV_FILEPATH, JSON_AEROPORTOS, JSON_EMISSORES, AIRCRAFT_CONFIG
)
def load_data():
"""Reads raw CSV and JSON to build the baseline demand and fleet dataframes."""
df = pd.read_csv(CSV_FILEPATH, low_memory=False)
df_airports = pd.read_json(JSON_AEROPORTOS, orient='index')
# Basic Cleaning
df.replace('NIL', 0, inplace=True)
df = df[(df['Localidade Decolagem'] != 0) & (df['Localidade Pouso'] != 0)]
df = df[df['Modelo'].isin(AIRCRAFT_CONFIG.keys())]
df['PAX'] = pd.to_numeric(df['PAX']).fillna(0)
df['Dep TimeStamp'] = pd.to_datetime(df['Data de Decolagem'].astype(str) + ' ' + df['Hora de Decolagem'].astype(str), format='mixed', dayfirst=True, errors='coerce')
df['Data Apenas'] = df['Dep TimeStamp'].dt.date
df['Combustível'] = pd.to_numeric(df['Combustível'], errors='coerce').fillna(0)
# Merge Coordinates
df = df.merge(df_airports[['lat', 'lon']], left_on='Localidade Decolagem', right_index=True, how='left')
df.rename(columns={'lat': 'lat_dep', 'lon': 'lon_dep'}, inplace=True)
df = df.merge(df_airports[['lat', 'lon']], left_on='Localidade Pouso', right_index=True, how='left')
df.rename(columns={'lat': 'lat_arr', 'lon': 'lon_arr'}, inplace=True)
# Distance & Time Computations
def calc_dist(row):
if pd.isna(row['lat_dep']) or pd.isna(row['lat_arr']): return 0.0
return vincenty((row['lat_dep'], row['lon_dep']), (row['lat_arr'], row['lon_arr']))
df['Distancia'] = df.apply(calc_dist, axis=1)
df['Tempo de Voo'] = pd.to_timedelta(df['Tempo de Voo'].astype(str), errors='coerce')
df['Tempo de Voo (h)'] = df['Tempo de Voo'].dt.total_seconds() / 3600.0
# Build Fleet Specifications
df_fleet = df.groupby(['Modelo', 'Emissor']).agg({
'Combustível': 'sum',
'Distancia': 'sum',
'Tempo de Voo (h)': 'sum',
'Matrícula': 'nunique'
})
df_fleet.rename(columns={'Matrícula': 'Tamanho da Frota'}, inplace=True)
df_fleet['Consumo (km/l)'] = (df_fleet['Distancia'] / df_fleet['Combustível']).replace([float('inf'), -float('inf')], 0).fillna(0)
df_fleet['Velocidade Média (km/h)'] = (df_fleet['Distancia'] / df_fleet['Tempo de Voo (h)']).replace([float('inf'), -float('inf')], 0).fillna(0)
models = df_fleet.index.get_level_values('Modelo')
df_fleet['Capacidade'] = models.map(lambda m: AIRCRAFT_CONFIG[m]['cap'])
df_fleet['Alcance'] = models.map(lambda m: AIRCRAFT_CONFIG[m]['range'])
df_fleet['Diaria_Litros'] = models.map(lambda m: AIRCRAFT_CONFIG[m]['daily_fuel'])
# Build Demands
df_valid = df.dropna(subset=['Data Apenas']).copy()
df_valid = df_valid[(df_valid['Localidade Decolagem'] != '0') & (df_valid['Localidade Pouso'] != '0')]
demands_grouped = df_valid.groupby(['Data Apenas', 'Localidade Decolagem', 'Localidade Pouso'])['PAX'].sum().reset_index()
demands = demands_grouped[demands_grouped['PAX'] > 0].copy()
# Load External Lookups
with open(JSON_EMISSORES, 'r') as f:
emissores_data = json.load(f)[0]
with open(JSON_AEROPORTOS, 'r') as f:
airports_geo = json.load(f)
# Validation Lists
valid_icaos = df_airports.index.intersection(list(set(demands['Localidade Decolagem'].unique().tolist() + demands['Localidade Pouso'].unique().tolist() + list(emissores_data.values())))).tolist()
return demands, df_fleet.reset_index(), valid_icaos, airports_geo

View File

@@ -0,0 +1,192 @@
"""
Worker script to run the SCIP solver in a separate process.
This allows capturing the C++ stdout (EnableOutput) and streaming it back to Streamlit.
"""
import sys
import pandas as pd
import math
import datetime
from ortools.linear_solver import pywraplp
import json
from vincenty import vincenty
import os
import sqlite3
# Ensure src module is visible if run standalone
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from src.fleet_assignment.config import DB_PATH, RESULTS_FILE, COST_FILE
def run_solver(time_limit_sec=300):
print(f"--- Starting Global Solver Worker (Limit: {time_limit_sec}s) ---")
JSON_AEROPORTOS = "airports.json"
conn = sqlite3.connect(DB_PATH)
df_frota_db = pd.read_sql_query("SELECT * FROM frota", conn)
demands_db = pd.read_sql_query("SELECT data_apenas as 'Data Apenas', local_dec as 'Localidade Decolagem', local_pou as 'Localidade Pouso', pax as 'PAX' FROM demandas", conn)
conn.close()
df_aeroportos = pd.read_json(JSON_AEROPORTOS, orient='index')
# Format demands as required by downstream logic
pax_demanda_completa = demands_db.groupby(['Data Apenas', 'Localidade Decolagem', 'Localidade Pouso'])['PAX'].sum().reset_index()
demands = pax_demanda_completa[pax_demanda_completa['PAX'] > 0].copy()
with open('emissores.json', 'r') as f:
emissores_data = json.load(f)[0]
icaos_emissores = list(emissores_data.values())
icaos_dec = demands['Localidade Decolagem'].unique().tolist()
icaos_pou = demands['Localidade Pouso'].unique().tolist()
all_icaos = [icao for icao in list(set(icaos_dec + icaos_pou + icaos_emissores)) if str(icao).strip() not in ['0', '']]
coords = df_aeroportos.loc[df_aeroportos.index.intersection(all_icaos), ['lat', 'lon']]
valid_icaos = coords.index.tolist()
dist_matrix = pd.DataFrame(index=valid_icaos, columns=valid_icaos)
for i in valid_icaos:
for j in valid_icaos:
if i == j: dist_matrix.loc[i, j] = 0.0
else:
lat1, lon1 = coords.loc[i, 'lat'], coords.loc[i, 'lon']
lat2, lon2 = coords.loc[j, 'lat'], coords.loc[j, 'lon']
dist_matrix.loc[i, j] = round(vincenty((lat1, lon1), (lat2, lon2)), 2)
AERONAVES_INTERESSE = ['C-97', 'C-95M', 'C-105', 'KC-390', 'KC-30', 'C-99A', 'C-98', 'C-98A']
CAPACIDADES_AERONAVES_INTERESSE = [30, 12, 73, 80, 238, 50, 10, 14]
ALCANCE_AERONAVES_INTERESSE = [1600, 1900, 5000, 6000, 14500, 2200, 2400, 2400]
DIARIAS_AERONAVES_INTERESSE = [x * 60 for x in [4, 3, 3, 3, 10, 3, 3, 3]]
frotas = []
for idx, row in df_frota_db.iterrows():
modelo, emissor = row['Modelo'], row['Emissor']
consumo = row['Consumo (km/l)']
if consumo > 0 and modelo in AERONAVES_INTERESSE:
a_idx = AERONAVES_INTERESSE.index(modelo)
frotas.append({
'modelo': str(modelo),
'emissor': str(emissor),
'base': emissores_data.get(str(emissor), None),
'frota_max': int(row['Tamanho da Frota']),
'consumo_kml': float(consumo),
'capacidade': int(CAPACIDADES_AERONAVES_INTERESSE[a_idx]),
'alcance': float(ALCANCE_AERONAVES_INTERESSE[a_idx]),
'velocidade_media': float(row['Velocidade Média (km/h)']),
'diaria_litros': float(DIARIAS_AERONAVES_INTERESSE[a_idx])
})
frotas = [f for f in frotas if f['base'] is not None and f['base'] in valid_icaos]
df_dias = pd.to_datetime(demands['Data Apenas']).dt.date
dias_unicos_list = sorted(df_dias.dropna().unique())
if not dias_unicos_list:
print("NO DEMAND DATA.")
return
min_date = min(dias_unicos_list)
max_date = max(dias_unicos_list)
todas_as_datas = [min_date + datetime.timedelta(days=i) for i in range((max_date - min_date).days + 1)]
print("Pre-computing distances and overnight stays...")
unique_routes = demands[['Localidade Decolagem', 'Localidade Pouso']].drop_duplicates()
route_info = {}
for idx, row in unique_routes.iterrows():
l_dec = str(row['Localidade Decolagem'])
l_pou = str(row['Localidade Pouso'])
if l_dec not in valid_icaos or l_pou not in valid_icaos:
continue
for f in frotas:
m, e, base = f['modelo'], f['emissor'], f['base']
dist_total = dist_matrix.loc[base, l_dec] + dist_matrix.loc[l_dec, l_pou] + dist_matrix.loc[l_pou, base]
tempo_missao = dist_total / f['velocidade_media'] if f['velocidade_media'] > 0 else float('inf')
if tempo_missao <= 96.0:
num_pernoites = max(0, math.ceil(tempo_missao / 12.0) - 1)
fator1 = 1.25 if dist_matrix.loc[base, l_dec] > f['alcance'] else 1.0
fator2 = 1.25 if dist_matrix.loc[l_dec, l_pou] > f['alcance'] else 1.0
fator3 = 1.25 if dist_matrix.loc[l_pou, base] > f['alcance'] else 1.0
comb_missao = ((dist_matrix.loc[base, l_dec]*fator1 + dist_matrix.loc[l_dec, l_pou]*fator2 + dist_matrix.loc[l_pou, base]*fator3) / f['consumo_kml']) + (num_pernoites * f['diaria_litros'])
route_info[(m, e, l_dec, l_pou)] = {'num_pernoites': num_pernoites, 'combustivel_missao': comb_missao}
solver = pywraplp.Solver.CreateSolver('SCIP')
solver.SetTimeLimit(time_limit_sec * 1000)
solver.EnableOutput() # O MAIS IMPORTANTE: Isso escreve no stdout para o subprocess ler!
print("Building Demand Matrix...")
x, s = {}, {}
for t in dias_unicos_list:
demandas_dia = demands[df_dias == t]
for idx, d_row in demandas_dia.iterrows():
l_dec, l_pou = str(d_row['Localidade Decolagem']), str(d_row['Localidade Pouso'])
if l_dec not in valid_icaos or l_pou not in valid_icaos: continue
s[(l_dec, l_pou, t)] = solver.NumVar(0, solver.infinity(), f"s_{l_dec}_{l_pou}_{t}")
restricao_pax = solver.Constraint(float(d_row['PAX']), solver.infinity(), "")
restricao_pax.SetCoefficient(s[(l_dec, l_pou, t)], 1)
for f in frotas:
m, e = f['modelo'], f['emissor']
if (m, e, l_dec, l_pou) in route_info:
x[(m, e, l_dec, l_pou, t)] = solver.IntVar(0, solver.infinity(), f"x_{m}_{e}_{l_dec}_{l_pou}_{t}")
restricao_pax.SetCoefficient(x[(m, e, l_dec, l_pou, t)], f['capacidade'])
print("Building Fleet Temporal Matrix...")
for f in frotas:
m, e, frota_max = f['modelo'], f['emissor'], f['frota_max']
for t_atual in todas_as_datas:
restricao_frota = solver.Constraint(0, frota_max, "")
for t_start in dias_unicos_list:
diff_days = (t_atual - t_start).days
if 0 <= diff_days <= 7:
for idx, d_row in demands[df_dias == t_start].iterrows():
l_dec, l_pou = str(d_row['Localidade Decolagem']), str(d_row['Localidade Pouso'])
if (m, e, l_dec, l_pou, t_start) in x:
if diff_days <= route_info[(m, e, l_dec, l_pou)]['num_pernoites']:
restricao_frota.SetCoefficient(x[(m, e, l_dec, l_pou, t_start)], 1)
print("\n--- STARTING SCIP OPTIMIZATION ---\n")
sys.stdout.flush()
objetivo = solver.Objective()
objetivo.SetMinimization()
for var in s.values(): objetivo.SetCoefficient(var, 1e8)
for (m, e, l_dec, l_pou, t), var in x.items():
objetivo.SetCoefficient(var, route_info[(m, e, l_dec, l_pou)]['combustivel_missao'])
status = solver.Solve()
print(f"\n--- SCIP FINISHED ---")
if status in [pywraplp.Solver.OPTIMAL, pywraplp.Solver.FEASIBLE]:
obj_val = solver.Objective().Value()
try:
best_bound = solver.Objective().BestBound()
except:
best_bound = obj_val
gap = abs((obj_val - best_bound) / obj_val) * 100 if obj_val > 0 else 0
print(f"Final Status: {'OPTIMAL' if status == pywraplp.Solver.OPTIMAL else 'FEASIBLE (Time Limit)'}")
print(f"Optimized Cost: {obj_val:,.2f} L")
with open(COST_FILE, "w") as f:
f.write(str(obj_val))
print(f"Dual Bound: {best_bound:,.2f} L")
print(f"Gap: {gap:.4f}%")
results = []
for (m, e, l_dec, l_pou, t), var in x.items():
if var.solution_value() > 0:
results.append({
'Data': t, 'Modelo': m, 'Emissor': e,
'Local Decolagem': l_dec, 'Local Pouso': l_pou,
'Qtd Aeronaves': int(var.solution_value()),
'Pernoites': route_info[(m, e, l_dec, l_pou)]['num_pernoites']
})
df_results = pd.DataFrame(results)
df_results.to_csv(RESULTS_FILE, sep=';', index=False)
print(f"Results successfully saved to {RESULTS_FILE}")
else:
print("\n[ERROR] Solver failed to find a solution.")
if __name__ == '__main__':
time_limit = int(sys.argv[1]) if len(sys.argv) > 1 else 300
run_solver(time_limit)

353
uv.lock generated
View File

@@ -19,6 +19,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" },
]
[[package]]
name = "altair"
version = "6.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
{ name = "jsonschema" },
{ name = "narwhals" },
{ name = "packaging" },
{ name = "typing-extensions", marker = "python_full_version < '3.15'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/97/9a0dc61efd4f2dee29cb6d8edbbacdb789ce48cbffd98efa2b3ab145b297/altair-6.2.1.tar.gz", hash = "sha256:ca0298fa20b1a4fae22eff8847b95f74912bd90544013ad36af192119883ea64", size = 766468, upload-time = "2026-06-05T16:23:36.57Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/78/b556548d92b9e29ae68a86e7b416888820900809e189e39caf308c7d44a3/altair-6.2.1-py3-none-any.whl", hash = "sha256:bf2fee3733c3a31a588e45b857a2495a88d506970deb87f74e1613f0247446b1", size = 797498, upload-time = "2026-06-05T16:23:34.799Z" },
]
[[package]]
name = "anyio"
version = "4.13.0"
@@ -163,6 +179,24 @@ css = [
{ name = "tinycss2" },
]
[[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 = "7.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" },
]
[[package]]
name = "ceao-809"
version = "0.1.0"
@@ -176,6 +210,8 @@ dependencies = [
{ name = "pulp" },
{ name = "pymap3d" },
{ name = "seaborn" },
{ name = "streamlit" },
{ name = "tabulate" },
{ name = "vincenty" },
]
@@ -189,6 +225,8 @@ requires-dist = [
{ name = "pulp", specifier = ">=3.3.2" },
{ name = "pymap3d", specifier = ">=3.2.0" },
{ name = "seaborn", specifier = ">=0.13.2" },
{ name = "streamlit", specifier = ">=1.58.0" },
{ name = "tabulate", specifier = ">=0.10.0" },
{ name = "vincenty", specifier = ">=0.1.4" },
]
@@ -331,6 +369,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
]
[[package]]
name = "click"
version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
@@ -531,6 +581,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" },
]
[[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.50"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gitdb" },
]
sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@@ -553,6 +627,42 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httptools"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" },
{ url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" },
{ url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" },
{ url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" },
{ url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" },
{ url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" },
{ url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" },
{ url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" },
{ url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" },
{ url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" },
{ url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" },
{ url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
{ url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
{ url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
{ url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
{ url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
{ url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
{ url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
{ url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
{ url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
{ url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
{ url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
{ url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
{ url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
{ url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
@@ -656,6 +766,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
]
[[package]]
name = "jedi"
version = "0.20.0"
@@ -1125,6 +1244,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" },
]
[[package]]
name = "narwhals"
version = "2.22.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/62/3c/c4ef2164a71c1a63d7f1ae411c4082c5fa872405106db60a4b7114989ad7/narwhals-2.22.1.tar.gz", hash = "sha256:d62920805a0a43b7ff8b54b0c0d3142d796f8a9301836ada37e573d6a33cbcd9", size = 647493, upload-time = "2026-06-05T12:34:34.051Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/ca/36339329c4604adbcc99c899b7eb1ce1a555c499b6a6860757dc9bfed36d/narwhals-2.22.1-py3-none-any.whl", hash = "sha256:60567d774edf77db53906f89d9fbd164e66e56d66d388e1e6990f17ac33cfb53", size = 454815, upload-time = "2026-06-05T12:34:32.289Z" },
]
[[package]]
name = "nbclient"
version = "0.11.0"
@@ -1565,6 +1693,49 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
]
[[package]]
name = "pyarrow"
version = "24.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" },
{ url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" },
{ url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" },
{ url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" },
{ url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" },
{ url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" },
{ url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" },
{ url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" },
{ url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" },
{ url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" },
{ url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" },
{ url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" },
{ url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" },
{ url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" },
{ url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" },
{ url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" },
{ url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" },
{ url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" },
{ url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" },
{ url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" },
{ url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" },
{ url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" },
{ url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" },
{ url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" },
{ url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" },
{ url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" },
{ url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" },
{ url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" },
{ url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" },
{ url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" },
{ url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" },
{ url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
@@ -1574,6 +1745,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pydeck"
version = "0.9.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/df/4e9e7f20f8034a37c6571c93809f6d22388c39978c98d174d656c1a18fd2/pydeck-0.9.2.tar.gz", hash = "sha256:c10d9035e81ead6385264cac8d19402471f6866a15ca1f7df1400f52142bcf87", size = 5849672, upload-time = "2026-04-16T18:30:30.089Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/24/b30ee7d723100fd822de1bb4c0adea62f3419884a75a536f35f355d1e7c0/pydeck-0.9.2-py2.py3-none-any.whl", hash = "sha256:8213dfeacc5f6bfe6825f61c8ee34e3850e8a31fc43924379ec98edb34a75b25", size = 11305615, upload-time = "2026-04-16T18:30:28.133Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
@@ -1622,6 +1806,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.32"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
]
[[package]]
name = "pywinpty"
version = "3.0.3"
@@ -1942,6 +2135,15 @@ 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.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" },
]
[[package]]
name = "soupsieve"
version = "2.8.4"
@@ -1965,6 +2167,72 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
]
[[package]]
name = "starlette"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
]
[[package]]
name = "streamlit"
version = "1.58.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altair" },
{ name = "anyio" },
{ name = "blinker" },
{ name = "cachetools" },
{ name = "click" },
{ name = "gitpython" },
{ name = "httptools" },
{ name = "itsdangerous" },
{ name = "numpy" },
{ name = "packaging" },
{ name = "pandas" },
{ name = "pillow" },
{ name = "protobuf" },
{ name = "pyarrow" },
{ name = "pydeck" },
{ name = "python-multipart" },
{ name = "requests" },
{ name = "starlette" },
{ name = "tenacity" },
{ name = "toml" },
{ name = "typing-extensions" },
{ name = "uvicorn" },
{ name = "watchdog", marker = "sys_platform != 'darwin'" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/91/74/20dac6d6200d6ec0e1c230fb8eeb6a1a423645eacb76e8d802adfc246456/streamlit-1.58.0.tar.gz", hash = "sha256:78a22e7085b053af7ce544442bf4b670771e68c509ba1bdaa056ba0708f49c3d", size = 8721149, upload-time = "2026-05-28T18:02:44.606Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/84/14c36a92fb24f8e1cea452f53b0744b5da69d52cdd2fe22e71e6fbf765d5/streamlit-1.58.0-py3-none-any.whl", hash = "sha256:4ca8a7afc5bd16a5f176ccf4be1e34e8121cad0240becd127fb58a103ea3178d", size = 9219185, upload-time = "2026-05-28T18:02:41.993Z" },
]
[[package]]
name = "tabulate"
version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" },
]
[[package]]
name = "tenacity"
version = "9.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
]
[[package]]
name = "terminado"
version = "0.18.1"
@@ -1991,6 +2259,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
]
[[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.6"
@@ -2053,12 +2330,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]
[[package]]
name = "uvicorn"
version = "0.49.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" },
]
[[package]]
name = "vincenty"
version = "0.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/de/296372fde237fdda627fb6127a34689a3c1b25b6531a180060bf8e11457f/vincenty-0.1.4.tar.gz", hash = "sha256:eaa2f2de835f369cbd71c1a01ccd4e0d412da0f4aeef7c9692242b9ce182785a", size = 2757, upload-time = "2016-01-24T08:49:26.739Z" }
[[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" },
]
[[package]]
name = "wcwidth"
version = "0.8.0"
@@ -2094,3 +2402,48 @@ sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c
wheels = [
{ url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" },
]
[[package]]
name = "websockets"
version = "16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" },
{ url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" },
{ url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" },
{ url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" },
{ url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" },
{ url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" },
{ url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" },
{ url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" },
{ url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" },
{ url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
{ url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
{ url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
{ url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
{ url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
{ url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
{ url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
{ url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
{ url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
{ url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
{ url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
{ url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
{ url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
{ url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
{ url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
{ url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
{ url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
{ url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
{ url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
{ url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
{ url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
{ url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
{ url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
{ url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
{ url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
{ url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
{ url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
]