first big update

This commit is contained in:
adish-rmr 2025-12-01 19:03:21 +01:00
parent 0489742c49
commit a95e711dba
12 changed files with 4067 additions and 1 deletions

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.12

View file

@ -1 +1,14 @@
# cosmoguard_frontend # cosmoguard_frontend
This repository contains a small Streamlit frontend prototype for Cosmoguard (Alpha v1).
**Run the Streamlit app**
Install dependencies and run the app locally (PowerShell):
```powershell
python -m pip install -r requirements.txt
streamlit run app.py
```
The home page shows a changelog, a feature list with statuses, and a simple reporter to submit bugs or feature requests (saved to `reports.txt`).

50
app.py Normal file
View file

@ -0,0 +1,50 @@
import streamlit as st
from datetime import datetime
import os
st.set_page_config(page_title="LMB Suite Alpha v1", layout="centered")
st.title("LMB Suite — Alpha v1")
st.markdown("""
LMB Suite is an experimental frontend for cosmetic ingredient data tooling.
Use this simple home page to see the changelog, current feature status,
and to report bugs or request features.
""")
st.header("Changelog")
with st.expander("Latest changes"):
st.write("""
- 2025-11-23: Alpha front page created with changelog and features list.
- 2025-11-20: Initial scaffolding and basic feature flags added.
""")
st.header("Features")
features = [
("ECHA (European Chemicals Agency)", "In Progress"),
("CoSIng (Cosmetic Ingredients)", "In Progress"),
("PubChem", "In Progress"),
("Ingredient Parsing", "Planned"),
("SED automatic AI calculation", "Planned"),
("Complete PIF generation", "Planned")
]
cols = st.columns([3,1])
with cols[0]:
st.subheader("Feature")
with cols[1]:
st.subheader("Status")
for name, status in features:
cols = st.columns([3,1])
with cols[0]:
st.write(name)
with cols[1]:
if status.lower() == "complete":
st.success(status)
elif status.lower() == "in progress":
st.warning(status)
else:
st.info(status)
st.markdown("---")
st.caption("LMB Suite Alpha v1 — Experimental.")

1016
echa_example.json Normal file

File diff suppressed because one or more lines are too long

947
echa_example2.json Normal file
View file

