Ferramentas/AjustaFundos/actions/file/json_file_merge.py

180 lines
6.3 KiB
Python

import json
import traceback
from pathlib import Path
from types import SimpleNamespace
from typing import Any, Optional, Literal, Union
from pydantic import BaseModel
from actions.file.json_file_saver import JsonFileSaver
class JsonFileMerger:
"""
Classe utilitária para unir um novo JSON com um existente em disco,
aplicando merge profundo e salvando o resultado final.
Mantém o mesmo padrão seguro de conversão da classe JsonFileSaver.
"""
def __init__(self, saver: Optional[JsonFileSaver] = None):
self.saver = saver or JsonFileSaver()
# ------------------------------------------------------------
# Carrega JSON existente (se houver)
# ------------------------------------------------------------
def _load_existing(self, file_path: Path) -> Any:
if not file_path.exists():
return {}
try:
with open(file_path, "r", encoding=self.saver.encoding) as f:
return json.load(f)
except Exception:
# Em caso de erro, considera como vazio para não quebrar o fluxo
return {}
# ------------------------------------------------------------
# Merge de listas (não duplica itens)
# ------------------------------------------------------------
def _merge_lists(self, base_list: list, new_list: list) -> list:
combined = base_list[:]
for item in new_list:
if item not in combined:
combined.append(item)
return combined
# ------------------------------------------------------------
# Merge profundo (deep merge) para dicionários
# ------------------------------------------------------------
def _deep_merge(self, base: dict, new: dict) -> dict:
for key, value in new.items():
if key in base:
# dict + dict → merge recursivo
if isinstance(base[key], dict) and isinstance(value, dict):
base[key] = self._deep_merge(base[key], value)
# list + list → merge de listas
elif isinstance(base[key], list) and isinstance(value, list):
base[key] = self._merge_lists(base[key], value)
# tipos diferentes ou simples → substitui
else:
base[key] = value
else:
base[key] = value
return base
# ------------------------------------------------------------
# Estratégias de merge
# ------------------------------------------------------------
def _apply_strategy(self, existing: Any, new: Any, strategy: str) -> Any:
# Se ambos forem listas, usa merge de listas
if isinstance(existing, list) and isinstance(new, list):
return self._merge_lists(existing, new)
# Se tipos forem diferentes, substitui completamente
if type(existing) is not type(new):
return new
# replace → ignora o que existia
if strategy == "replace":
return new
# update → comportamento similar a dict.update (apenas para dicts)
if strategy == "update":
if isinstance(existing, dict) and isinstance(new, dict):
existing.update(new)
return existing
# para outros tipos, apenas substitui
return new
# default / "deep" → deep merge para dicts
if strategy == "deep":
if isinstance(existing, dict) and isinstance(new, dict):
return self._deep_merge(existing, new)
# se não forem dicts, substitui
return new
# fallback: se for estratégia desconhecida, substitui
return new
# ------------------------------------------------------------
# NOVO MÉTODO — merge sem salvar (apenas processa)
# ------------------------------------------------------------
def merge_data(
self,
existing_json: Any,
new_json: Union[dict, BaseModel, SimpleNamespace],
strategy: Literal["replace", "update", "deep"] = "deep",
) -> Any:
new_clean = self.saver._convert(new_json)
return self._apply_strategy(existing_json, new_clean, strategy)
# ------------------------------------------------------------
# MÉTODO PRINCIPAL — AGORA USANDO merge_data()
# ------------------------------------------------------------
def merge_and_save(
self,
new_json: Union[dict, BaseModel, SimpleNamespace],
file_path: str,
strategy: Literal["replace", "update", "deep"] = "deep",
add_timestamp: bool = False,
) -> dict:
"""
Faz merge entre existing.json ← new_json e salva o resultado final.
Parâmetros:
new_json:
- Dado novo que será mesclado ao JSON existente.
Pode ser dict, BaseModel ou SimpleNamespace.
file_path:
- Caminho do arquivo JSON em disco.
strategy:
- "replace" → sobrescreve tudo
- "update" → comportamento semelhante a dict.update
- "deep" → merge recursivo (deep merge) para dicts
e merge de listas sem duplicação
add_timestamp:
- Se True, adiciona timestamp no nome do arquivo ao salvar.
Retorno:
dict com:
{
"success": bool,
"path": str,
"size": int,
"error": Optional[str],
}
"""
try:
path = Path(file_path)
path.parent.mkdir(parents=True, exist_ok=True)
# Carrega JSON existente
existing = self._load_existing(path)
# Novo merge utilizando merge_data()
merged = self.merge_data(existing, new_json, strategy)
# Salva em disco
return self.saver.save(
data=merged,
file_path=str(path),
as_json=True,
add_timestamp=add_timestamp,
mode="overwrite",
)
except Exception as e:
traceback.print_exc()
return {
"success": False,
"path": file_path,
"size": 0,
"error": str(e),
}