add functions esposition and ingredients
This commit is contained in:
parent
4aa49e171d
commit
a7d68971dd
3 changed files with 331 additions and 1 deletions
15
app.py
15
app.py
|
|
@ -108,6 +108,16 @@ def home():
|
||||||
with st.expander("📝 Registro degli aggiornamenti"):
|
with st.expander("📝 Registro degli aggiornamenti"):
|
||||||
# Placeholder for future versions
|
# Placeholder for future versions
|
||||||
st.markdown("""
|
st.markdown("""
|
||||||
|
### v0.3
|
||||||
|
*v0.3.0 - 2026-02-08
|
||||||
|
- Aggiunta pagina per la gestione dei preset dei parametri di esposizione
|
||||||
|
* Permette di creare, modificare e salvare preset personalizzati per i calcoli di esposizione
|
||||||
|
* In futuro per calcolare il MoS bisognerà prima selezionare un preset di esposizione
|
||||||
|
- Aggiunta pagina per la creazione degli ingredienti a partire da un CAS:
|
||||||
|
* Facendo una ricerca in 'Ingredienti' e selezionando un CAS, è possibile cliccare su 'Ricerca' per generare un nuovo ingrediente nel database a partire da quel CAS.
|
||||||
|
* Gli ingredienti creati in questo modo hanno già i dati di base, tossicologici e quelli normativi da CosIng
|
||||||
|
* E' consigliato cercare nuovi ingredienti tramite questa nuova funzione
|
||||||
|
|
||||||
### v0.2
|
### v0.2
|
||||||
*v0.2.1 - 2026-01-13*
|
*v0.2.1 - 2026-01-13*
|
||||||
- Fix minore su ricerca CosIng
|
- Fix minore su ricerca CosIng
|
||||||
|
|
@ -139,13 +149,16 @@ home_page = st.Page(home, title="Home", icon="🏠", default=True)
|
||||||
echa_page = st.Page("pages/echa.py", title="ECHA Database", icon="🧪")
|
echa_page = st.Page("pages/echa.py", title="ECHA Database", icon="🧪")
|
||||||
cosing_page = st.Page("pages/cosing.py", title="CosIng", icon="💄")
|
cosing_page = st.Page("pages/cosing.py", title="CosIng", icon="💄")
|
||||||
dap_page = st.Page("pages/dap.py", title="DAP", icon="🧬")
|
dap_page = st.Page("pages/dap.py", title="DAP", icon="🧬")
|
||||||
|
exposition_page = st.Page("pages/exposition_page.py", title="Esposizione", icon="☀️")
|
||||||
|
ingredients_page = st.Page("pages/ingredients_page.py", title="Ingredienti", icon="📋")
|
||||||
#pubchem_page = st.Page("pages/pubchem.py", title="PubChem", icon="🧬")
|
#pubchem_page = st.Page("pages/pubchem.py", title="PubChem", icon="🧬")
|
||||||
#cir_page = st.Page("pages/cir.py", title="CIR", icon="📊")
|
#cir_page = st.Page("pages/cir.py", title="CIR", icon="📊")
|
||||||
|
|
||||||
|
|
||||||
pg = st.navigation({
|
pg = st.navigation({
|
||||||
"Ricerca": [home_page],
|
"Ricerca": [home_page],
|
||||||
"Database": [echa_page, cosing_page, dap_page]
|
"Calcolatore MoS": [exposition_page, ingredients_page],
|
||||||
|
"Ricerche Online": [echa_page, cosing_page, dap_page]
|
||||||
})
|
})
|
||||||
|
|
||||||
pg.run()
|
pg.run()
|
||||||
121
pages/exposition_page.py
Normal file
121
pages/exposition_page.py
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
import streamlit as st
|
||||||
|
import requests
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
API_BASE = "https://api.cosmoguard.it/api/v1/"
|
||||||
|
|
||||||
|
EXPOSURE_ROUTES = ["Dermal", "Oral", "Inhalation", "Ocular"]
|
||||||
|
|
||||||
|
RITENZIONE_PRESETS = {
|
||||||
|
"Leave-on (1.0)": 1.0,
|
||||||
|
"Rinse-off (0.01)": 0.01,
|
||||||
|
"Dentifricio (0.05)": 0.05,
|
||||||
|
"Collutorio (0.10)": 0.10,
|
||||||
|
"Tintura (0.10)": 0.10,
|
||||||
|
}
|
||||||
|
|
||||||
|
st.set_page_config(page_title="Gestione Esposizione", layout="wide")
|
||||||
|
st.title("Gestione Preset Esposizione")
|
||||||
|
|
||||||
|
tab_crea, tab_lista = st.tabs(["Crea Preset", "Preset Esistenti"])
|
||||||
|
|
||||||
|
# --- TAB CREAZIONE ---
|
||||||
|
with tab_crea:
|
||||||
|
st.subheader("Nuovo Preset di Esposizione")
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
preset_name = st.text_input("Nome Preset", placeholder="es. Crema viso leave-on")
|
||||||
|
tipo_prodotto = st.text_input("Tipo Prodotto", placeholder="es. Crema")
|
||||||
|
luogo_applicazione = st.text_input("Luogo Applicazione", placeholder="es. Viso")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
sup_esposta = st.number_input("Superficie esposta (cm2)", min_value=1, max_value=17500, value=565)
|
||||||
|
freq_applicazione = st.number_input("Frequenza applicazione (al giorno)", min_value=1, value=1)
|
||||||
|
qta_giornaliera = st.number_input("Quantita giornaliera (g/die)", min_value=0.01, value=1.54, step=0.01, format="%.2f")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
col3, col4, col5 = st.columns(3)
|
||||||
|
|
||||||
|
with col3:
|
||||||
|
st.markdown("**Vie esposizione normali**")
|
||||||
|
esp_normali = st.multiselect("Normali", EXPOSURE_ROUTES, default=["Dermal"], key="esp_norm")
|
||||||
|
|
||||||
|
with col4:
|
||||||
|
st.markdown("**Vie esposizione secondarie**")
|
||||||
|
esp_secondarie = st.multiselect("Secondarie", EXPOSURE_ROUTES, default=[], key="esp_sec")
|
||||||
|
|
||||||
|
with col5:
|
||||||
|
st.markdown("**Vie esposizione nano**")
|
||||||
|
esp_nano = st.multiselect("Nano", EXPOSURE_ROUTES, default=["Dermal"], key="esp_nano")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
ritenzione_label = st.selectbox("Fattore di ritenzione", list(RITENZIONE_PRESETS.keys()))
|
||||||
|
ritenzione = RITENZIONE_PRESETS[ritenzione_label]
|
||||||
|
|
||||||
|
# Preview payload
|
||||||
|
payload = {
|
||||||
|
"preset_name": preset_name,
|
||||||
|
"tipo_prodotto": tipo_prodotto,
|
||||||
|
"luogo_applicazione": luogo_applicazione,
|
||||||
|
"esp_normali": esp_normali,
|
||||||
|
"esp_secondarie": esp_secondarie,
|
||||||
|
"esp_nano": esp_nano,
|
||||||
|
"sup_esposta": sup_esposta,
|
||||||
|
"freq_applicazione": freq_applicazione,
|
||||||
|
"qta_giornaliera": qta_giornaliera,
|
||||||
|
"ritenzione": ritenzione,
|
||||||
|
}
|
||||||
|
|
||||||
|
with st.expander("Anteprima JSON"):
|
||||||
|
st.json(payload)
|
||||||
|
|
||||||
|
if st.button("Crea Preset", type="primary", disabled=not preset_name):
|
||||||
|
try:
|
||||||
|
resp = requests.post(f"{API_BASE}/esposition/create", json=payload, timeout=10)
|
||||||
|
data = resp.json()
|
||||||
|
if data.get("success"):
|
||||||
|
st.success(f"Preset '{preset_name}' creato con successo (id: {data['data']['id_preset']})")
|
||||||
|
else:
|
||||||
|
st.error(f"Errore: {data.get('error', 'Sconosciuto')}")
|
||||||
|
except requests.ConnectionError:
|
||||||
|
st.error("Impossibile connettersi all'API. Verifica che il server sia attivo.")
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Errore: {e}")
|
||||||
|
|
||||||
|
# --- TAB LISTA ---
|
||||||
|
with tab_lista:
|
||||||
|
st.subheader("Preset Esistenti")
|
||||||
|
|
||||||
|
if st.button("Aggiorna lista"):
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{API_BASE}/esposition/presets", timeout=10)
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
if data.get("success") and data.get("data"):
|
||||||
|
st.info(f"Trovati {data['total']} preset")
|
||||||
|
|
||||||
|
df = pd.DataFrame(data["data"])
|
||||||
|
display_cols = [
|
||||||
|
"preset_name", "tipo_prodotto", "luogo_applicazione",
|
||||||
|
"sup_esposta", "freq_applicazione", "qta_giornaliera",
|
||||||
|
"ritenzione", "esposizione_calcolata", "esposizione_relativa",
|
||||||
|
]
|
||||||
|
existing = [c for c in display_cols if c in df.columns]
|
||||||
|
st.dataframe(df[existing], use_container_width=True)
|
||||||
|
|
||||||
|
with st.expander("Dettaglio completo"):
|
||||||
|
for preset in data["data"]:
|
||||||
|
st.markdown(f"**{preset['preset_name']}**")
|
||||||
|
st.json(preset)
|
||||||
|
st.markdown("---")
|
||||||
|
else:
|
||||||
|
st.warning("Nessun preset trovato.")
|
||||||
|
|
||||||
|
except requests.ConnectionError:
|
||||||
|
st.error("Impossibile connettersi all'API. Verifica che il server sia attivo.")
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Errore nel caricamento: {e}")
|
||||||
196
pages/ingredients_page.py
Normal file
196
pages/ingredients_page.py
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
import streamlit as st
|
||||||
|
import requests
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
API_BASE = "https://api.cosmoguard.it/api/v1/"
|
||||||
|
|
||||||
|
st.set_page_config(page_title="Ricerca Ingredienti", layout="wide")
|
||||||
|
st.title("Ricerca Ingredienti per CAS")
|
||||||
|
|
||||||
|
tab_ricerca, tab_risultato, tab_inventario = st.tabs(["Ricerca", "Risultato", "Inventario"])
|
||||||
|
|
||||||
|
# --- State ---
|
||||||
|
if "ingredient_data" not in st.session_state:
|
||||||
|
st.session_state.ingredient_data = None
|
||||||
|
if "ingredient_cas" not in st.session_state:
|
||||||
|
st.session_state.ingredient_cas = ""
|
||||||
|
|
||||||
|
# --- TAB INVENTARIO ---
|
||||||
|
with tab_inventario:
|
||||||
|
st.subheader("Ingredienti gia acquisiti")
|
||||||
|
|
||||||
|
if st.button("Aggiorna inventario"):
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{API_BASE}/ingredients/list", timeout=10)
|
||||||
|
result = resp.json()
|
||||||
|
|
||||||
|
if result.get("success") and result.get("data"):
|
||||||
|
st.info(f"Trovati {result['total']} ingredienti nel database")
|
||||||
|
|
||||||
|
df = pd.DataFrame(result["data"])
|
||||||
|
df["dap"] = df["dap"].map({True: "OK", False: "-"})
|
||||||
|
df["cosing"] = df["cosing"].map({True: "OK", False: "-"})
|
||||||
|
df["tox"] = df["tox"].map({True: "OK", False: "-"})
|
||||||
|
df = df.rename(columns={
|
||||||
|
"cas": "CAS",
|
||||||
|
"dap": "DAP",
|
||||||
|
"cosing": "COSING",
|
||||||
|
"tox": "TOX",
|
||||||
|
"created_at": "Data Acquisizione",
|
||||||
|
})
|
||||||
|
|
||||||
|
display_cols = ["CAS", "DAP", "COSING", "TOX", "Data Acquisizione"]
|
||||||
|
existing = [c for c in display_cols if c in df.columns]
|
||||||
|
st.dataframe(df[existing], use_container_width=True, hide_index=True)
|
||||||
|
else:
|
||||||
|
st.warning("Nessun ingrediente trovato nel database.")
|
||||||
|
|
||||||
|
except requests.ConnectionError:
|
||||||
|
st.error("Impossibile connettersi all'API. Verifica che il server sia attivo.")
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Errore nel caricamento: {e}")
|
||||||
|
|
||||||
|
# --- TAB RICERCA ---
|
||||||
|
with tab_ricerca:
|
||||||
|
st.subheader("Cerca un ingrediente")
|
||||||
|
|
||||||
|
cas_input = st.text_input("CAS Number", placeholder="es. 64-17-5")
|
||||||
|
|
||||||
|
if st.button("Cerca", type="primary", disabled=not cas_input):
|
||||||
|
with st.spinner(f"Ricerca in corso per {cas_input}..."):
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{API_BASE}/ingredients/search",
|
||||||
|
json={"cas": cas_input},
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
result = resp.json()
|
||||||
|
|
||||||
|
if result.get("success") and result.get("data"):
|
||||||
|
st.session_state.ingredient_data = result["data"]
|
||||||
|
st.session_state.ingredient_cas = cas_input
|
||||||
|
st.success(f"Ingrediente {cas_input} trovato")
|
||||||
|
else:
|
||||||
|
st.session_state.ingredient_data = None
|
||||||
|
st.error(result.get("error", f"Nessun dato per CAS {cas_input}"))
|
||||||
|
|
||||||
|
except requests.ConnectionError:
|
||||||
|
st.error("Impossibile connettersi all'API. Verifica che il server sia attivo.")
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Errore: {e}")
|
||||||
|
|
||||||
|
# --- TAB RISULTATO ---
|
||||||
|
with tab_risultato:
|
||||||
|
data = st.session_state.ingredient_data
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
st.info("Effettua una ricerca per visualizzare i risultati.")
|
||||||
|
else:
|
||||||
|
cas = data.get("cas", "")
|
||||||
|
st.subheader(f"Ingrediente: {cas}")
|
||||||
|
|
||||||
|
# --- Header con INCI e data ---
|
||||||
|
col_h1, col_h2 = st.columns(2)
|
||||||
|
with col_h1:
|
||||||
|
inci = data.get("inci") or []
|
||||||
|
st.markdown(f"**INCI:** {', '.join(inci) if inci else 'N/A'}")
|
||||||
|
with col_h2:
|
||||||
|
st.markdown(f"**Data creazione:** {data.get('creation_date', 'N/A')}")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# --- DAP Info ---
|
||||||
|
st.subheader("DAP Info")
|
||||||
|
dap = data.get("dap_info")
|
||||||
|
if dap:
|
||||||
|
col_d1, col_d2, col_d3 = st.columns(3)
|
||||||
|
with col_d1:
|
||||||
|
st.metric("Peso Molecolare (Da)", dap.get("molecular_weight", "N/A"))
|
||||||
|
st.metric("Log Pow", dap.get("log_pow", "N/A"))
|
||||||
|
with col_d2:
|
||||||
|
st.metric("TPSA", dap.get("tpsa", "N/A"))
|
||||||
|
st.metric("Punto Fusione (C)", dap.get("melting_point") or "N/A")
|
||||||
|
with col_d3:
|
||||||
|
dap_val = dap.get("dap_value", 0.5)
|
||||||
|
st.metric("DAP Value", f"{dap_val * 100:.0f}%")
|
||||||
|
st.metric("Alta Ionizzazione", dap.get("high_ionization") or "N/A")
|
||||||
|
else:
|
||||||
|
st.warning("Dati DAP non disponibili")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# --- COSING Info ---
|
||||||
|
st.subheader("COSING Info")
|
||||||
|
cosing_list = data.get("cosing_info")
|
||||||
|
if cosing_list:
|
||||||
|
for i, cosing in enumerate(cosing_list):
|
||||||
|
if len(cosing_list) > 1:
|
||||||
|
st.markdown(f"**Voce {i + 1}**")
|
||||||
|
|
||||||
|
col_c1, col_c2 = st.columns(2)
|
||||||
|
with col_c1:
|
||||||
|
names = cosing.get("common_names", [])
|
||||||
|
st.markdown(f"**Nomi comuni:** {', '.join(names) if names else 'N/A'}")
|
||||||
|
funcs = cosing.get("functionName", [])
|
||||||
|
if funcs:
|
||||||
|
st.markdown("**Funzioni:**")
|
||||||
|
for f in funcs:
|
||||||
|
st.markdown(f"- {f}")
|
||||||
|
|
||||||
|
with col_c2:
|
||||||
|
annex = cosing.get("annex", [])
|
||||||
|
if annex:
|
||||||
|
st.markdown("**Annex/Restrizioni:**")
|
||||||
|
for a in annex:
|
||||||
|
st.markdown(f"- {a}")
|
||||||
|
else:
|
||||||
|
st.markdown("**Annex:** Nessuna restrizione")
|
||||||
|
|
||||||
|
other = cosing.get("otherRestrictions", [])
|
||||||
|
if other:
|
||||||
|
st.markdown("**Altre restrizioni:**")
|
||||||
|
for o in other:
|
||||||
|
st.markdown(f"- {o}")
|
||||||
|
|
||||||
|
restriction = cosing.get("cosmeticRestriction", "")
|
||||||
|
if restriction:
|
||||||
|
st.warning(f"Restrizione cosmetica: {restriction}")
|
||||||
|
|
||||||
|
if i < len(cosing_list) - 1:
|
||||||
|
st.divider()
|
||||||
|
else:
|
||||||
|
st.warning("Dati COSING non disponibili")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# --- Toxicity ---
|
||||||
|
st.subheader("Tossicologia")
|
||||||
|
tox = data.get("toxicity")
|
||||||
|
if tox:
|
||||||
|
# Best case highlight
|
||||||
|
best = tox.get("best_case")
|
||||||
|
if best:
|
||||||
|
st.success(
|
||||||
|
f"Miglior indicatore: **{best['indicator']}** = {best['value']} {best['unit']} "
|
||||||
|
f"({best['route']}) — Fattore: {tox.get('factor', 'N/A')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Indicators table
|
||||||
|
indicators = tox.get("indicators", [])
|
||||||
|
if indicators:
|
||||||
|
df = pd.DataFrame(indicators)
|
||||||
|
col_order = ["indicator", "value", "unit", "route", "toxicity_type"]
|
||||||
|
existing = [c for c in col_order if c in df.columns]
|
||||||
|
df = df[existing]
|
||||||
|
df.columns = ["Indicatore", "Valore", "Unita", "Via", "Tipo"][:len(existing)]
|
||||||
|
st.dataframe(df, use_container_width=True, hide_index=True)
|
||||||
|
else:
|
||||||
|
st.warning("Dati tossicologici non disponibili")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# --- JSON completo ---
|
||||||
|
with st.expander("JSON completo"):
|
||||||
|
st.json(data)
|
||||||
Loading…
Reference in a new issue