@ -0,0 +1,947 @@
{
"_id": {
"$oid": "6923609db2339f52631f60fd"
},
"substance": {
"rmlCas": "56-81-5",
"rmlId": "100.000.263",
"rmlEc": "200-289-5",
"rmlName": "Glycerol"
},
"dossier_info": {
"lastUpdatedDate": "2022-09-14",
"registrationStatus": "Active",
"registrationStatusChangedDate": "2010-08-23",
"registrationRole": "Lead (joint submission)",
"assetExternalId": "3b5de1db-9033-4e96-9022-c68ca931d367",
"rootKey": "IUC5-6e7c951c-5670-46f1-adba-006d4fa0d3f2_fa0793fe-428d-4286-b96d-4b97c5dc72fe"
},
"index": {
"toxicological_information_link": "https://chem.echa.europa.eu/html-pages-prod/3b5de1db-9033-4e96-9022-c68ca931d367/documents/aec04834-0acf-4132-b6c8-9273ad6c6bbd_fa0793fe-428d-4286-b96d-4b97c5dc72fe.html",
"repeated_dose_toxicity_link": "https://chem.echa.europa.eu/html-pages-prod/3b5de1db-9033-4e96-9022-c68ca931d367/documents/IUC5-6a907739-cd5e-4fcf-9949-e31a6ed6fde5_fa0793fe-428d-4286-b96d-4b97c5dc72fe.html",
"acute_toxicity_link": "https://chem.echa.europa.eu/html-pages-prod/3b5de1db-9033-4e96-9022-c68ca931d367/documents/IUC5-600a9e6c-7f74-4855-97e8-92c31ebbeec2_fa0793fe-428d-4286-b96d-4b97c5dc72fe.html"
},
"toxicological_information": {
"sections": [
{
"label": "Administrative data"
},
{
"label": "Workers - Hazard via inhalation route",
"subsections": [
{
"label": "Systemic effects",
"subsections": [
{
"label": "Long term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"AssessmentFactor": "",
"DoseDescriptorStartingPoint": "",
"StDose": "",
"DoseDescriptorStartRTR": "",
"StDoseRTR": "",
"JustificationRTR": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"DiffInDurationAF": "",
"DiffInDurationJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustif": "",
"DatabaseQualityAF": "",
"DatabaseJustif": "",
"OthUncertaintiesAF": "",
"OthUncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
},
{
"label": "Acute/short term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"OverallAssessmentFactor": "",
"Extrapolated": false,
"DoseDescriptorStartingPoint": "",
"StDose": "",
"DoseDescriptorStartRTR": "",
"StDoseRTR": "",
"DoseDescriptorJustificationRTR": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustifAF": "",
"DatabaseQualityDatabaseQualityAF": "",
"DatabaseQualityJustif": "",
"OthUncertaintiesAF": "",
"UncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
}
]
},
{
"label": "Local effects",
"subsections": [
{
"label": "Long term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"AssessmentFactor": "",
"DoseDescriptorStart": "",
"StDose": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"DiffInDurationAF": "",
"DiffInDurationJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustif": "",
"DatabaseQualityAF": "",
"DatabaseJustif": "",
"OthUncertaintiesAF": "",
"OthUncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
},
{
"label": "Acute/short term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"OverallAssessmentFactor": "",
"Extrapolated": false,
"DoseDescriptorStart": "",
"StDose": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustifAF": "",
"DatabaseQualityDatabaseQualityAF": "",
"DatabaseQualityJustif": "",
"OthUncertaintiesAF": "",
"UncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
}
]
}
]
},
{
"label": "Workers - Hazard via dermal route",
"subsections": [
{
"label": "Systemic effects",
"subsections": [
{
"label": "Long term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"AssessmentFactor": "",
"DoseDescriptorStartingPoint": "",
"StDose": "",
"DoseDescriptorStartRTR": "",
"StDoseRTR": "",
"JustificationRTR": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"DiffInDurationAF": "",
"DiffInDurationJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustif": "",
"DatabaseQualityAF": "",
"DatabaseJustif": "",
"OthUncertaintiesAF": "",
"OthUncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
},
{
"label": "Acute/short term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"OverallAssessmentFactor": "",
"Extrapolated": false,
"DoseDescriptorStartingPoint": "",
"StDose": "",
"DoseDescriptorStartRTR": "",
"StDoseRTR": "",
"DoseDescriptorJustificationRTR": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustifAF": "",
"DatabaseQualityDatabaseQualityAF": "",
"DatabaseQualityJustif": "",
"OthUncertaintiesAF": "",
"UncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
}
]
},
{
"label": "Local effects",
"subsections": [
{
"label": "Long term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"AssessmentFactor": "",
"DoseDescriptorStart": "",
"StDose": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"DiffInDurationAF": "",
"DiffInDurationJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustif": "",
"DatabaseQualityAF": "",
"DatabaseJustif": "",
"OthUncertaintiesAF": "",
"OthUncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
},
{
"label": "Acute/short term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"OverallAssessmentFactor": "",
"DoseDescriptorStart": "",
"StDose": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustifAF": "",
"DatabaseQualityDatabaseQualityAF": "",
"DatabaseQualityJustif": "",
"OthUncertaintiesAF": "",
"UncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
}
]
}
]
},
{
"label": "Workers - Hazard for the eyes",
"subsections": [
{
"label": "Local effects",
"Conclusion": "no hazard identified"
}
]
},
{
"label": "Additional information - workers",
"DiscussionWorkers": "All NOAEL values are above the limit dose given in the OECD Test Guidelines thus derivation of DNELs is not justified.The only effect below a limit dose was the metaplasia of the squamous epithelium of the larynx seen in rats at 662 mg/m3 which was only minimal to slight, and thus is not interpreted as adverse."
},
{
"label": "General Population - Hazard via inhalation route",
"subsections": [
{
"label": "Systemic effects",
"subsections": [
{
"label": "Long term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"AssessmentFactor": "",
"DoseDescriptorStartingPoint": "",
"StDose": "",
"DoseDescriptorStartRTR": "",
"StDoseRTR": "",
"JustificationRTR": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"DiffInDurationAF": "",
"DiffInDurationJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustif": "",
"DatabaseQualityAF": "",
"DatabaseJustif": "",
"OthUncertaintiesAF": "",
"OthUncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
},
{
"label": "Acute/short term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"OverallAssessmentFactor": "",
"Extrapolated": false,
"DoseDescriptorStartingPoint": "",
"StDose": "",
"DoseDescriptorStartRTR": "",
"StDoseRTR": "",
"DoseDescriptorJustificationRTR": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustifAF": "",
"DatabaseQualityDatabaseQualityAF": "",
"DatabaseQualityJustif": "",
"OthUncertaintiesAF": "",
"UncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
}
]
},
{
"label": "Local effects",
"subsections": [
{
"label": "Long term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"AssessmentFactor": "",
"DoseDescriptorStart": "",
"StDose": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"DiffInDurationAF": "",
"DiffInDurationJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustif": "",
"DatabaseQualityAF": "",
"DatabaseJustif": "",
"OthUncertaintiesAF": "",
"OthUncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
},
{
"label": "Acute/short term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"OverallAssessmentFactor": "",
"Extrapolated": false,
"DoseDescriptorStart": "",
"StDose": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustifAF": "",
"DatabaseQualityDatabaseQualityAF": "",
"DatabaseQualityJustif": "",
"OthUncertaintiesAF": "",
"UncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
}
]
}
]
},
{
"label": "General Population - Hazard via dermal route",
"subsections": [
{
"label": "Systemic effects",
"subsections": [
{
"label": "Long term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"AssessmentFactor": "",
"DoseDescriptorStartingPoint": "",
"StDose": "",
"DoseDescriptorStartRTR": "",
"StDoseRTR": "",
"JustificationRTR": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"DiffInDurationAF": "",
"DiffInDurationJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustif": "",
"DatabaseQualityAF": "",
"DatabaseJustif": "",
"OthUncertaintiesAF": "",
"OthUncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
},
{
"label": "Acute/short term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"OverallAssessmentFactor": "",
"Extrapolated": false,
"DoseDescriptorStartingPoint": "",
"StDose": "",
"DoseDescriptorStartRTR": "",
"StDoseRTR": "",
"DoseDescriptorJustificationRTR": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustifAF": "",
"DatabaseQualityDatabaseQualityAF": "",
"DatabaseQualityJustif": "",
"OthUncertaintiesAF": "",
"UncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
}
]
},
{
"label": "Local effects",
"subsections": [
{
"label": "Long term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"AssessmentFactor": "",
"DoseDescriptorStart": "",
"StDose": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"DiffInDurationAF": "",
"DiffInDurationJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustif": "",
"DatabaseQualityAF": "",
"DatabaseJustif": "",
"OthUncertaintiesAF": "",
"OthUncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
},
{
"label": "Acute/short term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"OverallAssessmentFactor": "",
"DoseDescriptorStart": "",
"StDose": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustifAF": "",
"DatabaseQualityDatabaseQualityAF": "",
"DatabaseQualityJustif": "",
"OthUncertaintiesAF": "",
"UncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
}
]
}
]
},
{
"label": "General Population - Hazard via oral route",
"subsections": [
{
"label": "Systemic effects",
"subsections": [
{
"label": "Long term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"AssessmentFactor": "",
"DoseDescriptorStartingPoint": "",
"StDose": "",
"DoseDescriptorStartRTR": "",
"StDoseRTR": "",
"JustificationRTR": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"DiffInDurationAF": "",
"DiffInDurationJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustif": "",
"DatabaseQualityAF": "",
"DatabaseJustif": "",
"OthUncertaintiesAF": "",
"OthUncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
},
{
"label": "Acute/short term exposure",
"HazardAssessment": "no hazard identified",
"StDose": "",
"MostSensitiveEndpoint": "",
"subsections": [
{
"label": "DNEL related information",
"DerivationMethod": "",
"OverallAssessmentFactor": "",
"Extrapolated": false,
"DoseDescriptorStartingPoint": "",
"StDose": "",
"DoseDescriptorStartRTR": "",
"StDoseRTR": "",
"DoseDescriptorJustificationRTR": "",
"DoseResponseAF": "",
"DoseResponseJustif": "",
"InterspeciesAF": "",
"InterspeciesJustif": "",
"OthInterspeciesAF": "",
"OthInterspeciesJustif": "",
"IntraspeciesAF": "",
"IntraspeciesJustifAF": "",
"DatabaseQualityDatabaseQualityAF": "",
"DatabaseQualityJustif": "",
"OthUncertaintiesAF": "",
"UncertaintiesJustif": ""
},
{
"label": "Explanation for hazard conclusion"
}
]
}
]
}
]
},
{
"label": "General Population - Hazard for the eyes",
"subsections": [
{
"label": "Local effects",
"Conclusion": "no hazard identified"
}
]
},
{
"label": "Additional information - General Population",
"DiscussionGenPop": "All NOAEL values are above the limit dose given in the OECD Test Guidelines thus derivation of DNELs is not justified.The only effect below a limit dose was the metaplasia of the squamous epithelium of the larynx seen in rats at 662 mg/m3 which was only minimal to slight, and thus is not interpreted as adverse."
}
]
},
"acute_toxicity": {
"sections": [
{
"label": "Administrative data"
},
{
"label": "Description of key information",
"KeyInformation": "The acute oral LD50 was determined in three species, rat, mice and guinea pigs. In all three species the oral LD50 was >/= 11,500 mg/kg.The acute dermal toxicity of glycerin was examined in guinea pigs.The dermal LD50 was determined to be 45 ml/kg (56,750 mg/kg) in guinea pigs.In an inhalation study, rats were exposed to aerosol from test material at targeted concentrations of 1.0,2.0,or4.0mg glycerol/L for 6 hours. The 4 h inhalation LC50 was determined to be above 5.85 mg/L in rats."
},
{
"label": "Key value for assessment",
"subsections": [
{
"label": "Acute toxicity: via oral route",
"LinkToRelevantStudyRecord": "",
"EndpointConclusion": "no adverse effect observed",
"EffectLevelUnit": "LD50",
"EffectLevelValue": "11,500mg/kg bw"
},
{
"label": "Acute toxicity: via dermal route",
"LinkToRelevantStudyRecord": "",
"EndpointConclusion": "no adverse effect observed",
"EffectLevelUnit": "LD50",
"EffectLevelValue": "56,750mg/kg bw"
},
{
"label": "Acute toxicity: via inhalation route",
"LinkToRelevantStudyRecord": "",
"EndpointConclusion": "no adverse effect observed",
"EffectLevelUnit": "LC50",
"EffectLevelValue": ">5.85mg/L",
"PhysicalForm": "inhalation: aerosol"
},
{
"label": "Acute toxicity: other routes",
"LinkToRelevantStudyRecord": ""
}
]
},
{
"label": "Justification for classification or non-classification",
"JustifClassif": "There is no justification for classification based on data from available studies.",
"JustifClassif_STOTSE": ""
},
{
"label": "Additional information",
"Discussion": "Glycerol is essentially non-toxic following acute administration.In the available evaluations performed by official bodies (MAK 2007, OECD 2002, EFSA 2017) a number of additional acute toxicity studies were evaluated and summarised.All considered glycerol to be of low acute toxicity to mammals. The range of acute oral LD50 values derived from studies in experimental animals is between >4,000 and < 38,000 mg/kg, with the majority of values being between 23,000 and 38,000 mg/kg. For acute dermal toxicity a single LD50 of >18,700 mg/kg for rabbits was summarised by the OECD. The low level of acute toxicity in rodents is further supported by the results of the Danish (Q)SAR Database (LD50 rat oral 7500 mg/kg, LD50 mouse oral 13,000 mg/kg). There was no new relevant information identified up to and including 2021 (most recent literature research).(Q)SAR predicted profile for glycerol CAS -Nr 56-81-5, Danish (Q)SAR Database, http://qsar.food.dtu.dk Date: 09-02-2021Glycerin [MAK Value Documentation in German language, 2007]Re-evaluation of glycerol (E 422) as a food additive, EFSA Journal 2017;15(3):4720Glycerol, CAS Number 56-81-5, OECD SIDS Initial Assessment Report For SIAM 14 Paris, France, 26-28 March 2002",
"subsections": [
{
"label": "Attached background material"
}
]
}
]
},
"repeated_dose_toxicity": {
"sections": [
{
"label": "Administrative data"
},
{
"label": "Description of key information",
"KeyInformation": "In the best available dietary study, groups of 22 rats (Long-Evans)/sex/treatment received 5, 10 and 20% glycerol (natural or synthetic) in their diet (males 2000, 4000 and 8000 mg/kg bw; females 2500, 5000 and 10000 mg/kg bw) for 2 years. Although the results were not described in detail, based on this limited dietary study it can be concluded that no adverse effects were observed at up to 10,000 mg/kg bw.The effect of glycerine following administration for 90 days in a subchronic toxicity study was examined.  Rats fed 5 or 20% glycerine in the diet for 90 days gained weight at a faster rate than control animals. There were no adverse treatment related effects noted in male or female rats fed 5% glycerine in the diet.  In the male rats which received 20 percent glycerine, there was an increase in the final liver/body weight ratio and upon microscopic examination generalized cloudy swelling and hypertrophy of the parenchymal cells was observed. The only effect in the female rats on this level was some generalized cloudy selling upon microscopic examination of the liver.  A 5% glycerol in the diet corresponded to 4580 and 6450 mg/kg/day for male and female rats, respectively, after 4 weeks and a 20% glycerol in the diet corresponded to 18,750 and 25,800 mg/kg/day for male and female rats, respectively, after 4 weeks.A number of other studies have been incorporated in the dossier. These studies are considered less reliable indicators of the systemic effects of glycerol following repeated administration, mainly because of limited toxicity assessments and/or deficient experimental design. The effects they do report are consistent with those observed in the key studies and as such they may contribute to the overall assessment of toxicity of glycerol.The effects following repeated dermal application of glycerin was examined. There were no effects noted in rabbits dosed 8 hours/day, 5 days/week for 45 weeks with dose levels as high as 4.0 ml/kg.  Using a density of 1.2611 g/cm3 at 20 °C, a dose of 4.0 ml/kg corresponds to 5040 mg/kg/day.The subchronic toxicity of glycerol was examined following aerosol exposure. The NOAEC for local and systemic toxicity was 622 mg/m3.Migrated Data from field(s) Field \"Quality of whole database\" (Path: ENDPOINT_SUMMARY.RepeatedDoseToxicity.KeyValueForChemicalSafetyAssessment.RepeatedDoseToxicityInhalationLocalEffects.EndpointConclusion.DataBaseQuality): The available information meets the information requirements under REACH Field \"Quality of whole database\" (Path: ENDPOINT_SUMMARY.RepeatedDoseToxicity.KeyValueForChemicalSafetyAssessment.RepeatedDoseToxicityInhalationSystemicEffects.EndpointConclusion.DataBaseQuality): The available information meets the information requirements under REACH"
},
{
"label": "Key value for assessment",
"ToxicEffectType": "",
"EndpointConclusionSystemicEffectsOralRoute": "no adverse effect observed",
"EndpointConclusionSystemicEffectsDermal": "no adverse effect observed",
"EndpointConclusionSystemicEffectsInhalation": "",
"subsections": [
{
"label": "Short-term repeated dose toxicity systemic effects",
"subsections": [
{
"label": "Oral route",
"LinkToRelevantStudyRecord": "",
"EffectLevelUnit": "",
"EffectLevelValue": "",
"ExperimentalExposureTimePerWeek": "",
"Species": "",
"System": "",
"Organ": ""
},
{
"label": "Dermal",
"LinkToRelevantStudyRecord": "",
"EffectLevelUnit": "",
"EffectLevelValue": "",
"ExperimentalExposureTimePerWeek": "",
"Species": "",
"System": "",
"Organ": ""
},
{
"label": "Inhalation",
"LinkToRelevantStudyRecord": "",
"EffectLevelUnit": "",
"EffectLevelValue": "",
"ExperimentalExposureTimePerWeek": "",
"Species": "",
"System": "",
"Organ": ""
}
]
},
{
"label": "Sub-chronic toxicity systemic effects",
"subsections": [
{
"label": "Oral route",
"LinkToRelevantStudyRecord": "",
"EffectLevelUnit": "",
"EffectLevelValue": "",
"ExperimentalExposureTimePerWeek": "",
"Species": "",
"System": "",
"Organ": ""
},
{
"label": "Dermal",
"LinkToRelevantStudyRecord": "",
"EffectLevelUnit": "NOAEL",
"EffectLevelValue": "5,040mg/kg bw/day",
"ExperimentalExposureTimePerWeek": "",
"Species": "rabbit",
"System": "",
"Organ": ""
},
{
"label": "Inhalation",
"LinkToRelevantStudyRecord": "001 | Key | Experimental study",
"EffectLevelUnit": "NOAEC",
"EffectLevelValue": "622mg/m³",
"ExperimentalExposureTimePerWeek": "",
"Species": "rat",
"System": "",
"Organ": ""
}
]
},
{
"label": "Chronic toxicity systemic effects",
"subsections": [
{
"label": "Oral route",
"LinkToRelevantStudyRecord": "002 | Key | Experimental study001 | Key | Experimental study",
"EffectLevelUnit": "NOAEL",
"EffectLevelValue": "10,000mg/kg bw/day",
"ExperimentalExposureTimePerWeek": "",
"Species": "rat",
"System": "",
"Organ": ""
},
{
"label": "Dermal",
"LinkToRelevantStudyRecord": "",
"EffectLevelUnit": "",
"EffectLevelValue": "",
"ExperimentalExposureTimePerWeek": "",
"Species": "",
"System": "",
"Organ": ""
},
{
"label": "Inhalation",
"LinkToRelevantStudyRecord": "",
"EffectLevelUnit": "",
"EffectLevelValue": "",
"ExperimentalExposureTimePerWeek": "",
"Species": "",
"System": "",
"Organ": ""
}
]
},
{
"label": "Repeated dose toxicity local effects",
"subsections": [
{
"label": "Dermal",
"LinkToRelevantStudyRecord": "",
"EndpointConclusion": "",
"EffectLevelUnit": "",
"EffectLevelValue": "",
"TestType": "",
"Species": ""
},
{
"label": "Inhalation",
"LinkToRelevantStudyRecord": "003 | Other | Other result type001 | Key | Experimental study",
"EndpointConclusion": "no adverse effect observed",
"EffectLevelUnit": "NOAEC",
"EffectLevelValue": "662mg/m³",
"TestType": "subchronic",
"Species": "rat"
}
]
},
{
"label": "Repeated dose toxicity: other routes",
"LinkToRelevantStudyRecord": ""
}
]
},
{
"label": "Mode of Action Analysis / Human Relevance Framework",
"ModeOfActionAnalysis": ""
},
{
"label": "Justification for classification or non-classification (Specific target organ toxicity-repeated exposure (STOT RE))",
"JustifClassif": "There is no justification for classification based on data from available studies."
},
{
"label": "Additional information",
"Discussion": "Study via the dietary, dermal and respiratory route demonstrate the low level of concern about glycerol.In the available evaluations performed by official bodies (MAK 2007, OECD 2002, EFSA 2017) a number of additional repeated dose toxicity studies were evaluated and summarised.Repeated dose toxicity oralIn the OECD SIDS document the study reported by Hine et al. was identified as the most relevant key study. This study is included in the REACH dossier in detail as RSS.A number of other studies have been incorporated in a table in the OECD SIDS document. These studies are considered less reliable indicators of the systemic effects of glycerol following repeated administration, mainly because of limited toxicity assessments and/or deficient experimental design. The effects they do report are consistent with those observed in the key studies and as such they may contribute to the overall assessment of toxicity of glycerol.EFSA conclusion: Short-term or subchronic studies were not performed according to current test guidelines. In a subchronic toxicity study (in drinking water) in rats, the effects reported were observed with doses in the range of the LD50 for glycerol. The Panel considered that the local irritating effects of glycerol in the gastrointestinal tract reported in some gavage studies in rat (100% glycerol at 2,800 mg/kg bw per day, the lowest dose tested (Staples et al., 1967)), and dogs (100% glycerol at 5,600 mg/kg bw per day (Staples et al., 1967)) was likely due to the hygroscopic and osmotic effects of glycerol.From the available chronic toxicity and carcinogenicity studies, glycerol was not carcinogenic in mice and rats and did not show evidence of adverse effects in a 2-year chronic toxicity study. The Panel noted that no adverse effects were reported in rats receiving doses up to 10,000 mg/kg bw per day for 1 year, the highest dose tested. The Panel also noted that there was no increase in the tumours incidences in rats receiving doses up to 5,000 mg/kg bw per day for 2 years, the highest dose tested.The Panel considered that none of the animal studies available identified an adverse effect for glycerol.Repeated dose toxicity inhalationThe key studies identified and evaluated by the OECD are compiled as RSS in the dossier (Renne 1992). Based on an increased incidence of “minimal” to “mild” squamous metaplasia of the epiglottis, the NOAEC for local irritant effects to the upper respiratory tract was derived at 165 mg/m3 and 662 mg/m3 for systemic effects.The German Commission for the Investigation of Health Hazards of Chemical Compounds in the Work Area has re-evaluated glycerol [56-81-5]in 2016, considering the endpoints irritation of the respiratory tract.Based on the results of an expert workshop, the NOAEC value for local toxicity was re-evaluated. Because the metaplasia of the squamous epithelium of the larynx seen in rats at 662 mg/m3 with glycerol aerosol was only minimal to slight, is not interpreted as adverse (Kaufmann et al. 2009). Furthermore it has been taken into account, that the response does not increase with the exposure duration.The NOAEC in rats for local and systemic effects after 13-week exposure is 662 mg/m3.Glycerin [MAK Value Documentation in German language, 2007]The MAK Collection for Occupational Health and Safety 2017, Vol 2, No 2Re-evaluation of glycerol (E 422) as a food additive, EFSA Journal 2017;15(3):4720Glycerol, CAS Number 56-81-5, OECD SIDS Initial Assessment Report For SIAM 14 Paris, France, 26-28 March 2002Kaufmann W, Bader R, Ernst H, Harada T, Hardisty J, Kittel B, Kolling A, Pino M, Renne R, Rittinghausen S, Schulte A, Wöhrmann T, Rosenbruch M (2009) 1st International ESTP Expert Workshop: “Larynx squamous metaplasia”. A re-consideration of morphology and diagnostic approaches in rodent studies and its relevance for human risk assessment. Exp Toxicol Pathol 61: 591603",
"subsections": [
{
"label": "Attached background material"
}
]
}
]
}
}

