""" sessao.py — a fonte única da SD entre o upload e o download. O problema que esta camada resolve: a SD é carregada uma vez, vive em memória, é editada e reavaliada — e o exportador recebe ESSE objeto, não um caminho de arquivo. Sem isto, a correção feita no formulário nunca chegaria ao JSON. O que a sessão sabe: os dados (frontmatter, como CommentedMap do ruamel para o writeback preservar comentários), a prosa, e o resultado da última análise do exportador — a prévia do payload e a lista de pendências. Pendência vazia é a única condição para exportar, e ela é reavaliada no servidor a cada POST: o botão desabilitado é conforto de tela. Estado em memória de processo, uma SD por vez: é ferramenta local de uso individual. Reiniciar o servidor descarta a sessão — o SD.md de origem continua intacto no disco, e nada é gravado sem ação explícita. """ from __future__ import annotations import sys from dataclasses import dataclass, field from datetime import datetime from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) from exporta_sd import Pendencia, analisar # noqa: E402 from regras_sd import Canonico, linha_da_tabela # noqa: E402 def adotar_linhas_da_tabela(dados, canon: Canonico) -> dict[int, str]: """Escreve NO OBJETO DA SESSÃO a linha padrão da tabela onde o SD.md não declara `linha_os`. Devolve {índice do entregável: linha} do que adotou. Por que mutar aqui, e não deixar só o exportador consultar a tabela: o Salvar SD.md regrava este objeto. Com a linha adotada nele, a SD sai do disco autodeclarada e a tabela deixa de importar para ela — o risco de tabela envelhecida morde uma vez só. O comentário de linha diz de onde veio, para quem abrir o .md depois. O exportador faz a mesma consulta (mesmo helper, mesma tabela), então a CLI sobre o .md original e a app dão os mesmos bytes. Idempotente: só toca em quem não declara. """ tabela = canon.linhas_os if not tabela: return {} adotadas: dict[int, str] = {} for i, e in enumerate(dados.get("entregaveis") or []): if not isinstance(e, dict) or e.get("linha_os") not in (None, ""): continue padrao, _ = linha_da_tabela(tabela, e.get("item")) if not padrao: continue e["linha_os"] = padrao if hasattr(e, "yaml_add_eol_comment"): # CommentedMap do ruamel data = tabela.get("atualizado_em") or "sem data" e.yaml_add_eol_comment(f"da tabela linhas-os.yaml ({data}) — o SD.md não declarava", "linha_os") adotadas[i] = padrao return adotadas # --------------------------------------------------------------------------- # Estados — os que a tela precisa distinguir, e nenhum a mais # --------------------------------------------------------------------------- SEM_ARQUIVO = "sem_arquivo" COM_PENDENCIAS = "com_pendencias" PRONTA = "pronta" EXPORTADA = "exportada" ROTULO_ESTADO = { SEM_ARQUIVO: "Arquivo não enviado", COM_PENDENCIAS: "Com pendências", PRONTA: "Pronta para exportar", EXPORTADA: "JSON exportado", } @dataclass class Sessao: nome_arquivo: str dados: dict # CommentedMap do ruamel — FONTE ÚNICA, mutada pelo formulário corpo: str estado: str = COM_PENDENCIAS pendencias: list[Pendencia] = field(default_factory=list) previa: dict | None = None caminho_origem: Path | None = None ultimo_erro: str | None = None exportada_em: str | None = None # {índice do entregável: linha} adotado de linhas-os.yaml porque o SD.md # não declarava — a tela marca a origem, e o Salvar SD.md grava. linhas_da_tabela: dict = field(default_factory=dict) tabela_atualizada_em: str | None = None def reavaliar(self, canon: Canonico) -> None: """Roda o exportador em modo de análise sobre os dados COMO ESTÃO. É grátis e instantâneo; roda a cada edição. Uma edição depois da exportação volta a sessão para pronta/pendente: o que foi exportado era outra versão dos dados, e a tela não pode dizer "exportada" sobre esta. Antes de analisar, adota da tabela a linha de OS que o .md não declara (ver adotar_linhas_da_tabela) — e só então o exportador vê os dados. """ adotadas = adotar_linhas_da_tabela(self.dados, canon) if adotadas: self.linhas_da_tabela.update(adotadas) self.tabela_atualizada_em = (canon.linhas_os or {}).get("atualizado_em") self.previa, self.pendencias = analisar(self.dados, self.corpo, canon) self.estado = PRONTA if not self.pendencias else COM_PENDENCIAS self.exportada_em = None @property def pode_exportar(self) -> bool: return not self.pendencias def marcar_exportada(self) -> None: self.estado = EXPORTADA self.exportada_em = datetime.now().isoformat(timespec="seconds") def rotulo_estado(self) -> str: return ROTULO_ESTADO.get(self.estado, self.estado)