from __future__ import annotations import os from typing import Any from fastapi import APIRouter, File, Form, Request, UploadFile from fastapi.responses import FileResponse from pydantic import BaseModel, Field from app.services import auth_service, elaboracao_service from app.services.audit_log_service import log_event from app.services.session_store import session_store router = APIRouter(prefix="/api/elaboracao", tags=["elaboracao"]) class SessionPayload(BaseModel): session_id: str class ConfirmSheetPayload(SessionPayload): sheet_name: str class MapCoordsPayload(SessionPayload): col_lat: str col_lon: str class GeocodePayload(SessionPayload): col_cdlog: str col_num: str auto_200: bool = False class CorrecaoGeo(BaseModel): linha: int cdlog_corrigido: str | None = None numero_corrigido: str | None = None class GeocodeCorrecaoPayload(SessionPayload): correcoes: list[CorrecaoGeo] = Field(default_factory=list) auto_200: bool = False class ApplySelectionPayload(SessionPayload): coluna_y: str colunas_x: list[str] tipo_y: str | None = None coluna_area: str | None = None dicotomicas: list[str] = Field(default_factory=list) codigo_alocado: list[str] = Field(default_factory=list) percentuais: list[str] = Field(default_factory=list) outliers_anteriores: list[int] = Field(default_factory=list) grau_min_coef: int = 0 grau_min_f: int = 0 class ClassificarXPayload(SessionPayload): colunas_x: list[str] = Field(default_factory=list) class SearchTransformPayload(SessionPayload): grau_min_coef: int = 0 grau_min_f: int = 0 transformacoes_fixas: dict[str, str] = Field(default_factory=dict) transformacao_y_fixa: str | None = None class AdoptSuggestionPayload(SessionPayload): indice: int class FitModelPayload(SessionPayload): transformacao_y: str transformacoes_x: dict[str, str] = Field(default_factory=dict) dicotomicas: list[str] = Field(default_factory=list) codigo_alocado: list[str] = Field(default_factory=list) percentuais: list[str] = Field(default_factory=list) class DispersaoPayload(SessionPayload): eixo_x: str = "transformado" eixo_y_tipo: str = "y_transformado" eixo_y_residuo: str | None = None eixo_y_coluna: str | None = None class DispersaoInterativoPayload(SessionPayload): alvo: str = "secao10" class DiagnosticoInterativoPayload(SessionPayload): grafico: str = "obs_calc" class TransformPreviewPayload(SessionPayload): transformacao_y: str = "(x)" transformacoes_x: dict[str, str] = Field(default_factory=dict) class OutlierFiltro(BaseModel): variavel: str operador: str valor: float class OutlierFilterPayload(SessionPayload): filtros: list[OutlierFiltro] = Field(default_factory=list) class OutlierRestartPayload(SessionPayload): outliers_texto: str | None = None reincluir_texto: str | None = None grau_min_coef: int = 3 grau_min_f: int = 3 class OutlierSummaryPayload(SessionPayload): outliers_texto: str | None = None reincluir_texto: str | None = None class AvaliacaoPayload(SessionPayload): valores_x: dict[str, Any] indice_base: str | None = None class AvaliacaoKnnDetalhesPayload(SessionPayload): valores_x: dict[str, Any] class AvaliacaoDeletePayload(SessionPayload): indice: str | None = None indice_base: str | None = None class AvaliacaoBasePayload(SessionPayload): indice_base: str | None = None class ExportModeloPayload(SessionPayload): nome_arquivo: str elaborador: dict[str, Any] | None = None observacao_modelo: str | None = None class SalvarModeloRepositorioPayload(ExportModeloPayload): confirmar_substituicao: bool = False class UpdateMapaPayload(SessionPayload): variavel_mapa: str | None = None modo_mapa: str | None = None escala_extremo_abs: float | None = None class ColunaDataMercadoPayload(SessionPayload): coluna_data: str class RepositorioModeloPayload(SessionPayload): modelo_id: str @router.post("/upload") async def upload_file( session_id: str = Form(...), file: UploadFile = File(...), ) -> dict[str, Any]: session = session_store.get(session_id) conteudo = await file.read() elaboracao_service.save_uploaded_file(session, file.filename, conteudo) return elaboracao_service.load_uploaded_file(session) @router.get("/repositorio-modelos") def repositorio_modelos(request: Request) -> dict[str, Any]: user = auth_service.require_user(request) resposta = elaboracao_service.listar_modelos_repositorio() log_event( "repositorio", "listar_modelos_elaboracao", user=user, request=request, details={"total_modelos": resposta.get("total_modelos")}, ) return resposta @router.post("/repositorio-carregar") def repositorio_carregar(payload: RepositorioModeloPayload, request: Request) -> dict[str, Any]: session = session_store.get(payload.session_id) user = auth_service.require_user(request) resposta = elaboracao_service.carregar_modelo_repositorio(session, payload.modelo_id) log_event( "repositorio", "carregar_modelo_elaboracao", user=user, session_id=payload.session_id, request=request, details={"modelo_id": payload.modelo_id}, ) return resposta @router.post("/confirm-sheet") def confirm_sheet(payload: ConfirmSheetPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.load_uploaded_file(session, selected_sheet=payload.sheet_name) @router.post("/map-coords") def map_coords(payload: MapCoordsPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.mapear_coordenadas_manualmente(session, payload.col_lat, payload.col_lon) @router.post("/geocodificar") def geocodificar(payload: GeocodePayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.geocodificar(session, payload.col_cdlog, payload.col_num, auto_200=payload.auto_200) @router.post("/geocodificar-correcoes") def geocodificar_correcoes(payload: GeocodeCorrecaoPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) correcoes = [item.model_dump() for item in payload.correcoes] return elaboracao_service.aplicar_correcoes_geocodificacao(session, correcoes, auto_200=payload.auto_200) @router.post("/geocodificar-reiniciar") def geocodificar_reiniciar(payload: SessionPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.reiniciar_geocodificacao(session) @router.post("/geocodificar-excluir-coords") def geocodificar_excluir_coords(payload: SessionPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.excluir_coordenadas_para_geocodificacao(session) @router.post("/apply-selection") def apply_selection(payload: ApplySelectionPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.apply_selection( session, coluna_y=payload.coluna_y, colunas_x=payload.colunas_x, tipo_y=payload.tipo_y, coluna_area=payload.coluna_area, dicotomicas=payload.dicotomicas, codigo_alocado=payload.codigo_alocado, percentuais=payload.percentuais, outliers_anteriores=payload.outliers_anteriores, grau_min_coef=payload.grau_min_coef, grau_min_f=payload.grau_min_f, ) @router.post("/classify-x") def classify_x(payload: ClassificarXPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.classificar_tipos_variaveis_x(session, payload.colunas_x) @router.post("/search-transformations") def search_transformations(payload: SearchTransformPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.search_transformacoes( session, grau_min_coef=payload.grau_min_coef, grau_min_f=payload.grau_min_f, transformacoes_fixas_usuario=payload.transformacoes_fixas, transformacao_y_fixa_usuario=payload.transformacao_y_fixa, ) @router.post("/adopt-suggestion") def adopt_suggestion(payload: AdoptSuggestionPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.adotar_sugestao(session, payload.indice) @router.post("/fit-model") def fit_model(payload: FitModelPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.fit_model( session, transformacao_y=payload.transformacao_y, transformacoes_x=payload.transformacoes_x, dicotomicas=payload.dicotomicas, codigo_alocado=payload.codigo_alocado, percentuais=payload.percentuais, ) @router.post("/model-dispersao") def model_dispersao(payload: DispersaoPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.gerar_grafico_dispersao_modelo( session, eixo_x=payload.eixo_x, eixo_y_tipo=payload.eixo_y_tipo, eixo_y_residuo=payload.eixo_y_residuo, eixo_y_coluna=payload.eixo_y_coluna, ) @router.post("/dispersao-interativo") def dispersao_interativo(payload: DispersaoInterativoPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.obter_grafico_dispersao_interativo(session, payload.alvo) @router.post("/diagnostico-interativo") def diagnostico_interativo(payload: DiagnosticoInterativoPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.obter_grafico_diagnostico_interativo(session, payload.grafico) @router.post("/transform-preview") def transform_preview(payload: TransformPreviewPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.preview_transformacao_manual( session, transformacao_y=payload.transformacao_y, transformacoes_x=payload.transformacoes_x, ) @router.post("/outliers/apply-filters") def outliers_apply_filters(payload: OutlierFilterPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) filtros = [item.model_dump() for item in payload.filtros] return elaboracao_service.apply_outlier_filters(session, filtros) @router.post("/outliers/apply-filters-recursive") def outliers_apply_filters_recursive(payload: OutlierFilterPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) filtros = [item.model_dump() for item in payload.filtros] return elaboracao_service.apply_outlier_filters_recursive(session, filtros) @router.post("/outliers/restart") def outliers_restart(payload: OutlierRestartPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.reiniciar_iteracao( session, outliers_texto=payload.outliers_texto, reincluir_texto=payload.reincluir_texto, grau_min_coef=payload.grau_min_coef, grau_min_f=payload.grau_min_f, ) @router.post("/outliers/summary") def outliers_summary(payload: OutlierSummaryPayload) -> dict[str, str]: session = session_store.get(payload.session_id) texto = elaboracao_service.resumir_outliers( session.outliers_anteriores, payload.outliers_texto, payload.reincluir_texto, ) return {"resumo": texto} @router.post("/outliers/clear-history") def outliers_clear_history(payload: SessionPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.limpar_historico_outliers(session) @router.post("/evaluation/fields") def evaluation_fields(payload: SessionPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return {"campos": elaboracao_service.build_campos_avaliacao(session)} @router.post("/evaluation/calculate") def evaluation_calculate(payload: AvaliacaoPayload, request: Request) -> dict[str, Any]: session = session_store.get(payload.session_id) user = auth_service.require_user(request) resposta = elaboracao_service.calcular_avaliacao_elaboracao(session, payload.valores_x, payload.indice_base) log_event( "elaboracao", "avaliacao_calcular", user=user, session_id=payload.session_id, request=request, details={"total_avaliacoes": len(resposta.get("avaliacoes") or [])}, ) return resposta @router.post("/evaluation/knn-details") def evaluation_knn_details(payload: AvaliacaoKnnDetalhesPayload, request: Request) -> dict[str, Any]: session = session_store.get(payload.session_id) user = auth_service.require_user(request) resposta = elaboracao_service.detalhes_knn_avaliacao_elaboracao(session, payload.valores_x) log_event( "elaboracao", "avaliacao_knn_detalhes", user=user, session_id=payload.session_id, request=request, ) return resposta @router.post("/evaluation/clear") def evaluation_clear(payload: SessionPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.limpar_avaliacoes_elaboracao(session) @router.post("/evaluation/delete") def evaluation_delete(payload: AvaliacaoDeletePayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.excluir_avaliacao_elaboracao(session, payload.indice, payload.indice_base) @router.post("/evaluation/base") def evaluation_base(payload: AvaliacaoBasePayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.atualizar_base_avaliacao_elaboracao(session, payload.indice_base) @router.get("/evaluation/export") def evaluation_export(session_id: str) -> FileResponse: session = session_store.get(session_id) caminho = elaboracao_service.exportar_avaliacoes_elaboracao(session) return FileResponse( path=caminho, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", filename=os.path.basename(caminho), ) @router.get("/equation/export") def equation_export(session_id: str, mode: str = "excel") -> FileResponse: session = session_store.get(session_id) caminho, nome = elaboracao_service.exportar_equacao(session, mode) return FileResponse( path=caminho, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", filename=nome, ) @router.get("/avaliadores") def listar_avaliadores() -> dict[str, Any]: return {"avaliadores": elaboracao_service.list_avaliadores()} @router.post("/export-model") def export_model(payload: ExportModeloPayload, request: Request) -> FileResponse: session = session_store.get(payload.session_id) caminho, _ = elaboracao_service.exportar_modelo( session, payload.nome_arquivo, elaborador=payload.elaborador, observacao_modelo=payload.observacao_modelo, ) user = auth_service.require_user(request) log_event( "elaboracao", "exportar_modelo", user=user, session_id=payload.session_id, request=request, details={"nome_arquivo": os.path.basename(caminho)}, ) return FileResponse( path=caminho, media_type="application/octet-stream", filename=os.path.basename(caminho), ) @router.post("/save-model-repository") def save_model_repository(payload: SalvarModeloRepositorioPayload, request: Request) -> dict[str, Any]: session = session_store.get(payload.session_id) user = auth_service.require_admin(request) resposta = elaboracao_service.salvar_modelo_repositorio( session, payload.nome_arquivo, elaborador=payload.elaborador, observacao_modelo=payload.observacao_modelo, actor=user.get("usuario"), confirmar_substituicao=payload.confirmar_substituicao, ) log_event( "repositorio", "salvar_modelo_repositorio_elaboracao", user=user, session_id=payload.session_id, request=request, details={ "nome_arquivo": payload.nome_arquivo, "confirmar_substituicao": bool(payload.confirmar_substituicao), "substituidos": resposta.get("resultado_upload", {}).get("substituidos", []), }, ) return resposta @router.get("/export-base") def export_base(session_id: str, filtered: bool = True) -> FileResponse: session = session_store.get(session_id) caminho = elaboracao_service.exportar_base(session, usar_filtrado=filtered) return FileResponse( path=caminho, media_type="text/csv", filename=os.path.basename(caminho), ) @router.post("/map/update") def map_update(payload: UpdateMapaPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.atualizar_mapa(session, payload.variavel_mapa, payload.modo_mapa) @router.post("/residuos/map/update") def residuos_map_update(payload: UpdateMapaPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.atualizar_mapa_residuos( session, payload.variavel_mapa, payload.modo_mapa, payload.escala_extremo_abs, ) @router.post("/market-date/preview") def market_date_preview(payload: ColunaDataMercadoPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.previsualizar_coluna_data_mercado(session, payload.coluna_data) @router.post("/market-date/apply") def market_date_apply(payload: ColunaDataMercadoPayload) -> dict[str, Any]: session = session_store.get(payload.session_id) return elaboracao_service.aplicar_coluna_data_mercado(session, payload.coluna_data) @router.get("/context") def context(session_id: str) -> dict[str, Any]: session = session_store.get(session_id) return { "coluna_y": session.coluna_y, "tipo_y": session.tipo_y, "coluna_area": session.coluna_area, "colunas_area_candidatas": elaboracao_service.detectar_colunas_area(session.df_original) if session.df_original is not None else [], "colunas_x": session.colunas_x, "dicotomicas": session.dicotomicas, "codigo_alocado": session.codigo_alocado, "percentuais": session.percentuais, "outliers_anteriores": session.outliers_anteriores, "iteracao": session.iteracao, "coluna_data_mercado": session.coluna_data_mercado, "periodo_dados_mercado": { "coluna_data": session.coluna_data_mercado, "data_inicial": session.periodo_dados_mercado_inicio, "data_final": session.periodo_dados_mercado_fim, }, }