193
functions.py Normal file
View file

@ -0,0 +1,193 @@
import json
from typing import Any, Dict, List, Union
import requests
import pandas as pd
def remove_empty_values(data: Any) -> Any:
"""
Rimuove ricorsivamente tutte le chiavi con valori vuoti (stringa vuota, None, False, liste/dict vuoti).
Args:
data: Il dato da pulire (può essere dict, list o valore primitivo)
Returns:
Il dato pulito senza valori vuoti
"""
if isinstance(data, dict):
# Filtra il dizionario rimuovendo chiavi con valori vuoti
cleaned = {}
for key, value in data.items():
# Ricorsivamente pulisci il valore
cleaned_value = remove_empty_values(value)
# Aggiungi solo se il valore non è vuoto
if cleaned_value not in [None, "", [], {}, False]:
cleaned[key] = cleaned_value
return cleaned if cleaned else None
elif isinstance(data, list):
# Pulisci ogni elemento della lista
cleaned = []
for item in data:
cleaned_item = remove_empty_values(item)
if cleaned_item not in [None, "", [], {}, False]:
cleaned.append(cleaned_item)
return cleaned if cleaned else None
else:
# Per valori primitivi, ritorna il valore se non è vuoto
if data in [None, "", False]:
return None
return data
def transform_labels_to_keys(data: Any) -> Any:
"""
Trasforma ricorsivamente le 'label' in chiavi del dizionario.
Args:
data: Il dato da trasformare
Returns:
Il dato trasformato con le label come chiavi
"""
if isinstance(data, dict):
# Se c'è una label, usala come chiave principale
if 'label' in data:
label = data['label']
# Crea una copia del dizionario senza la label
new_dict = {k: v for k, v in data.items() if k != 'label'}
# Se ci sono subsections, trasformale ricorsivamente
if 'subsections' in new_dict:
subsections = new_dict.pop('subsections')
# Trasforma ogni subsection
for subsection in subsections:
transformed_sub = transform_labels_to_keys(subsection)
if isinstance(transformed_sub, dict):
new_dict.update(transformed_sub)
# Trasforma ricorsivamente tutti gli altri valori
for key, value in list(new_dict.items()):
new_dict[key] = transform_labels_to_keys(value)
# Ritorna con la label come chiave principale
return {label: new_dict if new_dict else {}}
else:
# Se non c'è label, trasforma ricorsivamente tutti i valori
result = {}
for key, value in data.items():
if key == 'subsections' and isinstance(value, list):
# Gestisci le subsections senza label nel dizionario padre
for subsection in value:
transformed = transform_labels_to_keys(subsection)
if isinstance(transformed, dict):
result.update(transformed)
elif key == 'sections' and isinstance(value, list):
# Gestisci le sections trasformandole in un dizionario
sections_dict = {}
for section in value:
transformed = transform_labels_to_keys(section)
if isinstance(transformed, dict):
sections_dict.update(transformed)
result[key] = sections_dict
else:
result[key] = transform_labels_to_keys(value)
return result
elif isinstance(data, list):
# Trasforma ogni elemento della lista
return [transform_labels_to_keys(item) for item in data]
else:
# Ritorna il valore così com'è
return data
def clean_and_transform_json(json_data: Union[str, dict], keep_false: bool = False) -> dict:
"""
Funzione principale che pulisce e trasforma il JSON.
Args:
json_data: Il JSON come stringa o dizionario
keep_false: Se True, mantiene i valori False (default: False li rimuove)
Returns:
Il JSON pulito e trasformato
"""
# Se è una stringa, parsala
if isinstance(json_data, str):
data = json.loads(json_data)
else:
data = json_data
# Step 1: Rimuovi i valori vuoti
if not keep_false:
cleaned_data = remove_empty_values(data)
else:
# Versione alternativa che mantiene False
def remove_empty_keep_false(d):
if isinstance(d, dict):
cleaned = {}
for key, value in d.items():
cleaned_value = remove_empty_keep_false(value)
if cleaned_value not in [None, "", [], {}]:
cleaned[key] = cleaned_value
return cleaned if cleaned else None
elif isinstance(d, list):
cleaned = []
for item in d:
cleaned_item = remove_empty_keep_false(item)
if cleaned_item not in [None, "", [], {}]:
cleaned.append(cleaned_item)
return cleaned if cleaned else None
else:
if d in [None, ""]:
return None
return d
cleaned_data = remove_empty_keep_false(data)
# Step 2: Trasforma le label in chiavi
if cleaned_data:
transformed_data = transform_labels_to_keys(cleaned_data)
else:
transformed_data = {}
return transformed_data
def api_req(query: str) -> Dict[str, Any]:
url = 'https://api.cosmoguard.it/api/v1/echa/search'
response = requests.post(url, json={'cas': query})
data = response.json()
if data['success'] == True:
results = data['data']
if results:
substance_info = data['data']['substance']
last_update = data['data']['dossier_info']['lastUpdatedDate']
tox = data['data']['index']['toxicological_information_link']
rpt = data['data']['index']['repeated_dose_toxicity_link']
act = data['data']['index']['acute_toxicity_link']
toxicological_information = data['data']['toxicological_information']
repeated_dose_toxicity = data['data']['repeated_dose_toxicity']
acute_toxicity = data['data']['acute_toxicity']
return {
'substance_info': substance_info,
'last_update': last_update,
'toxicological_information': toxicological_information,
'repeated_dose_toxicity': repeated_dose_toxicity,
'acute_toxicity': acute_toxicity,
'link': {
'toxicological_information': tox,
'repeated_dose_toxicity': rpt,
'acute_toxicity': act
}
}
else:
return {'error': 'No results found for the given CAS number.'}
else:
return data['error']

