cosmoguard-bd/src/pif_compiler/classes/main_cls.py
2026-02-08 19:29:22 +01:00

209 lines
7.1 KiB
Python

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