import json as js import re import requests as req from typing import Union #region Funzione che processa una lista di CAS presa da Cosing (Grazie Jem) def parse_cas_numbers(cas_string:list) -> list: # Siccome ci assicuriamo esternamente che esista almeno un cas possiamo prendere la stringa cas_string = cas_string[0] # Rimuoviamo parentesi e il loro contenuto cas_string = re.sub(r"\([^)]*\)", "", cas_string) # Eseguiamo uno split su vari possibili separatori cas_parts = re.split(r"[/;,]", cas_string) # Otteniamo una lista utilizzando i cas precedenti e rimuovendo spazi in eccesso cas_list = [cas.strip() for cas in cas_parts] # Alcuni cas sono divisi da un doppio dash (--) che dobbiamo rimuovere # è però necessario farlo ora in seconda battuta if len(cas_list) == 1 and "--" in cas_list[0]: cas_list = [cas.strip() for cas in cas_list[0].split("--")] # Siccome alcuni cas hanno valori non validi ("-") li cerchiamo e rimuoviamo if cas_list: while "-" in cas_list: cas_list.remove("-") return cas_list #endregion #region Funzione per eseguire una ricerca direttamente sul cosing # Il primo argomento è la stringa da cercare, il secondo indica il tipo di ricerca def cosing_search(text : str, mode : str = "name") -> Union[list,dict,None]: cosing_post_req = "https://api.tech.ec.europa.eu/search-api/prod/rest/search?apiKey=285a77fd-1257-4271-8507-f0c6b2961203&text=*&pageSize=100&pageNumber=1" agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0" # La modalità di ricerca base è quella per nome, che sia INCI o di altro tipo if mode == "name": search_query = {"bool": {"must":[ {"text": {"query":f"{text}","fields": ["inciName.exact","inciUsaName","innName.exact","phEurName","chemicalName","chemicalDescription"], "defaultOperator":"AND"}}]}} # In caso di ricerca per numero cas o EC il payload della richiesta è diverso elif mode in ["cas","ec"]: search_query = {"bool": {"must": [{"text": {"query": f"*{text}*","fields": ["casNo", "ecNo"]}}]}} # La ricerca per ID è necessaria dove serva recuperare gli identified ingredients elif mode == "id": search_query = {"bool":{"must":[{"term":{"substanceId":f"{text}"}}]}} # Se la mode inserita non è prevista lancio un errore else: raise ValueError # Creo il payload della mia request files = {"query": ("query",js.dumps(search_query),"application/json")} # Eseguo la post di ricerca risposta = req.post(cosing_post_req,headers={"user_agent":agent,"Connection":"keep-alive"},files=files) risposta = risposta.json() if risposta["results"]: return risposta["results"][0]["metadata"] # La funzione ritorna None quando non ho risultati dalla mia ricerca return risposta.status_code #endregion #region Funzione per pulire un json cosing e restituirlo def clean_cosing(json : dict, full : bool = True) -> dict: # Definisco i campi di nostro interesse e li divido in base al tipo di output finale che desidero string_cols = ["itemType","nameOfCommonIngredientsGlossary","inciName","phEurName","chemicalName","innName","substanceId","refNo"] list_cols = ["casNo","ecNo","functionName","otherRestrictions","sccsOpinion","sccsOpinionUrls","identifiedIngredient","annexNo","otherRegulations"] # Creo una lista con tutti i campi su cui ciclare total_keys = string_cols + list_cols # Definisco la base dell"url che mi serve per ottenere il link cosing della sostanza base_url = "https://ec.europa.eu/growth/tools-databases/cosing/details/" clean_json = {} # Ciclo su tutti i campi di interesse for key in total_keys: # Alcuni campi contengono una dicitura inutile che occupa solo spazio # per cui provvedo a rimuoverla while "" in json[key]: json[key].remove("") # Se il campo dovrà avere in output una lista, posso anche accettare le liste vuote del cosing come valore if key in list_cols: value = json[key] # Il cas e l"ec sono casi speciali, per cui eseguo delle funzioni in più quando lo tratto if key in ["casNo", "ecNo"]: if value: value = parse_cas_numbers(value) # Completiamo direttamente il json di ritorno ove presenti degli identifiedIngredient, # solo dove il flag "full" è true elif key == "identifiedIngredient": if full: if value: value = identified_ingredients(value) clean_json[key] = value else: # Questo nome di campo era troppo lungo e ho preferito semplificarlo if key == "nameOfCommonIngredientsGlossary": nKey = "commonName" # Dovendo rinominare alcuni campi, negli altri casi copio il nome iniziale else: nKey = key # Siccome voglio in output una stringa semplice e se usassi uno slicer su di una lista vuota # devo prima verificare che la lista cosing contenga dei valori if json[key]: clean_json[nKey] = json[key][0] else: clean_json[nKey] = "" # Il campo cosingUrl non esiste ancora, vado a crearlo unendo il substance ID all"url base clean_json["cosingUrl"] = f"{base_url}{json["substanceId"][0]}" return clean_json #endregion #region Funzione per completare, se necessario, un json cosing def identified_ingredients(id_list : list) -> list: identified = [] # Per ognuno degli ingredienti in IdentifiedIngredients eseguo una ricerca for id in id_list: ingredient = cosing_search(id,"id") if ingredient: # Vado a pulire i json appena trovati ingredient = clean_cosing(ingredient,full=False) # Ora salvo nella lista il documento pulito identified.append(ingredient) # Finito di popolare la lista con gli oggetti degli identifiedIngredient, la ritorno return identified #endregion