352
pages/echa.py Normal file
View file

@ -0,0 +1,352 @@
import streamlit as st
import json
import os
from typing import Any
import requests
import pandas as pd
from functions import clean_and_transform_json, api_req
st.set_page_config(page_title="Echa Toxicological Reports", layout="centered")
st.title("Echa Toxicological Reports")
result = {}
# Single-ingredient input
query = st.text_input("Search by CAS", value="50-00-0")
if st.button("Search"):
with st.spinner("Fetching data..."):
try:
result = api_req(query)
if 'error' in result:
st.error(result['error'])
else:
st.success(f"Data fetched successfully! Last updated: {result['last_update']}")
data = clean_and_transform_json(result)
tab1, tab2, tab3 = st.tabs(["Toxicological Information", "Repeated Dose Toxicity", "Acute Toxicity"])
with tab1:
st.header("Toxicological Information")
table_data = []
txi = data['toxicological_information']
for section_name, section_data in txi['sections'].items():
if section_name == "Administrative data":
continue
elif "Additional information" in section_name:
continue
elif "eyes" in section_name.lower():
population = 'Workers' if 'Workers' in section_name else 'General Population'
table_data.append({
'Population': population,
'Exposure Route': 'Eyes',
'Effect Type': 'Local effects',
'Exposure Duration': '-',
'Hazard Assessment': section_data['Local effects'].get('Conclusion', 'N/A')
})
else:
# Determine population and route
population = 'Workers' if 'Workers' in section_name else 'General Population'
if 'inhalation' in section_name.lower():
route = 'Inhalation'
elif 'dermal' in section_name.lower():
route = 'Dermal'
elif 'oral' in section_name.lower():
route = 'Oral'
else:
route = 'Unknown'
# Process nested data
for effect_type, effect_data in section_data.items():
if isinstance(effect_data, dict):
for exposure_duration, exposure_data in effect_data.items():
if isinstance(exposure_data, dict) and 'HazardAssessment' in exposure_data:
table_data.append({
'Population': population,
'Exposure Route': route,
'Effect Type': effect_type,
'Exposure Duration': exposure_duration,
'Hazard Assessment': exposure_data.get('HazardAssessment', 'N/A')
})
# Create DataFrame
df = pd.DataFrame(table_data)
# Display the main table
st.subheader("📊 Complete Hazard Assessment Overview")
st.dataframe(df, use_container_width=True, height=500)
# Display summary metrics
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total Assessments", len(df))
with col2:
st.metric("Workers Assessments", len(df[df['Population'] == 'Workers']))
with col3:
st.metric("General Population", len(df[df['Population'] == 'General Population']))
with col4:
st.metric("Unique Routes", df['Exposure Route'].nunique())
# Display additional information
st.markdown("---")
st.subheader("📝 Additional Information")
col1, col2 = st.columns(2)
with col1:
st.markdown("**Workers:**")
st.info(txi['sections']['Additional information - workers']['DiscussionWorkers'])
with col2:
st.markdown("**General Population:**")
st.info(txi['sections']['Additional information - General Population']['DiscussionGenPop'])
# Option to download the data as CSV
st.markdown("---")
csv = df.to_csv(index=False)
st.download_button(
label="📥 Download as CSV",
data=csv,
file_name='hazard_assessment_data.csv',
mime='text/csv'
)
with tab2:
st.header("Repeated Dose Toxicity")
table_data = []
key_values = data['repeated_dose_toxicity']['sections']['Key value for assessment']
# Process Sub-chronic toxicity - systemic effects
subchronic = key_values.get('Sub-chronic toxicity systemic effects', {})
for route, route_data in subchronic.items():
if route_data and isinstance(route_data, dict):
table_data.append({
'Toxicity Type': 'Sub-chronic',
'Effect Type': 'Systemic effects',
'Route': route,
'Effect Level': route_data.get('EffectLevelUnit', '-'),
'Value': route_data.get('EffectLevelValue', '-'),
'Species': route_data.get('Species', '-'),
'Study Reference': route_data.get('LinkToRelevantStudyRecord', '-')[:50] + '...' if route_data.get('LinkToRelevantStudyRecord', '') and len(route_data.get('LinkToRelevantStudyRecord', '')) > 50 else route_data.get('LinkToRelevantStudyRecord', '-')
})
# Process Chronic toxicity - systemic effects
chronic = key_values.get('Chronic toxicity systemic effects', {})
for route, route_data in chronic.items():
if route_data and isinstance(route_data, dict):
table_data.append({
'Toxicity Type': 'Chronic',
'Effect Type': 'Systemic effects',
'Route': route,
'Effect Level': route_data.get('EffectLevelUnit', '-'),
'Value': route_data.get('EffectLevelValue', '-'),
'Species': route_data.get('Species', '-'),
'Study Reference': route_data.get('LinkToRelevantStudyRecord', '-')[:50] + '...' if route_data.get('LinkToRelevantStudyRecord', '') and len(route_data.get('LinkToRelevantStudyRecord', '')) > 50 else route_data.get('LinkToRelevantStudyRecord', '-')
})
# Process Repeated dose toxicity - local effects
local_effects = key_values.get('Repeated dose toxicity local effects', {})
for route, route_data in local_effects.items():
if route_data and isinstance(route_data, dict):
table_data.append({
'Toxicity Type': route_data.get('TestType', 'Repeated dose').capitalize() if route_data.get('TestType') else 'Repeated dose',
'Effect Type': 'Local effects',
'Route': route,
'Effect Level': route_data.get('EffectLevelUnit', '-'),
'Value': route_data.get('EffectLevelValue', '-'),
'Species': route_data.get('Species', '-'),
'Study Reference': route_data.get('LinkToRelevantStudyRecord', '-')[:50] + '...' if route_data.get('LinkToRelevantStudyRecord', '') and len(route_data.get('LinkToRelevantStudyRecord', '')) > 50 else route_data.get('LinkToRelevantStudyRecord', '-')
})
# Add endpoint conclusions
conclusions_data = []
if key_values.get('EndpointConclusionSystemicEffectsOralRoute'):
conclusions_data.append({
'Route': 'Oral',
'Effect Type': 'Systemic effects',
'Conclusion': key_values.get('EndpointConclusionSystemicEffectsOralRoute')
})
if key_values.get('EndpointConclusionSystemicEffectsDermal'):
conclusions_data.append({
'Route': 'Dermal',
'Effect Type': 'Systemic effects',
'Conclusion': key_values.get('EndpointConclusionSystemicEffectsDermal')
})
# Create DataFrames
df_toxicity = pd.DataFrame(table_data) if table_data else pd.DataFrame()
df_conclusions = pd.DataFrame(conclusions_data) if conclusions_data else pd.DataFrame()
# Display the main toxicity table
st.subheader("📊 Toxicity Study Results")
if not df_toxicity.empty:
st.dataframe(df_toxicity, use_container_width=True, height=400)
else:
st.info("No toxicity data available")
# Display summary metrics
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total Studies", len(df_toxicity) if not df_toxicity.empty else 0)
with col2:
unique_routes = df_toxicity['Route'].nunique() if not df_toxicity.empty else 0
st.metric("Routes Tested", unique_routes)
with col3:
unique_species = df_toxicity['Species'].nunique() if not df_toxicity.empty else 0
st.metric("Species Tested", unique_species)
with col4:
st.metric("Classification", "Not required")
# Display endpoint conclusions
if not df_conclusions.empty:
st.markdown("---")
st.subheader("🎯 Endpoint Conclusions")
st.dataframe(df_conclusions, use_container_width=True, hide_index=True)
# Display key information
st.markdown("---")
st.subheader("📋 Key Study Information")
key_info = data['repeated_dose_toxicity']['sections']['Description of key information']['KeyInformation']
# Split the key information into paragraphs for better readability
key_info_paragraphs = key_info.split('.')
key_info_formatted = '. '.join([p.strip() for p in key_info_paragraphs if p.strip()])
st.info(key_info_formatted[:1000] + "..." if len(key_info_formatted) > 1000 else key_info_formatted)
# Display classification justification
st.markdown("---")
st.subheader("⚖️ Classification Justification")
justif = data['repeated_dose_toxicity']['sections']['Justification for classification or non-classification (Specific target organ toxicity-repeated exposure (STOT RE))']
st.success(justif.get('JustifClassif', 'No information available'))
# Display additional discussion (truncated)
st.markdown("---")
st.subheader("📝 Additional Discussion")
discussion = data['repeated_dose_toxicity']['sections']['Additional information'].get('Discussion', '')
if discussion:
# Show first 500 characters of discussion
with st.expander("Click to expand full discussion"):
st.text_area("", discussion, height=300, disabled=True)
# Option to download the data as CSV
st.markdown("---")
if not df_toxicity.empty:
csv = df_toxicity.to_csv(index=False)
st.download_button(
label="📥 Download Toxicity Data as CSV",
data=csv,
file_name='repeated_dose_toxicity_data.csv',
mime='text/csv'
)
with tab3:
st.header("Acute Toxicity")
table_data = []
key_values = data['acute_toxicity']['sections']['Key value for assessment']
# Process each route of exposure
for route_name, route_data in key_values.items():
if route_data and isinstance(route_data, dict) and 'EffectLevelUnit' in route_data:
# Extract route type
if 'oral' in route_name:
route = 'Oral'
species = 'Rat, Mice, Guinea pig' # From key information
elif 'dermal' in route_name:
route = 'Dermal'
species = 'Guinea pig'
elif 'inhalation' in route_name:
route = 'Inhalation'
species = 'Rat'
else:
route = 'Other'
species = '-'
table_data.append({
'Route of Exposure': route,
'Effect Level': route_data.get('EffectLevelUnit', '-'),
'Value': route_data.get('EffectLevelValue', '-'),
'Physical Form': route_data.get('PhysicalForm', '-'),
'Species': species,
'Endpoint Conclusion': route_data.get('EndpointConclusion', '-')
})
# Create DataFrame
df_toxicity = pd.DataFrame(table_data)
# Display the main toxicity table
st.subheader("📊 Acute Toxicity Study Results")
st.dataframe(
df_toxicity,
use_container_width=True,
height=200,
column_config={
"Route of Exposure": st.column_config.TextColumn("Route", width="small"),
"Effect Level": st.column_config.TextColumn("Parameter", width="small"),
"Value": st.column_config.TextColumn("Value", width="medium"),
"Physical Form": st.column_config.TextColumn("Form", width="medium"),
"Species": st.column_config.TextColumn("Test Species", width="medium"),
"Endpoint Conclusion": st.column_config.TextColumn("Conclusion", width="large")
}
)
# Display summary metrics
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Routes Tested", len(df_toxicity))
with col2:
st.metric("Oral LD50", "11,500 mg/kg bw")
with col3:
st.metric("Dermal LD50", "56,750 mg/kg bw")
with col4:
st.metric("Inhalation LC50", ">5.85 mg/L")
# Display key information
st.markdown("---")
st.subheader("📋 Key Study Information")
key_info = data['acute_toxicity']['sections']['Description of key information']['KeyInformation']
# Format the key information for better readability
key_info_formatted = key_info.replace(".", ".\n").replace("The acute", "\n• The acute").replace("In an", "\n• In an")
st.info(key_info_formatted)
# Display classification justification
st.markdown("---")
st.subheader("⚖️ Classification Justification")
justif = data['acute_toxicity']['sections']['Justification for classification or non-classification']
st.success(justif.get('JustifClassif', 'No information available'))
# Display additional discussion
st.markdown("---")
st.subheader("📝 Additional Information & Discussion")
discussion = data['acute_toxicity']['sections']['Additional information'].get('Discussion', '')
if discussion:
# Parse discussion to highlight key points
discussion_parts = discussion.split('.')
# Display main conclusion
st.info("💡 **Main Conclusion:** " + discussion_parts[0] + ".")
# Display full discussion in expander
with st.expander("Click to view full discussion and references"):
# Format the discussion for better readability
formatted_discussion = discussion.replace(".", ".\n\n")
st.text_area("", formatted_discussion, height=400, disabled=True)
# Option to download the data as CSV
st.markdown("---")
csv = df_toxicity.to_csv(index=False)
st.download_button(
label="📥 Download Acute Toxicity Data as CSV",
data=csv,
file_name='acute_toxicity_data.csv',
mime='text/csv'
)
except Exception as e:
st.error(f"An error occurred: {e}")

