import uuid from pydantic import BaseModel, Field, model_validator from typing import Dict, List, Optional from datetime import datetime as dt from pif_compiler.classes.models import ( StatoOrdine, Ingredient, Esposition, ) from pif_compiler.functions.db_utils import ( db_connect, upsert_cliente, upsert_compilatore, ) from pif_compiler.functions.common_log import get_logger logger = get_logger() class IngredientInput(BaseModel): cas_raw: str cas_list: List[str] = Field(default_factory=list) percentage: float @model_validator(mode='after') def clean_cas(self): cleaned = self.cas_raw.replace('\n', '') self.cas_list = [c.strip() for c in cleaned.split(';') if c.strip()] return self class Order(BaseModel): """Layer grezzo: riceve l'input, lo valida e lo salva su MongoDB.""" uuid_ordine: str = Field(default_factory=lambda: str(uuid.uuid4())) client_name: str compiler_name: str product_type: str date: str notes: str = "" stato: StatoOrdine = StatoOrdine.RICEVUTO ingredients_input: List[IngredientInput] total_percentage: float = 0 num_ingredients: int = 0 created_at: Optional[str] = None @model_validator(mode='after') def set_defaults(self): if self.created_at is None: self.created_at = dt.now().isoformat() if self.total_percentage == 0: self.total_percentage = sum(i.percentage for i in self.ingredients_input) if self.num_ingredients == 0: self.num_ingredients = len(self.ingredients_input) return self @classmethod def from_input(cls, data: dict): """Costruisce un Order a partire dal JSON grezzo di input.""" ingredients_input = [] for ing in data.get('ingredients', []): ingredients_input.append(IngredientInput( cas_raw=ing.get('CAS Number', ''), percentage=ing.get('Percentage (%)', 0) )) return cls( client_name=data.get('client_name', ''), compiler_name=data.get('compiler_name', ''), product_type=data.get('product_type', ''), date=data.get('date', ''), notes=data.get('notes', ''), ingredients_input=ingredients_input, ) def save(self): """Salva l'ordine su MongoDB (collection 'orders'). Ritorna il mongo_id.""" collection = db_connect(collection_name='orders') mongo_dict = self.model_dump() result = collection.replace_one( {"uuid_ordine": self.uuid_ordine}, mongo_dict, upsert=True ) if result.upserted_id: mongo_id = str(result.upserted_id) else: doc = collection.find_one({"uuid_ordine": self.uuid_ordine}, {"_id": 1}) mongo_id = str(doc["_id"]) logger.info(f"Ordine {self.uuid_ordine} salvato su MongoDB: {mongo_id}") return mongo_id def register(self): """Registra cliente e compilatore su PostgreSQL. Ritorna (id_cliente, id_compilatore).""" id_cliente = upsert_cliente(self.client_name) id_compilatore = upsert_compilatore(self.compiler_name) logger.info(f"Registrato cliente={id_cliente}, compilatore={id_compilatore}") return id_cliente, id_compilatore class Project(BaseModel): """Layer elaborato: contiene gli ingredienti arricchiti, l'esposizione e le statistiche.""" uuid_progetto: str = Field(default_factory=lambda: str(uuid.uuid4())) uuid_ordine: str stato: StatoOrdine = StatoOrdine.VALIDATO ingredients: List[Ingredient] = Field(default_factory=list) percentages: Dict[str, float] = Field(default_factory=dict) esposition: Optional[Esposition] = None created_at: Optional[str] = None @model_validator(mode='after') def set_created_at(self): if self.created_at is None: self.created_at = dt.now().isoformat() return self @classmethod def from_order(cls, order: Order): """Crea un progetto a partire da un ordine, estraendo la lista CAS e le percentuali.""" percentages = {} for ing_input in order.ingredients_input: for cas in ing_input.cas_list: percentages[cas] = ing_input.percentage return cls( uuid_ordine=order.uuid_ordine, percentages=percentages, ) def process_ingredients(self): """Arricchisce tutti gli ingredienti tramite Ingredient.get_or_create.""" self.stato = StatoOrdine.ARRICCHIMENTO self.ingredients = [] errori = 0 for cas in self.percentages: try: ingredient = Ingredient.get_or_create(cas) self.ingredients.append(ingredient) logger.info(f"Ingrediente {cas} processato") except Exception as e: logger.error(f"Errore processando CAS {cas}: {e}") errori += 1 self.stato = StatoOrdine.ARRICCHIMENTO_PARZIALE if errori > 0 else StatoOrdine.ARRICCHITO self.save() return self.ingredients def set_esposition(self, preset_name: str): """Carica un preset di esposizione da PostgreSQL per nome.""" presets = Esposition.get_presets() for p in presets: if p.preset_name == preset_name: self.esposition = p return p logger.warning(f"Preset '{preset_name}' non trovato") return None def save(self): """Salva il progetto su MongoDB (collection 'projects'). Ritorna il mongo_id.""" collection = db_connect(collection_name='projects') mongo_dict = self.model_dump() result = collection.replace_one( {"uuid_progetto": self.uuid_progetto}, mongo_dict, upsert=True ) if result.upserted_id: mongo_id = str(result.upserted_id) else: doc = collection.find_one({"uuid_progetto": self.uuid_progetto}, {"_id": 1}) mongo_id = str(doc["_id"]) logger.info(f"Progetto {self.uuid_progetto} salvato su MongoDB: {mongo_id}") return mongo_id def get_stats(self): """Ritorna statistiche sul progetto e sullo stato di arricchimento.""" stats = { "uuid_progetto": self.uuid_progetto, "uuid_ordine": self.uuid_ordine, "stato": self.stato.name, "has_esposition": self.esposition is not None, "num_ingredients": len(self.ingredients), "num_cas_input": len(self.percentages), } if self.ingredients: stats["enrichment"] = { "with_dap": sum(1 for i in self.ingredients if i.dap_info is not None), "with_cosing": sum(1 for i in self.ingredients if i.cosing_info is not None), "with_tox": sum(1 for i in self.ingredients if i.toxicity is not None), "with_noael": sum( 1 for i in self.ingredients if i.toxicity and any(ind.indicator == 'NOAEL' for ind in i.toxicity.indicators) ), } return stats