Skip to content

Commit b7091df

Browse files
authored
Fix autocomplete metadata schema compliance
1 parent 8d0a6f4 commit b7091df

2 files changed

Lines changed: 141 additions & 8 deletions

File tree

developer/autocomplete_metadata.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515

1616
import json
1717
import os
18+
import re
1819
import sys
1920
import urllib.request
21+
from html import unescape
2022

2123
import yaml
2224

@@ -142,6 +144,46 @@ def get_chembl_id_from_unichem(sources):
142144
return None
143145

144146

147+
def clean_text(value):
148+
if not isinstance(value, str):
149+
return value
150+
return re.sub(r"<[^>]+>", "", unescape(value)).strip()
151+
152+
153+
def safe_float(value):
154+
try:
155+
return float(value)
156+
except (TypeError, ValueError):
157+
return None
158+
159+
160+
def sanitize_sameas(sameas):
161+
patterns = {
162+
"ChEBI": r"^CHEBI:\d+$",
163+
"ChEMBL": r"^CHEMBL\d+$",
164+
"lipidmaps": r"^LM(FA|GL|GP|SP|ST|PR|SL|PK)[0-9]{4}([0-9a-zA-Z]{4,6})?$",
165+
"metabolights": r"^MTBL[CS]\d+$",
166+
"slm": r"^SLM:\d+$",
167+
"pdb.ligand": r"^[A-Za-z0-9]+$",
168+
"unii": r"^[A-Z0-9]+$",
169+
"cas": r"^\d{1,7}-\d{2}-\d$",
170+
}
171+
sanitized = {}
172+
for key, value in sameas.items():
173+
if key == "pubchem.compound":
174+
if isinstance(value, int):
175+
sanitized[key] = value
176+
else:
177+
try:
178+
sanitized[key] = int(value)
179+
except (TypeError, ValueError):
180+
pass
181+
continue
182+
if isinstance(value, str) and re.match(patterns.get(key, r".+"), value):
183+
sanitized[key] = value
184+
return sanitized
185+
186+
145187
def load_existing_metadata(path):
146188
if os.path.exists(path):
147189
with open(path, "r", encoding="utf-8") as f:
@@ -188,7 +230,7 @@ def main():
188230
chembl = get_chembl(inchikey)
189231
pubchem = get_pubchem(inchikey)
190232
sources = get_unichem(inchikey)
191-
sameas = extract_sameas(sources)
233+
sameas = sanitize_sameas(extract_sameas(sources))
192234

193235
cid = pubchem.get("CID", sameas.get("pubchem.compound"))
194236
synonyms = get_pubchem_synonyms(cid) if cid else []
@@ -216,6 +258,7 @@ def main():
216258
# 3. If still no synonyms, try PubChem synonyms
217259
if not alternate_names and synonyms:
218260
alternate_names = synonyms
261+
alternate_names = [clean_text(name) for name in alternate_names if clean_text(name)]
219262

220263
molecule_props = chembl.get("molecule_properties", {})
221264
molecule_structures = chembl.get("molecule_structures", {})
@@ -229,14 +272,23 @@ def main():
229272
else:
230273
image_url = ""
231274

275+
nmr_name = (
276+
existing.get("NMRlipids", {}).get("name")
277+
or clean_text(chembl.get("pref_name", ""))
278+
or clean_text(molecule_props.get("iupac_name", ""))
279+
or clean_text(pubchem.get("IUPACName", ""))
280+
or nmr_id
281+
)
282+
232283
bioschema = {
233-
"name": molecule_props.get("iupac_name") or pubchem.get("IUPACName", ""),
234-
"iupacName": molecule_props.get("iupac_name") or pubchem.get("IUPACName", ""),
284+
"name": clean_text(molecule_props.get("iupac_name")) or clean_text(pubchem.get("IUPACName", "")),
285+
"iupacName": clean_text(molecule_props.get("iupac_name")) or clean_text(pubchem.get("IUPACName", "")),
235286
"molecularFormula": molecule_props.get("full_molformula") or pubchem.get("MolecularFormula", ""),
236-
"molecularWeight": float(molecule_props.get("full_mwt") or pubchem.get("MolecularWeight", 0)),
237-
"inChI": molecule_structures.get("standard_inchi") or pubchem.get("InChI", ""),
238-
"inChIKey": molecule_structures.get("standard_inchi_key") or pubchem.get("InChIKey", ""),
239-
"smiles": molecule_structures.get("canonical_smiles") or pubchem.get("SMILES", ""),
287+
"molecularWeight": safe_float(molecule_props.get("full_mwt") or pubchem.get("MolecularWeight")),
288+
"inChI": clean_text(molecule_structures.get("standard_inchi")) or clean_text(pubchem.get("InChI", "")),
289+
"inChIKey": clean_text(molecule_structures.get("standard_inchi_key"))
290+
or clean_text(pubchem.get("InChIKey", "")),
291+
"smiles": clean_text(molecule_structures.get("canonical_smiles")) or clean_text(pubchem.get("SMILES", "")),
240292
"image": image_url,
241293
"description": "",
242294
}
@@ -245,7 +297,7 @@ def main():
245297
bioschema["alternateName"] = alternate_names
246298