10
pyproject.toml Normal file
View file

@ -0,0 +1,10 @@
[project]
name = "cosmoguard-frontend"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"marimo>=0.18.0",
"streamlit>=1.51.0",
]

27
report.py Normal file
View file

@ -0,0 +1,27 @@
import streamlit as st
from datetime import datetime
import os
st.header("Report a bug or request a feature")
st.write("Use the box below to submit a short report. Submissions are saved locally to `reports.txt`.")
report_type = st.selectbox("Type", ["Bug", "Feature request", "Other"])
report_email = st.text_input("Your name (optional)")
report_text = st.text_area("Describe the issue or request", height=160)
if st.button("Submit report"):
if not report_text.strip():
st.error("Please enter a description before submitting.")
else:
timestamp = datetime.utcnow().isoformat() + "Z"
entry = f"[{timestamp}]\t{report_type}\t{report_email}\t{report_text.replace('\n', ' ')}\n"
try:
base = os.path.dirname(__file__)
path = os.path.join(base, "reports.txt")
with open(path, "a", encoding="utf-8") as f:
f.write(entry)
st.success("Thanks — your report was saved.")
st.write("Saved to:", path)
st.markdown("---")
except Exception as e:
st.error(f"Could not save report: {e}")

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
streamlit>=1.0
requests>=2.0

1150
uv.lock Normal file

File diff suppressed because it is too large Load diff

304
work/json_cleaning.py Normal file
View file

@ -0,0 +1,304 @@
import marimo
__generated_with = "0.18.0"
app = marimo.App(width="medium")
@app.cell
def _():
import marimo as mo
import requests
return mo, requests
@app.cell
def _():
url = 'https://api.cosmoguard.it/api/v1/echa/search'
return (url,)
@app.cell
def _(requests, url):
response = requests.post(url, json={'cas': '50-00-0'})
data = response.json()
return (data,)
@app.cell
def _(data, mo):
if data['success'] == True:
results = data['data']
if results:
substance_info = data['data']['substance']
last_update = data['data']['dossier_info']['lastUpdatedDate']
tox = data['data']['index']['toxicological_information_link']
rpt = data['data']['index']['repeated_dose_toxicity_link']
act = data['data']['index']['acute_toxicity_link']
toxicological_information = data['data']['toxicological_information']
repeated_dose_toxicity = data['data']['repeated_dose_toxicity']
acute_toxicity = data['data']['acute_toxicity']
else:
mo.alert('No results found for the given CAS number.')
return (
acute_toxicity,
repeated_dose_toxicity,
substance_info,
toxicological_information,
)
@app.cell
def _(Any, Dict, requests):
def api_req(query: str) -> Dict[str, Any]:
url = 'https://api.cosmoguard.it/api/v1/echa/search'
response = requests.post(url, json={'cas': query})
data = response.json()
if data['success'] == True:
results = data['data']
if results:
substance_info = data['data']['substance']
last_update = data['data']['dossier_info']['lastUpdatedDate']
tox = data['data']['index']['toxicological_information_link']
rpt = data['data']['index']['repeated_dose_toxicity_link']
act = data['data']['index']['acute_toxicity_link']
toxicological_information = data['data']['toxicological_information']
repeated_dose_toxicity = data['data']['repeated_dose_toxicity']
acute_toxicity = data['data']['acute_toxicity']
return {
'substance_info': substance_info,
'last_update': last_update,
'toxicological_information': toxicological_information,
'repeated_dose_toxicity': repeated_dose_toxicity,
'acute_toxicity': acute_toxicity,
'link': {
'toxicological_information': tox,
'repeated_dose_toxicity': rpt,
'acute_toxicity': act
}
}
else:
return {'error': 'No results found for the given CAS number.'}
else:
return data['error']
return
@app.cell
def _(substance_info):
substance_info
return
@app.cell
def _(toxicological_information):
txi = toxicological_information
txi
return (txi,)
@app.cell
def _(txi):
txi_content = {
txi['sections'][1]['label']: {
txi['sections'][1]['subsections'][0]['label']: {
'HazardAssessment': txi['sections'][1]['subsections'][0]['subsections'][0]['HazardAssessment']
}
}
}
return (txi_content,)
@app.cell
def _(txi_content):
txi_content
return
@app.cell
def _():
import json
from typing import Any, Dict, List, Union
def remove_empty_values(data: Any) -> Any:
"""
Rimuove ricorsivamente tutte le chiavi con valori vuoti (stringa vuota, None, False, liste/dict vuoti).
Args:
data: Il dato da pulire (può essere dict, list o valore primitivo)
Returns:
Il dato pulito senza valori vuoti
"""
if isinstance(data, dict):
# Filtra il dizionario rimuovendo chiavi con valori vuoti
cleaned = {}
for key, value in data.items():
# Ricorsivamente pulisci il valore
cleaned_value = remove_empty_values(value)
# Aggiungi solo se il valore non è vuoto
if cleaned_value not in [None, "", [], {}, False]:
cleaned[key] = cleaned_value
return cleaned if cleaned else None
elif isinstance(data, list):
# Pulisci ogni elemento della lista
cleaned = []
for item in data:
cleaned_item = remove_empty_values(item)
if cleaned_item not in [None, "", [], {}, False]:
cleaned.append(cleaned_item)
return cleaned if cleaned else None
else:
# Per valori primitivi, ritorna il valore se non è vuoto
if data in [None, "", False]:
return None
return data
def transform_labels_to_keys(data: Any) -> Any:
"""
Trasforma ricorsivamente le 'label' in chiavi del dizionario.
Args:
data: Il dato da trasformare
Returns:
Il dato trasformato con le label come chiavi
"""
if isinstance(data, dict):
# Se c'è una label, usala come chiave principale
if 'label' in data:
label = data['label']
# Crea una copia del dizionario senza la label
new_dict = {k: v for k, v in data.items() if k != 'label'}
# Se ci sono subsections, trasformale ricorsivamente
if 'subsections' in new_dict:
subsections = new_dict.pop('subsections')
# Trasforma ogni subsection
for subsection in subsections:
transformed_sub = transform_labels_to_keys(subsection)
if isinstance(transformed_sub, dict):
new_dict.update(transformed_sub)
# Trasforma ricorsivamente tutti gli altri valori
for key, value in list(new_dict.items()):
new_dict[key] = transform_labels_to_keys(value)
# Ritorna con la label come chiave principale
return {label: new_dict if new_dict else {}}
else:
# Se non c'è label, trasforma ricorsivamente tutti i valori
result = {}
for key, value in data.items():
if key == 'subsections' and isinstance(value, list):
# Gestisci le subsections senza label nel dizionario padre
for subsection in value:
transformed = transform_labels_to_keys(subsection)
if isinstance(transformed, dict):
result.update(transformed)
elif key == 'sections' and isinstance(value, list):
# Gestisci le sections trasformandole in un dizionario
sections_dict = {}
for section in value:
transformed = transform_labels_to_keys(section)
if isinstance(transformed, dict):
sections_dict.update(transformed)
result[key] = sections_dict
else:
result[key] = transform_labels_to_keys(value)
return result
elif isinstance(data, list):
# Trasforma ogni elemento della lista
return [transform_labels_to_keys(item) for item in data]
else:
# Ritorna il valore così com'è
return data
def clean_and_transform_json(json_data: Union[str, dict], keep_false: bool = False) -> dict:
"""
Funzione principale che pulisce e trasforma il JSON.
Args:
json_data: Il JSON come stringa o dizionario
keep_false: Se True, mantiene i valori False (default: False li rimuove)
Returns:
Il JSON pulito e trasformato
"""
# Se è una stringa, parsala
if isinstance(json_data, str):
data = json.loads(json_data)
else:
data = json_data
# Step 1: Rimuovi i valori vuoti
if not keep_false:
cleaned_data = remove_empty_values(data)
else:
# Versione alternativa che mantiene False
def remove_empty_keep_false(d):
if isinstance(d, dict):
cleaned = {}
for key, value in d.items():
cleaned_value = remove_empty_keep_false(value)
if cleaned_value not in [None, "", [], {}]:
cleaned[key] = cleaned_value
return cleaned if cleaned else None
elif isinstance(d, list):
cleaned = []
for item in d:
cleaned_item = remove_empty_keep_false(item)
if cleaned_item not in [None, "", [], {}]:
cleaned.append(cleaned_item)
return cleaned if cleaned else None
else:
if d in [None, ""]:
return None
return d
cleaned_data = remove_empty_keep_false(data)
# Step 2: Trasforma le label in chiavi
if cleaned_data:
transformed_data = transform_labels_to_keys(cleaned_data)
else:
transformed_data = {}
return transformed_data
return Any, Dict, clean_and_transform_json
@app.cell
def _(clean_and_transform_json, txi):
txi_cleaned = clean_and_transform_json(txi)
txi_cleaned
return
@app.cell
def _(clean_and_transform_json, repeated_dose_toxicity):
rp_cl = clean_and_transform_json(repeated_dose_toxicity)
rp_cl
return
@app.cell
def _(acute_toxicity, clean_and_transform_json):
act_s = clean_and_transform_json(acute_toxicity)
act_s
return
if __name__ == "__main__":
app.run()