247299
new_data = {
248-
"NMRlipids": {"id": nmr_id, "name": "", "charge": ""},
300+
"NMRlipids": {"id": nmr_id, "name": nmr_name},
249301
"sameAs": sameas,
250302
"bioschema_properties": bioschema,
251303
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import importlib.util
2+
import json
3+
import sys
4+
from pathlib import Path
5+
6+
import yaml
7+
from jsonschema import Draft7Validator
8+
9+
10+
def load_autocomplete_module():
11+
module_path = Path(__file__).resolve().parents[1] / "developer" / "autocomplete_metadata.py"
12+
spec = importlib.util.spec_from_file_location("autocomplete_metadata", module_path)
13+
module = importlib.util.module_from_spec(spec)
14+
spec.loader.exec_module(module)
15+
return module
16+
17+
18+
def test_autocomplete_output_is_schema_compliant(tmp_path, monkeypatch):
19+
mod = load_autocomplete_module()
20+
21+
metadata_path = tmp_path / "Molecules" / "membrane" / "BOGUS" / "metadata.yaml"
22+
metadata_path.parent.mkdir(parents=True)
23+
metadata_path.write_text(
24+
yaml.safe_dump(
25+
{
26+
"NMRlipids": {"id": "BOGUS"},
27+
"bioschema_properties": {"inChIKey": "HEGSGKPQLMEBJL-RKQHYHRCSA-N"},
28+
}
29+
),
30+
encoding="utf-8",
31+
)
32+
33+
monkeypatch.setattr(mod, "get_chembl", lambda _: {"molecule_properties": {}, "molecule_structures": {}})
34+
monkeypatch.setattr(
35+
mod,
36+
"get_pubchem",
37+
lambda _: {
38+
"CID": 7906,
39+
"IUPACName": "(2R,3S)-name",
40+
"MolecularFormula": "C14H28O6",
41+
"MolecularWeight": 292.37,
42+
"InChI": "InChI=1S/...",
43+
"InChIKey": "HEGSGKPQLMEBJL-RKQHYHRCSA-N",
44+
"SMILES": "CCCCCCCCO<a>C@H]1[C@@H</a>CO)O)O)O",
45+
},
46+
)
47+
monkeypatch.setattr(
48+
mod,
49+
"get_unichem",
50+
lambda _: [
51+
{"shortName": "chembl", "compoundId": "CHEMBL446037"},
52+
{"shortName": "chebi", "compoundId": "1234"},
53+
],
54+
)
55+
monkeypatch.setattr(mod, "get_pubchem_synonyms", lambda _: [])
56+
monkeypatch.setattr(
57+
mod,
58+
"get_chebi",
59+
lambda _: {"names": {"SYNONYM": [{"type": "SYNONYM", "name": "1-<em>OD&lt;/small&gt;-glucopyranoside"}]}},
60+
)
61+
62+
monkeypatch.setattr(sys, "argv", ["autocomplete_metadata.py", str(metadata_path)])
63+
mod.main()
64+
65+
generated = yaml.safe_load(metadata_path.read_text(encoding="utf-8"))
66+
schema_path = (
67+
Path(__file__).resolve().parents[1]
68+
/ "src"
69+
/ "fairmd"
70+
/ "lipids"
71+
/ "schema_validation"
72+
/ "schema"
73+
/ "metadata_schema.json"
74+
)
75+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
76+
77+
errors = sorted(Draft7Validator(schema).iter_errors(generated), key=lambda e: e.path)
78+
assert not errors
79+
assert generated["NMRlipids"]["name"] == "(2R,3S)-name"
80+
assert generated["bioschema_properties"]["smiles"] == "CCCCCCCCOC@H]1[C@@HCO)O)O)O"
81+
assert generated["bioschema_properties"]["alternateName"] == ["1-OD-glucopyranoside"]

0 commit comments

Comments
 (0)